waybetter/packages/shell/tests/setup.test.ts

136 lines
7.7 KiB
TypeScript

// "Share my setup" (panel/setupModel.ts): what a shared setup can carry (the look, the mode and the
// non-default flags, nothing personal), that a link and a file read back the same, that anything
// unknown or malformed is dropped or refused rather than guessed, and that importing changes exactly
// what the plan lists.
import assert from 'node:assert/strict';
import test from 'node:test';
import type { FeatureTable } from '../../runtime/src/featureTable.ts';
import { tableDefaults } from '../../runtime/src/featureTable.ts';
import {
SETUP_FORMAT, SETUP_LINK, SETUP_MAX_BYTES, applySetup, makeSetup, readSetup, setupFile, setupLink, setupPlan, type SetupState,
} from '../src/panel/setupModel.ts';
const TABLE: FeatureTable = {
areas: [
{ id: 'navigation', name: 'Navigation+', description: 'd', owner: 'wb-navigation-plus.plugin.js', namespace: 'navigation-plus' },
{ id: 'reader', name: 'Reader+', description: 'd', owner: 'wb-reader-plus.plugin.js', namespace: 'reader-plus' },
],
features: [
{ id: 'fixScroll', area: null, group: 'fixes', default: true, backing: 'app' },
{ id: 'hideTrends', area: 'navigation', default: false, backing: 'module' },
{ id: 'navigation.focusMode', area: 'navigation', default: false, backing: 'module' },
{ id: 'hideAds', area: 'reader', default: true, backing: 'app' },
],
modes: [
{ id: 'basic', name: 'b', description: 'b', flags: { 'area.navigation': false, 'area.reader': false } },
{ id: 'advanced', name: 'a', description: 'a', recommended: true, flags: { 'navigation.focusMode': true } },
],
};
const DEFAULTS = tableDefaults(TABLE);
const MINE: SetupState = {
theme: { preset: 'nord', accent: '#88c0d0', icons: 'pixel' },
mode: 'advanced',
flags: { ...DEFAULTS, 'navigation.focusMode': true, hideTrends: true, hideAds: false },
};
test('a setup carries the look, the mode and the non-default flags, and nothing else', () => {
const setup = makeSetup('x', TABLE, MINE);
assert.deepEqual(setup, {
format: SETUP_FORMAT, version: 1, app: 'x',
flags: { hideTrends: true, 'navigation.focusMode': true, hideAds: false },
theme: { preset: 'nord', accent: '#88c0d0', icons: 'pixel' },
mode: 'advanced',
});
// Whatever else the state object happens to hold never reaches the file.
const leaky = { ...MINE, profiles: ['@someone'], notes: { a: 'b' }, drafts: ['text'] } as SetupState;
assert.deepEqual(Object.keys(makeSetup('x', TABLE, leaky)).sort(), ['app', 'flags', 'format', 'mode', 'theme', 'version']);
// A mode the table does not know is saved as none; an app without modes saves no mode at all.
assert.equal(makeSetup('x', TABLE, { ...MINE, mode: 'gone' }).mode, null);
assert.equal('mode' in makeSetup('x', { ...TABLE, modes: [] }, MINE), false);
});
test('a link and a file read back to the same setup', () => {
const setup = makeSetup('x', TABLE, MINE);
const link = setupLink(setup);
assert.ok(link.startsWith(`${SETUP_LINK}?app=x&d=`));
assert.match(link.slice(link.indexOf('d=') + 2), /^[A-Za-z0-9_-]+$/, 'URL-safe, no padding');
for (const input of [link, setupFile(setup), ` ${link}\n`]) {
const read = readSetup(input, 'x', TABLE);
assert.ok(read.ok, input);
assert.deepEqual(read.setup, setup);
assert.deepEqual(read.dropped, []);
}
// An empty setup (everything at its default) is still a valid link.
const empty = readSetup(setupLink({ format: SETUP_FORMAT, version: 1, app: 'x', flags: {} }), 'x', TABLE);
assert.ok(empty.ok);
assert.deepEqual(empty.setup.flags, {});
});
test('another app, another format, a newer version or an oversized paste is refused whole', () => {
const setup = makeSetup('kick', TABLE, MINE);
assert.deepEqual(readSetup(setupLink(setup), 'x', TABLE), { ok: false, error: 'app', app: 'kick' });
assert.deepEqual(readSetup('{"format":"waybetter-backup"}', 'x', TABLE), { ok: false, error: 'format' });
assert.deepEqual(readSetup(JSON.stringify({ ...makeSetup('x', TABLE, MINE), version: 2 }), 'x', TABLE), { ok: false, error: 'version' });
assert.deepEqual(readSetup('not json', 'x', TABLE), { ok: false, error: 'format' });
assert.deepEqual(readSetup(`${SETUP_LINK}?app=x&d=%%%`, 'x', TABLE), { ok: false, error: 'format' });
assert.deepEqual(readSetup(`${SETUP_LINK}?app=x&d=bm90IGpzb24`, 'x', TABLE), { ok: false, error: 'format' });
assert.deepEqual(readSetup('x'.repeat(SETUP_MAX_BYTES + 1), 'x', TABLE), { ok: false, error: 'size' });
assert.deepEqual(readSetup('[]', 'x', TABLE), { ok: false, error: 'format' });
});
test('unknown flags, a foreign mode and a malformed theme are dropped and named, never guessed', () => {
const raw = {
format: SETUP_FORMAT, version: 1, app: 'x',
flags: { hideTrends: true, 'p.someone.plugin': true, hideAds: 'yes', 'area.chat': false },
theme: { preset: '<script>', accent: 'red' },
mode: 'power',
notes: { '@someone': 'secret' },
};
const read = readSetup(JSON.stringify(raw), 'x', TABLE);
assert.ok(read.ok);
assert.deepEqual(read.setup, { format: SETUP_FORMAT, version: 1, app: 'x', flags: { hideTrends: true } });
assert.deepEqual(read.dropped.sort(), ['area.chat', 'hideAds', 'mode', 'p.someone.plugin', 'theme']);
assert.equal('notes' in read.setup, false, 'fields a setup does not have are not carried over');
});
test('the plan lists exactly what changes; applying it changes nothing else', () => {
const setup = makeSetup('x', TABLE, MINE);
const now: SetupState = { theme: { preset: 'x', accent: null }, mode: null, flags: DEFAULTS };
const themes = new Set(['x', 'nord']);
const packs = new Set(['pixel']);
const plan = setupPlan(TABLE, now, setup, themes, packs);
assert.deepEqual(plan, {
on: ['hideTrends', 'navigation.focusMode'], off: ['hideAds'], same: false,
theme: { from: 'x', to: 'nord' }, accent: { from: null, to: '#88c0d0' }, icons: { from: undefined, to: 'pixel' },
mode: { from: null, to: 'advanced' },
});
const after = applySetup(TABLE, now, plan);
assert.deepEqual(after, MINE);
// Applied twice: nothing more to do.
assert.equal(setupPlan(TABLE, after, setup, themes, packs).same, true);
});
test('a theme or icon pack that is not installed here is named and left alone', () => {
const setup = makeSetup('x', TABLE, { ...MINE, theme: { preset: 'my-own-theme', accent: '#112233', icons: 'someone-pack' } });
const now: SetupState = { theme: { preset: 'x', accent: null }, mode: 'advanced', flags: MINE.flags };
const plan = setupPlan(TABLE, now, setup, new Set(['x']), new Set());
assert.equal(plan.missingTheme, 'my-own-theme');
assert.equal(plan.missingIcons, 'someone-pack');
assert.equal(plan.theme, undefined);
assert.equal(plan.accent, undefined, 'an accent belongs to its theme');
assert.equal(plan.icons, undefined);
assert.equal(plan.same, true, 'nothing else differs, so nothing would change');
assert.deepEqual(applySetup(TABLE, now, plan), now);
// "site" (the site's own icons) is a choice every app has.
const site = setupPlan(TABLE, now, makeSetup('x', TABLE, { ...MINE, theme: { preset: 'x', accent: null, icons: 'site' } }), new Set(['x']), new Set());
assert.deepEqual(site.icons, { from: undefined, to: 'site' });
});
test('a setup without a theme or mode leaves the look and the mode alone', () => {
const setup = readSetup(JSON.stringify({ format: SETUP_FORMAT, version: 1, app: 'x', flags: { hideTrends: true } }), 'x', TABLE);
assert.ok(setup.ok);
const now: SetupState = { theme: { preset: 'nord', accent: '#88c0d0' }, mode: 'basic', flags: DEFAULTS };
const plan = setupPlan(TABLE, now, setup.setup, new Set(['nord']), new Set());
assert.deepEqual(plan, { on: ['hideTrends'], off: [], same: false });
assert.deepEqual(applySetup(TABLE, now, plan), { ...now, flags: { ...DEFAULTS, hideTrends: true } });
});