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'; import { createBrowserEnvironment } from './mini-dom.mts'; export { createBrowserEnvironment }; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, '..', '..'); const entry = resolve(root, 'src/components/RuntimeConfigPanel.ts'); 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 trustedHtml(html) { return String(html ?? ''); } export function setTrustedHtml(el, html) { el.innerHTML = String(html ?? ''); } 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); } export function safeHtmlToString(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 = '***'; `], ['panel-gating-stub', ` export const PanelGateReason = { NONE: 'none', ANONYMOUS: 'anonymous', UNVERIFIED: 'unverified', FREE_TIER: 'free_tier' }; export function getPanelGateReason() { return PanelGateReason.NONE; } `], ['dodo-checkout-stub', ` export const DodoPayments = { Initialize() {}, Checkout: { open() {}, }, }; `], ['dodo-empty-stub', 'export {};'], ]); 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'], ['@/services/panel-gating', 'panel-gating-stub'], ['dodopayments-checkout', 'dodo-checkout-stub'], ['dodopayments', 'dodo-empty-stub'], ['@dodopayments/core', 'dodo-empty-stub'], ['@dodopayments/convex', 'dodo-empty-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, }; }