107 lines
8.5 KiB
JavaScript
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 });
|
|
}
|