Files
TREK/client/src/native/windowFold.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

86 lines
3.0 KiB
TypeScript

// FE-NATIVE-FOLD-001 onwards: the fold an Android foldable reports, as two-pane screens use it.
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { NativeWindowLayout } from './trekShell';
import { _resetWindowFold, foldOf, useWindowFold } from './windowFold';
const shell = vi.hoisted(() => ({
features: [] as string[],
layout: null as unknown,
listener: null as null | ((layout: unknown) => void),
}));
vi.mock('./capabilities', () => ({
hasNativeFeature: (feature: string) => shell.features.includes(feature),
useNativeCapabilities: () => ({
caps: { shellApi: 3, platform: 'android', appVersion: '', build: '', features: shell.features },
updateRequired: false,
}),
}));
vi.mock('./trekShell', () => ({
TrekShell: {
getWindowLayout: vi.fn(() => Promise.resolve(shell.layout)),
addListener: vi.fn((_event: string, fn: (layout: unknown) => void) => {
shell.listener = fn;
return Promise.resolve({ remove: () => Promise.resolve() });
}),
},
}));
function layout(over: Partial<NativeWindowLayout> = {}): NativeWindowLayout {
return {
width: 841,
height: 701,
widthClass: 'expanded',
heightClass: 'medium',
posture: 'halfOpened',
orientation: 'vertical',
separating: true,
hinge: { left: 420, top: 0, right: 421, bottom: 701 },
...over,
};
}
beforeEach(() => {
_resetWindowFold();
shell.features = [];
shell.layout = null;
shell.listener = null;
});
afterEach(() => _resetWindowFold());
describe('foldOf', () => {
it('FE-NATIVE-FOLD-001: a book posture splits the window at its hinge', () => {
expect(foldOf(layout())).toEqual({ start: 420, end: 421 });
});
it('FE-NATIVE-FOLD-002: a seamless flat fold, a horizontal one or none at all splits nothing', () => {
expect(foldOf(layout({ posture: 'flat', separating: false }))).toBeNull();
expect(foldOf(layout({ orientation: 'horizontal' }))).toBeNull();
expect(foldOf(layout({ posture: 'none', hinge: undefined }))).toBeNull();
expect(foldOf(null)).toBeNull();
// A hinge outside the window is not one to lay panes against.
expect(foldOf(layout({ hinge: { left: 0, top: 0, right: 0, bottom: 701 } }))).toBeNull();
});
});
describe('useWindowFold', () => {
it('FE-NATIVE-FOLD-003: without the shell feature there is no fold to follow', () => {
const { result } = renderHook(() => useWindowFold());
expect(result.current).toBeNull();
expect(shell.listener).toBeNull();
});
it('FE-NATIVE-FOLD-004: follows the layout the Android shell reports, as the device folds and unfolds', async () => {
shell.features = ['windowLayout'];
shell.layout = layout();
const { result } = renderHook(() => useWindowFold());
await waitFor(() => expect(result.current).toEqual({ start: 420, end: 421 }));
act(() => shell.listener?.(layout({ posture: 'flat', separating: false })));
expect(result.current).toBeNull();
});
});