Skip to content

Commit dc78a4a

Browse files
Merge branch 'main' into test-contributors-empty-fallback
2 parents 862f942 + 2e604a1 commit dc78a4a

29 files changed

Lines changed: 1920 additions & 14 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { render } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { BoxIcon, CheckIcon, CloseIcon, CopyIcon, ZapIcon } from './Icons';
5+
6+
describe('Icons accessibility behavior', () => {
7+
it('renders CopyIcon as decorative with aria-hidden="true"', () => {
8+
const { container } = render(<CopyIcon />);
9+
const svg = container.querySelector('svg');
10+
expect(svg?.getAttribute('aria-hidden')).toBe('true');
11+
});
12+
13+
it('renders ZapIcon as decorative with aria-hidden="true"', () => {
14+
const { container } = render(<ZapIcon />);
15+
const svg = container.querySelector('svg');
16+
expect(svg?.getAttribute('aria-hidden')).toBe('true');
17+
});
18+
19+
it('renders BoxIcon as decorative with aria-hidden="true"', () => {
20+
const { container } = render(<BoxIcon />);
21+
const svg = container.querySelector('svg');
22+
expect(svg?.getAttribute('aria-hidden')).toBe('true');
23+
});
24+
25+
it('renders CheckIcon as decorative with aria-hidden="true"', () => {
26+
const { container } = render(<CheckIcon />);
27+
const svg = container.querySelector('svg');
28+
expect(svg?.getAttribute('aria-hidden')).toBe('true');
29+
});
30+
31+
it('renders CloseIcon as decorative with aria-hidden="true"', () => {
32+
const { container } = render(<CloseIcon />);
33+
const svg = container.querySelector('svg');
34+
expect(svg?.getAttribute('aria-hidden')).toBe('true');
35+
});
36+
37+
it('includes basic structural roles on SVG elements', () => {
38+
const { container } = render(<CopyIcon />);
39+
const svg = container.querySelector('svg');
40+
// Lucide components use standard svg elements which imply a graphics role,
41+
// and setting aria-hidden="true" makes it decorative.
42+
expect(svg?.tagName.toLowerCase()).toBe('svg');
43+
});
44+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { fireEvent, 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+
describe('Icons mouse interactivity & event propagation', () => {
7+
it('allows mouse event propagation when CopyIcon is clicked', () => {
8+
const handleClick = vi.fn();
9+
const { container } = render(
10+
<button onClick={handleClick}>
11+
<CopyIcon />
12+
</button>
13+
);
14+
15+
const svg = container.querySelector('svg');
16+
if (svg) {
17+
fireEvent.click(svg);
18+
}
19+
20+
// Verifies that clicks on the SVG propagate up to interactive parent elements
21+
expect(handleClick).toHaveBeenCalledTimes(1);
22+
});
23+
24+
it('allows mouse event propagation when ZapIcon is clicked', () => {
25+
const handleClick = vi.fn();
26+
const { container } = render(
27+
<button onClick={handleClick}>
28+
<ZapIcon />
29+
</button>
30+
);
31+
32+
const svg = container.querySelector('svg');
33+
if (svg) fireEvent.click(svg);
34+
35+
expect(handleClick).toHaveBeenCalledTimes(1);
36+
});
37+
38+
it('allows mouse event propagation when BoxIcon is clicked', () => {
39+
const handleClick = vi.fn();
40+
const { container } = render(
41+
<div onClick={handleClick}>
42+
<BoxIcon />
43+
</div>
44+
);
45+
46+
const svg = container.querySelector('svg');
47+
if (svg) fireEvent.click(svg);
48+
49+
expect(handleClick).toHaveBeenCalledTimes(1);
50+
});
51+
52+
it('allows mouse event propagation when CheckIcon is clicked', () => {
53+
const handleClick = vi.fn();
54+
const { container } = render(
55+
<button onClick={handleClick}>
56+
<CheckIcon />
57+
</button>
58+
);
59+
60+
const svg = container.querySelector('svg');
61+
if (svg) fireEvent.click(svg);
62+
63+
expect(handleClick).toHaveBeenCalledTimes(1);
64+
});
65+
66+
it('allows mouse event propagation when CloseIcon is clicked', () => {
67+
const handleClick = vi.fn();
68+
const { container } = render(
69+
<button onClick={handleClick}>
70+
<CloseIcon />
71+
</button>
72+
);
73+
74+
const svg = container.querySelector('svg');
75+
if (svg) fireEvent.click(svg);
76+
77+
expect(handleClick).toHaveBeenCalledTimes(1);
78+
});
79+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { render } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { BoxIcon, CheckIcon, CloseIcon, CopyIcon, ZapIcon } from './Icons';
5+
6+
describe('Icons theme contrast behavior', () => {
7+
it('renders CheckIcon with fixed semantic color for success state', () => {
8+
const { container } = render(<CheckIcon />);
9+
const svg = container.querySelector('svg');
10+
// Verifies the icon maintains visual cohesion by using a fixed color
11+
// that works well in both light and dark modes
12+
expect(svg?.getAttribute('stroke')).toBe('#10b981');
13+
});
14+
15+
it('renders CopyIcon with inherited color for theme adaptability', () => {
16+
const { container } = render(<CopyIcon />);
17+
const svg = container.querySelector('svg');
18+
// Verifies the icon uses currentColor to adapt to parent theme context
19+
expect(svg?.getAttribute('stroke')).toBe('currentColor');
20+
});
21+
22+
it('renders ZapIcon with inherited color for theme adaptability', () => {
23+
const { container } = render(<ZapIcon />);
24+
const svg = container.querySelector('svg');
25+
expect(svg?.getAttribute('stroke')).toBe('currentColor');
26+
});
27+
28+
it('renders BoxIcon with inherited color for theme adaptability', () => {
29+
const { container } = render(<BoxIcon />);
30+
const svg = container.querySelector('svg');
31+
expect(svg?.getAttribute('stroke')).toBe('currentColor');
32+
});
33+
34+
it('renders CloseIcon with inherited color for theme adaptability', () => {
35+
const { container } = render(<CloseIcon />);
36+
const svg = container.querySelector('svg');
37+
expect(svg?.getAttribute('stroke')).toBe('currentColor');
38+
});
39+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expectTypeOf, it } from 'vitest';
2+
import { CopyIcon, ZapIcon, BoxIcon, CheckIcon, CloseIcon } from './Icons';
3+
4+
describe('Icons Type Compiler Validation', () => {
5+
it('exports icon components as functions', () => {
6+
expectTypeOf(CopyIcon).toBeFunction();
7+
expectTypeOf(ZapIcon).toBeFunction();
8+
expectTypeOf(BoxIcon).toBeFunction();
9+
expectTypeOf(CheckIcon).toBeFunction();
10+
expectTypeOf(CloseIcon).toBeFunction();
11+
});
12+
13+
it('accepts no parameters for icon components', () => {
14+
expectTypeOf(CopyIcon).parameters.toEqualTypeOf<[]>();
15+
expectTypeOf(ZapIcon).parameters.toEqualTypeOf<[]>();
16+
expectTypeOf(BoxIcon).parameters.toEqualTypeOf<[]>();
17+
expectTypeOf(CheckIcon).parameters.toEqualTypeOf<[]>();
18+
expectTypeOf(CloseIcon).parameters.toEqualTypeOf<[]>();
19+
});
20+
21+
it('all icon components return compatible types', () => {
22+
expectTypeOf<ReturnType<typeof CopyIcon>>().toEqualTypeOf<ReturnType<typeof ZapIcon>>();
23+
24+
expectTypeOf<ReturnType<typeof ZapIcon>>().toEqualTypeOf<ReturnType<typeof BoxIcon>>();
25+
26+
expectTypeOf<ReturnType<typeof BoxIcon>>().toEqualTypeOf<ReturnType<typeof CheckIcon>>();
27+
28+
expectTypeOf<ReturnType<typeof CheckIcon>>().toEqualTypeOf<ReturnType<typeof CloseIcon>>();
29+
});
30+
31+
it('maintains compatible return types across icons', () => {
32+
expectTypeOf<ReturnType<typeof CopyIcon>>().toEqualTypeOf<ReturnType<typeof ZapIcon>>();
33+
34+
expectTypeOf<ReturnType<typeof BoxIcon>>().toEqualTypeOf<ReturnType<typeof CheckIcon>>();
35+
36+
expectTypeOf<ReturnType<typeof CheckIcon>>().toEqualTypeOf<ReturnType<typeof CloseIcon>>();
37+
});
38+
39+
it('all icon components share the same callable signature', () => {
40+
expectTypeOf(CopyIcon).toEqualTypeOf<typeof ZapIcon>();
41+
expectTypeOf(ZapIcon).toEqualTypeOf<typeof BoxIcon>();
42+
expectTypeOf(BoxIcon).toEqualTypeOf<typeof CheckIcon>();
43+
expectTypeOf(CheckIcon).toEqualTypeOf<typeof CloseIcon>();
44+
});
45+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { render, cleanup } from '@testing-library/react';
2+
import '@testing-library/jest-dom/vitest';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
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', () => {
13+
const scrollToMock = vi.fn();
14+
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
15+
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
16+
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
20+
mockUsePathname.mockReturnValue('/dashboard');
21+
22+
Object.defineProperty(window, 'scrollTo', {
23+
writable: true,
24+
value: scrollToMock,
25+
});
26+
27+
sessionStorage.clear();
28+
});
29+
30+
afterEach(() => {
31+
cleanup();
32+
});
33+
34+
it('mounts cleanly without crashing', () => {
35+
const { container } = render(<ScrollRestoration />);
36+
37+
expect(container).toBeDefined();
38+
});
39+
40+
it('restores saved scroll position on mount', () => {
41+
sessionStorage.setItem('scroll-position-/dashboard', '250');
42+
43+
render(<ScrollRestoration />);
44+
45+
expect(scrollToMock).toHaveBeenCalledWith(0, 250);
46+
});
47+
48+
it('registers scroll listener and saves scroll position', () => {
49+
render(<ScrollRestoration />);
50+
51+
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
52+
53+
Object.defineProperty(window, 'scrollY', {
54+
writable: true,
55+
configurable: true,
56+
value: 500,
57+
});
58+
59+
const handler = addEventListenerSpy.mock.calls.find(
60+
([event]) => event === 'scroll'
61+
)?.[1] as EventListener;
62+
63+
handler(new Event('scroll'));
64+
65+
expect(sessionStorage.getItem('scroll-position-/dashboard')).toBe('500');
66+
});
67+
68+
it('does not scroll when no saved position exists', () => {
69+
render(<ScrollRestoration />);
70+
71+
expect(scrollToMock).not.toHaveBeenCalled();
72+
});
73+
74+
it('removes scroll listener on unmount', () => {
75+
const { unmount } = render(<ScrollRestoration />);
76+
77+
const handler = addEventListenerSpy.mock.calls.find(([event]) => event === 'scroll')?.[1];
78+
79+
unmount();
80+
81+
expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', handler);
82+
});
83+
});

app/contributors/loading.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { render, screen } from '@testing-library/react';
2+
import '@testing-library/jest-dom/vitest';
3+
import { describe, it, expect } from 'vitest';
4+
import Loading from './loading';
5+
6+
describe('Contributors Loading', () => {
7+
it('renders loading fallback component', () => {
8+
render(<Loading />);
9+
10+
expect(screen.getByRole('status')).toBeInTheDocument();
11+
});
12+
13+
it('renders loading messages correctly', () => {
14+
render(<Loading />);
15+
16+
expect(screen.getByText(/loading the collective/i)).toBeInTheDocument();
17+
18+
expect(screen.getByText(/fetching contributor data from github/i)).toBeInTheDocument();
19+
});
20+
21+
it('contains animated loader elements', () => {
22+
const { container } = render(<Loading />);
23+
24+
expect(container.querySelector('.animate-spin')).toBeInTheDocument();
25+
26+
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
27+
});
28+
29+
it('maintains centered flex layout and themed backdrop', () => {
30+
const { container } = render(<Loading />);
31+
32+
const wrapper = container.firstChild as HTMLElement;
33+
34+
expect(wrapper).toHaveClass('flex');
35+
expect(wrapper).toHaveClass('min-h-screen');
36+
expect(wrapper).toHaveClass('items-center');
37+
expect(wrapper).toHaveClass('justify-center');
38+
expect(wrapper).toHaveClass('bg-[#050505]');
39+
});
40+
41+
it('renders loader container with accessibility attributes', () => {
42+
render(<Loading />);
43+
44+
const status = screen.getByRole('status');
45+
46+
expect(status).toHaveAttribute('aria-live', 'polite');
47+
});
48+
});

0 commit comments

Comments
 (0)