Files
ComfyAssets-ComfyUI_PromptM…/tests/js/workspace-theme.test.js
Vito d4bb5c5ba9 feat(ui): add the tested opt-in Prompt Manager workspace shell (#198)
* docs(ui): prototype unified prompt management application

* docs(ui): add masonry and per-image tagging to application concept

* docs(ui): make gallery concept use continuous infinite scrolling

* feat(ui): introduce the tested application workspace shell

* fix(docs): preserve concept controls across cached navigation

* test(ci): emit Python tracebacks for native test crashes

* test(ui): cover workspace restoration across cached navigation
2026-10-17 10:36:30 -07:00

143 lines
7.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createScope } from '../../web/js/shared/toolkit/core.js';
import { mountWorkspaceTheme } from '../../web/js/workspace/theme.mjs';
function fixture({ dark = false, preference, fetchImpl, storageDenied = false, pathname = '/prompt_manager/workspace' } = {}) {
const media = new EventTarget(); media.matches = dark;
const win = new EventTarget(); win.location = { pathname }; win.matchMedia = () => media;
const documentElement = { style: { colorScheme: 'light dark' } };
const root = { dataset: {}, ownerDocument: { documentElement } };
const saved = new Map(preference === undefined ? [] : [['pm_workspace_theme', preference]]);
const storage = {
getItem(key) { if (storageDenied) throw new Error('denied'); return saved.get(key); },
setItem(key, value) { if (storageDenied) throw new Error('denied'); saved.set(key, value); },
};
const calls = [];
const scope = createScope();
const theme = mountWorkspaceTheme({ root, scope, win, storage, fetchImpl: fetchImpl || (async (url, init) => {
calls.push({ url, init }); return { ok: true, json: async () => 'dark' };
}) });
return { root, documentElement, saved, media, win, scope, theme, calls };
}
const response = value => ({ ok: true, json: async () => value });
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
test('follow uses host palette over OS and preserves reverse proxy prefix', async () => {
const f = fixture({ pathname: '/proxy/comfy/prompt_manager/workspace' });
await f.theme.ready;
assert.equal(f.root.dataset.caTheme, 'dark');
assert.equal(f.documentElement.style.colorScheme, 'dark');
assert.equal(f.calls[0].url, '/proxy/comfy/api/settings/Comfy.ColorPalette');
f.scope.dispose();
});
test('supported light palettes accept string and wrapped values', async () => {
for (const palette of ['light', 'solarized', 'github', 'arc']) {
const f = fixture({ dark: true, fetchImpl: async () => response({ value: palette }) });
await f.theme.ready;
assert.equal(f.root.dataset.caTheme, 'light', palette);
f.scope.dispose();
}
});
test('explicit overrides persist and follow restores host choice', async () => {
const f = fixture({ preference: 'light' }); await f.theme.ready;
assert.equal(f.root.dataset.caTheme, 'light');
f.theme.setPreference('dark'); assert.equal(f.root.dataset.caTheme, 'dark');
assert.equal(f.saved.get('pm_workspace_theme'), 'dark');
f.theme.setPreference('follow'); assert.equal(f.root.dataset.caTheme, 'dark');
assert.equal(f.saved.get('pm_workspace_theme'), 'follow');
assert.throws(() => f.theme.setPreference('orange'), /preference/i);
f.scope.dispose();
});
test('unknown host palettes and invalid stored preferences follow changing OS', async () => {
const f = fixture({ preference: 'orange', fetchImpl: async () => response('custom') });
await f.theme.ready; assert.equal(f.root.dataset.caTheme, 'light');
f.media.matches = true; f.media.dispatchEvent(new Event('change'));
assert.equal(f.root.dataset.caTheme, 'dark'); f.scope.dispose();
});
test('network, HTTP, JSON and storage failures recover using system theme', async () => {
for (const fetchImpl of [async () => { throw new Error('offline'); }, async () => ({ ok: false }), async () => ({ ok: true, json: async () => { throw new Error('invalid'); } })]) {
const f = fixture({ dark: true, storageDenied: true, fetchImpl });
await f.theme.ready; assert.equal(f.root.dataset.caTheme, 'dark');
assert.doesNotThrow(() => f.theme.setPreference('light'));
assert.equal(f.root.dataset.caTheme, 'light'); f.scope.dispose();
}
});
test('focus refreshes host palette and stale responses cannot replace newest palette', async () => {
const pending = [];
const f = fixture({ fetchImpl: (url, init) => new Promise(resolve => pending.push({ resolve, signal: init.signal })) });
f.win.dispatchEvent(new Event('focus'));
assert.equal(pending.length, 2); assert.equal(pending[0].signal.aborted, true);
pending[1].resolve(response('light')); await tick();
pending[0].resolve(response('dark')); await f.theme.ready;
assert.equal(f.root.dataset.caTheme, 'light'); f.scope.dispose();
});
test('cleanup aborts requests, restores attributes and native scheme and removes listeners', async () => {
let resolve, fSignal;
const f = fixture({ fetchImpl: (url, init) => new Promise(done => { resolve = done; fSignal = init.signal; }) });
f.scope.dispose(); assert.equal(fSignal.aborted, true);
assert.equal(f.root.dataset.caTheme, undefined);
assert.equal(f.documentElement.style.colorScheme, 'light dark');
f.win.dispatchEvent(new Event('focus')); f.media.dispatchEvent(new Event('change'));
resolve(response('dark')); await f.theme.ready;
assert.equal(f.root.dataset.caTheme, undefined);
f.theme.setPreference('dark'); assert.equal(f.root.dataset.caTheme, undefined);
});
test('cleanup restores an existing toolkit theme', async () => {
const f = fixture(); await f.theme.ready; f.scope.dispose();
f.root.dataset.caTheme = 'light';
const scope = createScope();
const theme = mountWorkspaceTheme({ root: f.root, scope, win: f.win, storage: f.saved, fetchImpl: async () => response('dark') });
await theme.ready; scope.dispose(); assert.equal(f.root.dataset.caTheme, 'light');
});
test('malformed palette payloads fall back without losing native theme alignment', async () => {
for (const payload of [null, {}, {value: 42}, 42]) {
const f = fixture({ dark: true, fetchImpl: async () => response(payload) });
await f.theme.ready;
assert.equal(f.root.dataset.caTheme, 'dark');
assert.equal(f.documentElement.style.colorScheme, 'dark');
f.scope.dispose();
}
});
test('focus failure clears obsolete host palette and resumes system following', async () => {
let calls = 0;
const f = fixture({ fetchImpl: async () => {
if (++calls === 1) return response('dark');
throw new Error('host offline');
} });
await f.theme.ready;
assert.equal(f.root.dataset.caTheme, 'dark');
f.win.dispatchEvent(new Event('focus')); await tick();
assert.equal(f.root.dataset.caTheme, 'light');
f.scope.dispose();
f.win.dispatchEvent(new Event('focus')); await tick();
assert.equal(calls, 2);
});
test('blocked localStorage getter and missing proxy marker stay usable', async () => {
const f = fixture(); await f.theme.ready; f.scope.dispose();
Object.defineProperty(f.win, 'localStorage', { get() { throw new Error('denied'); } });
f.win.location.pathname = '/standalone';
const scope = createScope();
let url;
const theme = mountWorkspaceTheme({ root: f.root, win: f.win, scope,
fetchImpl: async value => { url = value; return response(' Obsidian '); } });
assert.equal(theme.preference, 'follow');
await theme.ready;
assert.equal(url, '/api/settings/Comfy.ColorPalette');
assert.equal(f.root.dataset.caTheme, 'dark');
theme.setPreference('light');
assert.equal(theme.preference, 'light');
scope.dispose();
});