Skip to content

Commit d908c86

Browse files
Merge branch 'main' into fix/og-bypass-cache
2 parents 1386c0f + b9202b0 commit d908c86

26 files changed

Lines changed: 2156 additions & 24 deletions
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { render, screen } from '@testing-library/react';
2+
import '@testing-library/jest-dom/vitest';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import CompareClient from './CompareClient';
5+
import React, { type ReactNode } from 'react';
6+
7+
vi.mock('next/navigation', () => ({
8+
useRouter: () => ({
9+
replace: vi.fn(),
10+
}),
11+
useSearchParams: () => ({
12+
get: vi.fn(() => null),
13+
}),
14+
}));
15+
16+
vi.mock('framer-motion', () => ({
17+
motion: new Proxy(
18+
{},
19+
{
20+
get: (_, tag) => {
21+
return ({ children, ...props }: { children?: ReactNode; [key: string]: unknown }) =>
22+
React.createElement(tag as string, props, children);
23+
},
24+
}
25+
),
26+
AnimatePresence: ({ children }: { children?: ReactNode }) => <>{children}</>,
27+
}));
28+
29+
vi.mock('recharts', () => ({
30+
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
31+
RadarChart: () => <div data-testid="radar-chart" />,
32+
Radar: () => <div />,
33+
PolarGrid: () => <div />,
34+
PolarAngleAxis: () => <div />,
35+
PolarRadiusAxis: () => <div />,
36+
Tooltip: () => <div />,
37+
}));
38+
39+
describe('ComparePage Theme Contrast (Variation 3)', () => {
40+
it('renders heading with dark and light text contrast classes', () => {
41+
render(<CompareClient />);
42+
43+
const heading = screen.getByRole('heading', {
44+
name: /compare developers/i,
45+
});
46+
47+
expect(heading).toHaveClass('text-gray-900');
48+
expect(heading).toHaveClass('dark:text-white');
49+
});
50+
51+
it('renders username input fields with dark and light theme styling', () => {
52+
render(<CompareClient />);
53+
54+
const user1 = screen.getByPlaceholderText(/github username #1/i);
55+
const user2 = screen.getByPlaceholderText(/github username #2/i);
56+
57+
expect(user1).toHaveClass('bg-white');
58+
expect(user1).toHaveClass('dark:bg-[#0a0a0a]');
59+
expect(user1).toHaveClass('text-gray-900');
60+
expect(user1).toHaveClass('dark:text-white');
61+
62+
expect(user2).toHaveClass('bg-white');
63+
expect(user2).toHaveClass('dark:bg-[#0a0a0a]');
64+
expect(user2).toHaveClass('text-gray-900');
65+
expect(user2).toHaveClass('dark:text-white');
66+
});
67+
68+
it('renders compare button with proper contrast classes for both themes', () => {
69+
render(<CompareClient />);
70+
71+
const compareButton = screen.getByRole('button', {
72+
name: /compare/i,
73+
});
74+
75+
expect(compareButton).toHaveClass('bg-black');
76+
expect(compareButton).toHaveClass('dark:bg-white');
77+
expect(compareButton).toHaveClass('text-white');
78+
expect(compareButton).toHaveClass('dark:text-black');
79+
});
80+
81+
it('applies border contrast styling to form controls', () => {
82+
render(<CompareClient />);
83+
84+
const user1 = screen.getByPlaceholderText(/github username #1/i);
85+
86+
expect(user1).toHaveClass('border-black/10');
87+
expect(user1).toHaveClass('dark:border-[rgba(255,255,255,0.1)]');
88+
});
89+
90+
it('renders decorative badge with theme-aware background styling', () => {
91+
render(<CompareClient />);
92+
93+
const badge = screen.getByText(/developer showdown/i);
94+
95+
const container = badge.parentElement;
96+
97+
expect(container).toHaveClass('bg-gray-100');
98+
expect(container).toHaveClass('dark:bg-[#111]');
99+
expect(container).toHaveClass('border-black/10');
100+
expect(container).toHaveClass('dark:border-[rgba(255,255,255,0.1)]');
101+
});
102+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { render } from '@testing-library/react';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
import { BoxIcon, CheckIcon, CloseIcon, CopyIcon, ZapIcon } from './Icons';
4+
5+
afterEach(() => {
6+
vi.unstubAllGlobals();
7+
});
8+
9+
describe('Icons — responsive breakpoints', () => {
10+
it('icon dimensions are bounded values that will not cause horizontal overflow on a 375 px mobile viewport', () => {
11+
const { container: c1 } = render(<CopyIcon />);
12+
const { container: c2 } = render(<ZapIcon />);
13+
const { container: c3 } = render(<BoxIcon />);
14+
const { container: c4 } = render(<CheckIcon />);
15+
const { container: c5 } = render(<CloseIcon />);
16+
17+
const MOBILE_WIDTH = 375;
18+
for (const container of [c1, c2, c3, c4, c5]) {
19+
const svg = container.querySelector('svg');
20+
expect(svg).not.toBeNull();
21+
const widthAttr = svg!.getAttribute('width');
22+
expect(widthAttr).not.toBeNull();
23+
const width = Number(widthAttr);
24+
expect(Number.isFinite(width)).toBe(true);
25+
expect(width).toBeGreaterThan(0);
26+
expect(width).toBeLessThanOrEqual(MOBILE_WIDTH);
27+
}
28+
});
29+
30+
it('all icons use explicit numeric width and height attributes — no percentage or auto values that could cause layout instability', () => {
31+
const icons = [
32+
render(<CopyIcon />),
33+
render(<ZapIcon />),
34+
render(<BoxIcon />),
35+
render(<CheckIcon />),
36+
render(<CloseIcon />),
37+
];
38+
39+
for (const { container } of icons) {
40+
const svg = container.querySelector('svg');
41+
const width = svg?.getAttribute('width');
42+
const height = svg?.getAttribute('height');
43+
expect(Number(width)).toBeGreaterThan(0);
44+
expect(Number(height)).toBeGreaterThan(0);
45+
}
46+
});
47+
48+
it('CopyIcon and CheckIcon use compact 20 px sizing appropriate for inline/mobile use', () => {
49+
const { container: copyContainer } = render(<CopyIcon />);
50+
const { container: checkContainer } = render(<CheckIcon />);
51+
52+
expect(Number(copyContainer.querySelector('svg')?.getAttribute('width'))).toBe(20);
53+
expect(Number(copyContainer.querySelector('svg')?.getAttribute('height'))).toBe(20);
54+
expect(Number(checkContainer.querySelector('svg')?.getAttribute('width'))).toBe(20);
55+
expect(Number(checkContainer.querySelector('svg')?.getAttribute('height'))).toBe(20);
56+
});
57+
58+
it('CloseIcon uses a smaller 18 px footprint suitable for dismissal controls on narrow viewports', () => {
59+
const { container } = render(<CloseIcon />);
60+
const svg = container.querySelector('svg');
61+
62+
expect(Number(svg?.getAttribute('width'))).toBe(18);
63+
expect(Number(svg?.getAttribute('height'))).toBe(18);
64+
});
65+
66+
it('ZapIcon and BoxIcon use 24 px sizing that stays within standard mobile touch-target bounds', () => {
67+
const { container: zapContainer } = render(<ZapIcon />);
68+
const { container: boxContainer } = render(<BoxIcon />);
69+
70+
const TOUCH_TARGET = 48;
71+
for (const container of [zapContainer, boxContainer]) {
72+
const width = Number(container.querySelector('svg')?.getAttribute('width'));
73+
const height = Number(container.querySelector('svg')?.getAttribute('height'));
74+
expect(width).toBe(24);
75+
expect(height).toBe(24);
76+
expect(width).toBeLessThanOrEqual(TOUCH_TARGET);
77+
expect(height).toBeLessThanOrEqual(TOUCH_TARGET);
78+
}
79+
});
80+
});
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import React, { forwardRef } from 'react';
2+
import { cleanup, render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { afterAll, beforeAll, afterEach, describe, expect, it, vi } from 'vitest';
5+
import Template from './template';
6+
7+
// Use forwardRef to ensure we properly handle components that expect refs,
8+
// preventing warnings or breaks when animation libraries are mocked out.
9+
type MotionDivProps = React.HTMLAttributes<HTMLDivElement> & {
10+
children?: React.ReactNode;
11+
};
12+
13+
vi.mock('framer-motion', () => {
14+
const MockDiv = forwardRef<HTMLDivElement, MotionDivProps>(({ children, ...props }, ref) => (
15+
<div ref={ref} {...props}>
16+
{children}
17+
</div>
18+
));
19+
MockDiv.displayName = 'MotionDiv';
20+
return {
21+
motion: {
22+
div: MockDiv,
23+
},
24+
};
25+
});
26+
27+
// Store original window globals so we can cleanly restore them between tests.
28+
const originalMatchMedia = window.matchMedia;
29+
const originalInnerWidth = window.innerWidth;
30+
31+
beforeAll(() => {
32+
// Override matchMedia to support dynamic queries based on the simulated window width.
33+
Object.defineProperty(window, 'matchMedia', {
34+
writable: true,
35+
configurable: true,
36+
value: vi.fn().mockImplementation((query) => {
37+
const match = query.match(/max-width:\s*(\d+)px/);
38+
let matches = false;
39+
if (match) {
40+
matches = window.innerWidth <= parseInt(match[1], 10);
41+
}
42+
return {
43+
matches,
44+
media: query,
45+
onchange: null,
46+
addListener: vi.fn(),
47+
removeListener: vi.fn(),
48+
addEventListener: vi.fn(),
49+
removeEventListener: vi.fn(),
50+
dispatchEvent: vi.fn(),
51+
};
52+
}),
53+
});
54+
});
55+
56+
afterAll(() => {
57+
Object.defineProperty(window, 'matchMedia', {
58+
writable: true,
59+
configurable: true,
60+
value: originalMatchMedia,
61+
});
62+
});
63+
64+
afterEach(() => {
65+
cleanup();
66+
vi.restoreAllMocks();
67+
Object.defineProperty(window, 'innerWidth', {
68+
writable: true,
69+
configurable: true,
70+
value: originalInnerWidth,
71+
});
72+
});
73+
74+
// A standard responsive fixture layout wrapped inside the Template
75+
function ResponsiveFixture() {
76+
const [menuOpen, setMenuOpen] = React.useState(false);
77+
78+
return (
79+
<Template>
80+
<div className="w-full max-w-full overflow-hidden" data-testid="app-container">
81+
<nav className="flex flex-wrap items-center justify-between p-4" data-testid="navigation">
82+
<div className="text-lg font-bold">Logo</div>
83+
84+
{/* Mobile Toggle State */}
85+
<button
86+
className="block p-2 md:hidden"
87+
data-testid="mobile-toggle"
88+
onClick={() => setMenuOpen(!menuOpen)}
89+
aria-expanded={menuOpen}
90+
>
91+
Menu
92+
</button>
93+
94+
{/* Nav Links */}
95+
<ul
96+
data-testid="nav-links"
97+
className={`${menuOpen ? 'flex' : 'hidden'} w-full flex-col md:flex md:w-auto md:flex-row`}
98+
>
99+
<li>Link 1</li>
100+
<li>Link 2</li>
101+
</ul>
102+
</nav>
103+
104+
{/* Columns that reflow on mobile */}
105+
<main className="flex w-full flex-col gap-4 md:flex-row" data-testid="content-columns">
106+
<section className="w-full flex-1" data-testid="column-1">
107+
Column 1
108+
</section>
109+
<section className="w-full flex-1" data-testid="column-2">
110+
Column 2
111+
</section>
112+
</main>
113+
</div>
114+
</Template>
115+
);
116+
}
117+
118+
describe('AppTemplate Responsive Breakpoints (Variation 7)', () => {
119+
// Since JSDOM doesn't natively compute CSS media queries to reflow layout,
120+
// we validate the "Responsive Class Contract" applied to our elements.
121+
it('Case 1: Verifies the presence of responsive flex-col and md:flex-row classes for column reflow', () => {
122+
render(<ResponsiveFixture />);
123+
const columnsContainer = screen.getByTestId('content-columns');
124+
125+
// Validates the component applies the correct responsive variants
126+
expect(columnsContainer.className).toContain('flex-col');
127+
expect(columnsContainer.className).toContain('md:flex-row');
128+
});
129+
130+
it('Case 2: Prevents absolute widths that cause horizontal scrollbars', () => {
131+
render(<ResponsiveFixture />);
132+
const container = screen.getByTestId('app-container');
133+
134+
// Expect responsive fluid classes, reject hardcoded pixel widths
135+
expect(container.className).toContain('w-full');
136+
expect(container.className).toContain('max-w-full');
137+
expect(container.className).not.toMatch(/w-\[\d+px\]/);
138+
});
139+
140+
it('Case 3: Scales navigation components gracefully on smaller viewports', () => {
141+
render(<ResponsiveFixture />);
142+
const nav = screen.getByTestId('navigation');
143+
144+
expect(nav.className).toContain('flex-wrap');
145+
expect(nav.className).toContain('justify-between');
146+
});
147+
148+
it('Case 4: Asserts mobile-specific toggle states respond cleanly', async () => {
149+
// using userEvent for realistic interaction simulation
150+
const user = userEvent.setup();
151+
render(<ResponsiveFixture />);
152+
const toggle = screen.getByTestId('mobile-toggle');
153+
const navLinks = screen.getByTestId('nav-links');
154+
155+
// Default mobile state (closed)
156+
expect(toggle.getAttribute('aria-expanded')).toBe('false');
157+
expect(navLinks.className).toContain('hidden');
158+
159+
// Toggle mobile menu (open)
160+
await user.click(toggle);
161+
expect(toggle.getAttribute('aria-expanded')).toBe('true');
162+
expect(navLinks.className).toContain('flex');
163+
expect(navLinks.className).not.toContain('hidden');
164+
});
165+
166+
it('Case 5: Verifies the desktop layout responsive variant is present', () => {
167+
render(<ResponsiveFixture />);
168+
const columnsContainer = screen.getByTestId('content-columns');
169+
170+
// Asserts that the desktop override class is available to the browser engine
171+
expect(columnsContainer.className).toContain('md:flex-row');
172+
});
173+
});

0 commit comments

Comments
 (0)