Skip to content

Commit e7ec0ec

Browse files
authored
fix(frontend): handle copy repo clipboard failures (JhaSourav07#2984)
## Description Fixes JhaSourav07#2983 ## Pillar - [x] 🛠️ Other (Bug fix, refactoring, docs) ## What changed `CopyRepoButton` previously awaited `navigator.clipboard.writeText(repoUrl)` without catching failures. If the browser rejected the clipboard write, the async click handler failed and the button gave no feedback. This PR adds an explicit copy state: - `Copied!` is shown only after the clipboard write resolves - `Copy failed` is shown briefly when the clipboard write rejects - the button returns to `Copy URL` after the same short reset window I also added focused tests for both successful and rejected clipboard writes while keeping the existing render-scaling coverage. ## Visual Preview N/A - small button state behavior fix. ## Local verification - `npm run test -- app/components/CopyRepoButton.massive-scaling.test.tsx` passed - `npm run format:check` passed - `npm run lint` passed with one existing warning outside this change - `npm run typecheck` passed - `npm run test` passed: 119 files, 1775 passed, 1 expected fail - `npm run build` still fails locally with the existing opaque webpack error; no file-level diagnostics were emitted, and recent CI production builds for this repo have passed despite the same local behavior ## GSSoC 2026 This issue and PR are raised under GSSoC 2026. Please add the relevant GSSoC/level labels if they do not sync from the issue automatically. ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` / `npm run format:check` and `npm run lint` locally. - [x] I have run `npm run test` locally. - [x] My commits follow the Conventional Commits format. - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse quality standard.
2 parents 847c8ea + e9ce89e commit e7ec0ec

2 files changed

Lines changed: 51 additions & 10 deletions

File tree

app/components/CopyRepoButton.massive-scaling.test.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { render, screen } from '@testing-library/react';
2-
import { describe, it, expect, vi } from 'vitest';
1+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2+
import { beforeEach, describe, it, expect, vi } from 'vitest';
33
import CopyRepoButton from './CopyRepoButton';
44

55
vi.mock('lucide-react', () => ({
66
Copy: () => <svg data-testid="copy-icon" />,
77
}));
88

9+
beforeEach(() => {
10+
vi.clearAllMocks();
11+
Object.assign(navigator, {
12+
clipboard: {
13+
writeText: vi.fn().mockResolvedValue(undefined),
14+
},
15+
});
16+
});
17+
918
describe('CopyRepoButton Massive Scaling', () => {
1019
it('renders 100 buttons without crashing', () => {
1120
render(
@@ -63,4 +72,35 @@ describe('CopyRepoButton Massive Scaling', () => {
6372
}
6473
}).not.toThrow();
6574
});
75+
76+
it('shows copied state after a successful clipboard write', async () => {
77+
render(<CopyRepoButton />);
78+
79+
fireEvent.click(screen.getByRole('button', { name: /copy url/i }));
80+
81+
await waitFor(() => {
82+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
83+
'https://github.com/JhaSourav07/commitpulse'
84+
);
85+
});
86+
87+
expect(screen.getByText('Copied!')).toBeDefined();
88+
});
89+
90+
it('shows an error state when clipboard write fails', async () => {
91+
vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce(new Error('Permission denied'));
92+
93+
render(<CopyRepoButton />);
94+
95+
fireEvent.click(screen.getByRole('button', { name: /copy url/i }));
96+
97+
await waitFor(() => {
98+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
99+
'https://github.com/JhaSourav07/commitpulse'
100+
);
101+
});
102+
103+
expect(screen.queryByText('Copied!')).toBeNull();
104+
expect(screen.getByText('Copy failed')).toBeDefined();
105+
});
66106
});

app/components/CopyRepoButton.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import { useState } from 'react';
44
import { Copy } from 'lucide-react';
55

66
export default function CopyRepoButton() {
7-
const [copied, setCopied] = useState(false);
7+
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle');
88

99
const repoUrl = 'https://github.com/JhaSourav07/commitpulse';
1010

1111
const handleCopy = async () => {
12-
await navigator.clipboard.writeText(repoUrl);
12+
try {
13+
await navigator.clipboard.writeText(repoUrl);
14+
setCopyState('copied');
15+
} catch {
16+
setCopyState('error');
17+
}
1318

14-
setCopied(true);
15-
16-
setTimeout(() => {
17-
setCopied(false);
18-
}, 2000);
19+
setTimeout(() => setCopyState('idle'), 2000);
1920
};
2021

2122
return (
@@ -24,7 +25,7 @@ export default function CopyRepoButton() {
2425
className="inline-flex items-center gap-2 rounded-2xl border border-black/10 bg-white/60 px-8 py-4 font-semibold transition-all duration-300 hover:scale-105 dark:border-white/10 dark:bg-white/5"
2526
>
2627
<Copy className="h-5 w-5" />
27-
{copied ? 'Copied!' : 'Copy URL'}
28+
{copyState === 'copied' ? 'Copied!' : copyState === 'error' ? 'Copy failed' : 'Copy URL'}
2829
</button>
2930
);
3031
}

0 commit comments

Comments
 (0)