Skip to content

Commit f1bc995

Browse files
test(CopyRepoButton-timezone-boundaries): verify timezone-boundary copy behaviour
1 parent 5ab5e42 commit f1bc995

2 files changed

Lines changed: 92 additions & 3 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { cleanup, render, screen, fireEvent, act } from '@testing-library/react';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import CopyRepoButton, { REPO_URL } from './CopyRepoButton';
4+
5+
beforeEach(() => {
6+
vi.stubGlobal('navigator', {
7+
...navigator,
8+
clipboard: { writeText: vi.fn() },
9+
});
10+
});
11+
12+
afterEach(() => {
13+
vi.clearAllTimers();
14+
vi.useRealTimers();
15+
vi.unstubAllGlobals();
16+
vi.restoreAllMocks();
17+
cleanup();
18+
});
19+
20+
describe('CopyRepoButton', () => {
21+
it('renders with the default "Copy URL" label on mount', () => {
22+
render(<CopyRepoButton />);
23+
expect(screen.getByRole('button').textContent).toContain('Copy URL');
24+
});
25+
26+
it('transitions to "Copied!" immediately after a successful clipboard write', async () => {
27+
vi.useFakeTimers();
28+
vi.mocked(navigator.clipboard.writeText).mockResolvedValue(undefined);
29+
render(<CopyRepoButton />);
30+
31+
await act(async () => {
32+
fireEvent.click(screen.getByRole('button'));
33+
});
34+
35+
expect(screen.getByRole('button').textContent).toContain('Copied!');
36+
});
37+
38+
it('resets to "Copy URL" exactly after the 2000 ms timeout boundary', async () => {
39+
vi.useFakeTimers();
40+
vi.mocked(navigator.clipboard.writeText).mockResolvedValue(undefined);
41+
render(<CopyRepoButton />);
42+
43+
await act(async () => {
44+
fireEvent.click(screen.getByRole('button'));
45+
});
46+
47+
expect(screen.getByRole('button').textContent).toContain('Copied!');
48+
49+
await act(async () => {
50+
vi.advanceTimersByTime(1999);
51+
});
52+
expect(screen.getByRole('button').textContent).toContain('Copied!');
53+
54+
await act(async () => {
55+
vi.advanceTimersByTime(1);
56+
});
57+
expect(screen.getByRole('button').textContent).toContain('Copy URL');
58+
});
59+
60+
it('shows "Copy failed" and resets after 2000 ms when the clipboard API rejects', async () => {
61+
vi.useFakeTimers();
62+
vi.mocked(navigator.clipboard.writeText).mockRejectedValue(new Error('Not allowed'));
63+
render(<CopyRepoButton />);
64+
65+
await act(async () => {
66+
fireEvent.click(screen.getByRole('button'));
67+
});
68+
69+
expect(screen.getByRole('button').textContent).toContain('Copy failed');
70+
71+
await act(async () => {
72+
vi.advanceTimersByTime(2000);
73+
});
74+
expect(screen.getByRole('button').textContent).toContain('Copy URL');
75+
});
76+
77+
it('writes the exact repo URL exported from the component to the clipboard', async () => {
78+
vi.useFakeTimers();
79+
vi.mocked(navigator.clipboard.writeText).mockResolvedValue(undefined);
80+
render(<CopyRepoButton />);
81+
82+
await act(async () => {
83+
fireEvent.click(screen.getByRole('button'));
84+
});
85+
86+
expect(navigator.clipboard.writeText).toHaveBeenCalledOnce();
87+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(REPO_URL);
88+
});
89+
});

app/components/CopyRepoButton.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import { useState } from 'react';
44
import { Copy } from 'lucide-react';
55

6+
export const REPO_URL = 'https://github.com/JhaSourav07/commitpulse';
7+
68
export default function CopyRepoButton() {
79
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle');
810

9-
const repoUrl = 'https://github.com/JhaSourav07/commitpulse';
10-
1111
const handleCopy = async () => {
1212
try {
13-
await navigator.clipboard.writeText(repoUrl);
13+
await navigator.clipboard.writeText(REPO_URL);
1414
setCopyState('copied');
1515
} catch {
1616
setCopyState('error');

0 commit comments

Comments
 (0)