1- import React from "react" ;
2- import { render , screen , fireEvent , waitFor } from "@testing-library/react" ;
3- import "@testing-library/jest-dom" ;
1+ import { describe , it , expect , beforeEach , vi } from "vitest" ;
2+ import { render , screen } from "@testing-library/react" ;
3+ import userEvent from "@testing-library/user-event" ;
4+ import "@testing-library/jest-dom/vitest" ;
5+ import confetti from "canvas-confetti" ;
46import { PaymentSuccessAnimation } from "./PaymentSuccessAnimation" ;
5- import { NextIntlClientProvider } from "next-intl" ;
6-
7- // Mock framer-motion
8- jest . mock ( "framer-motion" , ( ) => ( {
9- motion : {
10- div : ( { children, ...props } : any ) => < div { ...props } > { children } </ div > ,
11- button : ( { children, ...props } : any ) => < button { ...props } > { children } </ button > ,
12- } ,
13- AnimatePresence : ( { children } : any ) => < > { children } </ > ,
7+
8+ vi . mock ( "next-intl" , ( ) => ( {
9+ useTranslations : ( ) => ( key : string ) => key ,
1410} ) ) ;
1511
16- // Mock canvas-confetti
17- jest . mock ( "canvas-confetti" , ( ) => jest . fn ( ) ) ;
18-
19- // Mock next-intl
20- jest . mock ( "next-intl" , ( ) => ( {
21- useTranslations : ( ) => ( key : string ) => {
22- const translations : Record < string , string > = {
23- "payment.successAnnounce" : "Payment successful! {amount} {asset} has been received." ,
24- "common.close" : "Close" ,
25- "payment.amountReceived" : "Amount Received" ,
26- "payment.successMessage" : "Your payment has been processed successfully." ,
27- "payment.transactionId" : "Transaction ID" ,
28- "common.continue" : "Continue" ,
29- "payment.successHint" : "Press the continue button or close button to dismiss this success message." ,
30- } ;
31- return translations [ key ] || key ;
32- } ,
12+ vi . mock ( "canvas-confetti" , ( ) => ( {
13+ default : vi . fn ( ) ,
3314} ) ) ;
3415
3516describe ( "PaymentSuccessAnimation" , ( ) => {
36- const defaultProps = {
37- show : true ,
38- onComplete : jest . fn ( ) ,
39- amount : "100" ,
40- asset : "XLM" ,
41- txId : "tx123" ,
42- } ;
43-
44- const renderComponent = ( props = { } ) => {
45- const messages = {
46- "payment.successAnnounce" : "Payment successful! {amount} {asset} has been received." ,
47- "common.close" : "Close" ,
48- "payment.amountReceived" : "Amount Received" ,
49- "payment.successMessage" : "Your payment has been processed successfully." ,
50- "payment.transactionId" : "Transaction ID" ,
51- "common.continue" : "Continue" ,
52- "payment.successHint" : "Press the continue button or close button to dismiss this success message." ,
53- } ;
54-
55- return render (
56- < NextIntlClientProvider messages = { messages } locale = "en" >
57- < PaymentSuccessAnimation { ...defaultProps } { ...props } />
58- </ NextIntlClientProvider >
59- ) ;
60- } ;
61-
6217 beforeEach ( ( ) => {
63- jest . clearAllMocks ( ) ;
18+ vi . clearAllMocks ( ) ;
19+ vi . useRealTimers ( ) ;
6420 } ) ;
6521
66- describe ( "Rendering" , ( ) => {
67- it ( "renders nothing when show is false" , ( ) => {
68- renderComponent ( { show : false } ) ;
69- expect ( screen . queryByRole ( "dialog" ) ) . not . toBeInTheDocument ( ) ;
70- } ) ;
71-
72- it ( "renders success animation when show is true" , ( ) => {
73- renderComponent ( ) ;
74- expect ( screen . getByRole ( "dialog" ) ) . toBeInTheDocument ( ) ;
75- expect ( screen . getByText ( "Payment Successful!" ) ) . toBeInTheDocument ( ) ;
76- expect ( screen . getByText ( "100 XLM" ) ) . toBeInTheDocument ( ) ;
77- } ) ;
78-
79- it ( "displays correct amount and asset" , ( ) => {
80- renderComponent ( { amount : "500" , asset : "USD" } ) ;
81- expect ( screen . getByText ( "500 USD" ) ) . toBeInTheDocument ( ) ;
82- } ) ;
83-
84- it ( "displays transaction ID when provided" , ( ) => {
85- renderComponent ( ) ;
86- expect ( screen . getByText ( "tx123" ) ) . toBeInTheDocument ( ) ;
87- } ) ;
88-
89- it ( "does not display transaction ID when not provided" , ( ) => {
90- renderComponent ( { txId : undefined } ) ;
91- expect ( screen . queryByText ( "Transaction ID" ) ) . not . toBeInTheDocument ( ) ;
92- } ) ;
22+ it ( "renders nothing when show is false" , ( ) => {
23+ render ( < PaymentSuccessAnimation show = { false } /> ) ;
24+ expect ( screen . queryByRole ( "dialog" ) ) . not . toBeInTheDocument ( ) ;
9325 } ) ;
9426
95- describe ( "Accessibility" , ( ) => {
96- it ( "has correct ARIA attributes" , ( ) => {
97- renderComponent ( ) ;
98- const dialog = screen . getByRole ( "dialog" ) ;
99- expect ( dialog ) . toHaveAttribute ( "aria-modal" , "true" ) ;
100- expect ( dialog ) . toHaveAttribute ( "aria-labelledby" , "payment-success-title" ) ;
101- expect ( dialog ) . toHaveAttribute ( "aria-describedby" , "payment-success-description" ) ;
102- } ) ;
103-
104- it ( "has screen reader announcement" , ( ) => {
105- renderComponent ( ) ;
106- const announcement = screen . getByRole ( "status" ) ;
107- expect ( announcement ) . toHaveAttribute ( "aria-live" , "assertive" ) ;
108- expect ( announcement ) . toHaveAttribute ( "aria-atomic" , "true" ) ;
109- expect ( announcement ) . toHaveTextContent ( "Payment successful! 100 XLM has been received." ) ;
110- } ) ;
111-
112- it ( "has accessible close button" , ( ) => {
113- renderComponent ( ) ;
114- const closeButton = screen . getByLabelText ( "Close" ) ;
115- expect ( closeButton ) . toBeInTheDocument ( ) ;
116- } ) ;
117-
118- it ( "has accessible continue button" , ( ) => {
119- renderComponent ( ) ;
120- const continueButton = screen . getByLabelText ( "Continue" ) ;
121- expect ( continueButton ) . toBeInTheDocument ( ) ;
122- } ) ;
123-
124- it ( "has screen reader hint" , ( ) => {
125- renderComponent ( ) ;
126- const hint = screen . getByText ( "Press the continue button or close button to dismiss this success message." ) ;
127- expect ( hint ) . toHaveClass ( "sr-only" ) ;
128- } ) ;
129- } ) ;
27+ it ( "renders dialog content when show is true" , ( ) => {
28+ render ( < PaymentSuccessAnimation show amount = "100" asset = "XLM" txId = "tx123" /> ) ;
13029
131- describe ( "Interactions" , ( ) => {
132- it ( "calls onComplete when close button is clicked" , ( ) => {
133- renderComponent ( ) ;
134- const closeButton = screen . getByLabelText ( "Close" ) ;
135- fireEvent . click ( closeButton ) ;
136- expect ( defaultProps . onComplete ) . toHaveBeenCalledTimes ( 1 ) ;
137- } ) ;
138-
139- it ( "calls onComplete when continue button is clicked" , ( ) => {
140- renderComponent ( ) ;
141- const continueButton = screen . getByLabelText ( "Continue" ) ;
142- fireEvent . click ( continueButton ) ;
143- expect ( defaultProps . onComplete ) . toHaveBeenCalledTimes ( 1 ) ;
144- } ) ;
145-
146- it ( "calls onComplete after animation timeout" , async ( ) => {
147- renderComponent ( ) ;
148- await waitFor ( ( ) => {
149- expect ( defaultProps . onComplete ) . toHaveBeenCalledTimes ( 1 ) ;
150- } , { timeout : 4100 } ) ;
151- } ) ;
30+ expect ( screen . getByRole ( "dialog" ) ) . toBeInTheDocument ( ) ;
31+ expect ( screen . getByText ( "payment.successTitle" ) ) . toBeInTheDocument ( ) ;
32+ expect ( screen . getByText ( "100 XLM" ) ) . toBeInTheDocument ( ) ;
33+ expect ( screen . getByText ( "tx123" ) ) . toBeInTheDocument ( ) ;
15234 } ) ;
15335
154- describe ( "Confetti Animation" , ( ) => {
155- it ( "triggers confetti on mount when show is true" , ( ) => {
156- const mockConfetti = require ( "canvas-confetti" ) ;
157- renderComponent ( ) ;
158- expect ( mockConfetti ) . toHaveBeenCalled ( ) ;
159- } ) ;
160-
161- it ( "does not trigger confetti when show is false" , ( ) => {
162- const mockConfetti = require ( "canvas-confetti" ) ;
163- renderComponent ( { show : false } ) ;
164- expect ( mockConfetti ) . not . toHaveBeenCalled ( ) ;
165- } ) ;
166-
167- it ( "does not trigger confetti again on re-render" , ( ) => {
168- const mockConfetti = require ( "canvas-confetti" ) ;
169- const { rerender } = renderComponent ( ) ;
170- rerender (
171- < NextIntlClientProvider messages = { { } } locale = "en" >
172- < PaymentSuccessAnimation { ...defaultProps } show = { true } />
173- </ NextIntlClientProvider >
174- ) ;
175- expect ( mockConfetti ) . toHaveBeenCalledTimes ( 1 ) ;
176- } ) ;
36+ it ( "triggers confetti once on show" , ( ) => {
37+ const { rerender } = render ( < PaymentSuccessAnimation show amount = "1" asset = "XLM" /> ) ;
38+ rerender ( < PaymentSuccessAnimation show amount = "1" asset = "XLM" /> ) ;
39+
40+ expect ( confetti ) . toHaveBeenCalledTimes ( 1 ) ;
17741 } ) ;
17842
179- describe ( "Animation States" , ( ) => {
180- it ( "resets announcement state after timeout" , async ( ) => {
181- renderComponent ( ) ;
182- // First render should set hasAnnounced to true
183- await waitFor ( ( ) => {
184- expect ( screen . getByRole ( "status" ) ) . toBeInTheDocument ( ) ;
185- } ) ;
186-
187- // Wait for reset timeout
188- await new Promise ( resolve => setTimeout ( resolve , 1100 ) ) ;
189-
190- // Re-render to check if it would announce again
191- const { rerender } = render (
192- < NextIntlClientProvider messages = { { } } locale = "en" >
193- < PaymentSuccessAnimation { ...defaultProps } show = { false } />
194- </ NextIntlClientProvider >
195- ) ;
196-
197- rerender (
198- < NextIntlClientProvider messages = { { } } locale = "en" >
199- < PaymentSuccessAnimation { ...defaultProps } show = { true } />
200- </ NextIntlClientProvider >
201- ) ;
202-
203- // Should announce again after reset
204- expect ( screen . getByRole ( "status" ) ) . toBeInTheDocument ( ) ;
205- } ) ;
43+ it ( "calls onComplete from buttons" , async ( ) => {
44+ const onComplete = vi . fn ( ) ;
45+ render ( < PaymentSuccessAnimation show onComplete = { onComplete } /> ) ;
46+
47+ await userEvent . click ( screen . getByLabelText ( "common.close" ) ) ;
48+ await userEvent . click ( screen . getByLabelText ( "common.continue" ) ) ;
49+
50+ expect ( onComplete ) . toHaveBeenCalledTimes ( 2 ) ;
20651 } ) ;
20752
208- describe ( "Props Handling" , ( ) => {
209- it ( "uses default values for optional props" , ( ) => {
210- renderComponent ( {
211- show : true ,
212- onComplete : jest . fn ( ) ,
213- amount : undefined ,
214- asset : undefined ,
215- txId : undefined ,
216- } ) ;
217- expect ( screen . getByText ( "0 XLM" ) ) . toBeInTheDocument ( ) ;
218- } ) ;
219-
220- it ( "handles custom onComplete callback" , ( ) => {
221- const customOnComplete = jest . fn ( ) ;
222- renderComponent ( { onComplete : customOnComplete } ) ;
223-
224- const continueButton = screen . getByLabelText ( "Continue" ) ;
225- fireEvent . click ( continueButton ) ;
226-
227- expect ( customOnComplete ) . toHaveBeenCalledTimes ( 1 ) ;
228- } ) ;
53+ it ( "calls onComplete after timeout" , ( ) => {
54+ const onComplete = vi . fn ( ) ;
55+ vi . useFakeTimers ( ) ;
56+ render ( < PaymentSuccessAnimation show onComplete = { onComplete } /> ) ;
57+
58+ vi . advanceTimersByTime ( 4000 ) ;
59+ expect ( onComplete ) . toHaveBeenCalledTimes ( 1 ) ;
60+ vi . useRealTimers ( ) ;
22961 } ) ;
23062
231- describe ( "Translation Integration" , ( ) => {
232- it ( "uses translated text for UI elements" , ( ) => {
233- renderComponent ( ) ;
234- expect ( screen . getByText ( "Amount Received" ) ) . toBeInTheDocument ( ) ;
235- expect ( screen . getByText ( "Your payment has been processed successfully." ) ) . toBeInTheDocument ( ) ;
236- } ) ;
237-
238- it ( "falls back to keys when translations are missing" , ( ) => {
239- const messages = { } ;
240- const { rerender } = render (
241- < NextIntlClientProvider messages = { messages } locale = "en" >
242- < PaymentSuccessAnimation { ...defaultProps } />
243- </ NextIntlClientProvider >
244- ) ;
245-
246- // Should still render with fallback keys
247- expect ( screen . getByRole ( "dialog" ) ) . toBeInTheDocument ( ) ;
248- } ) ;
63+ it ( "has expected screen reader semantics" , ( ) => {
64+ render ( < PaymentSuccessAnimation show amount = "20" asset = "USDC" /> ) ;
65+
66+ const dialog = screen . getByRole ( "dialog" ) ;
67+ expect ( dialog ) . toHaveAttribute ( "aria-labelledby" , "payment-success-title" ) ;
68+ expect ( dialog ) . toHaveAttribute ( "aria-describedby" , "payment-success-description" ) ;
69+
70+ const status = screen . getByRole ( "status" ) ;
71+ expect ( status ) . toHaveAttribute ( "aria-live" , "assertive" ) ;
72+ expect ( status ) . toHaveTextContent ( "payment.successAnnounce" ) ;
24973 } ) ;
250- } ) ;
74+ } ) ;
0 commit comments