Browse + install color themes from the VS Code Marketplace straight from Cmd-K and Settings → Appearance. The Electron main process resolves the extension, unzips the .vsix with a hand-rolled zip reader (zlib only, no new deps), and hands back the raw theme JSON; the renderer converts it to a DesktopTheme with a small seed → color-mix mapping. - Folds an extension's light + dark variants into one theme family, so the light/dark toggle switches Solarized/GitHub variants and installing in dark mode stays dark. - Guarantees accent contrast (WCAG AA) so imported sidebar labels read instead of vanishing into the surface. - Filters icon/product-icon packs out of the Themes-category search. - "Install theme…" lives atop the Cmd-K theme picker; imports fold into the Light/Dark groups by the modes they support.
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { BUILTIN_THEMES, DEFAULT_SKIN_NAME } from './presets'
|
|
import { $userThemes, installUserTheme, isUserTheme, listAllThemes, removeUserTheme, resolveTheme } from './user-themes'
|
|
import { convertVscodeColorTheme } from './vscode'
|
|
|
|
const makeTheme = (label: string) =>
|
|
convertVscodeColorTheme({
|
|
name: label,
|
|
type: 'dark',
|
|
colors: { 'editor.background': '#101014', 'editor.foreground': '#fafafa', focusBorder: '#7aa2f7' }
|
|
}).theme
|
|
|
|
describe('user theme registry', () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear()
|
|
$userThemes.set({})
|
|
})
|
|
|
|
it('installs a theme into the merged registry and persists it', () => {
|
|
const theme = installUserTheme(makeTheme('Tokyo Night'))
|
|
|
|
expect(isUserTheme(theme.name)).toBe(true)
|
|
expect(resolveTheme(theme.name)).toEqual(theme)
|
|
expect(listAllThemes().map(t => t.name)).toContain(theme.name)
|
|
expect(window.localStorage.getItem('hermes-desktop-user-themes-v1')).toContain(theme.name)
|
|
})
|
|
|
|
it('lists built-ins before user themes', () => {
|
|
installUserTheme(makeTheme('Custom'))
|
|
const names = listAllThemes().map(t => t.name)
|
|
|
|
expect(names.slice(0, Object.keys(BUILTIN_THEMES).length)).toEqual(Object.keys(BUILTIN_THEMES))
|
|
expect(names.at(-1)).toBe('vsc-custom')
|
|
})
|
|
|
|
it('removes a theme', () => {
|
|
const theme = installUserTheme(makeTheme('Throwaway'))
|
|
removeUserTheme(theme.name)
|
|
|
|
expect(isUserTheme(theme.name)).toBe(false)
|
|
expect(resolveTheme(theme.name)).toBeUndefined()
|
|
})
|
|
|
|
it('resolves built-ins through the same lookup', () => {
|
|
expect(resolveTheme(DEFAULT_SKIN_NAME)).toBe(BUILTIN_THEMES[DEFAULT_SKIN_NAME])
|
|
})
|
|
|
|
it('refuses to shadow a built-in name', () => {
|
|
const builtinName = makeTheme('x')
|
|
builtinName.name = DEFAULT_SKIN_NAME
|
|
|
|
expect(() => installUserTheme(builtinName)).toThrow(/built-in/)
|
|
})
|
|
|
|
it('rejects a theme missing required colors', () => {
|
|
const broken = makeTheme('Broken')
|
|
// @ts-expect-error — intentionally corrupt the palette for the test.
|
|
broken.colors = { background: '#000000' }
|
|
|
|
expect(() => installUserTheme(broken)).toThrow(/colors/)
|
|
})
|
|
})
|