/** * Tests for the runtime theme-CSS security floor and injection sync. * Community themes are free-form; the floor only blocks the hard safety * invariants (network, scripts, breakout, unscoped @keyframes, size). */ import { afterEach, describe, expect, it } from 'vitest'; import { validateThemeCss, injectTheme, syncInjectedThemes, } from './themeInjection'; import type { InstalledTheme } from '../../store/installedThemesStore'; const ATTR = 'data-installed-theme'; function mk(id: string, css: string): InstalledTheme { return { id, name: id, author: 'a', version: '1.0.0', description: '', mode: 'dark', css, installedAt: 0 }; } const block = (id: string, body = '--accent:#fff;') => `[data-theme='${id}']{ ${body} }`; const injected = () => document.head.querySelectorAll(`style[${ATTR}]`); afterEach(() => { document.head.querySelectorAll(`style[${ATTR}]`).forEach((el) => el.remove()); }); describe('validateThemeCss (security floor)', () => { it('accepts a simple scoped rule', () => { expect(validateThemeCss(block('dracula'), 'dracula')).not.toBeNull(); }); it('accepts free-form selectors and structure', () => { const css = `html { color: red; } .sidebar { background: #000; } [data-theme='x'] .player-bar { opacity: 0.9; }`; expect(validateThemeCss(css, 'x')).not.toBeNull(); }); it('accepts @media and multiple rules', () => { const css = `${block('x')} @media (min-width: 600px) { .sidebar { width: 200px; } }`; expect(validateThemeCss(css, 'x')).not.toBeNull(); }); it('accepts @keyframes namespaced with the theme id, and its animation use', () => { const css = `@keyframes x-pulse { from { opacity: 1 } to { opacity: 0.5 } } .sidebar { animation: x-pulse 2s infinite; }`; expect(validateThemeCss(css, 'x')).not.toBeNull(); }); it('accepts a data: url()', () => { const css = block('x', `--select-arrow: url("data:image/svg+xml,%3Csvg%3E%3C/svg%3E");`); expect(validateThemeCss(css, 'x')).not.toBeNull(); }); it('rejects @keyframes not namespaced with the theme id', () => { expect(validateThemeCss(`@keyframes pulse { from {} to {} } ${block('x')}`, 'x')).toBeNull(); }); it('rejects @import', () => { expect(validateThemeCss(`@import 'evil.css'; ${block('x')}`, 'x')).toBeNull(); }); it('rejects @property (global custom-prop registration)', () => { const css = `@property --x { syntax: ''; inherits: false; initial-value: red; } ${block('x')}`; expect(validateThemeCss(css, 'x')).toBeNull(); }); it('rejects a non-data url()', () => { expect(validateThemeCss(block('x', `--accent: url(https://evil.test/x.png);`), 'x')).toBeNull(); }); it('rejects /