/** * UI smoke tests: load the real page scripts (popup.js / options.js) as ES * modules against a mocked browser API and verify that they initialize the * DOM. This catches import/export mismatches that pure unit tests miss * (e.g. a named import that the module does not export -> SyntaxError and * the whole page script never runs). */ import { LANGUAGES } from '../src/shared/constants.js'; const { describe, it, assertTrue, assertEqual } = window.TestRunner; /** * Install a fake `browser` global before importing page scripts. */ function installFakeBrowser() { const data = new Map(); const listeners = []; const local = { async get(defaults) { const result = {}; for (const key of Object.keys(defaults)) { result[key] = data.has(key) ? data.get(key) : defaults[key]; } return result; }, async set(obj) { const changes = {}; for (const [key, value] of Object.entries(obj)) { changes[key] = { oldValue: data.get(key), newValue: value }; data.set(key, value); } for (const listener of listeners) listener(changes, 'local'); } }; globalThis.browser = { storage: { local, onChanged: { addListener(fn) { listeners.push(fn); } } }, runtime: { /** @returns {Promise<{ok: boolean, models?: string[]}>} */ sendMessage: async () => ({ ok: true, models: ['fake-model'] }), openOptionsPage: () => {} } }; return globalThis.browser; } /** * Run an ES module script by importing it with a cache-busting query. * The page's markup is added inside a sandbox container; the script runs, * async init settles, assertions are collected via a probe callback, and * finally the sandbox is removed and the runner's own elements restored. * @param {string} scriptPath * @param {string} markup * @param {() => *} probe assertions to run while the sandbox is attached */ async function runPageScript(scriptPath, markup, probe) { installFakeBrowser(); // Preserve the runner's result elements across the DOM swap. const results = document.getElementById('results'); const summary = document.getElementById('summary'); const resultsParent = results ? results.parentNode : null; const summaryParent = summary ? summary.parentNode : null; const resultsNext = results ? results.nextSibling : null; const summaryNext = summary ? summary.nextSibling : null; const sandbox = document.createElement('div'); sandbox.id = 'smoke-sandbox'; sandbox.innerHTML = markup; document.body.append(sandbox); try { // Import with cache-buster so repeated runs re-execute the script. await import(`${scriptPath}?smoke=${Date.now()}-${Math.random().toString(36).slice(2)}`); // Let async init settle. await new Promise((resolve) => setTimeout(resolve, 50)); return probe(); } finally { sandbox.remove(); if (results && resultsParent) resultsParent.insertBefore(results, resultsNext); if (summary && summaryParent) summaryParent.insertBefore(summary, summaryNext); // Reset the fake browser so later tests use the real API if needed. delete globalThis.browser; } } describe('popup page script smoke', () => { it('initializes language options and model select from markup', async () => { const r = await runPageScript('../src/popup/popup.js', `
`, () => ({ langOptions: document.getElementById('language-select').options.length, langValue: document.getElementById('language-select').value, modelOptions: document.getElementById('model-select').options.length, firstModel: document.getElementById('model-select').options[0]?.textContent ?? null })); assertEqual(r.langOptions, LANGUAGES.length); assertEqual(r.langValue, 'ja'); // default target language assertTrue(r.modelOptions > 0, 'model select should be populated'); assertEqual(r.firstModel, 'fake-model'); }); }); describe('options page script smoke', () => { it('initializes language options and loads models into the select', async () => { const r = await runPageScript('../src/options/options.js', ` `, () => ({ langOptions: document.getElementById('target-language').options.length, langValue: document.getElementById('target-language').value, modelOptions: document.getElementById('model').options.length, firstModel: document.getElementById('model').options[0]?.textContent ?? null, host: document.getElementById('ollama-host').value })); assertEqual(r.langOptions, LANGUAGES.length); assertEqual(r.langValue, 'ja'); assertTrue(r.modelOptions > 0, 'model select should be populated by loadModels'); assertEqual(r.firstModel, 'fake-model'); assertEqual(r.host, 'http://localhost:11434'); }); });