Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app/my-tickets/gate-code-copy.test.ts
Original file line number Diff line number Diff line change
@@ -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(/<CopyButton value=\{secret\} label="Copy gate code" \/>/);
});
});
77 changes: 77 additions & 0 deletions src/components/copy-button.test.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>>();
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(<CopyButton value={value} label={label} />));
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('');
});
});
Loading