Files
YouTubeDL/tests/history-browser.test.mjs
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

107 lines
8.5 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const artifacts = path.join(root, "test-artifacts");
await fs.mkdir(artifacts, { recursive: true });
const profile = await fs.mkdtemp(path.join(os.tmpdir(), "ytdl-history-test-"));
const context = await chromium.launchPersistentContext(profile, { headless: true, viewport: { width: 1180, height: 900 },
executablePath: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", reducedMotion: "reduce",
ignoreDefaultArgs: ["--disable-extensions"], args: ["--enable-unsafe-extension-debugging"] });
const errors = [];
context.on("page", page => page.on("pageerror", error => errors.push(error.message)));
const videoUrl = "https://www.youtube.com/watch?v=jNQXAC9IVRw";
const records = {
saved: { id: "saved", title: "Subnautica · Путешествие в глубину", kind: "download", status: "complete", stage: "complete", createdAt: 1791620000,
url: videoUrl, result: { filename: "Путешествие в глубину.mp4", path: "D:\\Videos\\Путешествие в глубину.mp4", size: 512 * 1048576 } },
cancelled: { id: "cancelled", title: "Отменённое скачивание", kind: "download", status: "cancelled", createdAt: 1791610000 },
error: { id: "error", title: "Не удалось скачать видео", kind: "download", status: "error", error: "YouTube запросил подтверждение входа. Подключите вход из текущего профиля браузера в настройках.", url: videoUrl, createdAt: 1791630000 },
updateError: { id: "update-error", title: "Обновление компонентов", kind: "update", status: "error", error: "Не удалось получить обновление FFmpeg. Проверьте соединение и повторите попытку.", createdAt: 1791600000 },
unsafe: { id: "unsafe", title: '<img src=x onerror="alert(1)">', status: "error", url: "https://evil.test/", error: 'Ошибка <script>alert(1)</script>\n' + "Длинное сообщение. ".repeat(30), createdAt: 1791590000 },
updateComplete: { id: "update-complete", title: "Обновление компонентов", kind: "update", status: "complete", createdAt: 1791580000 },
};
try {
const cdp = await context.browser().newBrowserCDPSession();
const { id } = await cdp.send("Extensions.loadUnpacked", { path: path.join(root, "extension") });
let worker = context.serviceWorkers()[0] || await context.waitForEvent("serviceworker");
await worker.evaluate(async jobs => { await chrome.storage.local.set({ jobs, theme: "dark" }); }, records);
const reloaded = context.waitForEvent("serviceworker", { predicate: next => next !== worker, timeout: 20000 });
await worker.evaluate(() => { setTimeout(() => chrome.runtime.reload(), 50); });
worker = await reloaded;
const page = await context.newPage();
await page.goto(`chrome-extension://${id}/history.html`);
await page.locator('#downloads-list [data-job-id="saved"]').waitFor();
await page.waitForTimeout(100);
assert.equal(await page.locator('#downloads-list .history-entry').count(), 2);
assert.equal(await page.locator('#downloads-count').textContent(), "2");
assert.equal(await page.locator('#errors-count').textContent(), "3");
assert.match(await page.locator('[data-job-id="saved"] .entry-meta').textContent(), /512 МБ/);
assert.match(await page.locator('[data-job-id="saved"] .entry-path').textContent(), /D:\\Videos/);
assert.equal(await page.locator('[data-job-id="saved"] a').getAttribute("href"), videoUrl);
assert.equal(await page.locator('#errors-panel').isVisible(), false);
assert.equal(await page.locator('#history-connection').isVisible(), false, "The real history port must be accepted by the service worker");
await page.screenshot({ path: path.join(artifacts, "history-downloads-dark.png") });
await page.locator('#errors-tab').click();
assert.equal(await page.locator('#downloads-panel').isVisible(), false);
assert.equal(await page.locator('#errors-list .history-entry').count(), 3);
assert.equal(await page.locator('[data-job-id="unsafe"] img, [data-job-id="unsafe"] script, [data-job-id="unsafe"] a').count(), 0, "Titles/errors are text, and unsafe URLs have no link");
await page.screenshot({ path: path.join(artifacts, "history-errors-dark.png") });
await page.locator('#errors-tab').press('ArrowLeft');
assert.equal(await page.locator('#downloads-tab').getAttribute('aria-selected'), "true");
assert.equal(await page.locator('#downloads-tab').evaluate(node => node === document.activeElement), true);
// An actual extension UI port must create an actual browser tab.
const [opened] = await Promise.all([context.waitForEvent("page"), page.evaluate(async () => {
const { createUiConnection } = await import(chrome.runtime.getURL("ui-connection.js"));
const { call } = createUiConnection({}); await call("open_history", { theme: "dark" });
})]);
await opened.waitForURL(`chrome-extension://${id}/history.html?theme=dark`);
await opened.locator('#downloads-list .history-entry').first().waitFor();
await opened.close();
await page.setViewportSize({ width: 360, height: 800 });
await page.locator('#theme').click(); // dark -> auto
await page.locator('#theme').click(); // auto -> light
assert.equal(await page.locator('html').getAttribute('data-theme'), "light");
await page.locator('#errors-tab').click();
assert.equal(await page.locator('html').evaluate(node => node.scrollWidth > node.clientWidth), false, "History and long errors must fit a narrow viewport");
await page.screenshot({ path: path.join(artifacts, "history-errors-light-360.png"), fullPage: true });
await page.reload();
assert.equal(await page.locator('html').getAttribute('data-theme'), "light");
await page.locator('#downloads-list [data-job-id="saved"]').waitFor();
assert.equal(await page.locator('#downloads-count').textContent(), "2", "Stored records survive page reload");
// Browser fixture covers arriving terminal events without starting a real download.
await context.route("https://ui.ytdl.test/**", async route => {
const name = new URL(route.request().url()).pathname.slice(1);
if (!["history.html", "history.js", "history.css", "styles.css", "theme-init.js", "shared.js", "ui-connection.js"].includes(name)) return route.abort();
await route.fulfill({ body: await fs.readFile(path.join(root, "extension", name)), contentType: name.endsWith(".html") ? "text/html" : name.endsWith(".css") ? "text/css" : "application/javascript" });
});
const fixture = await context.newPage();
await fixture.addInitScript({ path: path.join(root, "tests/ui-fixture.js") });
await fixture.goto("https://ui.ytdl.test/history.html");
await fixture.locator('#downloads-empty').waitFor();
await fixture.evaluate(records => { window.__emitJob(records.saved); }, records);
await fixture.locator('#downloads-list [data-job-id="saved"]').waitFor();
await fixture.locator('#errors-tab').click();
await fixture.evaluate(records => { window.__emitJob(records.error); }, records);
await fixture.locator('#errors-list [data-job-id="error"]').waitFor();
assert.equal(await fixture.locator('#errors-tab').getAttribute('aria-selected'), "true", "New records must preserve the chosen tab");
await fixture.evaluate(() => window.__emitJob({ id: "active", kind: "download", status: "running", stage: "downloading", createdAt: 1791650000 }));
assert.equal(await fixture.locator('#downloads-count').textContent(), "1");
assert.equal(await fixture.locator('#errors-count').textContent(), "1");
await fixture.evaluate(records => {
window.__uiDisconnect();
window.__fixtureStorage.jobs[records.updateError.id] = records.updateError;
}, records);
await fixture.waitForFunction(() => document.getElementById('errors-count').textContent === "2");
assert.equal(await fixture.locator('#history-connection').isVisible(), false, "History reconnects and re-reads local records without reload");
assert.deepEqual(errors, []);
console.log("History passed in actual Edge extension: persistent records, separate tabs, real new-tab action, safe text, themes, keyboard, 360px layout and live completion/error updates.");
} finally {
await context.close();
if (path.dirname(path.resolve(profile)) === path.resolve(os.tmpdir()) && path.basename(profile).startsWith("ytdl-history-test-")) await fs.rm(profile, { recursive: true, force: true });
}