import { build } from 'esbuild'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, '..', '..'); const entry = resolve(root, 'src/components/RuntimeConfigPanel.ts'); class MiniClassList { constructor() { this.values = new Set(); } add(...tokens) { tokens.forEach((token) => this.values.add(token)); } remove(...tokens) { tokens.forEach((token) => this.values.delete(token)); } contains(token) { return this.values.has(token); } toggle(token, force) { if (force === true) { this.values.add(token); return true; } if (force === false) { this.values.delete(token); return false; } if (this.values.has(token)) { this.values.delete(token); return false; } this.values.add(token); return true; } setFromString(value) { this.values = new Set(String(value).split(/\s+/).filter(Boolean)); } toString() { return Array.from(this.values).join(' '); } } class MiniNode extends EventTarget { constructor() { super(); this.childNodes = []; this.parentNode = null; this.parentElement = null; } appendChild(child) { if (child instanceof MiniDocumentFragment) { const children = [...child.childNodes]; children.forEach((node) => this.appendChild(node)); return child; } if (child.parentNode) { child.parentNode.removeChild(child); } child.parentNode = this; child.parentElement = this instanceof MiniElement ? this : null; this.childNodes.push(child); return child; } removeChild(child) { const index = this.childNodes.indexOf(child); if (index >= 0) { this.childNodes.splice(index, 1); child.parentNode = null; child.parentElement = null; } return child; } insertBefore(child, referenceNode) { if (referenceNode == null) { return this.appendChild(child); } if (child.parentNode) { child.parentNode.removeChild(child); } const index = this.childNodes.indexOf(referenceNode); if (index === -1) { return this.appendChild(child); } child.parentNode = this; child.parentElement = this instanceof MiniElement ? this : null; this.childNodes.splice(index, 0, child); return child; } get firstChild() { return this.childNodes[0] ?? null; } get lastChild() { return this.childNodes.at(-1) ?? null; } get textContent() { return this.childNodes.map((child) => child.textContent ?? '').join(''); } set textContent(value) { this.childNodes = [new MiniText(value ?? '')]; } } class MiniText extends MiniNode { constructor(value) { super(); this.value = String(value); } get textContent() { return this.value; } set textContent(value) { this.value = String(value); } get outerHTML() { return this.value; } } class MiniDocumentFragment extends MiniNode { get outerHTML() { return this.childNodes.map((child) => child.outerHTML ?? child.textContent ?? '').join(''); } } class MiniElement extends MiniNode { constructor(tagName) { super(); this.tagName = tagName.toUpperCase(); this.attributes = new Map(); this.classList = new MiniClassList(); this.dataset = {}; this.style = {}; this._innerHTML = ''; this.id = ''; this.title = ''; this.disabled = false; } get className() { return this.classList.toString(); } set className(value) { this.classList.setFromString(value); } get innerHTML() { if (this._innerHTML) return this._innerHTML; return this.childNodes.map((child) => child.outerHTML ?? child.textContent ?? '').join(''); } set innerHTML(value) { this._innerHTML = String(value); this.childNodes = []; } appendChild(child) { this._innerHTML = ''; return super.appendChild(child); } insertBefore(child, referenceNode) { this._innerHTML = ''; return super.insertBefore(child, referenceNode); } removeChild(child) { this._innerHTML = ''; return super.removeChild(child); } setAttribute(name, value) { const stringValue = String(value); this.attributes.set(name, stringValue); if (name === 'class') { this.className = stringValue; } else if (name === 'id') { this.id = stringValue; } else if (name.startsWith('data-')) { const key = name .slice(5) .split('-') .map((part, index) => (index === 0 ? part : `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)) .join(''); this.dataset[key] = stringValue; } } getAttribute(name) { return this.attributes.get(name) ?? null; } hasAttribute(name) { return this.attributes.has(name); } removeAttribute(name) { this.attributes.delete(name); if (name === 'class') this.className = ''; } querySelector() { return null; } querySelectorAll() { return []; } closest() { return null; } remove() { if (this.parentNode) { this.parentNode.removeChild(this); } } getBoundingClientRect() { return { width: 1, height: 1, top: 0, left: 0, right: 1, bottom: 1 }; } get nextElementSibling() { if (!this.parentNode) return null; const siblings = this.parentNode.childNodes.filter((child) => child instanceof MiniElement); const index = siblings.indexOf(this); return index >= 0 ? siblings[index + 1] ?? null : null; } get isConnected() { let current = this.parentNode; while (current) { if (current === globalThis.document?.body || current === globalThis.document?.documentElement) { return true; } current = current.parentNode; } return false; } get outerHTML() { return `<${this.tagName.toLowerCase()}>${this.innerHTML}`; } } class MiniStorage { constructor() { this.values = new Map(); } getItem(key) { return this.values.has(key) ? this.values.get(key) : null; } setItem(key, value) { this.values.set(key, String(value)); } removeItem(key) { this.values.delete(key); } clear() { this.values.clear(); } } class MiniDocument extends EventTarget { constructor() { super(); this.documentElement = new MiniElement('html'); this.documentElement.clientHeight = 800; this.documentElement.clientWidth = 1200; this.body = new MiniElement('body'); this.documentElement.appendChild(this.body); } createElement(tagName) { return new MiniElement(tagName); } createTextNode(value) { return new MiniText(value); } createDocumentFragment() { return new MiniDocumentFragment(); } } function createBrowserEnvironment() { const document = new MiniDocument(); const localStorage = new MiniStorage(); const window = { document, localStorage, innerHeight: 800, innerWidth: 1200, addEventListener() {}, removeEventListener() {}, open() {}, getComputedStyle() { return { display: '', visibility: '', gridTemplateColumns: 'none', columnGap: '0', }; }, }; return { document, localStorage, window, requestAnimationFrame() { return 1; }, cancelAnimationFrame() {}, }; } function snapshotGlobal(name) { return { exists: Object.prototype.hasOwnProperty.call(globalThis, name), value: globalThis[name], }; } function restoreGlobal(name, snapshot) { if (snapshot.exists) { globalThis[name] = snapshot.value; return; } delete globalThis[name]; } function createRuntimeState() { return { features: [], availableIds: new Set(), configuredCount: 0, listeners: new Set(), }; } async function loadRuntimeConfigPanel() { const tempDir = mkdtempSync(join(tmpdir(), 'wm-runtime-config-panel-')); const outfile = join(tempDir, 'RuntimeConfigPanel.bundle.mjs'); const stubModules = new Map([ ['runtime-config-stub', ` const state = globalThis.__wmRuntimeConfigPanelTestState; export const RUNTIME_FEATURES = state.features; export function getEffectiveSecrets() { return []; } export function getRuntimeConfigSnapshot() { const secrets = Object.fromEntries( Array.from({ length: state.configuredCount }, (_, index) => [ 'SECRET_' + (index + 1), { value: 'set', source: 'vault' }, ]), ); return { featureToggles: {}, secrets }; } export function getSecretState() { return { present: false, valid: false, source: 'missing' }; } export function isFeatureAvailable(featureId) { return state.availableIds.has(featureId); } export function isFeatureEnabled() { return true; } export function setFeatureToggle() {} export async function setSecretValue() {} export function subscribeRuntimeConfig(listener) { state.listeners.add(listener); return () => state.listeners.delete(listener); } export function validateSecret() { return { valid: true }; } export async function verifySecretWithApi() { return { valid: true }; } `], ['runtime-stub', `export function isDesktopRuntime() { return true; }`], ['tauri-bridge-stub', `export async function invokeTauri() {}`], ['i18n-stub', `export function t(key) { return key; }`], ['dom-utils-stub', ` function append(parent, child) { if (child == null || child === false) return; if (typeof child === 'string' || typeof child === 'number') { parent.appendChild(document.createTextNode(String(child))); return; } parent.appendChild(child); } export function h(tag, propsOrChild, ...children) { const el = document.createElement(tag); let allChildren = children; if ( propsOrChild != null && typeof propsOrChild === 'object' && !('tagName' in propsOrChild) && !('textContent' in propsOrChild) ) { for (const [key, value] of Object.entries(propsOrChild)) { if (value == null || value === false) continue; if (key === 'className') { el.className = value; } else if (key === 'style' && typeof value === 'object') { Object.assign(el.style, value); } else if (key === 'dataset' && typeof value === 'object') { Object.assign(el.dataset, value); } else if (key.startsWith('on') && typeof value === 'function') { el.addEventListener(key.slice(2).toLowerCase(), value); } else if (value === true) { el.setAttribute(key, ''); } else { el.setAttribute(key, String(value)); } } } else { allChildren = [propsOrChild, ...children]; } allChildren.forEach((child) => append(el, child)); return el; } export function replaceChildren(el, ...children) { el.innerHTML = ''; children.forEach((child) => append(el, child)); } export function safeHtml() { return document.createDocumentFragment(); } `], ['analytics-stub', `export function trackPanelResized() {} export function trackFeatureToggle() {}`], ['ai-flow-settings-stub', `export function getAiFlowSettings() { return { badgeAnimation: false }; }`], ['sanitize-stub', `export function escapeHtml(value) { return String(value); }`], ['ollama-models-stub', `export async function fetchOllamaModels() { return []; }`], ['settings-constants-stub', ` export const SIGNUP_URLS = {}; export const PLAINTEXT_KEYS = new Set(); export const MASKED_SENTINEL = '***'; `], ]); const aliasMap = new Map([ ['@/services/runtime-config', 'runtime-config-stub'], ['../services/runtime', 'runtime-stub'], ['@/services/runtime', 'runtime-stub'], ['../services/tauri-bridge', 'tauri-bridge-stub'], ['@/services/tauri-bridge', 'tauri-bridge-stub'], ['../services/i18n', 'i18n-stub'], ['@/services/i18n', 'i18n-stub'], ['../utils/dom-utils', 'dom-utils-stub'], ['@/services/analytics', 'analytics-stub'], ['@/services/ai-flow-settings', 'ai-flow-settings-stub'], ['@/utils/sanitize', 'sanitize-stub'], ['@/services/ollama-models', 'ollama-models-stub'], ['@/services/settings-constants', 'settings-constants-stub'], ]); const plugin = { name: 'runtime-config-panel-test-stubs', setup(buildApi) { buildApi.onResolve({ filter: /.*/ }, (args) => { const target = aliasMap.get(args.path); return target ? { path: target, namespace: 'stub' } : null; }); buildApi.onLoad({ filter: /.*/, namespace: 'stub' }, (args) => ({ contents: stubModules.get(args.path), loader: 'js', })); }, }; const result = await build({ entryPoints: [entry], bundle: true, format: 'esm', platform: 'browser', target: 'es2020', write: false, plugins: [plugin], }); writeFileSync(outfile, result.outputFiles[0].text, 'utf8'); const mod = await import(`${pathToFileURL(outfile).href}?t=${Date.now()}`); return { RuntimeConfigPanel: mod.RuntimeConfigPanel, cleanupBundle() { rmSync(tempDir, { recursive: true, force: true }); }, }; } export async function createRuntimeConfigPanelHarness() { const originalGlobals = { document: snapshotGlobal('document'), window: snapshotGlobal('window'), localStorage: snapshotGlobal('localStorage'), requestAnimationFrame: snapshotGlobal('requestAnimationFrame'), cancelAnimationFrame: snapshotGlobal('cancelAnimationFrame'), }; const browserEnvironment = createBrowserEnvironment(); const runtimeState = createRuntimeState(); globalThis.document = browserEnvironment.document; globalThis.window = browserEnvironment.window; globalThis.localStorage = browserEnvironment.localStorage; globalThis.requestAnimationFrame = browserEnvironment.requestAnimationFrame; globalThis.cancelAnimationFrame = browserEnvironment.cancelAnimationFrame; globalThis.__wmRuntimeConfigPanelTestState = runtimeState; let RuntimeConfigPanel; let cleanupBundle; try { ({ RuntimeConfigPanel, cleanupBundle } = await loadRuntimeConfigPanel()); } catch (error) { delete globalThis.__wmRuntimeConfigPanelTestState; restoreGlobal('document', originalGlobals.document); restoreGlobal('window', originalGlobals.window); restoreGlobal('localStorage', originalGlobals.localStorage); restoreGlobal('requestAnimationFrame', originalGlobals.requestAnimationFrame); restoreGlobal('cancelAnimationFrame', originalGlobals.cancelAnimationFrame); throw error; } const activePanels = []; function setRuntimeState({ totalFeatures, availableFeatures, configuredCount, }) { runtimeState.features.splice( 0, runtimeState.features.length, ...Array.from({ length: totalFeatures }, (_, index) => ({ id: `feature-${index + 1}` })), ); runtimeState.availableIds = new Set( runtimeState.features.slice(0, availableFeatures).map((feature) => feature.id), ); runtimeState.configuredCount = configuredCount; } function createPanel(options = { mode: 'alert' }) { const panel = new RuntimeConfigPanel(options); activePanels.push(panel); return panel; } function emitRuntimeConfigChange() { for (const listener of [...runtimeState.listeners]) { listener(); } } function isHidden(panel) { return panel.getElement().classList.contains('hidden'); } function getAlertState(panel) { const match = panel.content.innerHTML.match(/data-alert-state="([^"]+)"/); return match?.[1] ?? null; } function reset() { while (activePanels.length > 0) { activePanels.pop()?.destroy(); } runtimeState.features.length = 0; runtimeState.availableIds = new Set(); runtimeState.configuredCount = 0; runtimeState.listeners.clear(); browserEnvironment.localStorage.clear(); } function cleanup() { reset(); cleanupBundle(); delete globalThis.__wmRuntimeConfigPanelTestState; restoreGlobal('document', originalGlobals.document); restoreGlobal('window', originalGlobals.window); restoreGlobal('localStorage', originalGlobals.localStorage); restoreGlobal('requestAnimationFrame', originalGlobals.requestAnimationFrame); restoreGlobal('cancelAnimationFrame', originalGlobals.cancelAnimationFrame); } return { createPanel, emitRuntimeConfigChange, getAlertState, isHidden, reset, cleanup, setRuntimeState, }; }