Files
swyxffplugin/test-server/test-background.mjs
T
SvenandClaude Opus 5 99757361ef first commit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:05:58 +02:00

210 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import vm from "node:vm";
import path from "node:path";
import { fileURLToPath } from "node:url";
// Prüft die Kommandoverarbeitung von background.js mit gestubbten browser-/WebSocket-APIs.
const SRC = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "background.js");
let tabs = [
{ id: 42, windowId: 1, index: 0, title: "Kundenakte Muster GmbH", url: "https://crm.example.local/kunden/4711", active: false, status: "complete" },
{ id: 43, windowId: 1, index: 1, title: "Posteingang", url: "https://mail.example.local/", active: true, status: "complete" },
{ id: 44, windowId: 2, index: 0, title: "Suche", url: "https://www.example.com/?q=1", active: true, status: "complete" }
];
let nextTabId = 100;
const calls = [];
const broadcasts = []; // browser.runtime.sendMessage aus dem Hintergrundskript
const messageListeners = []; // Empfaenger fuer Popup-/Options-Anfragen
const browserStub = {
storage: {
local: { get: async (defaults) => ({ ...defaults }), set: async () => {} },
onChanged: { addListener: () => {} }
},
runtime: {
getManifest: () => ({ version: "1.0.0" }),
sendMessage: (msg) => { broadcasts.push(msg); return Promise.resolve(); },
onMessage: { addListener: (fn) => { messageListeners.push(fn); } }
},
browserAction: { setBadgeText: () => {}, setBadgeBackgroundColor: () => {} },
tabs: {
query: async (q) => tabs.filter((t) => {
if (q.currentWindow && t.windowId !== 1) return false;
if (q.windowId !== undefined && t.windowId !== q.windowId) return false;
if (q.active && !t.active) return false;
return true;
}),
update: async (id, props) => {
const tab = tabs.find((t) => t.id === id);
if (!tab) throw new Error(`Invalid tab ID: ${id}`);
if (props.active) tabs.forEach((t) => { if (t.windowId === tab.windowId) t.active = t.id === id; });
calls.push(`tabs.update(${id})`);
return tab;
},
create: async (props) => {
const tab = { id: nextTabId++, windowId: props.windowId ?? 1, index: tabs.length, title: "", url: props.url, active: props.active !== false, status: "loading" };
tabs.push(tab);
calls.push(`tabs.create(${props.url})`);
return tab;
},
remove: async (idOrIds) => {
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
for (const id of ids) {
if (!tabs.some((t) => t.id === id)) throw new Error(`Invalid tab ID: ${id}`);
}
tabs = tabs.filter((t) => !ids.includes(t.id));
calls.push(`tabs.remove(${JSON.stringify(idOrIds)})`);
}
},
windows: {
getAll: async () => [{ id: 1 }, { id: 2 }],
update: async (id) => { calls.push(`windows.update(${id})`); return { id }; },
create: async (props) => {
const tab = { id: nextTabId++, windowId: 9, index: 0, title: "", url: props.url, active: true };
tabs.push(tab);
return { id: 9, tabs: [tab] };
}
}
};
const sent = [];
let sock = null;
class WebSocketStub {
static OPEN = 1; static CONNECTING = 0;
constructor(url) {
this.url = url; this.readyState = 1; this.handlers = {}; sock = this;
queueMicrotask(() => this.fire("open", {}));
}
addEventListener(type, fn) { (this.handlers[type] ||= []).push(fn); }
fire(type, ev) { (this.handlers[type] || []).forEach((fn) => fn(ev)); }
send(data) { sent.push(JSON.parse(data)); }
close() { this.readyState = 3; }
}
const ctx = vm.createContext({
browser: browserStub, WebSocket: WebSocketStub, console,
setTimeout, clearTimeout, queueMicrotask, URL, Promise, JSON, Math, Object, Array, Number, String, Error, Map, Set
});
vm.runInContext(fs.readFileSync(SRC, "utf8"), ctx, { filename: "background.js" });
const tick = () => new Promise((r) => setTimeout(r, 30));
await tick();
// Was beim Verbindungsaufbau gesendet wurde, bevor die Kommandotests `sent` leeren.
const handshake = sent.slice();
let failures = 0;
function check(label, ok, detail) {
if (!ok) failures++;
console.log(`${ok ? "✅" : "❌"} ${label}${detail === undefined ? "" : `\n → ${detail}`}`);
}
/** Fragt den Status so ab, wie Popup und Einstellungsseite es tun. */
async function getStatus() {
const results = messageListeners.map((fn) => fn({ cmd: "getStatus" })).filter(Boolean);
return await Promise.resolve(results[0]);
}
async function req(message, label, check) {
sent.length = 0;
sock.fire("message", { data: JSON.stringify(message) });
await tick();
const reply = sent[0] ?? null;
const ok = check(reply);
if (!ok) failures++;
console.log(`${ok ? "✅" : "❌"} ${label}\n → ${reply === null ? "(keine Antwort)" : JSON.stringify(reply)}`);
}
/* ---- Anmeldung und Statusanzeige ---- */
const hello = handshake.find((m) => m && m.cmd === "hello");
check("hello wird ohne Token gesendet", !!hello && !("token" in hello),
hello ? JSON.stringify(hello) : "(nichts gesendet)");
check("hello nennt Client, Version und Aktionen",
!!hello && hello.client === "swyx-tab-bridge" && hello.version === "1.0.0" &&
Array.isArray(hello.actions) && hello.actions.includes("list"));
const status = await getStatus();
check("Status meldet die Verbindung", status?.state === "connected",
JSON.stringify(status));
check("Status nennt den Zeitpunkt des Verbindungsaufbaus",
typeof status?.connectedSince === "number" && status.connectedSince <= Date.now());
check("Statuswechsel wird an Popup und Einstellungen gemeldet",
broadcasts.some((m) => m?.type === "statusChanged" && m.status?.state === "connected"));
/* ---- die drei SwyxTray-Aktionen ---- */
await req({ type: "tab", id: 1, action: "list" }, "list",
(r) => r?.cmd === "tabresult" && r.id === 1 && r.ok === true &&
r.tabs.length === 3 &&
JSON.stringify(r.tabs[0]) === JSON.stringify({ id: 42, title: "Kundenakte Muster GmbH", url: "https://crm.example.local/kunden/4711", active: false }));
await req({ type: "tab", id: 2, action: "open", url: "https://crm.example.local/kunden/4711" }, "open",
(r) => r?.cmd === "tabresult" && r.id === 2 && r.ok === true && r.tabId === 100);
await req({ type: "tab", id: 3, action: "close", tabId: 42 }, "close",
(r) => r?.cmd === "tabresult" && r.id === 3 && r.ok === true && JSON.stringify(r) === '{"cmd":"tabresult","id":3,"ok":true}');
/* ---- Fehlerfälle ---- */
await req({ type: "tab", id: 4, action: "open" }, "open ohne url",
(r) => r?.ok === false && r.error === "Feld 'url' fehlt.");
await req({ type: "tab", id: 5, action: "open", url: "javascript:alert(1)" }, "open mit javascript: abgewiesen",
(r) => r?.ok === false && r.error === "Protokoll nicht erlaubt: javascript:");
await req({ type: "tab", id: 6, action: "close" }, "close ohne tabId",
(r) => r?.ok === false && r.error === "Feld 'tabId' (number) fehlt oder ist ungültig.");
await req({ type: "tab", id: 7, action: "close", tabId: 9999 }, "close mit unbekannter tabId",
(r) => r?.cmd === "tabresult" && r.id === 7 && r.ok === false && /9999/.test(r.error));
await req({ type: "tab", id: 8, action: "foo" }, "unbekannte Aktion",
(r) => r?.ok === false && r.error === "Unbekannte Aktion 'foo'.");
/* ---- Erweiterungen ---- */
await req({ type: "tab", id: 9, action: "activate", tabId: 44 }, "activate",
(r) => r?.ok === true && r.tabId === 44 && calls.includes("windows.update(2)"));
await req({ type: "tab", id: 10, action: "focus", url: "https://mail.example.local/#posteingang" }, "focus vorhandener Tab",
(r) => r?.ok === true && r.focused === true && r.opened === false && r.tabId === 43);
await req({ type: "tab", id: 11, action: "focus", url: "https://crm.example.local/kunden/9999" }, "focus nicht offen ⇒ neuer Tab",
(r) => r?.ok === true && r.focused === false && r.opened === true && r.tabId >= 100);
await req({ type: "tab", id: 12, action: "open", url: "https://mail.example.local/", reuse: true }, "open mit reuse",
(r) => r?.ok === true && r.tabId === 43 && r.reused === true);
await req({ type: "tab", id: 13, action: "list", currentWindowOnly: true }, "list nur aktuelles Fenster",
(r) => r?.ok === true && r.tabs.length === tabs.filter((t) => t.windowId === 1).length);
/* ---- Nachrichten, die ignoriert werden ---- */
await req({ id: 3, cmd: "call", number: "+49 30 1234567" }, "Telefonie-Nachricht ignoriert", (r) => r === null);
await req({ type: "call", id: 4, action: "answer" }, "type != tab ignoriert", (r) => r === null);
await req({ cmd: "tabresult", id: 1, ok: true }, "eigene Antwort-Struktur ignoriert", (r) => r === null);
sent.length = 0;
sock.fire("message", { data: "{kein json" });
await tick();
const brokenOk = sent.length === 0;
if (!brokenOk) failures++;
console.log(`${brokenOk ? "✅" : "❌"} kaputtes JSON ignoriert\n → ${sent.length ? JSON.stringify(sent[0]) : "(keine Antwort)"}`);
/* ---- id-Typen ---- */
await req({ type: "tab", id: "abc-1", action: "list" }, "id als String wird gespiegelt",
(r) => r?.id === "abc-1" && r.ok === true);
await req({ type: "tab", action: "list" }, "Anfrage ohne id",
(r) => r?.ok === true && r.id === null);
console.log(`\n${failures === 0 ? "Alle Tests bestanden." : failures + " Test(s) fehlgeschlagen."}`);
process.exit(failures ? 1 : 0);