99 lines
6.5 KiB
TypeScript
99 lines
6.5 KiB
TypeScript
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<string, { id: string; contentComponent?: string; title?: string; params: Record<string, unknown> }>,
|
|
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);
|
|
});
|