mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
52f0fb882f
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).
101 lines
4.7 KiB
TypeScript
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/
|
|
);
|
|
});
|
|
});
|