/** * Test mocks: fetch (NDJSON stream with controllable chunk boundaries), * browser storage, and browser runtime port. */ (() => { 'use strict'; /** * Create a Response whose body streams the given NDJSON lines, split into * arbitrary byte chunks (to test JSON-boundary tolerance). * @param {string[]} lines NDJSON lines * @param {number[]} [chunkSizes] byte sizes per chunk (cycled) * @returns {Response} */ function ndjsonResponse(lines, chunkSizes) { const text = lines.join('\n') + '\n'; const bytes = new TextEncoder().encode(text); const sizes = chunkSizes && chunkSizes.length ? chunkSizes : [bytes.length]; let offset = 0; let index = 0; const stream = new ReadableStream({ pull(controller) { if (offset >= bytes.length) { controller.close(); return; } const size = sizes[index % sizes.length]; index++; const end = Math.min(offset + size, bytes.length); controller.enqueue(bytes.slice(offset, end)); offset = end; } }); return new Response(stream, { status: 200, headers: { 'Content-Type': 'application/x-ndjson' } }); } /** * @param {number} status * @returns {Response} */ function errorResponse(status) { return new Response(JSON.stringify({ error: 'boom' }), { status, headers: { 'Content-Type': 'application/json' } }); } /** * Fake browser.storage.local backed by a Map. * @returns {object} browser-like API */ function fakeStorage() { 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'); } }; return { local, onChanged: { addListener(fn) { listeners.push(fn); } } }; } /** * Fake runtime.Port with a message queue. */ function fakePort() { const listeners = { message: [], disconnect: [] }; return { name: 'translation-stream', sender: { id: 'test-extension' }, postMessage: () => {}, onMessage: { addListener: (fn) => listeners.message.push(fn) }, onDisconnect: { addListener: (fn) => listeners.disconnect.push(fn) }, __emit(message) { for (const fn of listeners.message) fn(message); }, __emitDisconnect() { for (const fn of listeners.disconnect) fn(); } }; } /** * Fake browser.runtime sufficient for message-router tests. */ function fakeRuntime() { const connectListeners = []; const messageListeners = []; return { onConnect: { addListener: (fn) => connectListeners.push(fn), __emit(port) { for (const fn of connectListeners) fn(port); } }, onMessage: { addListener: (fn) => messageListeners.push(fn), __listener() { return messageListeners[messageListeners.length - 1]; } } }; } window.TestMocks = { ndjsonResponse, errorResponse, fakeStorage, fakePort, fakeRuntime }; })();