Skip to content

Commit 8ac847a

Browse files
test(template): verify responsive multi-device columns and mobile layouts
1 parent e3369ea commit 8ac847a

1 file changed

Lines changed: 173 additions & 0 deletions

File tree

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)