145 lines
9.7 KiB
JavaScript
145 lines
9.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import os from "node:os";
|
|
import { randomBytes } from "node:crypto";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { fileURLToPath } from "node:url";
|
|
import { chromium } from "playwright";
|
|
|
|
// Real Edge, real ports, and the bundled installer/helper. Only their host name
|
|
// is isolated so testing removal never touches the user's installed helper.
|
|
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
const artifacts = path.join(root, "test-artifacts");
|
|
const build = path.join(root, ".build");
|
|
await fs.mkdir(build, { recursive: true }); await fs.mkdir(artifacts, { recursive: true });
|
|
const work = await fs.mkdtemp(path.join(build, "reconnect-"));
|
|
const extension = path.join(work, "extension"), helper = path.join(work, "helper");
|
|
const name = `com.youtubedl.test_${randomBytes(8).toString("hex")}`;
|
|
const run = promisify(execFile);
|
|
let manifestPath, context;
|
|
const profile = await fs.mkdtemp(path.join(os.tmpdir(), "ytdl-reconnect-test-"));
|
|
const userRegistration = async () => (await run("powershell.exe", ["-NoProfile", "-Command",
|
|
"(Get-Item -LiteralPath 'Registry::HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\com.youtubedl.native' -ErrorAction SilentlyContinue).GetValue('')"])).stdout.trim();
|
|
const beforeRegistration = await userRegistration();
|
|
async function register(remove = false) {
|
|
await run("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path.join(root, "tests/test-native-registration.ps1"),
|
|
"-Name", name, "-ManifestPath", manifestPath, ...(remove ? ["-Remove"] : [])]);
|
|
}
|
|
async function install() {
|
|
const result = await run(path.join(root, "dist/YouTubeDL-Helper-Setup.exe"), ["--silent", "--no-register", "--target-root", helper], { timeout: 180000, windowsHide: true });
|
|
assert.match(result.stdout, /100%|Готово/);
|
|
const marker = JSON.parse((await fs.readFile(path.join(helper, "installation.json"), "utf8")).replace(/^\uFEFF/, ""));
|
|
assert.equal(path.dirname(path.dirname(path.resolve(marker.current))), path.resolve(helper));
|
|
const commonPath = path.join(marker.current, "host/common.py");
|
|
await fs.writeFile(commonPath, (await fs.readFile(commonPath, "utf8")).replace('HOST_NAME = "com.youtubedl.native"', `HOST_NAME = "${name}"`));
|
|
manifestPath = path.join(marker.current, ".runtime/native-host.json");
|
|
const manifest = JSON.parse((await fs.readFile(manifestPath, "utf8")).replace(/^\uFEFF/, ""));
|
|
manifest.name = name; await fs.writeFile(manifestPath, JSON.stringify(manifest));
|
|
await register();
|
|
}
|
|
try {
|
|
await fs.cp(path.join(root, "extension"), extension, { recursive: true });
|
|
const sharedPath = path.join(extension, "shared.js");
|
|
await fs.writeFile(sharedPath, (await fs.readFile(sharedPath, "utf8")).replace('"com.youtubedl.native"', `"${name}"`));
|
|
context = await chromium.launchPersistentContext(profile, {
|
|
headless: true, viewport: { width: 520, height: 820 },
|
|
executablePath: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
|
|
ignoreDefaultArgs: ["--disable-extensions"], args: ["--enable-unsafe-extension-debugging"],
|
|
});
|
|
const cdp = await context.browser().newBrowserCDPSession();
|
|
const { id } = await cdp.send("Extensions.loadUnpacked", { path: extension });
|
|
const worker = context.serviceWorkers()[0] || await context.waitForEvent("serviceworker");
|
|
// Capture real ports to trigger both sides of a disconnect deterministically.
|
|
await worker.evaluate(() => {
|
|
globalThis.__testUiPorts = []; globalThis.__testNativePorts = [];
|
|
chrome.runtime.onConnect.addListener(port => { if (port.name === "ytdl-ui") globalThis.__testUiPorts.push(port); });
|
|
const original = chrome.runtime.connectNative.bind(chrome.runtime);
|
|
chrome.runtime.connectNative = (...args) => { const port = original(...args); globalThis.__testNativePorts.push(port); return port; };
|
|
});
|
|
const page = await context.newPage(), errors = [];
|
|
page.on("pageerror", error => errors.push(error.message));
|
|
await page.addInitScript(() => {
|
|
window.__testUiPorts = [];
|
|
const original = chrome.runtime.connect.bind(chrome.runtime);
|
|
chrome.runtime.connect = (...args) => { const port = original(...args); window.__testUiPorts.push(port); return port; };
|
|
window.__pageToken = crypto.randomUUID();
|
|
});
|
|
await page.goto(`chrome-extension://${id}/downloader.html?theme=dark`);
|
|
const token = await page.evaluate(() => window.__pageToken);
|
|
await page.locator('#setup[open]').waitFor(); await page.locator('#setup-next').click();
|
|
await page.locator('#setup-helper[data-state="missing"]').waitFor();
|
|
await worker.evaluate(() => globalThis.__testUiPorts.at(-1).disconnect());
|
|
await page.locator('#setup-helper[data-state="offline"]').waitFor();
|
|
console.log("Real UI port disconnected while the helper was missing. Installing a private offline bundle.");
|
|
await install();
|
|
await page.locator('#setup-check').click();
|
|
await page.locator('#setup-helper[data-state="running"]').waitFor({ timeout: 90000 });
|
|
assert.match(await page.locator('#setup-helper-message').textContent(), /5\/5/);
|
|
// Chrome does not signal onDisconnect to the end that called disconnect().
|
|
// This reproduces the exact synchronous 'disconnected port object' race.
|
|
await page.evaluate(() => window.__testUiPorts.at(-1).disconnect());
|
|
await page.locator('#setup-check').click();
|
|
await page.locator('#setup-check:not([disabled])').waitFor();
|
|
assert.equal(await page.locator('#setup-helper').getAttribute('data-state'), 'running');
|
|
assert.equal(await page.evaluate(() => window.__pageToken), token);
|
|
await page.screenshot({ path: path.join(artifacts, "helper-reconnect-setup.png") });
|
|
console.log("Wizard Check passed after installation and both real port-disconnect cases, without page reload.");
|
|
|
|
await page.locator('#setup-skip').click();
|
|
await page.locator('#helper-panel').evaluate(panel => { panel.open = true; });
|
|
const other = await context.newPage(); other.on('pageerror', error => errors.push(error.message));
|
|
await other.goto(`chrome-extension://${id}/downloader.html?theme=dark`);
|
|
await other.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
await worker.evaluate(() => globalThis.__testUiPorts.forEach(port => { try { port.disconnect(); } catch {} }));
|
|
await page.locator('#helper-panel[data-state="offline"]').waitFor();
|
|
await page.locator('#helper-start').click();
|
|
await page.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
await other.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
await page.locator('#helper-remove').click();
|
|
await page.locator('#helper-panel[data-state="missing"]').waitFor();
|
|
await other.locator('#helper-panel[data-state="missing"]').waitFor();
|
|
assert.equal(await page.locator('.dependency-row[data-status="installed"]').count(), 0);
|
|
await page.screenshot({ path: path.join(artifacts, "helper-reconnect-removed.png") });
|
|
const deadline = Date.now() + 60000;
|
|
while (Date.now() < deadline) {
|
|
try { await fs.access(path.join(helper, 'installation.json')); }
|
|
catch { break; }
|
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
}
|
|
await assert.rejects(fs.access(path.join(helper, 'installation.json')));
|
|
console.log("Start and actual helper removal passed; both open menus report Missing immediately.");
|
|
await install();
|
|
await page.locator('#helper-start').click();
|
|
await page.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
await other.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
// External registration removal and native-port loss must be discovered by polling.
|
|
await register(true);
|
|
await worker.evaluate(() => globalThis.__testNativePorts.at(-1).disconnect());
|
|
await page.locator('#helper-panel[data-state="missing"]').waitFor({ timeout: 25000 });
|
|
await other.locator('#helper-panel[data-state="missing"]').waitFor({ timeout: 25000 });
|
|
await register();
|
|
await page.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
await other.locator('#helper-panel[data-state="running"]').waitFor({ timeout: 90000 });
|
|
assert.equal(await page.evaluate(() => window.__pageToken), token);
|
|
assert.equal(await page.locator('.dependency-row[data-status="installed"]').count(), 5);
|
|
assert.equal(await page.locator('#alert').isVisible(), false);
|
|
assert.deepEqual(errors, []);
|
|
assert.equal(await userRegistration(), beforeRegistration);
|
|
await page.screenshot({ path: path.join(artifacts, "helper-reconnect-ready.png") });
|
|
await fs.writeFile(path.join(artifacts, "helper-reconnect-result.json"), JSON.stringify({
|
|
version: JSON.parse(await fs.readFile(path.join(root,'extension/manifest.json'))).version,
|
|
actualEdge: true, actualInstaller: true, actualNativePorts: true, checkAfterInstall: true,
|
|
startAfterDisconnect: true, removalUpdatesAllMenus: true, reinstall: true,
|
|
pollingDetectsExternalChanges: true, noPageReload: true, userRegistrationUnchanged: true,
|
|
}, null, 2));
|
|
console.log("Real helper reconnect passed: Check, Start, removal, reinstall, external changes, two open menus, no page reload.");
|
|
} finally {
|
|
if (context) await context.close();
|
|
if (manifestPath) await register(true);
|
|
assert.equal(await userRegistration(), beforeRegistration);
|
|
if (path.dirname(path.resolve(profile)) === path.resolve(os.tmpdir()) && path.basename(profile).startsWith("ytdl-reconnect-test-")) await fs.rm(profile, { recursive: true, force: true, maxRetries: 20, retryDelay: 200 });
|
|
if (path.dirname(path.resolve(work)) === path.resolve(build) && path.basename(work).startsWith("reconnect-")) await fs.rm(work, { recursive: true, force: true, maxRetries: 20, retryDelay: 200 });
|
|
}
|