diff --git a/src/main/ThemeRegistry.bootstrap.js b/src/main/ThemeRegistry.bootstrap.js new file mode 100644 index 0000000..88a75ac --- /dev/null +++ b/src/main/ThemeRegistry.bootstrap.js @@ -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 }; diff --git a/tests/main/theme-registry-bootstrap.test.js b/tests/main/theme-registry-bootstrap.test.js new file mode 100644 index 0000000..7f0fea6 --- /dev/null +++ b/tests/main/theme-registry-bootstrap.test.js @@ -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); + }); +});