Files
YouTubeDL/tests/native-connection.test.mjs
T
DepressedCat 5f2aad00f4
Check extension and helper / check (push) Canceled after 0s
Publish YouTubeDL 1.4.5 and website on Gitea
2026-10-10 23:53:17 +03:00

113 lines
7.1 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
test("native lifecycle recovers stale ports and publishes install/remove/update state to all menus", async () => {
const ports = [], listeners = {}, storage = {}, pending = new Map(), updates = [];
let installed = false;
let helperVersion = "1.3.2";
let componentsReady = true;
const noEvent = { addListener() {} };
const runtime = { id: "test-id", getManifest: () => ({ version: "1.4.5" }), getURL: name => `chrome-extension://test-id/${name}`,
onConnect: { addListener: listener => { listeners.connect = listener; } }, onMessage: noEvent,
connectNative() {
let receive, disconnected;
const port = { sent: [], dead: false,
onMessage: { addListener: listener => { receive = listener; } },
onDisconnect: { addListener: listener => { disconnected = listener; } },
disconnect() { this.dead = true; queueMicrotask(() => disconnected()); },
emit: message => receive(message),
postMessage(message) {
if (this.dead) throw new Error("Attempting to use a disconnected port object");
this.sent.push(message);
if (!installed) {
queueMicrotask(() => { this.dead = true; runtime.lastError = { message: "Specified native messaging host not found." }; disconnected(); delete runtime.lastError; });
return;
}
const result = message.action === "hello" ? { version: helperVersion, ready: componentsReady, gpu: "Test GPU" }
: message.action === "ping" ? { alive: true }
: message.action === "helper_uninstall" ? { removed: true }
: message.action === "helper_update" ? { jobId: "update-job" } : {};
if (message.action === "helper_uninstall") installed = false;
queueMicrotask(() => receive({ id: message.id, ok: true, result }));
},
};
ports.push(port); return port;
},
};
globalThis.chrome = { runtime,
storage: { local: { get: async () => storage, set: async values => Object.assign(storage, values) } },
notifications: { clear: async () => {}, create: async () => {}, update: async () => true, onButtonClicked: noEvent },
tabs: { sendMessage: async () => {} },
action: { onClicked: noEvent, setBadgeText() {}, setBadgeBackgroundColor() {} },
};
await import("../extension/background.js?native-lifecycle-test");
let send;
const attach = collect => listeners.connect({ name: "ytdl-ui", sender: { id: runtime.id, url: runtime.getURL("downloader.html") },
onDisconnect: noEvent, onMessage: { addListener: listener => { send = listener; } },
postMessage(message) {
if (message.id) { pending.get(message.id)?.(message); pending.delete(message.id); }
else collect.push(message);
},
});
const otherMenu = []; attach(otherMenu); attach(updates);
const call = (action, payload = {}) => new Promise(resolve => {
const id = crypto.randomUUID(); pending.set(id, resolve); send({ id, action, payload });
});
assert.equal((await call("helper_start")).ok, false);
assert.equal(updates.at(-1).helper.status, "missing");
installed = true;
assert.equal((await call("helper_status")).result.status, "running", "Polling discovers a newly installed helper");
ports.at(-1).dead = true;
assert.equal((await call("helper_start")).ok, true, "One click must recover a stale native port before its disconnect callback");
const fresh = ports.at(-1);
const speedJob = { id: "speed-dropout", status: "running", stage: "downloading", track: "video", speed: 4.2 * 1048576 };
fresh.emit({ event: "job", job: speedJob });
await new Promise(resolve => setImmediate(resolve));
fresh.emit({ event: "job", job: { ...speedJob, speed: null } });
await new Promise(resolve => setImmediate(resolve));
assert.equal(updates.filter(message => message.job?.id === speedJob.id).at(-1).job.speed, speedJob.speed, "Native progress normalizes a missing speed before publishing to menus and notifications");
fresh.emit({ event: "job", job: { ...speedJob, track: "audio", speed: null } });
await new Promise(resolve => setImmediate(resolve));
assert.equal(updates.filter(message => message.job?.id === speedJob.id).at(-1).job.speed, null, "A new audio track must not inherit the video's speed");
fresh.emit({ event: "job", job: { ...speedJob, status: "cancelled", stage: "cancelled", speed: null } });
await new Promise(resolve => setImmediate(resolve));
ports.at(-2).disconnect(); await new Promise(resolve => setImmediate(resolve));
assert.equal((await call("helper_status")).result.status, "running", "Old disconnect must not invalidate the replacement");
assert.equal((await call("helper_remove")).ok, true);
assert.equal(updates.at(-1).helper.status, "missing");
assert.equal(otherMenu.at(-1).helper.status, "missing", "Removal updates every open menu");
assert.equal((await call("helper_status")).result.status, "missing");
installed = true;
assert.equal((await call("helper_start")).ok, true);
await call("helper_update", { title: "Обновление компонентов" });
const updatingPort = ports.at(-1);
updatingPort.emit({ event: "job", job: { id: "update-job", kind: "update", status: "complete", stage: "complete", result: { restartRequired: true } } });
await new Promise(resolve => setTimeout(resolve, 20));
assert.notEqual(ports.at(-1), updatingPort);
assert.equal((await call("helper_status")).result.status, "running", "Component update reconnects and republishes measured capabilities");
componentsReady = false;
ports.at(-1).emit({ event: "job", job: { id: "broken-component", kind: "dependency", status: "error", stage: "error", error: "Компонент недоступен" } });
await new Promise(resolve => setTimeout(resolve, 20));
assert.equal((await call("state")).result.helper.hardware.ready, false, "A failed component operation refreshes readiness");
componentsReady = true;
ports.at(-1).emit({ event: "job", job: { id: "restored-component", kind: "dependency", status: "complete", stage: "complete" } });
await new Promise(resolve => setTimeout(resolve, 20));
assert.equal((await call("state")).result.helper.hardware.ready, true, "Restoring a component refreshes readiness without reload");
assert.ok(fresh.sent.some(message => message.action === "helper_uninstall"));
for (const [version, update] of [["1.3.0", "helper"], ["1.3.3", "extension"]]) {
helperVersion = version;
ports.at(-1).disconnect();
await new Promise(resolve => setImmediate(resolve));
assert.equal((await call("helper_start")).ok, false);
const state = (await call("state")).result.helper;
assert.equal(state.status, "incompatible");
assert.equal(state.compatibility.update, update);
assert.equal(state.hardware.version, version);
const port = ports.at(-1);
assert.equal((await call("download", { url: "https://youtu.be/jNQXAC9IVRw" })).ok, false, 'Incompatible versions cannot launch a download');
assert.ok(!port.sent.some(message => message.action === "download"));
}
helperVersion = "1.3.2";
assert.equal((await call("helper_status")).result.status, "running", 'Version repair is detected by the Check button without reloading');
});