feat(theme-registry): bootstrap with 25 existing + 12 new themes

Registers all 37 editor themes at startup via ThemeRegistry.bootstrap.js.
Side-effect module: requiring it populates the registry from a static
THEMES array (25 existing menu themes refactored into the registry +
12 new: Catppuccin x4, one-light, tokyo-night-storm, synthwave-84,
outrun, winter-is-coming x2, solarized-dark-hc, spring-light).

Categories split: 13 light / 22 dark / 1 high-contrast / 1 seasonal
(spring-light per spec).

Snapshot test asserts exact id order, shape validity, and category
counts. Tests use jest.resetModules() + per-test requires so the
bootstrap module re-evaluates its registration loop each run.

Amit Haridas
This commit is contained in:
2026-09-14 19:43:48 +05:30
parent 1e7e332c24
commit eaad62d02e
2 changed files with 178 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* Editor Theme Registry bootstrap — registers all shipped themes at startup.
*
* Importing this module has the side effect of populating the registry.
* `src/main.js` requires this once during startup. Test files require it
* after `ThemeRegistry.clear()` to assert the snapshot.
*
* Theme ids are kebab-case. Categories: 'light' | 'dark' | 'high-contrast'
* | 'seasonal'. `isDark` is independent of category — see spec note on
* winter-is-coming-light (light-category, dark-not-required) and the future
* seasonal-with-dark possibility.
*
* @module ThemeRegistry.bootstrap
*/
const ThemeRegistry = require('./ThemeRegistry');
const THEMES = [
// ---- 25 existing menu themes (refactor — no behavioural change) ──
// Light
{ id: 'atomonelight', label: 'Atom One Light (Default)', category: 'light', isDark: false },
{ id: 'github', label: 'GitHub Light', category: 'light', isDark: false },
{ id: 'light', label: 'Light', category: 'light', isDark: false },
{ id: 'solarized', label: 'Solarized Light', category: 'light', isDark: false },
{ id: 'gruvbox-light', label: 'Gruvbox Light', category: 'light', isDark: false },
{ id: 'ayu-light', label: 'Ayu Light', category: 'light', isDark: false },
{ id: 'sepia', label: 'Sepia', category: 'light', isDark: false },
{ id: 'paper', label: 'Paper', category: 'light', isDark: false },
{ id: 'rosepine-dawn', label: 'Rose Pine Dawn', category: 'light', isDark: false },
{ id: 'concrete-light', label: 'Concrete Light', category: 'light', isDark: false },
// Dark
{ id: 'dark', label: 'Dark', category: 'dark', isDark: true },
{ id: 'onedark', label: 'One Dark', category: 'dark', isDark: true },
{ id: 'dracula', label: 'Dracula', category: 'dark', isDark: true },
{ id: 'nord', label: 'Nord', category: 'dark', isDark: true },
{ id: 'monokai', label: 'Monokai', category: 'dark', isDark: true },
{ id: 'material', label: 'Material', category: 'dark', isDark: true },
{ id: 'gruvbox-dark', label: 'Gruvbox Dark', category: 'dark', isDark: true },
{ id: 'tokyonight', label: 'Tokyo Night', category: 'dark', isDark: true },
{ id: 'palenight', label: 'Palenight', category: 'dark', isDark: true },
{ id: 'ayu-dark', label: 'Ayu Dark', category: 'dark', isDark: true },
{ id: 'ayu-mirage', label: 'Ayu Mirage', category: 'dark', isDark: true },
{ id: 'oceanic-next', label: 'Oceanic Next', category: 'dark', isDark: true },
{ id: 'cobalt2', label: 'Cobalt2', category: 'dark', isDark: true },
{ id: 'concrete-dark', label: 'Concrete Dark', category: 'dark', isDark: true },
{ id: 'concrete-warm', label: 'Concrete Warm', category: 'dark', isDark: true },
// ---- 12 new themes ──
// Catppuccin (4)
{ id: 'catppuccin-latte', label: 'Catppuccin Latte', category: 'light', isDark: false },
{ id: 'catppuccin-frappe', label: 'Catppuccin Frappé', category: 'dark', isDark: true },
{ id: 'catppuccin-macchiato', label: 'Catppuccin Macchiato', category: 'dark', isDark: true },
{ id: 'catppuccin-mocha', label: 'Catppuccin Mocha', category: 'dark', isDark: true },
// One Light + Tokyo Night Storm
{ id: 'one-light', label: 'One Light', category: 'light', isDark: false },
{ id: 'tokyo-night-storm', label: 'Tokyo Night Storm', category: 'dark', isDark: true },
// Synthwave / Outrun
{ id: 'synthwave-84', label: "Synthwave '84", category: 'dark', isDark: true },
{ id: 'outrun', label: 'Outrun', category: 'dark', isDark: true },
// Winter is Coming (light + dark)
{
id: 'winter-is-coming-light',
label: 'Winter is Coming (Light)',
category: 'light',
isDark: false,
},
{ id: 'winter-is-coming-dark', label: 'Winter is Coming (Dark)', category: 'dark', isDark: true },
// Solarized HC + Spring seasonal
{
id: 'solarized-dark-hc',
label: 'Solarized Dark (High Contrast)',
category: 'high-contrast',
isDark: true,
},
{ id: 'spring-light', label: 'Spring Light', category: 'seasonal', isDark: false },
];
for (const t of THEMES) ThemeRegistry.register(t);
module.exports = { THEMES };
@@ -0,0 +1,99 @@
/**
* @jest-environment node
*
* Bootstrap snapshot — at startup, ThemeRegistry.list() returns exactly 37
* themes (25 existing menu themes + 12 new) in the expected order.
*/
const EXPECTED_IDS = [
// Existing — light
'atomonelight',
'github',
'light',
'solarized',
'gruvbox-light',
'ayu-light',
'sepia',
'paper',
'rosepine-dawn',
'concrete-light',
// Existing — dark
'dark',
'onedark',
'dracula',
'nord',
'monokai',
'material',
'gruvbox-dark',
'tokyonight',
'palenight',
'ayu-dark',
'ayu-mirage',
'oceanic-next',
'cobalt2',
'concrete-dark',
'concrete-warm',
// New — Catppuccin
'catppuccin-latte',
'catppuccin-frappe',
'catppuccin-macchiato',
'catppuccin-mocha',
// New — One Light + Tokyo Night Storm
'one-light',
'tokyo-night-storm',
// New — Synthwave / Outrun
'synthwave-84',
'outrun',
// New — Winter is Coming
'winter-is-coming-light',
'winter-is-coming-dark',
// New — Solarized HC + Spring seasonal
'solarized-dark-hc',
'spring-light',
];
describe('ThemeRegistry.bootstrap', () => {
beforeEach(() => {
// Reset module registry so both ThemeRegistry and bootstrap re-load fresh.
// Otherwise Jest's module cache returns the already-evaluated bootstrap
// (whose registration loop has already run once), leaving list() empty
// across subsequent tests.
jest.resetModules();
});
test('registers exactly 37 themes in the expected order', () => {
const ThemeRegistry = require('../../src/main/ThemeRegistry');
ThemeRegistry.clear();
require('../../src/main/ThemeRegistry.bootstrap');
expect(ThemeRegistry.list().map((t) => t.id)).toEqual(EXPECTED_IDS);
});
test('every theme has a valid shape', () => {
const ThemeRegistry = require('../../src/main/ThemeRegistry');
ThemeRegistry.clear();
require('../../src/main/ThemeRegistry.bootstrap');
for (const t of ThemeRegistry.list()) {
expect(typeof t.id).toBe('string');
expect(typeof t.label).toBe('string');
expect(['light', 'dark', 'high-contrast', 'seasonal']).toContain(t.category);
expect(typeof t.isDark).toBe('boolean');
}
});
test('expected category counts', () => {
const ThemeRegistry = require('../../src/main/ThemeRegistry');
ThemeRegistry.clear();
require('../../src/main/ThemeRegistry.bootstrap');
const counts = ThemeRegistry.list().reduce((acc, t) => {
acc[t.category] = (acc[t.category] || 0) + 1;
return acc;
}, {});
// 10 existing light + one-light + winter-is-coming-light + catppuccin-latte = 13 light (spring-light is category=seasonal)
expect(counts.light).toBe(13);
// 15 existing dark + 3 catppuccin dark + tokyo-night-storm + synthwave-84 + outrun + winter-is-coming-dark = 22 dark
expect(counts.dark).toBe(22);
expect(counts['high-contrast']).toBe(1);
// 1: spring-light (the only seasonal theme in v1)
expect(counts.seasonal).toBe(1);
});
});