pcbjam/web/standalone/src/lib/mobile-mode.test.ts
Istvan Matejcsok dc7c60f723 feat(mobile): canvas-only mobile mode — pinch-zoom/pan/tap gestures + chrome-less editors
On a mobile device (or ?mobile=1) the editors run canvas-only with touch
gestures driving the view:

- touch-gestures.ts: pure recognizer (unit-tested) + DOM shim installed in
  boot preRun — one-finger drag → synthetic middle-drag (pan), pinch →
  synthetic wheel at the centroid (zoom-to-cursor), tap → left click.
  preRun registration order is what lets stopImmediatePropagation suppress
  the wx layer's single-finger→LEFT-drag touch mapping.
- kicadSetChrome(bool) embind: hides all AUI panes except DrawFrame + the
  menubar/status bar via generic wx APIs (kicad fork untouched); boot polls
  it after runtime init. Pairs with the wxwidgets IsShown layout fix.
- mobile-mode.ts: ?mobile=1/0 override or UA-CH/coarse-pointer autodetect;
  shell hides its overlays and the inherent-to-mobile preflight warnings.
- e2e: mobile-chromium project (Pixel 7) + 4 specs (chrome-less, tap,
  pinch, pan) with screenshot-invertibility assertions; also fixes
  tool-switch.spec's stale pre-scope-refactor URLs (was broken on main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:15:37 +02:00

57 lines
2.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { isMobileMode, type MobileModeWindow } from "./mobile-mode";
/**
* Mobile-mode resolution (features/mobile): the explicit `?mobile=` URL param
* always wins; otherwise fall back to device detection (UA-CH mobile flag, or
* coarse pointer + narrow viewport — the same signals capabilities.ts warns on).
*/
function fakeWin(opts: {
search?: string;
uaMobile?: boolean;
coarse?: boolean;
narrow?: boolean;
noMatchMedia?: boolean;
}): MobileModeWindow {
return {
location: { search: opts.search ?? "" },
navigator: { userAgentData: opts.uaMobile === undefined ? undefined : { mobile: opts.uaMobile } },
matchMedia: opts.noMatchMedia
? undefined
: (query: string) => ({
matches: query.includes("pointer") ? (opts.coarse ?? false) : (opts.narrow ?? false),
}),
};
}
describe("isMobileMode", () => {
it("?mobile=1 forces mobile mode on a desktop device", () => {
expect(isMobileMode(fakeWin({ search: "?mobile=1" }))).toBe(true);
expect(isMobileMode(fakeWin({ search: "?foo=bar&mobile=true" }))).toBe(true);
});
it("?mobile=0 forces desktop mode on a mobile device", () => {
expect(
isMobileMode(fakeWin({ search: "?mobile=0", uaMobile: true, coarse: true, narrow: true })),
).toBe(false);
expect(isMobileMode(fakeWin({ search: "?mobile=false", uaMobile: true }))).toBe(false);
});
it("auto-detects via userAgentData.mobile", () => {
expect(isMobileMode(fakeWin({ uaMobile: true }))).toBe(true);
expect(isMobileMode(fakeWin({ uaMobile: false }))).toBe(false);
});
it("auto-detects via coarse pointer + narrow viewport", () => {
expect(isMobileMode(fakeWin({ coarse: true, narrow: true }))).toBe(true);
// a touch-screen desktop (coarse but wide) is NOT mobile
expect(isMobileMode(fakeWin({ coarse: true, narrow: false }))).toBe(false);
expect(isMobileMode(fakeWin({ coarse: false, narrow: true }))).toBe(false);
});
it("defaults to desktop when nothing is detectable", () => {
expect(isMobileMode(fakeWin({}))).toBe(false);
expect(isMobileMode(fakeWin({ noMatchMedia: true }))).toBe(false);
});
});