Skip to content

Commit 239754e

Browse files
authored
Merge pull request #835 from ACOB-DEV/codex/fe-user-permissions-optimistic-a11y-tests-payment-success-state
feat: improve User Permissions UX and Payment Success state logic
2 parents 493b89d + aae33ad commit 239754e

4 files changed

Lines changed: 212 additions & 886 deletions

File tree

Lines changed: 53 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -1,250 +1,74 @@
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";
46
import { 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

3516
describe("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

Comments
 (0)