From f13c3cc29dc5aecee3abf1c6c2799f0463b052e0 Mon Sep 17 00:00:00 2001 From: floraispretty <331200294+floraispretty@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:15:31 +0100 Subject: [PATCH 1/2] test(copy-button): cover one-click copy and live-region announcements CopyButton (added in 16b8bcf) had no tests. Cover the labelled type=button, the empty polite live region at rest, copying the value and announcing 'Copied', 'Copy failed' when the clipboard is unavailable, and clearing the announcement after 2s. Refs #242 --- src/components/copy-button.test.tsx | 77 +++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/components/copy-button.test.tsx diff --git a/src/components/copy-button.test.tsx b/src/components/copy-button.test.tsx new file mode 100644 index 0000000..da2f941 --- /dev/null +++ b/src/components/copy-button.test.tsx @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; + +const copyToClipboard = vi.fn<(text: string) => Promise>(); +vi.mock('@/lib/clipboard', () => ({ copyToClipboard: (t: string) => copyToClipboard(t) })); + +import { CopyButton } from './copy-button'; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +let host: HTMLDivElement; +let root: Root; + +beforeEach(() => { + vi.useFakeTimers(); + copyToClipboard.mockReset(); + host = document.createElement('div'); + document.body.appendChild(host); + root = createRoot(host); +}); + +afterEach(() => { + act(() => root.unmount()); + host.remove(); + vi.useRealTimers(); +}); + +function render(value = 'GATE-1234', label = 'Copy gate code') { + act(() => root.render()); + return { + button: host.querySelector('button')!, + status: host.querySelector('[role="status"]')!, + }; +} + +async function click(button: HTMLButtonElement) { + await act(async () => { + button.click(); + }); +} + +describe('CopyButton (#242)', () => { + it('is a labelled button with an empty polite live region at rest', () => { + const { button, status } = render(); + expect(button.getAttribute('type')).toBe('button'); + expect(button.getAttribute('aria-label')).toBe('Copy gate code'); + expect(status.getAttribute('aria-live')).toBe('polite'); + expect(status.textContent).toBe(''); + }); + + it('copies the value in one click and announces "Copied"', async () => { + copyToClipboard.mockResolvedValue(true); + const { button, status } = render('GATE-1234'); + await click(button); + expect(copyToClipboard).toHaveBeenCalledWith('GATE-1234'); + expect(status.textContent).toBe('Copied'); + }); + + it('announces "Copy failed" when the clipboard is unavailable', async () => { + copyToClipboard.mockResolvedValue(false); + const { button, status } = render(); + await click(button); + expect(status.textContent).toBe('Copy failed'); + }); + + it('clears the announcement after 2 seconds', async () => { + copyToClipboard.mockResolvedValue(true); + const { button, status } = render(); + await click(button); + expect(status.textContent).toBe('Copied'); + act(() => { + vi.advanceTimersByTime(2000); + }); + expect(status.textContent).toBe(''); + }); +}); From deb4f79b1829a80213b78c834c22b54a32338192 Mon Sep 17 00:00:00 2001 From: floraispretty <331200294+floraispretty@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:15:31 +0100 Subject: [PATCH 2/2] test(my-tickets): assert the gate code uses the shared CopyButton Pins the #242 'done when' condition so a refactor of My tickets can't silently drop the copy control. Closes #242 --- src/app/my-tickets/gate-code-copy.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/app/my-tickets/gate-code-copy.test.ts diff --git a/src/app/my-tickets/gate-code-copy.test.ts b/src/app/my-tickets/gate-code-copy.test.ts new file mode 100644 index 0000000..a2d5353 --- /dev/null +++ b/src/app/my-tickets/gate-code-copy.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +// #242 "done when": the shared CopyButton is used on My tickets for the gate code. +describe('My tickets gate code (#242)', () => { + it('renders CopyButton for the gate code', () => { + const page = readFileSync(join(__dirname, 'page.tsx'), 'utf8'); + expect(page).toMatch(/import \{ CopyButton \} from '@\/components\/copy-button'/); + expect(page).toMatch(//); + }); +});