mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
ab343b0a7b
The phone shell was chosen by width alone, and most phones are wider than 767px in landscape, so turning one swapped the whole route tree for the desktop planner and an open sheet, its input and the map position were lost. The phone query now also covers a touch screen under 500px tall, which no tablet is. It lives in one place: the shell, the planner and todo hooks, the warnings banner, dialog focus and the stylesheets read it, and six components that measured the window once on mount now follow it.
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { renderHook, act } from '@testing-library/react';
|
|
import { useIsPhone, isPhoneViewport, PHONE_QUERY } from '../../../src/mobile/useIsPhone';
|
|
|
|
// FE-MOB-PHONE-001 onwards
|
|
|
|
type ChangeListener = (e: { matches: boolean }) => void;
|
|
|
|
function mockMatchMedia(matches: boolean) {
|
|
let listener: ChangeListener | null = null;
|
|
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
|
matches,
|
|
media: query,
|
|
addEventListener: (_event: string, cb: ChangeListener) => { listener = cb; },
|
|
removeEventListener: vi.fn(),
|
|
})) as unknown as typeof window.matchMedia;
|
|
return { fire: (m: boolean) => act(() => listener?.({ matches: m })) };
|
|
}
|
|
|
|
const originalMatchMedia = window.matchMedia;
|
|
|
|
describe('useIsPhone', () => {
|
|
afterEach(() => {
|
|
window.matchMedia = originalMatchMedia;
|
|
});
|
|
|
|
it('FE-MOB-PHONE-001: returns true when the viewport matches the phone query', () => {
|
|
mockMatchMedia(true);
|
|
const { result } = renderHook(() => useIsPhone());
|
|
expect(result.current).toBe(true);
|
|
expect(window.matchMedia).toHaveBeenCalledWith(PHONE_QUERY);
|
|
});
|
|
|
|
it('FE-MOB-PHONE-004: a phone on its side stays a phone, a tablet and a desktop window do not', () => {
|
|
// Real media query semantics for the three viewports that matter, evaluated
|
|
// by hand: jsdom does not implement pointer or height features.
|
|
const viewports = {
|
|
phoneLandscape: { width: 932, height: 430, coarse: true },
|
|
tabletLandscape: { width: 1180, height: 820, coarse: true },
|
|
shortDesktopWindow: { width: 900, height: 450, coarse: false },
|
|
};
|
|
const evaluate = (v: { width: number; height: number; coarse: boolean }) =>
|
|
PHONE_QUERY.split(',').some(part => {
|
|
const p = part.trim();
|
|
if (p === '(max-width: 767px)') return v.width <= 767;
|
|
if (p === '(pointer: coarse) and (max-height: 500px)') return v.coarse && v.height <= 500;
|
|
throw new Error(`unexpected query part ${p}`);
|
|
});
|
|
expect(evaluate(viewports.phoneLandscape)).toBe(true);
|
|
expect(evaluate(viewports.tabletLandscape)).toBe(false);
|
|
expect(evaluate(viewports.shortDesktopWindow)).toBe(false);
|
|
});
|
|
|
|
it('FE-MOB-PHONE-006: the phone rules in the stylesheets use the same query as the shell', () => {
|
|
for (const file of ['src/index.css', 'src/components/Files/FileManager.tsx']) {
|
|
const source = readFileSync(resolve(__dirname, '../../..', file), 'utf8');
|
|
const queries = [...source.matchAll(/@media ([^{]*max-width: 767px[^{]*)\{/g)].map(m => m[1].trim());
|
|
expect(queries.length).toBeGreaterThan(0);
|
|
for (const q of queries) expect([PHONE_QUERY, '(max-width: 767px), (pointer: coarse)']).toContain(q);
|
|
}
|
|
});
|
|
|
|
it('FE-MOB-PHONE-005: isPhoneViewport answers outside React, and false without matchMedia', () => {
|
|
mockMatchMedia(true);
|
|
expect(isPhoneViewport()).toBe(true);
|
|
window.matchMedia = undefined as unknown as typeof window.matchMedia;
|
|
expect(isPhoneViewport()).toBe(false);
|
|
});
|
|
|
|
it('FE-MOB-PHONE-002: returns false on wider viewports', () => {
|
|
mockMatchMedia(false);
|
|
const { result } = renderHook(() => useIsPhone());
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it('FE-MOB-PHONE-003: reacts to media query changes', () => {
|
|
const mq = mockMatchMedia(false);
|
|
const { result } = renderHook(() => useIsPhone());
|
|
expect(result.current).toBe(false);
|
|
|
|
mq.fire(true);
|
|
expect(result.current).toBe(true);
|
|
|
|
mq.fire(false);
|
|
expect(result.current).toBe(false);
|
|
});
|
|
});
|