waybetter/packages/shell/tests/frame.test.ts
CRT 60faed66be shell: the shell frame, createShell(site) (moved from wayBetter for X)
packages/shell/src/frame (waybetter/shell/frame): the whole shell of an app. An app describes its
site (ShellSite: names and pages, account rules, search, address chips and completions, its panel
tab, the new-tab page, palette items, deck column kinds and flags and an optional compose dock, the
media panel's composer rule, a theme store, overlays, the native features it serves) and passes
its Tauri API and i18n instance; createShell returns the root component.

- Shell.tsx: dockview host (tabs in the titlebar row, splits, drag-link drops, shortcuts, context
  menu, deck back-and-forth, session restore, workspace on start, overlay raise); state.ts (pane
  placeholders, tabs_sync one-in-flight via syncGate, shell_raise layer, profiles, layout).
- WebPanel (address bar), NewTabPage, CommandPalette + paletteModel (frame groups; site items),
  DeckView + AddColumnDialog + deckStore (site column kinds), PaneTheme + themeStore
  (createThemeStore over the app's ThemeAdapter) + chromeTheme (ChromeTheme input) + themeScopes,
  Titlebar + TitlebarExtras + icons + brand (wB mark data URI), AccountSwitcher + accountModel,
  workspaces + workspaceModel, backupModel, syncHost, devHost + devModel, pluginCommandStore,
  MediaPanel + mediaModel, searchInput, hooks, bridge (installPreviewMocks), i18n, boot.ts
  (bootScript: the first-frame boot.js), CSS themed with --wb-* tokens.
- paneModel: paneVisibility, raiseSupported, cssRgb; deckModel: isSourcePage.
- Includes the live-QA titlebar fix (dock ResizeObserver + rAF titlebar marks, dock sized by its
  insets).
- tests/frame.test.ts: every frame key has English and is used, palette order with a site,
  address fallback, layout gate, boot script, chrome variables, switcher rules, workspaces,
  deck settings with site flags.
- npm package 0.4.0: exports ./shell/frame, ./shell/frame/*, ./shell/frame/*.css (copied by the
  build); dockview and acorn optional peers; root devDependencies dockview, acorn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:47:10 +07:00

148 lines
8.9 KiB
TypeScript

// The shell frame's pure parts: its words (every key it uses has English and every English key is
// used), the palette's ordering with a site's items, the address fallback, the one-in-flight layout
// sync, the first-frame script, the chrome variables, the account switcher's rules, workspaces and
// the deck settings with a site's flags.
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { accountAction, accountStartUrl, tabsByAccount, type PaneInfo } from '../src/frame/accountModel.ts';
import { BOOT_CACHE_KEY, bootScript } from '../src/frame/boot.ts';
import { CHROME_VAR_NAMES, chromeVars } from '../src/frame/chromeTheme.ts';
import { defaultDeckSettings, normalizeDeckSettings } from '../src/frame/deckStore.ts';
import { FRAME_MESSAGES } from '../src/frame/frameMessages.ts';
import { attachTarget } from '../src/frame/mediaModel.ts';
import { buildItems, type PaletteContext } from '../src/frame/paletteModel.ts';
import { toUrl } from '../src/frame/searchInput.ts';
import { configureSite, type ShellSite } from '../src/frame/site.ts';
import { latestOnly } from '../src/frame/syncGate.ts';
import { captureWorkspace, planRestore } from '../src/frame/workspaceModel.ts';
import { normalizeProfiles } from '../src/profileModel.ts';
const here = dirname(fileURLToPath(import.meta.url));
const FRAME = join(here, '../src/frame');
const files = (dir: string): string[] => readdirSync(dir).flatMap(name => {
const path = join(dir, name);
return statSync(path).isDirectory() ? files(path) : [path];
});
test('every key the frame uses has English, and every English key is used', () => {
const sources = files(FRAME).filter(file => /\.tsx?$/.test(file) && !file.endsWith('frameMessages.ts')).map(file => readFileSync(file, 'utf8')).join('\n');
const used = new Set([...sources.matchAll(/'(wb\.[A-Za-z0-9_.]+)'/g)].map(match => match[1]!));
const en = JSON.parse(readFileSync(join(here, '../../waybetter/locales/en.json'), 'utf8')).messages as Record<string, string>;
for (const key of used) assert.ok(key in en, `${key} is in the framework catalog`);
// `wb.backup.section.<id>` is built at runtime (backupModel.sectionLabel).
for (const key of Object.keys(FRAME_MESSAGES)) assert.ok(used.has(key) || key.startsWith('wb.backup.section.'), `${key} is used`);
});
const site = {
name: 'Example', product: 'wayBetter for Example', logo: '', homeUrl: 'https://example.com/home', loginUrl: 'https://example.com/login', logoutUrl: 'https://example.com/logout',
isSiteUrl: (url: string) => url.startsWith('https://example.com/'),
accounts: { handle: /^\w{1,15}$/, avatarPrefixes: [], handlePrefix: '@' },
searchUrl: (text: string) => `https://example.com/search?q=${encodeURIComponent(text)}`,
address: () => null,
panel: { id: 'app', title: 'App', component: () => null },
newTab: { search: () => 'Search', links: [] },
palette: {
placeholder: () => 'Search',
search: text => ({ id: 'search', group: 'Search', label: `Search for ${text}`, action: { type: 'open', url: `https://example.com/search?q=${text}` } }),
completions: text => (text.endsWith(':') ? [{ id: 'op:from:', group: 'Operators', label: 'from:', action: { type: 'complete', text: `${text}someone` } }] : []),
sections: [{ id: 'themes', label: () => 'Themes', keywords: 'colors', icon: 'palette' }],
items: () => [{ id: 'page:/home', group: 'Example', label: 'Home', keywords: 'feed', action: { type: 'open', url: 'https://example.com/home' } }],
},
deck: {
kinds: [{ kind: 'home', label: 'Home', icon: 'home', hint: '' }], urlFor: () => 'https://example.com/home', title: () => ({ title: 'Home' }),
parseValue: () => '', invalid: () => '', flags: [{ key: 'quiet', label: 'Quiet', hint: '' }], extraFlags: ['hidden'], defaultFlags: { quiet: true },
},
} satisfies ShellSite;
configureSite(site);
const context: PaletteContext = {
tabs: [{ id: 't1', title: 'Home', url: 'https://example.com/home' }],
profiles: [{ id: 'default', name: 'Main' }],
activeProfile: 'default',
decks: [],
};
const setup = { site: site.palette, product: site.product, decks: true, media: false };
test('palette: frame groups, then the app, plugins and the site; the literal search stays last', () => {
const empty = buildItems(context, '', setup);
assert.deepEqual([...new Set(empty.map(item => item.group))], ['Tabs', 'Deck', 'Workspaces', 'Profiles', 'wayBetter for Example', 'Example']);
assert.ok(!empty.some(item => item.id === 'media'), 'no media item without a media panel');
const typed = buildItems(context, 'hom', setup);
assert.equal(typed.at(-1)?.id, 'search');
assert.equal(typed.filter(item => item.id === 'search').length, 1);
assert.equal(buildItems(context, 'x from:', setup)[0]?.id, 'op:from:', 'completions come first');
assert.ok(!buildItems(context, '', { ...setup, decks: false }).some(item => item.group === 'Deck'));
});
test('address text: URLs, bare domains, else the site search', () => {
assert.equal(toUrl(' https://a.dev/x ', site.searchUrl), 'https://a.dev/x');
assert.equal(toUrl('a.dev/x', site.searchUrl), 'https://a.dev/x');
assert.equal(toUrl('two words', site.searchUrl), 'https://example.com/search?q=two%20words');
});
test('layout sync: one send in flight, only the newest waits', async () => {
const sent: number[] = [];
let release!: () => void;
const send = latestOnly<number>(async value => { sent.push(value); if (value === 1) await new Promise<void>(resolve => { release = resolve; }); });
const first = send(1);
void send(2);
const third = send(3);
release();
await Promise.all([first, third]);
assert.deepEqual(sent, [1, 3]);
});
test('boot script: paints only the shell webview, only --wb-* variables', () => {
const source = bootScript();
assert.doesNotThrow(() => new Function(source));
assert.ok(source.includes(JSON.stringify(BOOT_CACHE_KEY)));
assert.ok(source.includes("label !== \"shell\""));
assert.ok(bootScript({ label: 'main' }).includes("label !== \"main\""));
});
test('chrome variables: none for the own style, fonts only without colors, every name listed', () => {
assert.deepEqual(chromeVars({ colors: null }, true), {});
assert.deepEqual(chromeVars({ chromeFont: 'Inter', colors: null }), { '--wb-f-chrome': 'Inter' });
const vars = chromeVars({ colors: { bg: '#ffffff', surface: '#f4f4f4', elevated: '#eeeeee', border: '#dddddd', text: '#111111', muted: '#666666', accent: '#1d9bf0' } });
assert.equal(vars['--wb-c-scheme'], 'light');
for (const name of Object.keys(vars)) assert.ok(CHROME_VAR_NAMES.includes(name), name);
});
test('account switcher: a visible pane on the site first, else the feed or sign-in beside', () => {
const settings = normalizeProfiles({ active: 'default', profiles: [
{ id: 'default', name: 'Main', color: '#1d9bf0', account: { handle: 'me', name: 'Me', signedIn: true } },
{ id: 'pnew1', name: 'New', color: '#f91880' },
] }, site.accounts);
const panes: PaneInfo[] = [
{ id: 'a', kind: 'web', profile: 'default', url: 'https://other.dev/', visible: true },
{ id: 'b', kind: 'web', profile: 'default', url: 'https://example.com/home', visible: true },
];
const pages = { homeUrl: site.homeUrl, loginUrl: site.loginUrl, isSiteUrl: site.isSiteUrl };
assert.deepEqual(accountAction(settings, panes, null, 'default', pages), { type: 'focus', paneId: 'b' });
assert.deepEqual(accountAction(settings, panes, null, 'pnew1', pages), { type: 'open', url: site.loginUrl, profile: 'pnew1', where: 'beside' });
assert.equal(accountStartUrl(settings.profiles[0]!, pages), site.homeUrl);
assert.deepEqual([...tabsByAccount(settings, panes).keys()], ['default']);
assert.equal(attachTarget([{ id: 'a', url: 'https://other.dev/' }, { id: 'b', url: 'https://example.com/x' }], 'a', site.isSiteUrl)?.id, 'b');
});
test('workspaces: web tabs get fresh ids on restore, the app panel keeps its own', () => {
const layout = {
grid: { root: { type: 'branch', data: [{ type: 'leaf', data: { views: ['w1', 'app'], activeView: 'w1', id: '1' }, size: 100 }], size: 100 }, width: 100, height: 100, orientation: 'HORIZONTAL' },
panels: {
w1: { id: 'w1', contentComponent: 'web', params: { kind: 'web', url: 'https://example.com/home', loading: true } },
app: { id: 'app', contentComponent: 'panel', params: { kind: 'panel', section: 'themes', nonce: 4 } },
},
};
const workspace = captureWorkspace({ id: 'one', name: 'One', layout });
assert.deepEqual(workspace.layout.panels.w1?.params, { kind: 'web', url: 'https://example.com/home' });
const plan = planRestore(workspace, () => 'fresh');
assert.deepEqual(Object.keys(plan.layout.panels).sort(), ['app', 'fresh']);
});
test('deck settings: the site\'s default flags; unknown flags are dropped', () => {
assert.deepEqual(defaultDeckSettings().flags, { quiet: true });
assert.deepEqual(normalizeDeckSettings({ flags: { quiet: false, hidden: true, other: true } }).flags, { quiet: false, hidden: true });
});