Skip to content
Open
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Deliberately compact test fixture (one benchmark row per line, <=60 lines);
# oxfmt/prettier would pretty-print it to ~530 lines. See overview-data.test.ts drift guard.
packages/app/cypress/fixtures/api/overview-rows.json
101 changes: 101 additions & 0 deletions packages/app/cypress/component/header.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,24 @@ import { PathnameContext } from 'next/dist/shared/lib/hooks-client-context.share
import { Header } from '@/components/header/header';
import { ThemeProvider } from '@/components/ui/theme-provider';

// Component specs mount outside the Next app shell, so the global stylesheet is
// never injected. The layout assertions below need real Tailwind output, and
// next-style-loader inserts before this anchor node, so both must exist before
// the stylesheet module evaluates.
const cssAnchor = document.createElement('noscript');
cssAnchor.id = '__next_css__DO_NOT_USE__';
document.head.append(cssAnchor);
require('@/app/globals.css');

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});

/** Minimum touch target, per WCAG 2.5.8 and the `size-11` / `min-h-11` utilities. */
const MIN_TOUCH_PX = 44;
/** Tolerance for sub-pixel layout rounding. */
const EPSILON = 0.5;

function createMockRouter() {
return {
push: cy.stub(),
Expand All @@ -20,6 +34,10 @@ function createMockRouter() {
};
}

function rectOf(selector: string) {
return cy.get(selector).then(($el) => $el[0].getBoundingClientRect());
}

describe('Header', () => {
beforeEach(() => {
const mockRouter = createMockRouter();
Expand Down Expand Up @@ -84,4 +102,87 @@ describe('Header', () => {
cy.contains('a', 'Supporters').should('be.visible').and('have.attr', 'href', '/quotes');
});
});

describe('at 320x700', () => {
beforeEach(() => {
cy.viewport(320, 700);
});

it('hides the GitHub star control', () => {
cy.get('[data-testid="header-star-button"]').should('not.be.visible');
});

it('keeps the remaining controls inside the header bounds', () => {
cy.get('[data-testid="header"]').then(($header) => {
const bounds = $header[0].getBoundingClientRect();
const selectors = [
'[data-testid="header-brand"]',
'[data-testid="language-toggle"]',
'[data-testid="theme-toggle"]',
'[data-testid="mobile-menu-toggle"]',
];
selectors.forEach((selector) => {
rectOf(selector).then((rect) => {
expect(rect.left, `${selector} left edge`).to.be.at.least(bounds.left - EPSILON);
expect(rect.right, `${selector} right edge`).to.be.at.most(bounds.right + EPSILON);
});
});
});
});

it('gives the brand and language controls a 44px touch height', () => {
['[data-testid="header-brand"]', '[data-testid="language-toggle"]'].forEach((selector) => {
rectOf(selector).then((rect) => {
expect(rect.height, `${selector} height`).to.be.at.least(MIN_TOUCH_PX - EPSILON);
});
});
});

it('gives the icon controls a 44px touch target in both dimensions', () => {
['[data-testid="theme-toggle"]', '[data-testid="mobile-menu-toggle"]'].forEach((selector) => {
rectOf(selector).then((rect) => {
expect(rect.width, `${selector} width`).to.be.at.least(MIN_TOUCH_PX - EPSILON);
expect(rect.height, `${selector} height`).to.be.at.least(MIN_TOUCH_PX - EPSILON);
});
});
});

it('does not overflow horizontally', () => {
cy.get('[data-testid="header"]').then(($header) => {
const header = $header[0];
expect(header.scrollWidth, 'header scrollWidth').to.be.at.most(header.clientWidth);
});
});

it('still opens the menu and exposes its links', () => {
cy.get('[data-testid="mobile-menu-toggle"]').click();
cy.get('[data-testid="mobile-menu"]').should('be.visible');
cy.get('[data-testid="mobile-menu"]').within(() => {
['Home', 'Dashboard', 'Comparisons', 'Supporters', 'Datasets', 'Articles', 'About'].forEach(
(label) => {
cy.contains('a', label).should('be.visible');
},
);
});
cy.get('[data-testid="mobile-menu"] a').each(($link) => {
const rect = $link[0].getBoundingClientRect();
expect(rect.height, `${$link.text()} link height`).to.be.at.least(MIN_TOUCH_PX - EPSILON);
});
});

it('exposes the minecraft audio toggles in the mobile menu without overflowing', () => {
cy.get('[data-testid="theme-toggle"]').click();
cy.get('[data-testid="theme-toggle"]').click();
cy.get('html').should('have.class', 'minecraft');
cy.get('[data-testid="mobile-menu-toggle"]').click();
cy.get('[data-testid="mobile-menu"]').within(() => {
cy.get('button[aria-label="Mute music"]').should('be.visible');
cy.get('button[aria-label="Mute click sounds"]').should('be.visible');
});
cy.get('[data-testid="header"]').then(($header) => {
const header = $header[0];
expect(header.scrollWidth, 'header scrollWidth').to.be.at.most(header.clientWidth);
});
});
});
});
Loading
Loading