@@ -5,10 +5,22 @@ import { PathnameContext } from 'next/dist/shared/lib/hooks-client-context.share
55import { Header } from '@/components/header/header' ;
66import { ThemeProvider } from '@/components/ui/theme-provider' ;
77
8+ // Mounted outside the Next app shell; next-style-loader inserts the global
9+ // stylesheet before this anchor, so it must exist before the import below.
10+ const cssAnchor = document . createElement ( 'noscript' ) ;
11+ cssAnchor . id = '__next_css__DO_NOT_USE__' ;
12+ document . head . append ( cssAnchor ) ;
13+ require ( '@/app/globals.css' ) ;
14+
815const queryClient = new QueryClient ( {
916 defaultOptions : { queries : { retry : false } } ,
1017} ) ;
1118
19+ /** Minimum touch target, per WCAG 2.5.8 and the `size-11` / `min-h-11` utilities. */
20+ const MIN_TOUCH_PX = 44 ;
21+ /** Tolerance for sub-pixel layout rounding. */
22+ const EPSILON = 0.5 ;
23+
1224function createMockRouter ( ) {
1325 return {
1426 push : cy . stub ( ) ,
@@ -20,6 +32,10 @@ function createMockRouter() {
2032 } ;
2133}
2234
35+ function rectOf ( selector : string ) {
36+ return cy . get ( selector ) . then ( ( $el ) => $el [ 0 ] . getBoundingClientRect ( ) ) ;
37+ }
38+
2339describe ( 'Header' , ( ) => {
2440 beforeEach ( ( ) => {
2541 const mockRouter = createMockRouter ( ) ;
@@ -84,4 +100,87 @@ describe('Header', () => {
84100 cy . contains ( 'a' , 'Supporters' ) . should ( 'be.visible' ) . and ( 'have.attr' , 'href' , '/quotes' ) ;
85101 } ) ;
86102 } ) ;
103+
104+ describe ( 'at 320x700' , ( ) => {
105+ beforeEach ( ( ) => {
106+ cy . viewport ( 320 , 700 ) ;
107+ } ) ;
108+
109+ it ( 'hides the GitHub star control' , ( ) => {
110+ cy . get ( '[data-testid="header-star-button"]' ) . should ( 'not.be.visible' ) ;
111+ } ) ;
112+
113+ it ( 'keeps the remaining controls inside the header bounds' , ( ) => {
114+ cy . get ( '[data-testid="header"]' ) . then ( ( $header ) => {
115+ const bounds = $header [ 0 ] . getBoundingClientRect ( ) ;
116+ const selectors = [
117+ '[data-testid="header-brand"]' ,
118+ '[data-testid="language-toggle"]' ,
119+ '[data-testid="theme-toggle"]' ,
120+ '[data-testid="mobile-menu-toggle"]' ,
121+ ] ;
122+ selectors . forEach ( ( selector ) => {
123+ rectOf ( selector ) . then ( ( rect ) => {
124+ expect ( rect . left , `${ selector } left edge` ) . to . be . at . least ( bounds . left - EPSILON ) ;
125+ expect ( rect . right , `${ selector } right edge` ) . to . be . at . most ( bounds . right + EPSILON ) ;
126+ } ) ;
127+ } ) ;
128+ } ) ;
129+ } ) ;
130+
131+ it ( 'gives the brand and language controls a 44px touch height' , ( ) => {
132+ [ '[data-testid="header-brand"]' , '[data-testid="language-toggle"]' ] . forEach ( ( selector ) => {
133+ rectOf ( selector ) . then ( ( rect ) => {
134+ expect ( rect . height , `${ selector } height` ) . to . be . at . least ( MIN_TOUCH_PX - EPSILON ) ;
135+ } ) ;
136+ } ) ;
137+ } ) ;
138+
139+ it ( 'gives the icon controls a 44px touch target in both dimensions' , ( ) => {
140+ [ '[data-testid="theme-toggle"]' , '[data-testid="mobile-menu-toggle"]' ] . forEach ( ( selector ) => {
141+ rectOf ( selector ) . then ( ( rect ) => {
142+ expect ( rect . width , `${ selector } width` ) . to . be . at . least ( MIN_TOUCH_PX - EPSILON ) ;
143+ expect ( rect . height , `${ selector } height` ) . to . be . at . least ( MIN_TOUCH_PX - EPSILON ) ;
144+ } ) ;
145+ } ) ;
146+ } ) ;
147+
148+ it ( 'does not overflow horizontally' , ( ) => {
149+ cy . get ( '[data-testid="header"]' ) . then ( ( $header ) => {
150+ const header = $header [ 0 ] ;
151+ expect ( header . scrollWidth , 'header scrollWidth' ) . to . be . at . most ( header . clientWidth ) ;
152+ } ) ;
153+ } ) ;
154+
155+ it ( 'still opens the menu and exposes its links' , ( ) => {
156+ cy . get ( '[data-testid="mobile-menu-toggle"]' ) . click ( ) ;
157+ cy . get ( '[data-testid="mobile-menu"]' ) . should ( 'be.visible' ) ;
158+ cy . get ( '[data-testid="mobile-menu"]' ) . within ( ( ) => {
159+ [ 'Home' , 'Dashboard' , 'Comparisons' , 'Supporters' , 'Datasets' , 'Articles' , 'About' ] . forEach (
160+ ( label ) => {
161+ cy . contains ( 'a' , label ) . should ( 'be.visible' ) ;
162+ } ,
163+ ) ;
164+ } ) ;
165+ cy . get ( '[data-testid="mobile-menu"] a' ) . each ( ( $link ) => {
166+ const rect = $link [ 0 ] . getBoundingClientRect ( ) ;
167+ expect ( rect . height , `${ $link . text ( ) } link height` ) . to . be . at . least ( MIN_TOUCH_PX - EPSILON ) ;
168+ } ) ;
169+ } ) ;
170+
171+ it ( 'exposes the minecraft audio toggles in the mobile menu without overflowing' , ( ) => {
172+ cy . get ( '[data-testid="theme-toggle"]' ) . click ( ) ;
173+ cy . get ( '[data-testid="theme-toggle"]' ) . click ( ) ;
174+ cy . get ( 'html' ) . should ( 'have.class' , 'minecraft' ) ;
175+ cy . get ( '[data-testid="mobile-menu-toggle"]' ) . click ( ) ;
176+ cy . get ( '[data-testid="mobile-menu"]' ) . within ( ( ) => {
177+ cy . get ( 'button[aria-label="Mute music"]' ) . should ( 'be.visible' ) ;
178+ cy . get ( 'button[aria-label="Mute click sounds"]' ) . should ( 'be.visible' ) ;
179+ } ) ;
180+ cy . get ( '[data-testid="header"]' ) . then ( ( $header ) => {
181+ const header = $header [ 0 ] ;
182+ expect ( header . scrollWidth , 'header scrollWidth' ) . to . be . at . most ( header . clientWidth ) ;
183+ } ) ;
184+ } ) ;
185+ } ) ;
87186} ) ;
0 commit comments