mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(theme-registry): buildThemeMenu converts registry → Electron MenuItem[]
Adds src/main/themeMenuBuilder.js — a pure module that consumes ThemeRegistry.list() + ThemeRegistry.categories() and the injected setTheme/getCurrentThemeId callbacks to produce the View → Theme submenu's MenuItemTemplate[] in the same shape as the previous hardcoded block in src/main.js:1137-1245. - Radio-style items with checked=true on the active theme id - Grouped by category in registry order with separators between non-empty categories - No Electron / electron-store imports — keeps the module pure and unit-testable under @jest-environment node - Tests use jest.resetModules() + per-test require to avoid the module-cache leakage the bootstrap test surfaced in T2 Amit Haridas
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Editor Theme menu builder — converts ThemeRegistry output into Electron
|
||||
* MenuItem[] suitable for the View → Theme submenu.
|
||||
*
|
||||
* Pure: takes `setTheme` + `getCurrentThemeId` as injected dependencies so
|
||||
* the builder never touches electron-store or `mainWindow`. Tested in
|
||||
* isolation under `@jest-environment node`.
|
||||
*
|
||||
* Menu shape (matches the previous hardcoded block in src/main.js:1137-1245):
|
||||
* [item, item, …, { type: 'separator' }, item, item, …, separator, …]
|
||||
* grouped by category in registry order, with separators between non-empty
|
||||
* categories. Each theme item is a radio-style MenuItem so Electron shows a
|
||||
* checkmark next to the active theme.
|
||||
*
|
||||
* @module themeMenuBuilder
|
||||
*/
|
||||
|
||||
const ThemeRegistry = require('./ThemeRegistry');
|
||||
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {(id: string) => void} deps.setTheme
|
||||
* @param {() => string} deps.getCurrentThemeId
|
||||
* @returns {Array<object>} Electron MenuItemTemplate[]
|
||||
*/
|
||||
function buildThemeMenu({ setTheme, getCurrentThemeId }) {
|
||||
const currentId = getCurrentThemeId();
|
||||
const items = [];
|
||||
const cats = ThemeRegistry.categories();
|
||||
let nonEmptySeen = 0;
|
||||
|
||||
for (const cat of cats) {
|
||||
const inCat = ThemeRegistry.list().filter((t) => t.category === cat);
|
||||
if (inCat.length === 0) continue;
|
||||
if (nonEmptySeen > 0) items.push({ type: 'separator' });
|
||||
nonEmptySeen++;
|
||||
|
||||
for (const t of inCat) {
|
||||
items.push({
|
||||
label: t.label,
|
||||
type: 'radio',
|
||||
checked: t.id === currentId,
|
||||
click: () => setTheme(t.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
module.exports = { buildThemeMenu };
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*
|
||||
* themeMenuBuilder tests — pure module. We pass in a fake `setTheme` and a
|
||||
* fake `getCurrentThemeId` so we never touch electron-store or Electron.
|
||||
*
|
||||
* Note on isolation: ThemeRegistry holds state in a module-scoped Map.
|
||||
* `jest.resetModules()` drops the cache so each test re-requires a fresh
|
||||
* ThemeRegistry + buildThemeMenu pair, preventing leakage across tests.
|
||||
*/
|
||||
let ThemeRegistry;
|
||||
let buildThemeMenu;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ThemeRegistry = require('../../src/main/ThemeRegistry');
|
||||
buildThemeMenu = require('../../src/main/themeMenuBuilder').buildThemeMenu;
|
||||
ThemeRegistry.clear();
|
||||
});
|
||||
|
||||
describe('buildThemeMenu', () => {
|
||||
test('groups themes by category in registry order', () => {
|
||||
ThemeRegistry.register({ id: 'l1', label: 'L1', category: 'light', isDark: false });
|
||||
ThemeRegistry.register({ id: 'l2', label: 'L2', category: 'light', isDark: false });
|
||||
ThemeRegistry.register({ id: 'd1', label: 'D1', category: 'dark', isDark: true });
|
||||
ThemeRegistry.register({ id: 'd2', label: 'D2', category: 'dark', isDark: true });
|
||||
ThemeRegistry.register({ id: 'hc1', label: 'HC1', category: 'high-contrast', isDark: true });
|
||||
ThemeRegistry.register({ id: 's1', label: 'S1', category: 'seasonal', isDark: false });
|
||||
|
||||
const setTheme = jest.fn();
|
||||
const items = buildThemeMenu({ setTheme, getCurrentThemeId: () => 'd1' });
|
||||
|
||||
// Expected order: light (2 items), separator, dark (2 items), separator, hc (1), separator, seasonal (1)
|
||||
expect(items.map((i) => i.label || i.type)).toEqual([
|
||||
'L1',
|
||||
'L2',
|
||||
'separator',
|
||||
'D1',
|
||||
'D2',
|
||||
'separator',
|
||||
'HC1',
|
||||
'separator',
|
||||
'S1',
|
||||
]);
|
||||
});
|
||||
|
||||
test('marks the currently-selected theme as checked=true', () => {
|
||||
ThemeRegistry.register({ id: 'a', label: 'A', category: 'light', isDark: false });
|
||||
ThemeRegistry.register({ id: 'b', label: 'B', category: 'dark', isDark: true });
|
||||
|
||||
const items = buildThemeMenu({ setTheme: jest.fn(), getCurrentThemeId: () => 'b' });
|
||||
const aItem = items.find((i) => i.label === 'A');
|
||||
const bItem = items.find((i) => i.label === 'B');
|
||||
expect(aItem.type).toBe('radio');
|
||||
expect(aItem.checked).toBe(false);
|
||||
expect(bItem.type).toBe('radio');
|
||||
expect(bItem.checked).toBe(true);
|
||||
});
|
||||
|
||||
test('click handler calls setTheme with the theme id', () => {
|
||||
ThemeRegistry.register({ id: 'a', label: 'A', category: 'light', isDark: false });
|
||||
const setTheme = jest.fn();
|
||||
const items = buildThemeMenu({ setTheme, getCurrentThemeId: () => 'a' });
|
||||
items[0].click();
|
||||
expect(setTheme).toHaveBeenCalledWith('a');
|
||||
});
|
||||
|
||||
test('falls back to no theme checked when current id unknown', () => {
|
||||
ThemeRegistry.register({ id: 'a', label: 'A', category: 'light', isDark: false });
|
||||
const items = buildThemeMenu({
|
||||
setTheme: jest.fn(),
|
||||
getCurrentThemeId: () => 'deleted-theme',
|
||||
});
|
||||
expect(items.find((i) => i.label === 'A').checked).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user