import assert from 'node:assert/strict'; import test from 'node:test'; import { EMPTY_WORKSPACES, WORKSPACE_LIMITS, captureWorkspace, normalizeWorkspaces, planRestore, removeWorkspace, renameWorkspace, setDefaultWorkspace, upsertWorkspace, workspaceId, workspaceTabs, } from '../../../waybetter/packages/shell/src/frame/workspaceModel.ts'; import '../src/i18n/index.ts'; /** A dockview `toJSON()` as the shell produces it: two groups side by side, a deck tab, live state. */ const live = () => ({ grid: { root: { type: 'branch', data: [ { type: 'leaf', data: { views: ['ta', 'tb', 'deck-d1'], activeView: 'tb', id: '1' }, size: 700 }, { type: 'leaf', data: { views: ['better-twttr', 'ts'], activeView: 'better-twttr', id: '2' }, size: 500 }, ], size: 900 }, width: 1200, height: 900, orientation: 'HORIZONTAL', }, panels: { ta: { id: 'ta', contentComponent: 'web', title: 'Home / X', params: { kind: 'web', url: 'https://x.com/home', profile: 'default', loading: true, favicon: 'https://abs.twimg.com/favicon.ico', besideOf: '2' } }, tb: { id: 'tb', contentComponent: 'web', title: 'Notifications', params: { kind: 'web', url: 'https://x.com/notifications', profile: 'work', pinned: true } }, 'deck-d1': { id: 'deck-d1', contentComponent: 'deck', title: 'Deck', params: { kind: 'deck', deckId: 'd1' } }, 'better-twttr': { id: 'better-twttr', contentComponent: 'panel', title: 'wayBetter', params: { kind: 'panel', section: 'themes', nonce: 123 } }, ts: { id: 'ts', contentComponent: 'start', title: 'New tab', params: { kind: 'start', profile: 'work' } }, bad: { id: 'bad', contentComponent: 'web', params: { kind: 'web', url: 'javascript:alert(1)' } }, } as Record }>, activeGroup: '2', }); test('save keeps each tab URL, account and pane theme; drops transient state and unsafe tabs', () => { const workspace = captureWorkspace({ id: 'launch', name: ' Launch day ', layout: live(), paneThemes: { ta: 'nord', gone: 'paper', ts: 'Not Valid' }, decks: [{ id: 'd1', name: 'Deck', columns: [] } as { id: string }, { id: 'd2' }], now: new Date('2026-09-29T10:00:00Z'), }); assert.equal(workspace.name, 'Launch day'); assert.equal(workspace.savedAt, '2026-09-29T10:00:00.000Z'); assert.deepEqual(workspace.layout.panels.ta!.params, { kind: 'web', url: 'https://x.com/home', profile: 'default' }); assert.deepEqual(workspace.layout.panels.tb!.params, { kind: 'web', url: 'https://x.com/notifications', profile: 'work', pinned: true }); assert.deepEqual(workspace.layout.panels['better-twttr']!.params, { kind: 'panel', section: 'themes' }); assert.ok(!('bad' in workspace.layout.panels)); assert.deepEqual(workspace.paneThemes, { ta: 'nord' }); assert.deepEqual(workspace.decks, [{ id: 'd1', name: 'Deck', columns: [] }], 'only decks the layout shows'); assert.deepEqual(workspaceTabs(workspace).map(tab => tab.kind), ['web', 'web', 'deck', 'panel', 'start']); assert.ok(!JSON.stringify(workspace).includes('loading') && !JSON.stringify(workspace).includes('nonce')); }); test('restore opens fresh tabs (new ids) with the same URLs, accounts, order and pane themes', () => { const workspace = captureWorkspace({ id: 'launch', name: 'Launch', layout: live(), paneThemes: { ta: 'nord', tb: 'paper' } }); let n = 0; const plan = planRestore(workspace, () => `new${++n}`, profile => profile !== 'work'); const leaves = (plan.layout.grid as { root: { data: Array<{ data: { views: string[]; activeView: string } }> } }).root.data.map(leaf => leaf.data); assert.deepEqual(leaves[0]!.views, ['new1', 'new2', 'deck-d1'], 'web tabs renamed, the deck keeps its id'); assert.equal(leaves[0]!.activeView, 'new2'); assert.deepEqual(leaves[1]!.views, ['better-twttr', 'new3']); assert.equal(plan.layout.panels.new1!.id, 'new1'); assert.equal((plan.layout.panels.new1!.params as { url: string }).url, 'https://x.com/home'); assert.ok(!('profile' in (plan.layout.panels.new2!.params as object)), 'a removed account falls back to the default'); assert.deepEqual(plan.paneThemes, { new1: 'nord', new2: 'paper' }); assert.deepEqual(plan.ids, { ta: 'new1', tb: 'new2', 'deck-d1': 'deck-d1', 'better-twttr': 'better-twttr', ts: 'new3' }); // Restoring twice never reuses a live tab's id. const again = planRestore(workspace, () => `again${++n}`); assert.ok(Object.keys(again.layout.panels).every(id => !id.startsWith('new'))); }); test('the store is bounded, names are required and the default must exist', () => { let store = EMPTY_WORKSPACES; const make = (id: string) => captureWorkspace({ id, name: id, layout: live() }); for (let n = 0; n < WORKSPACE_LIMITS.count; n += 1) store = upsertWorkspace(store, make(`w${n}`)); assert.throws(() => upsertWorkspace(store, make('one-more')), /At most/); store = upsertWorkspace(store, { ...make('w0'), name: 'Replaced' }); assert.equal(store.items.length, WORKSPACE_LIMITS.count); assert.equal(store.items[0]!.name, 'Replaced'); store = setDefaultWorkspace(store, 'w3'); assert.equal(store.default, 'w3'); assert.equal(setDefaultWorkspace(store, 'nope').default, null); store = removeWorkspace(store, 'w3'); assert.equal(store.default, null, 'deleting the default clears it'); assert.throws(() => renameWorkspace(store, 'w1', ' ')); assert.equal(renameWorkspace(store, 'w1', 'Night shift').items[1]!.name, 'Night shift'); assert.equal(workspaceId('Launch Day!', ['launch-day']), 'launch-day-2'); assert.equal(workspaceId('日本', []), 'workspace'); assert.throws(() => captureWorkspace({ id: 'x', name: 'Empty', layout: { grid: { root: {} }, panels: {} } }), /no tabs/); const huge = live(); for (let n = 0; n < 70; n += 1) huge.panels[`t${n}`] = { id: `t${n}`, params: { kind: 'start' } }; (huge.grid.root.data[0]!.data.views as string[]).push(...Array.from({ length: 70 }, (_, n) => `t${n}`)); assert.throws(() => captureWorkspace({ id: 'big', name: 'Big', layout: huge }), /at most 64 tabs/); }); test('whatever native code returns is normalized, never trusted blindly', () => { const store = normalizeWorkspaces({ version: 1, default: 'gone', items: [ captureWorkspace({ id: 'ok', name: 'OK', layout: live() }), { id: 'Bad Id', name: 'x', layout: live() }, { id: 'noname', name: '', layout: live() }, { id: 'ok', name: 'Duplicate', layout: live() }, { id: 'nolayout', name: 'No layout' }, ] }); assert.deepEqual(store.items.map(item => item.id), ['ok']); assert.equal(store.default, null); assert.deepEqual(normalizeWorkspaces('garbage'), EMPTY_WORKSPACES); });