Files
TREK/client/tests/unit/mobile/sizeClass.test.ts
T
Maurice 6906f80ff8 feat(native): Android tablets get the shortcuts, two panes meet at a foldable's fold
The Android shell now reports keyCommands and windowLayout, so the large
screen layout no longer assumes an iPad:

- The keyboard host hands the shortcuts to the app whenever the shell has
  the keyCommands feature, whatever the platform. Before it also required
  iOS, so an Android tablet with a keyboard answered the keys in the page
  and never listed them in the system's shortcuts.
- useWindowFold follows the layout the Android shell reports (feature
  windowLayout). When a vertical fold separates the window, a foldable half
  open like a book, MobileShell marks it with data-fold and its place, and
  the leading pane (the trip's plan, a tab's summary, a list beside its
  detail) ends at the fold while the trailing one starts there, on either
  reading direction. A flat fold that does not separate stays one screen.
- The split size class starts at 840pt, Android's expanded width, instead
  of 900pt, so a foldable unfolded on its side (about 841dp) gets two panes
  like an iPad does. An 11" iPad upright (834pt) stays a single pane. The
  address screen of the native shell follows the same width.
- TrekShell exports NativeWindowLayout for it.

Also: the sheet's width and anchor types and the collab board type stay
private to their modules, which nothing else imports (knip).
2026-11-07 23:25:13 +01:00

68 lines
3.0 KiB
TypeScript

import { renderHook } from '@testing-library/react';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { SPLIT_QUERY, WIDE_QUERY, isWideViewport, useIsSplit, useIsWide } from '../../../src/hooks/sizeClass';
import { setSizeClass } from '../../helpers/sizeClass';
// FE-MOB-SIZE-001 onwards
const root = resolve(__dirname, '../../..');
describe('size classes', () => {
it('FE-MOB-SIZE-001: the Tailwind variants and the stylesheet use the same queries as the hooks', () => {
const tailwind = readFileSync(resolve(root, 'tailwind.config.js'), 'utf8');
expect(tailwind).toContain(`addVariant('wide', '@media ${WIDE_QUERY}')`);
expect(tailwind).toContain(`addVariant('split', '@media ${SPLIT_QUERY}')`);
const css = readFileSync(resolve(root, 'src/mobile/mobile.css'), 'utf8');
const queries = [...css.matchAll(/@media ([^{]*min-height: 501px[^{]*)\{/g)].map((m) => m[1].trim());
expect(queries.length).toBeGreaterThan(0);
for (const q of queries) expect(q).toBe(WIDE_QUERY);
});
it('FE-MOB-SIZE-002: a phone on its side stays compact, a tablet and an unfolded foldable do not', () => {
// The queries evaluated by hand for the sizes that matter: jsdom has no height feature.
const evaluate = (query: string, w: number, h: number) => {
const minW = Number(/min-width: (\d+)px/.exec(query)?.[1]);
const minH = Number(/min-height: (\d+)px/.exec(query)?.[1]);
return w >= minW && h >= minH;
};
const sizes: Record<string, [number, number, boolean, boolean]> = {
phone: [402, 874, false, false],
phoneLandscape: [956, 440, false, false],
ipadMiniPortrait: [744, 1133, true, false],
ipad13Landscape: [1376, 1032, true, true],
ipad11SplitHalf: [590, 834, false, false],
ipad13SplitHalf: [683, 1032, true, false],
duoUnfoldedLandscape: [1017, 699, true, true],
duoUnfoldedPortrait: [699, 1017, true, false],
ipad11Portrait: [834, 1194, true, false],
androidFoldUnfoldedLandscape: [841, 701, true, true],
androidFoldUnfoldedPortrait: [701, 841, true, false],
};
for (const [name, [w, h, wide, split]] of Object.entries(sizes)) {
expect([name, evaluate(WIDE_QUERY, w, h)]).toEqual([name, wide]);
expect([name, evaluate(SPLIT_QUERY, w, h)]).toEqual([name, split]);
}
});
it('FE-MOB-SIZE-003: the hooks follow a change of size class, as on a rotation or a fold', () => {
const wide = renderHook(() => useIsWide());
const split = renderHook(() => useIsSplit());
expect(wide.result.current).toBe(false);
expect(split.result.current).toBe(false);
setSizeClass('wide');
expect(wide.result.current).toBe(true);
expect(split.result.current).toBe(false);
expect(isWideViewport()).toBe(true);
setSizeClass('split');
expect(split.result.current).toBe(true);
setSizeClass('compact');
expect(wide.result.current).toBe(false);
expect(split.result.current).toBe(false);
});
});