// 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; for (const key of used) assert.ok(key in en, `${key} is in the framework catalog`); // `wb.backup.section.` 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(async value => { sent.push(value); if (value === 1) await new Promise(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 }); });