Files
TREK/client/tests/unit/native/nativeShell.test.ts
T
Maurice 52f0fb882f feat(native): redesign the app's first screens around a living landscape
The screens the app ships itself (welcome, plans, address, own server and
the states between) move from the dark card layout to a light design on an
animated aerial landscape drawn by a WebGL shader (native-shell/scenery.ts):
an island, a coastal town, farmland and mountains in one world.

- Welcome: the landscape with the TREK wordmark, three perks in one card,
  "Explore cloud plans" and "I already have a TREK" as two buttons.
- Plans: one tab per plan; choosing one flies the camera to its place,
  further inland with every plan. Each plan reads as a kind of trip, the
  price sits in a card with a monthly/yearly switch and counts to its new
  amount in the store's own format (native-shell/amount.ts), and a yearly
  price shows what it makes a month. Restore purchases, the renewal terms
  and the links stay as the store rules require.
- Address: the address writes itself on a glass card over the landscape;
  free, the password rules and the agreement sit on white cards.
- Own server: the server address on the glass card; a failed check turns
  the card red with what went wrong, proxy sign-in and client certificate
  panels in the same style.
- States: server not reachable, certificate needed, already subscribed,
  setting up (with its steps, then "Arrived") and the slow setup.
- iPad, an unfolded phone and a phone on its side get two columns, the
  landscape beside the content; on a dual-screen device each half takes
  one screen. Arabic and Hebrew mirror through logical sides only.
- The shader caps the pixel ratio at 2, drops to a lower resolution on a
  slow device, stops in the background and is one still frame with
  reduced motion.
- The sign-in and error page bars over a proxy's page (iOS and Android)
  take the same look: a white card and a dark button.
- Plus Jakarta Sans is bundled for the text, with Montserrat standing in
  for Cyrillic; the wordmark fonts and the old preview images are gone.
- New strings in all 28 languages; the keys of the old screens removed.
- The dev sample plans follow the cloud's plans (start, basis, plus, pro).
2026-11-12 14:18:42 +01:00

101 lines
4.7 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { directionOf, pickLocale, translator } from '../../../native-shell/i18n';
// FE-NATIVE-SHELL-001 to FE-NATIVE-SHELL-006
//
// The app's own first screens (client/native-shell): which language and reading
// direction they start in, and the layout rules in shell.css that no component
// test reaches (jsdom applies no stylesheet, so these read the real file).
/** Every rule of a stylesheet as its selector list, its declarations and the media query around it, comments dropped. */
function rules(css: string): { selectors: string[]; body: string; media: string }[] {
const out: { selectors: string[]; body: string; media: string }[] = [];
const stack: string[] = [];
let buf = '';
for (const ch of css.replace(/\/\*[\s\S]*?\*\//g, '')) {
if (ch === '{') {
stack.push(buf.trim());
buf = '';
} else if (ch === '}') {
const head = stack.pop() ?? '';
if (!head.startsWith('@')) {
const media = stack.filter((h) => h.startsWith('@media')).join(' ');
out.push({ selectors: head.split(',').map((sel) => sel.trim()), body: buf, media });
}
buf = '';
} else {
buf += ch;
}
}
return out;
}
const css = readFileSync(resolve(process.cwd(), 'native-shell/shell.css'), 'utf8');
const all = rules(css);
const find = (selector: string, media?: RegExp) =>
all.filter((r) => r.selectors.includes(selector) && (!media || media.test(r.media)));
describe('native shell', () => {
it('FE-NATIVE-SHELL-001: Arabic and Hebrew start right to left, every other language left to right', () => {
expect(pickLocale(['ar-SA'])).toBe('ar');
expect(pickLocale(['he-IL'])).toBe('he');
expect(directionOf('ar')).toBe('rtl');
expect(directionOf('he')).toBe('rtl');
expect(directionOf('ru')).toBe('ltr');
expect(directionOf('de')).toBe('ltr');
});
it('FE-NATIVE-SHELL-002: the first screens speak without dashes as punctuation', () => {
for (const code of ['en', 'de']) {
const t = translator(code);
for (const key of ['native.welcome.lead', 'native.plans.yearlyIs', 'native.provisioning.readySub']) {
expect(t(key)).not.toMatch(/[\u2013\u2014]| - /);
}
}
});
it("FE-NATIVE-SHELL-003: Russian keeps the shell's faces through Cyrillic stand-ins under the same family names", () => {
const faces = css.match(/@font-face\s*\{[^}]*\}/g) ?? [];
const cyrillic = faces.filter((f) => /unicode-range:\s*U\+0301, U\+0400-045F/.test(f));
expect(cyrillic.filter((f) => /font-family:\s*'Poppins'/.test(f))).toHaveLength(3);
expect(cyrillic.filter((f) => /font-family:\s*'Plus Jakarta Sans'/.test(f))).toHaveLength(3);
for (const face of faces) {
const file = /url\('\.\/fonts\/([^']+)'\)/.exec(face)?.[1];
expect(file).toBeTruthy();
expect(() => readFileSync(resolve(process.cwd(), 'native-shell/fonts', file as string))).not.toThrow();
}
});
it('FE-NATIVE-SHELL-004: a wide screen or a phone on its side puts the landscape beside a column of its own', () => {
const wide = /min-width: 900px\), \(orientation: landscape\) and \(max-height: 500px\)/;
expect(find('.scr', wide)[0]?.body).toMatch(/flex-direction:\s*row/);
expect(find('.body', wide)[0]?.body).toMatch(/overflow-y:\s*auto/);
// On a phone the content is no box of its own: it flows in the screen's column.
expect(find('.body')[0]?.body).toMatch(/display:\s*contents/);
// Two screens with a hinge: the landscape on one, the content on the other.
expect(find('.body', /horizontal-viewport-segments: 2/)[0]?.body).toMatch(/env\(viewport-segment-width 1 0\)/);
});
it('FE-NATIVE-SHELL-005: a screen taller than the window scrolls instead of overflowing both edges', () => {
expect(find('.scr')[0]?.body).toMatch(/overflow-y:\s*auto/);
const shell = find('.shell')[0]?.body ?? '';
expect(shell).toMatch(/overflow-x:\s*clip/);
expect(shell).not.toMatch(/justify-content:\s*center/);
// The button at the foot stays in reach while the screen scrolls under it.
expect(find('.dock')[0]?.body).toMatch(/position:\s*sticky/);
});
it('FE-NATIVE-SHELL-006: in Arabic and Hebrew the layout mirrors through logical sides only', () => {
expect(find(":root[dir='rtl'] .back svg")[0]?.body).toMatch(/transform:\s*scaleX\(-1\)/);
expect(find(":root[dir='rtl'] .cycle[data-year='true'] .thumb")[0]?.body).toMatch(/translateX\(-100%\)/);
expect(find('.fld .edge')[0]?.body).toMatch(/inset-inline-end:\s*14px/);
expect(find('.card')[0]?.body).toMatch(/inset-inline:/);
expect(css).not.toMatch(
/margin-left|margin-right|padding-left|padding-right|text-align:\s*left|[^-]left:\s|[^-]right:\s/
);
});
});