This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376import assert from 'node:assert/strict';import {readFile} from 'node:fs/promises';import test from 'node:test';import vm from 'node:vm';
const source = await readFile(new URL('../extension/shared/theme-model.js', import.meta.url), 'utf8');const context = vm.createContext({crypto: {randomUUID: () => 'generated-theme-id'}, URL});vm.runInContext(source, context, {filename: 'theme-model.js'});const themes = context.BskyPlusThemes;const plain = value => JSON.parse(JSON.stringify(value));
function validTheme(overrides = {}) { const base = overrides.base || 'light'; return { schemaVersion: 1, id: 'night-owl_01', name: 'Night owl', base, colors: {...themes.PRESETS[base]}, ...overrides, };}
test('hue shifts rotate colors in either direction and leave neutral colors unchanged', () => { const colors = { red: '#FF0000', muted: '#804040', black: '#000000', white: '#FFFFFF', gray: '#777777' }; assert.deepEqual(plain(themes.shiftHue(colors, 120)), { red: '#00FF00', muted: '#408040', black: '#000000', white: '#FFFFFF', gray: '#777777', }); assert.equal(themes.shiftHue(colors, -120).red, '#0000FF'); assert.equal(themes.shiftHue(colors, 480).red, '#00FF00'); assert.deepEqual(plain(themes.shiftHue(colors, 360)), colors); assert.equal(colors.red, '#FF0000'); assert.throws(() => themes.shiftHue(colors, NaN), /finite hue shift/);});
test('hue shifts cover complete palettes and preserve channel extrema', () => { for (const palette of Object.values(themes.PRESETS)) { const shifted = themes.shiftHue(palette, 73); assert.deepEqual(Object.keys(shifted), Object.keys(palette)); for (const key of themes.KEYS) { const channels = hex => hex.slice(1).match(/../g).map(value => parseInt(value, 16)); assert.equal(Math.min(...channels(shifted[key])), Math.min(...channels(palette[key]))); assert.equal(Math.max(...channels(shifted[key])), Math.max(...channels(palette[key]))); } assert.deepEqual(plain(themes.shiftHue(palette, 0)), plain(palette)); }});
test('contrast checker catches unreadable text on secondary surfaces and colored buttons', () => { const colors = { ...themes.PRESETS.dark, surfaceRaised: '#FFFFFF', primary: '#FFFFFF' }; const issues = themes.contrastIssues(colors); assert.ok(issues.some(issue => issue.foreground === 'text' && issue.background === 'surfaceRaised' && issue.ratio === 1)); assert.ok(issues.some(issue => issue.foreground === 'onPrimary' && issue.background === 'primary' && issue.ratio === 1)); assert.ok(issues.every(issue => issue.ratio < 3)); assert.ok(!themes.contrastIssues(themes.PRESETS.dark).some(issue => issue.foreground === 'text'));});
test('theme schema accepts only a complete version-one theme and normalizes it', () => { const theme = validTheme({ name: ' Night owl ', colors: {...themes.PRESETS.light, text: '#aBc123'}, ignored: 'discarded', });
assert.deepEqual(plain(themes.validateTheme(theme)), { schemaVersion: 1, id: 'night-owl_01', name: 'Night owl', base: 'light', colors: {...plain(themes.PRESETS.light), text: '#ABC123'}, });});
test('theme schema rejects unsupported versions, unsafe IDs, names, bases, and incomplete colors', () => { assert.throws(() => themes.validateTheme(null), /Unsupported theme format/); assert.throws(() => themes.validateTheme(validTheme({schemaVersion: 2})), /Unsupported theme format/); assert.throws(() => themes.validateTheme(validTheme({id: '../theme'})), /Invalid theme ID/); assert.throws(() => themes.validateTheme(validTheme({id: 'x'.repeat(81)})), /Invalid theme ID/); assert.throws(() => themes.validateTheme(validTheme({name: ' '})), /between 1 and 80/); assert.throws(() => themes.validateTheme(validTheme({name: 'x'.repeat(81)})), /between 1 and 80/); assert.throws(() => themes.validateTheme(validTheme({base: 'system'})), /Choose a light, dim, or dark base/); assert.throws(() => themes.validateTheme(validTheme({colors: {...themes.PRESETS.light, primary: 'blue'}})), /six-digit hex color for primary/); assert.throws(() => themes.validateTheme(validTheme({colors: {...themes.PRESETS.light, primary: '#12345678'}})), /six-digit hex color for primary/); assert.throws(() => themes.validateTheme(validTheme({colors: {}})), /six-digit hex color for background/);});
test('theme factory uses a safe requested base and falls back to dim', () => { const light = themes.createTheme('Light personal', 'light'); assert.equal(light.id, 'generated-theme-id'); assert.equal(light.base, 'light'); assert.deepEqual(light.colors, themes.PRESETS.light);
const fallback = themes.createTheme(); assert.equal(fallback.base, 'dim'); assert.deepEqual(fallback.colors, themes.PRESETS.dim); assert.equal(themes.createTheme('Bad base', 'invalid').base, 'dim');});
test('library normalization removes malformed, duplicate, and inactive entries without losing valid themes', () => { const first = validTheme({id: 'first', name: 'First'}); const duplicate = validTheme({id: 'first', name: 'Second copy'}); const second = validTheme({id: 'second', name: 'Second', base: 'dark', colors: {...themes.PRESETS.dark}}); const malformed = validTheme({id: 'broken', colors: {...themes.PRESETS.light, warning: '#fff'}});
assert.deepEqual(plain(themes.normalizeLibrary({ schemaVersion: 1, activeId: 'first', themes: [first, malformed, duplicate, second], })), { schemaVersion: 1, activeId: 'first', themes: [plain(themes.validateTheme(first)), plain(themes.validateTheme(second))], });
assert.deepEqual(themes.normalizeLibrary({schemaVersion: 1, activeId: 'missing', themes: [first]}).activeId, null); assert.deepEqual(plain(themes.normalizeLibrary({schemaVersion: 0, activeId: 'first', themes: [first]})), { schemaVersion: 1, activeId: null, themes: [], }); assert.deepEqual(plain(themes.normalizeLibrary(undefined)), {schemaVersion: 1, activeId: null, themes: []});});
test('library normalization caps input at one hundred candidates before validation', () => { const candidates = Array.from({length: 101}, (_, index) => validTheme({id: 'theme-' + index, name: 'Theme ' + index})); const library = themes.normalizeLibrary({schemaVersion: 1, activeId: 'theme-100', themes: candidates}); assert.equal(library.themes.length, 100); assert.equal(library.activeId, null); assert.equal(library.themes.at(-1).id, 'theme-99');});
test('deleted builtin choices persist only known unique ids and cannot stay active', () => { const builtinIds = Object.keys(themes.BUILTIN_THEMES); const deleted = themes.normalizeLibrary({ schemaVersion: 1, activeId: builtinIds[0], themes: [], deletedBuiltinIds: [builtinIds[0], 'unknown-theme', builtinIds[0], 42], }); assert.deepEqual(plain(deleted), { schemaVersion: 1, activeId: null, themes: [], deletedBuiltinIds: [builtinIds[0]], }); assert.deepEqual(plain(themes.availableBuiltinThemes(deleted).map(theme => theme.id)), builtinIds.slice(1));
const remainingActive = themes.normalizeLibrary({ schemaVersion: 1, activeId: builtinIds[1], themes: [], deletedBuiltinIds: [builtinIds[0]], }); assert.equal(remainingActive.activeId, builtinIds[1]); assert.equal(themes.availableBuiltinThemes(remainingActive).length, builtinIds.length - 1); assert.deepEqual(plain(themes.normalizeLibrary({ schemaVersion: 1, activeId: null, themes: [], deletedBuiltinIds: ['unknown-theme'] })), { schemaVersion: 1, activeId: null, themes: [], }, 'unknown ids are discarded and an empty tombstone list preserves the legacy shape');});
test('export is a clean, canonical theme document without secrets or extraneous fields', () => { const theme = validTheme({ token: 'secret-token', endpoint: 'https://api.example.test', colors: {...themes.PRESETS.light, apiKey: 'secret-key'}, }); const exported = themes.exportTheme(theme); const parsed = JSON.parse(exported);
assert.equal(exported.endsWith('\n'), true); assert.deepEqual(Object.keys(parsed).sort(), ['base', 'colors', 'id', 'name', 'schemaVersion']); assert.equal('token' in parsed, false); assert.equal('endpoint' in parsed, false); assert.equal('apiKey' in parsed.colors, false); assert.equal(parsed.colors.primary, '#006AFF');});
test('WitchSky AT URIs accept raw and PDSls forms and reject other collections', () => { const uri = 'at://did:plc:l37td5yhxl2irrzrgvei4qay/app.witchsky.theme.colors/3mvpzxkrbms25'; assert.deepEqual(plain(themes.parseWitchSkyUri(uri)), { uri, repo: 'did:plc:l37td5yhxl2irrzrgvei4qay', rkey: '3mvpzxkrbms25', }); assert.deepEqual(plain(themes.parseWitchSkyUri('https://pds.ls/' + uri + '#record')), plain(themes.parseWitchSkyUri(uri))); assert.throws(() => themes.parseWitchSkyUri('at://did:plc:l37td5yhxl2irrzrgvei4qay/app.bsky.feed.post/3mvpzxkrbms25'), /complete WitchSky/); assert.throws(() => themes.parseWitchSkyUri('https://example.com/theme'), /complete WitchSky/);});
test('WitchSky did:web URIs resolve root and path DID documents safely', () => { const uri = 'at://did:web:vessel.darkworld.download/app.witchsky.theme.colors/3mv3ih7bfgs2m'; assert.deepEqual(plain(themes.parseWitchSkyUri(uri)), { uri, repo: 'did:web:vessel.darkworld.download', rkey: '3mv3ih7bfgs2m', didDocumentUrl: 'https://vessel.darkworld.download/.well-known/did.json', }); assert.equal(themes.parseWitchSkyUri( 'at://did:web:example.test%3A8443:team:alice/app.witchsky.theme.colors/abc123', ).didDocumentUrl, 'https://example.test:8443/team/alice/did.json'); assert.equal(themes.parseWitchSkyUri( 'at://did:web:example.test:team%20one/app.witchsky.theme.colors/abc123', ).didDocumentUrl, 'https://example.test/team%20one/did.json'); for (const repo of [ 'did:web:bad host.example', 'did:web:example.test%2F..', 'did:web:example.test:..', 'did:web:example.test:%2Fadmin', 'did:web:example.test:%ZZ', ]) { assert.throws(() => themes.parseWitchSkyUri(`at://${repo}/app.witchsky.theme.colors/abc123`), /complete WitchSky/); }});
test('WitchSky base and variants become separate complete Bsky+ themes', () => { const record = { $type: 'app.witchsky.theme.colors', name: 'Coffee', base: { name: 'Default', colors: { canvas: '#1c1310', surface: '#2e2018', surfaceRaised: '#251a15', text: '#f4deb8', textMuted: '#e8c28a', border: '#3d2b1f', accent: '#d4975c', accentSoft: '#4d3828', onAccent: '#2c221a', positive: '#4caf7d', warning: '#e2b76f', critical: '#e06c6c', favorite: '#cc5577', } }, variants: [ { name: 'Light', colors: { canvas: '#f7f2e9', surface: '#efe3d0', surfaceRaised: '#fdf8f0', text: '#1c1310' } }, { name: 'Dark', colors: { canvas: '#050505', surface: '#171717', surfaceRaised: '#111111', text: '#f7f1e8' } }, ], }; const imported = plain(themes.witchSkyThemes(record));
assert.deepEqual(imported.map(theme => theme.name), ['Coffee — Default', 'Coffee — Light', 'Coffee — Dark']); assert.deepEqual(imported.map(theme => theme.base), ['dark', 'light', 'dark']); assert.equal(imported[0].colors.background, '#1C1310'); assert.equal(imported[0].colors.primary, '#D4975C'); assert.equal(imported[0].colors.negative, '#E06C6C'); assert.equal(imported[0].colors.like, '#CC5577'); assert.equal(imported[1].colors.background, '#F7F2E9'); assert.equal(imported[1].colors.positive, '#4CAF7D'); imported.forEach(theme => assert.doesNotThrow(() => themes.validateTheme(theme)));});
test('WitchSky conversion rejects malformed records and translucent colors', () => { assert.throws(() => themes.witchSkyThemes(null), /not a WitchSky/); assert.throws(() => themes.witchSkyThemes({ $type: 'app.witchsky.theme.colors', name: 'Broken', base: { colors: {} } }), /missing/); assert.throws(() => themes.witchSkyThemes({ $type: 'app.witchsky.theme.colors', name: 'Broken', base: { colors: { canvas: '#00000080' } }, }), /opaque/);});
test('source palettes produce exact ALF light, dark, and dim inversions', () => { const light = themes.palette('light'); const dark = themes.palette('dark'); const dim = themes.palette('dim');
assert.equal(light.contrast_0, '#FFFFFF'); assert.equal(light.primary_25, '#F5F9FF'); assert.equal(light.primary_500, '#006AFF'); assert.equal(dark.contrast_0, '#000000'); assert.equal(dark.contrast_1000, '#FFFFFF'); assert.equal(dark.primary_25, '#001533'); assert.equal(dark.primary_500, '#006AFF'); assert.equal(dim.contrast_0, '#151D28'); assert.equal(dim.primary_25, '#122136'); assert.equal(dim.primary_500, '#0F73FF'); assert.equal(dim.negative_975, '#FFF5F7');});
test('mapping is property-aware for neutral surfaces, borders, and text', () => { const theme = validTheme({ colors: { ...themes.PRESETS.light, background: '#101112', hover: '#202122', border: '#303132', borderStrong: '#404142', text: '#505152', textMuted: '#606162', textSubtle: '#707172', textInverse: '#808182', }, });
assert.equal(themes.mapColor('#DCE2EA', 'background-color', theme, 'light'), '#202122'); assert.equal(themes.mapColor('#DCE2EA', 'border-color', theme, 'light'), '#303132'); assert.notEqual(themes.mapColor('#DCE2EA', 'color', theme, 'light'), '#202122'); assert.equal(themes.mapColor('#000000', 'color', theme, 'light'), '#505152'); assert.equal(themes.mapColor('#FFFFFF', 'color', theme, 'dark'), '#505152'); assert.equal(themes.mapColor('#DCE2EA', 'outline-color', theme, 'light'), theme.colors.focus);});
test('mapping handles primary, like, error, alpha overlays, and shadows separately', () => { const theme = validTheme({ colors: { ...themes.PRESETS.light, primary: '#112233', primaryHover: '#223344', primaryMuted: '#334455', link: '#445566', like: '#556677', negative: '#667788', negativeMuted: '#778899', overlay: '#8899AA', shadow: '#99AABB', }, });
assert.equal(themes.mapColor('#006AFF', 'background-color', theme, 'light'), '#112233'); assert.equal(themes.mapColor('#006AFF', 'color', theme, 'light'), '#445566'); assert.equal(themes.mapColor('#EC4899', 'fill', theme, 'light'), '#556677'); assert.equal(themes.mapColor('#E91646', 'color', theme, 'light'), '#667788'); assert.equal(themes.mapColor('rgba(0, 0, 0, 0.5)', 'background-color', theme, 'light'), 'rgba(136, 153, 170, 0.5)'); assert.equal(themes.mapColor('#00000080', 'box-shadow', theme, 'light'), 'rgba(153, 170, 187, 0.5019607843137255)'); assert.equal(themes.mapColor('transparent', 'background-color', theme, 'light'), null); assert.equal(themes.mapColor('#123456', 'color', theme, 'light'), null);});
test('contrast ratios use WCAG relative luminance', () => { assert.equal(themes.contrastRatio('#000000', '#FFFFFF'), 21); assert.equal(themes.contrastRatio('#123456', '#123456'), 1); assert.equal(themes.contrastRatio('#123456', '#FEDCBA'), themes.contrastRatio('#FEDCBA', '#123456')); assert.ok(themes.contrastRatio('#1D3A5F', '#FFFFFF') >= 4.5);});
test('built-in Bluesky themes remain active choices and cannot be shadowed in saved libraries', () => { const light = themes.BUILTIN_THEMES['bluesky-light']; assert.deepEqual(Object.keys(themes.BUILTIN_THEMES).filter(id => id.startsWith('bluesky-')), ['bluesky-light', 'bluesky-dim', 'bluesky-dark']); assert.equal(light.name, 'Bluesky Light'); assert.equal(themes.normalizeLibrary({ schemaVersion: 1, activeId: light.id, themes: [] }).activeId, light.id); const impostor = { ...light, colors: { ...light.colors } }; assert.deepEqual(plain(themes.normalizeLibrary({ schemaVersion: 1, activeId: light.id, themes: [impostor] })), { schemaVersion: 1, activeId: light.id, themes: [], });});
test('included Helium themes preserve imported names and palettes as frozen builtins', () => { const ids = [ 'bskyplus-coffee-espresso-light', 'bskyplus-floras-moat-floras-moat', 'bskyplus-witchsky-mu-pink-midnight', 'bskyplus-snuffbait-dark-coven', 'bskyplus-latte-mauve', ]; const imported = ids.map(id => themes.BUILTIN_THEMES[id]); assert.deepEqual(imported.map(theme => theme.name), [ 'Coffee / Espresso — Light', "Flora's Moat — Flora's Moat", 'Witchsky + mu Pink — Midnight', 'snuffbait dark — Coven', 'Latte — Mauve', ]); assert.deepEqual(imported.map(theme => theme.base), ['light', 'dark', 'dark', 'dark', 'light']); assert.deepEqual(Object.keys(themes.PRESETS), ['light', 'dim', 'dark'], 'the existing light, dim, and dark factories remain unchanged');
for (let index = 0; index < imported.length; index += 1) { const theme = imported[index]; assert.ok(theme, `included theme ${ids[index]} exists`); assert.equal(theme.id, ids[index]); assert.equal(themes.isBuiltinTheme(theme.id), true); assert.deepEqual(plain(themes.validateTheme(theme)), plain(theme)); assert.deepEqual(Object.keys(theme.colors).sort(), [...themes.KEYS].sort()); assert.equal(Object.isFrozen(theme), true); assert.equal(Object.isFrozen(theme.colors), true); } assert.equal(imported[0].colors.primary, '#A55A1F'); assert.equal(imported[1].colors.background, '#333333'); assert.equal(imported[2].colors.background, '#150B08'); assert.equal(imported[3].colors.text, '#FFE0FF'); assert.equal(imported[4].colors.primary, '#8839EF');
assert.equal(themes.normalizeLibrary(undefined).activeId, null, 'adding included themes does not auto-activate one'); const userTheme = validTheme({id: 'existing-user-theme', name: 'Existing user theme'}); const saved = themes.normalizeLibrary({schemaVersion: 1, activeId: userTheme.id, themes: [userTheme]}); assert.equal(saved.activeId, userTheme.id, 'an existing user theme remains active'); assert.deepEqual(plain(saved.themes), [plain(themes.validateTheme(userTheme))]);
const activeBuiltin = themes.normalizeLibrary({schemaVersion: 1, activeId: imported[0].id, themes: []}); assert.equal(activeBuiltin.activeId, imported[0].id, 'included themes can be selected without storing a copy'); const shadowedBuiltin = themes.normalizeLibrary({schemaVersion: 1, activeId: imported[0].id, themes: [imported[0]]}); assert.deepEqual(plain(shadowedBuiltin), {schemaVersion: 1, activeId: imported[0].id, themes: []});});