Skip to content

Commit 20d8931

Browse files
Merge branch 'main' into test/refresh-policy-mocks
2 parents 5ce53fd + 65413a4 commit 20d8931

36 files changed

Lines changed: 2496 additions & 163 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { fireEvent, render, screen } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import CopyRepoButton from './CopyRepoButton';
5+
6+
vi.mock('lucide-react', () => ({
7+
Copy: () => <svg aria-hidden="true" data-testid="copy-icon" />,
8+
}));
9+
10+
Object.assign(navigator, {
11+
clipboard: {
12+
writeText: vi.fn().mockResolvedValue(undefined),
13+
},
14+
});
15+
16+
describe('CopyRepoButton accessibility behavior', () => {
17+
it('renders as an accessible button with visible label', () => {
18+
render(<CopyRepoButton />);
19+
20+
expect(screen.getByRole('button', { name: /copy url/i })).toBeDefined();
21+
});
22+
23+
it('keeps decorative copy icon hidden from assistive technology', () => {
24+
render(<CopyRepoButton />);
25+
26+
expect(screen.getByTestId('copy-icon').getAttribute('aria-hidden')).toBe('true');
27+
});
28+
29+
it('is keyboard focusable through the native button element', () => {
30+
render(<CopyRepoButton />);
31+
32+
const button = screen.getByRole('button', { name: /copy url/i });
33+
button.focus();
34+
35+
expect(document.activeElement).toBe(button);
36+
});
37+
38+
it('updates accessible button name after successful copy action', async () => {
39+
render(<CopyRepoButton />);
40+
41+
const button = screen.getByRole('button', { name: /copy url/i });
42+
fireEvent.click(button);
43+
44+
expect(await screen.findByRole('button', { name: /copied/i })).toBeDefined();
45+
});
46+
47+
it('preserves semantic button role after interaction', async () => {
48+
render(<CopyRepoButton />);
49+
50+
fireEvent.click(screen.getByRole('button', { name: /copy url/i }));
51+
52+
expect(await screen.findByRole('button', { name: /copied/i })).toBeDefined();
53+
expect(screen.getAllByRole('button')).toHaveLength(1);
54+
});
55+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { render } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { BoxIcon, CheckIcon, CloseIcon, CopyIcon, ZapIcon } from './Icons';
5+
6+
type IconProps = React.SVGProps<SVGSVGElement>;
7+
8+
vi.mock('lucide-react', () => ({
9+
Copy: (props: IconProps) => <svg data-testid="copy-icon" {...props} />,
10+
Zap: (props: IconProps) => <svg data-testid="zap-icon" {...props} />,
11+
Box: (props: IconProps) => <svg data-testid="box-icon" {...props} />,
12+
Check: (props: IconProps) => <svg data-testid="check-icon" {...props} />,
13+
X: (props: IconProps) => <svg data-testid="close-icon" {...props} />,
14+
}));
15+
16+
describe('Icons empty fallback behavior', () => {
17+
it('renders CopyIcon without crashing', () => {
18+
const { getByTestId } = render(<CopyIcon />);
19+
expect(getByTestId('copy-icon')).toBeDefined();
20+
});
21+
22+
it('renders ZapIcon without crashing', () => {
23+
const { getByTestId } = render(<ZapIcon />);
24+
expect(getByTestId('zap-icon')).toBeDefined();
25+
});
26+
27+
it('renders BoxIcon without crashing', () => {
28+
const { getByTestId } = render(<BoxIcon />);
29+
expect(getByTestId('box-icon')).toBeDefined();
30+
});
31+
32+
it('renders CheckIcon with fallback styling', () => {
33+
const { getByTestId } = render(<CheckIcon />);
34+
const icon = getByTestId('check-icon');
35+
36+
expect(icon.getAttribute('stroke')).toBe('#10b981');
37+
});
38+
39+
it('renders CloseIcon without runtime errors', () => {
40+
const { getByTestId } = render(<CloseIcon />);
41+
expect(getByTestId('close-icon')).toBeDefined();
42+
});
43+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, expect, it, vi } from 'vitest';
4+
5+
import { BoxIcon, CheckIcon, CloseIcon, CopyIcon, ZapIcon } from './Icons';
6+
7+
type IconProps = React.SVGProps<SVGSVGElement>;
8+
9+
vi.mock('lucide-react', () => ({
10+
Copy: (props: IconProps) => <svg data-testid="copy-icon" {...props} />,
11+
Zap: (props: IconProps) => <svg data-testid="zap-icon" {...props} />,
12+
Box: (props: IconProps) => <svg data-testid="box-icon" {...props} />,
13+
Check: (props: IconProps) => <svg data-testid="check-icon" {...props} />,
14+
X: (props: IconProps) => <svg data-testid="close-icon" {...props} />,
15+
}));
16+
17+
describe('Icons massive scaling behavior', () => {
18+
it('renders 500 CopyIcon instances without crashing', () => {
19+
render(
20+
<>
21+
{Array.from({ length: 500 }, (_, index) => (
22+
<CopyIcon key={index} />
23+
))}
24+
</>
25+
);
26+
27+
expect(screen.getAllByTestId('copy-icon')).toHaveLength(500);
28+
});
29+
30+
it('renders 500 ZapIcon instances without crashing', () => {
31+
render(
32+
<>
33+
{Array.from({ length: 500 }, (_, index) => (
34+
<ZapIcon key={index} />
35+
))}
36+
</>
37+
);
38+
39+
expect(screen.getAllByTestId('zap-icon')).toHaveLength(500);
40+
});
41+
42+
it('renders mixed icon sets at high volume', () => {
43+
render(
44+
<>
45+
{Array.from({ length: 100 }, (_, index) => (
46+
<div key={index}>
47+
<CopyIcon />
48+
<ZapIcon />
49+
<BoxIcon />
50+
<CheckIcon />
51+
<CloseIcon />
52+
</div>
53+
))}
54+
</>
55+
);
56+
57+
expect(screen.getAllByTestId('copy-icon')).toHaveLength(100);
58+
expect(screen.getAllByTestId('zap-icon')).toHaveLength(100);
59+
expect(screen.getAllByTestId('box-icon')).toHaveLength(100);
60+
expect(screen.getAllByTestId('check-icon')).toHaveLength(100);
61+
expect(screen.getAllByTestId('close-icon')).toHaveLength(100);
62+
});
63+
64+
it('preserves icon dimension attributes across many rendered icons', () => {
65+
render(
66+
<>
67+
{Array.from({ length: 100 }, (_, index) => (
68+
<CopyIcon key={index} />
69+
))}
70+
</>
71+
);
72+
73+
expect(
74+
screen.getAllByTestId('copy-icon').every((icon) => icon.getAttribute('width') === '20')
75+
).toBe(true);
76+
});
77+
78+
it('preserves CheckIcon stroke styling across high-volume renders', () => {
79+
render(
80+
<>
81+
{Array.from({ length: 100 }, (_, index) => (
82+
<CheckIcon key={index} />
83+
))}
84+
</>
85+
);
86+
87+
expect(
88+
screen.getAllByTestId('check-icon').every((icon) => icon.getAttribute('stroke') === '#10b981')
89+
).toBe(true);
90+
});
91+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { render } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import ScrollRestoration from './ScrollRestoration';
5+
6+
const mockUsePathname = vi.fn();
7+
8+
vi.mock('next/navigation', () => ({
9+
usePathname: () => mockUsePathname(),
10+
}));
11+
12+
describe('ScrollRestoration empty fallback behavior', () => {
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
sessionStorage.clear();
16+
mockUsePathname.mockReturnValue('/');
17+
window.scrollTo = vi.fn();
18+
});
19+
20+
it('renders null without crashing when no saved scroll position exists', () => {
21+
const { container } = render(<ScrollRestoration />);
22+
23+
expect(container.firstChild).toBeNull();
24+
});
25+
26+
it('does not scroll when sessionStorage has no saved position', () => {
27+
render(<ScrollRestoration />);
28+
29+
expect(window.scrollTo).not.toHaveBeenCalled();
30+
});
31+
32+
it('restores saved scroll position when value exists', () => {
33+
sessionStorage.setItem('scroll-position-/', '250');
34+
35+
render(<ScrollRestoration />);
36+
37+
expect(window.scrollTo).toHaveBeenCalledWith(0, 250);
38+
});
39+
40+
it('stores current scroll position on scroll event', () => {
41+
Object.defineProperty(window, 'scrollY', {
42+
value: 300,
43+
configurable: true,
44+
});
45+
46+
render(<ScrollRestoration />);
47+
48+
window.dispatchEvent(new Event('scroll'));
49+
50+
expect(sessionStorage.getItem('scroll-position-/')).toBe('300');
51+
});
52+
53+
it('uses pathname-specific storage keys for empty fallback safety', () => {
54+
mockUsePathname.mockReturnValue('/dashboard');
55+
56+
render(<ScrollRestoration />);
57+
58+
window.dispatchEvent(new Event('scroll'));
59+
60+
expect(sessionStorage.getItem('scroll-position-/dashboard')).toBeDefined();
61+
});
62+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { render } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import ScrollRestoration from './ScrollRestoration';
5+
6+
const mockUsePathname = vi.fn();
7+
8+
vi.mock('next/navigation', () => ({
9+
usePathname: () => mockUsePathname(),
10+
}));
11+
12+
describe('ScrollRestoration massive scaling behavior', () => {
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
sessionStorage.clear();
16+
mockUsePathname.mockReturnValue('/');
17+
window.scrollTo = vi.fn();
18+
});
19+
20+
it('renders repeatedly without crashing under high mount volume', () => {
21+
expect(() => {
22+
for (let index = 0; index < 100; index++) {
23+
render(<ScrollRestoration />);
24+
}
25+
}).not.toThrow();
26+
});
27+
28+
it('handles many stored scroll positions without breaking restoration', () => {
29+
for (let index = 0; index < 500; index++) {
30+
sessionStorage.setItem(`scroll-position-/page-${index}`, String(index));
31+
}
32+
33+
mockUsePathname.mockReturnValue('/page-499');
34+
35+
render(<ScrollRestoration />);
36+
37+
expect(window.scrollTo).toHaveBeenCalledWith(0, 499);
38+
});
39+
40+
it('stores scroll position correctly after many scroll events', () => {
41+
render(<ScrollRestoration />);
42+
43+
for (let index = 0; index < 100; index++) {
44+
Object.defineProperty(window, 'scrollY', {
45+
value: index,
46+
configurable: true,
47+
});
48+
49+
window.dispatchEvent(new Event('scroll'));
50+
}
51+
52+
expect(sessionStorage.getItem('scroll-position-/')).toBe('99');
53+
});
54+
55+
it('keeps pathname-specific keys isolated at scale', () => {
56+
for (let index = 0; index < 50; index++) {
57+
mockUsePathname.mockReturnValue(`/dashboard-${index}`);
58+
59+
const { unmount } = render(<ScrollRestoration />);
60+
61+
Object.defineProperty(window, 'scrollY', {
62+
value: index * 10,
63+
configurable: true,
64+
});
65+
66+
window.dispatchEvent(new Event('scroll'));
67+
unmount();
68+
}
69+
70+
expect(sessionStorage.getItem('scroll-position-/dashboard-0')).toBe('0');
71+
expect(sessionStorage.getItem('scroll-position-/dashboard-49')).toBe('490');
72+
});
73+
it('removes scroll listener safely after many unmount cycles', () => {
74+
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
75+
76+
for (let index = 0; index < 25; index++) {
77+
const { unmount } = render(<ScrollRestoration />);
78+
unmount();
79+
}
80+
81+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(25);
82+
});
83+
});

0 commit comments

Comments
 (0)