Files
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

240 lines
20 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs/promises";
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 browser = await chromium.launch({ headless: true,
executablePath: process.env.YTDL_TEST_BROWSER || "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" });
const context = await browser.newContext({ viewport: { width: 1180, height: 860 }, reducedMotion: "reduce" });
await context.addInitScript({ path: path.join(root, "tests/ui-fixture.js") });
await context.route("**/*", async route => {
const url = new URL(route.request().url());
if (url.hostname === "ui.ytdl.test") {
const name = url.pathname.slice(1).split("/").pop();
if (!["downloader.html", "styles.css", "downloader.js", "onboarding.js", "ui-connection.js", "theme-init.js", "shared.js", "session.js"].includes(name)) return route.abort();
const type = name.endsWith(".html") ? "text/html" : name.endsWith(".css") ? "text/css" : "application/javascript";
return route.fulfill({ body: await fs.readFile(path.join(root, "extension", name)), contentType: type });
}
if (url.hostname === "www.youtube.com") return route.fulfill({ contentType: "text/html", body: `<!doctype html><html dark><head><meta charset="utf-8"><style>body{margin:0;background:#0f0f0f;color:#eee;font:14px "Segoe UI",sans-serif}main{width:780px;margin:35px}.player{height:330px;background:linear-gradient(135deg,#162c43,#0a1421);border-radius:15px;display:grid;place-items:center;color:#65849c;font-size:27px}h1{font-size:22px;margin:22px 0}#owner{display:flex;align-items:center;gap:9px}.channel{margin-right:12px}.channel b{display:block}.channel small{color:#999}#subscribe-button button{border:0;border-radius:22px;background:#eee;padding:11px 17px;color:#111;font-weight:600}.note{color:#8d98a6;margin-top:35px}.avatar{width:38px;height:38px;background:#577295;border-radius:50%}</style></head><body><main><div class="player">Deep learning · chapter 1</div><h1>But what is a neural network?</h1><ytd-watch-metadata><div id="owner"><span class="avatar"></span><span class="channel"><b>3Blue1Brown</b><small>Наглядная математика</small></span><div id="subscribe-button"><button>Подписаться</button></div></div></ytd-watch-metadata><p class="note">Тест интерфейса YouTubeDL</p></main></body></html>` });
return route.abort();
});
const page = await context.newPage();
const errors = [];
page.on("pageerror", error => errors.push(error.message));
try {
await page.goto("https://www.youtube.com/watch?v=aircAruvnKk");
await page.addScriptTag({ path: path.join(root, "extension/controls.js") });
await page.addScriptTag({ path: path.join(root, "extension/content.js") });
await page.evaluate(() => window.addEventListener("message", event => {
if (event.origin === "https://ui.ytdl.test" && event.data?.type === "FIXTURE_CLOSE_MENU") window.__contentMessage({ type: "CLOSE_MENU" });
}));
assert.equal(await page.locator("#subscribe-button + #ytdl-subscribe-download").count(), 1, "Button must be next to Subscribe");
const siteDownload = page.locator("#ytdl-subscribe-download .download");
assert.equal(await siteDownload.locator("svg").count(), 1, "Keep the site download icon");
const beforeHover = await siteDownload.boundingBox();
await siteDownload.hover();
assert.deepEqual(await siteDownload.boundingBox(), beforeHover, "The site button must not move on hover");
await siteDownload.click();
assert.equal(await page.locator("#ytdl-menu").count(), 0, "Download starts directly without a choice menu");
assert.deepEqual(await page.evaluate(() => window.__quickRequests), ["https://www.youtube.com/watch?v=aircAruvnKk"]);
await page.locator("#ytdl-subscribe-download .settings").click();
const frame = page.frameLocator("#ytdl-menu iframe");
await frame.locator("#download-form").waitFor({ state: "visible" });
assert.equal(await frame.locator("#download svg").count(), 0);
assert.ok(!(await frame.locator("#download").textContent()).includes("↓"));
assert.equal(await frame.locator("#helper-status").textContent(), "Помощник работает");
assert.equal(await frame.locator('.dependency-row[data-status="installed"]').count(), 5);
assert.equal(await frame.locator("#quality option").count(), 4);
await frame.locator("#quality").selectOption("137");
const actionSizes = await frame.locator('.form-actions > button').evaluateAll(buttons => buttons.map(button => ({ width: button.getBoundingClientRect().width, height: button.getBoundingClientRect().height, font: getComputedStyle(button).fontSize })));
assert.ok(Math.abs(actionSizes[0].width - actionSizes[1].width) < 1);
assert.equal(actionSizes[0].height, actionSizes[1].height); assert.equal(actionSizes[0].font, actionSizes[1].font);
await frame.locator('.form-actions').screenshot({path:path.join(artifacts,'form-actions.png')});
await frame.locator('body').evaluate(() => window.__emitJob({ id: 'old-download', title: 'Сохранённый ролик', status: 'complete', stage: 'complete', result: {path:'D:/Videos/old.mp4'}, createdAt: 1 }));
assert.equal(await frame.locator('[data-job-id="old-download"]').count(), 0, 'Completed history moves to its own page');
await frame.locator('#open-history').click();
await frame.locator('body').evaluate(() => {
if (window.__historyRequests.length !== 1) throw new Error('History button must open a new tab');
});
await frame.locator('body').evaluate(() => window.__emitJob({id:'active-download',title:'Текущая загрузка',status:'running',stage:'downloading',percent:12,createdAt:2}));
assert.equal(await frame.locator('#active-jobs .job-card').isVisible(), true);
await frame.locator('body').evaluate(() => window.__emitJob({id:'active-download',title:'Текущая загрузка',status:'complete',stage:'complete',result:{path:'D:/Videos/new.mp4'},createdAt:2}));
assert.equal(await frame.locator('#active-jobs-section').isVisible(), false);
assert.equal(await frame.locator('#open-history').isVisible(), true);
await frame.locator("html[data-theme=dark]").waitFor();
await frame.locator("body").evaluate(() => { for (const animation of document.getAnimations()) if (Number.isFinite(animation.effect.getTiming().iterations)) animation.finish(); });
await page.screenshot({ path: path.join(artifacts, "menu-dark.png"), animations: "disabled" });
await frame.locator(".mode-switch label").filter({ hasText: /^Аудио$/ }).click();
assert.equal(await frame.locator("#video-options").isVisible(), false);
assert.equal(await frame.locator("#container option").count(), 5);
await frame.locator("#container").selectOption("mp3");
await frame.locator("#pick-folder").click();
await frame.locator("#pick-folder:not([disabled])").waitFor();
assert.equal(await frame.locator("#folder").inputValue(), "D:\\Videos\\YouTube");
await frame.locator("#download").click();
await page.locator("#ytdl-menu").waitFor({ state: "detached" });
const job = { id: "fixture-job", title: "But what is a neural network?", status: "running", stage: "encoding", engine: "nvenc", percent: 64, speed: "12.8x" };
await page.evaluate(job => window.__contentMessage({ type: "JOB_PROGRESS", job }), job);
await page.locator("#ytdl-notifications .toast").waitFor();
assert.match(await page.locator("#ytdl-notifications .stage").textContent(), /NVENC/);
await page.screenshot({ path: path.join(artifacts, "progress-toast.png"), animations: "disabled" });
await page.evaluate(() => { document.documentElement.removeAttribute("dark"); document.body.style.background = "#f9fafc"; document.body.style.color = "#182234"; });
await page.locator("#ytdl-notifications:not([dark])").waitFor();
await page.locator("#ytdl-subscribe-download .settings").click();
const lightFrame = page.frameLocator("#ytdl-menu iframe");
await lightFrame.locator("#download-form").waitFor({ state: "visible" });
assert.equal(await lightFrame.locator("html").getAttribute("data-theme"), "light");
await page.screenshot({ path: path.join(artifacts, "menu-light.png"), animations: "disabled" });
await page.setViewportSize({ width: 340, height: 720 });
const box = await page.locator("#ytdl-menu").boundingBox();
assert.ok(box.x >= 0 && box.x + box.width <= 340, "Menu must stay inside a narrow viewport");
await lightFrame.locator("#download-form").waitFor({ state: "visible" });
const overflow = await lightFrame.locator("html").evaluate(node => node.scrollWidth > node.clientWidth);
assert.equal(overflow, false, "No horizontal overflow");
await lightFrame.locator("html").evaluate(() => window.__helperOffline());
assert.equal(await lightFrame.locator("#helper-status").textContent(), "Помощник отключён");
assert.equal(await lightFrame.locator('.dependency-row[data-status="unknown"]').count(), 5);
await lightFrame.locator("#helper-start").click();
await lightFrame.locator("#download-form").waitFor({ state: "visible" });
assert.equal(await lightFrame.locator("#helper-status").textContent(), "Помощник работает");
await lightFrame.locator("#close").click();
await page.locator("#ytdl-menu").waitFor({ state: "detached" });
await page.evaluate(() => {
const thumbnail = document.createElement("ytd-thumbnail");
thumbnail.innerHTML = '<a id="thumbnail" href="/watch?v=jNQXAC9IVRw" style="display:block;width:260px;height:146px;background:#30506b"><img alt="Test video" style="width:100%;height:100%"></a>';
document.querySelector("main").prepend(thumbnail);
});
await page.locator(".ytdl-thumbnail-download .download").click();
assert.equal(page.url(), "https://www.youtube.com/watch?v=aircAruvnKk", "A thumbnail download must not navigate");
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), "https://www.youtube.com/watch?v=jNQXAC9IVRw");
await page.locator(".ytdl-thumbnail-download .settings").click();
await page.frameLocator("#ytdl-menu iframe").locator("#download-form").waitFor({ state: "visible" });
assert.equal(new URL(await page.locator("#ytdl-menu iframe").getAttribute("src")).searchParams.get("url"), "https://www.youtube.com/watch?v=jNQXAC9IVRw");
await page.frameLocator('#ytdl-menu iframe').locator('#download-form').waitFor({state:'visible'});
await page.frameLocator('#ytdl-menu iframe').locator('#folder').press('Escape');
await page.locator('#ytdl-menu').waitFor({state:'detached'});
await page.evaluate(() => {
const preview = document.createElement('ytm-shorts-lockup-view-model');
preview.style.cssText='display:block;width:120px;height:230px;position:relative';
preview.innerHTML = '<a href="/shorts/Mf9GCn_LsUI" style="display:block;height:200px"><yt-thumbnail-view-model><img alt="Short" style="width:120px;height:200px"></yt-thumbnail-view-model></a>';
preview.addEventListener('mouseenter', () => {
const replacement=document.createElement('a'); replacement.href='/shorts/Mf9GCn_LsUI'; replacement.style.cssText='display:block;height:200px';
replacement.innerHTML='<video style="width:120px;height:200px;background:#24364c"></video>';
preview.querySelector('a').replaceWith(replacement);
const player=document.createElement('div'); player.className='hover-player'; player.style.cssText='position:absolute;inset:0 0 30px;z-index:30;background:#24364c'; preview.append(player);
}, {once:true});
document.querySelector('main').prepend(preview);
});
await page.locator('ytm-shorts-lockup-view-model[data-ytdl-preview]').waitFor();
await page.locator('ytm-shorts-lockup-view-model').hover();
await page.locator('ytm-shorts-lockup-view-model .hover-player').waitFor();
const shortKey=await page.locator('ytm-shorts-lockup-view-model').getAttribute('data-ytdl-preview');
const shortButtons=page.locator(`[data-ytdl-preview-for="${shortKey}"]`);
assert.equal(await shortButtons.evaluate(node => node.parentElement === document.documentElement), true);
await shortButtons.locator('.download').click();
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), 'https://www.youtube.com/watch?v=Mf9GCn_LsUI');
await shortButtons.locator('.settings').click();
await page.frameLocator('#ytdl-menu iframe').locator('#download-form').waitFor({state:'visible'});
await page.frameLocator('#ytdl-menu iframe').locator('#close').click();
await page.locator('#ytdl-menu').waitFor({state:'detached'});
await page.evaluate(() => {
history.pushState({}, "", "/shorts/jNQXAC9IVRw");
document.body.innerHTML = '<ytd-shorts><ytd-reel-video-renderer is-active><div id="actions" style="position:fixed;right:20px;bottom:180px"></div></ytd-reel-video-renderer></ytd-shorts>';
document.dispatchEvent(new Event("yt-navigate-finish"));
});
await page.locator("#actions #ytdl-shorts-download .download").click();
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), "https://www.youtube.com/watch?v=jNQXAC9IVRw");
await page.locator("#ytdl-shorts-download .settings").click();
await page.frameLocator("#ytdl-menu iframe").locator("#download-form").waitFor({ state: "visible" });
await page.screenshot({ path: path.join(artifacts, "shorts-settings.png"), animations: "disabled" });
await page.evaluate(() => window.__contentMessage({ type: 'JOB_PROGRESS', job: {
id: 'component-complete', title: 'FFmpeg', kind: 'dependency', stage: 'complete', status: 'complete', percent: 100, result: { title: 'FFmpeg' }
} }));
assert.equal(await page.locator('#ytdl-notifications [data-id="component-complete"] .action').isVisible(), false);
const components = await context.newPage();
await components.addInitScript(() => { window.__dependencyMissing = true; });
await components.goto('https://ui.ytdl.test/downloader.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjNQXAC9IVRw');
if (!await components.locator('#helper-panel').evaluate(panel => panel.open)) await components.locator('#helper-panel > summary').click();
await components.locator('.dependencies summary').click();
await components.locator('.dependency-row[data-dependency="ffmpeg"][data-status="missing"]').waitFor();
assert.equal(await components.locator('#download-form').isVisible(), false);
assert.equal(await components.locator('#engine option[value="nvenc"]').isDisabled(), true);
assert.equal(await components.locator('.dependency-row button').count(), 0);
assert.equal(await components.locator('#helper-install').textContent(), 'Восстановить');
assert.equal(await components.locator('#helper-install').getAttribute('href'), 'https://git.lonarpgproxy.space/OwnerLink/YouTubeDL/releases/latest');
assert.equal(await components.locator('#helper-install').getAttribute('target'), '_blank');
assert.equal(await components.locator('#helper-install').getAttribute('download'), null);
// A repaired bundle reports new capabilities after the helper restarts.
await components.evaluate(() => { window.__dependencyMissing = false; });
await components.locator('#helper-start').click();
await components.locator('#download-form').waitFor({state:'visible'});
assert.equal(await components.locator('.dependency-row[data-status="installed"]').count(), 5);
assert.equal(await components.locator('#engine option[value="nvenc"]').isDisabled(), false);
await components.close();
const amdPage=await context.newPage();
await amdPage.addInitScript(() => { window.__amdOnly=true; });
await amdPage.goto('https://ui.ytdl.test/downloader.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjNQXAC9IVRw');
await amdPage.locator('#download-form').waitFor({state:'visible'});
assert.equal(await amdPage.locator('#engine option[value="nvenc"]').isDisabled(),true);
assert.equal(await amdPage.locator('#engine option[value="amf"]').isDisabled(),false);
await amdPage.locator('.advanced summary').click();
await amdPage.locator('#codec').selectOption('hevc'); await amdPage.locator('#engine').selectOption('amf');
assert.match(await amdPage.locator('#processing-hint').textContent(),/AMD AMF/);
await amdPage.locator('#codec').selectOption('vp9');
assert.match(await amdPage.locator('#processing-hint').textContent(),/недоступен/);
assert.equal(await amdPage.locator('#download').isDisabled(),true);
await amdPage.locator('#codec').selectOption('hevc');
assert.equal(await amdPage.locator('#download').isDisabled(),false);
await amdPage.locator('#helper-panel > summary').click();
assert.equal(await amdPage.locator('#helper-install').textContent(),'Переустановить');
await amdPage.locator('#auto-update').uncheck();
await amdPage.locator('#helper-update').click();
await amdPage.locator('#active-jobs .job-stage').filter({hasText:'Скачиваю FFmpeg'}).waitFor();
assert.equal(await amdPage.locator('#helper-update').isDisabled(),true);
assert.equal(await amdPage.evaluate(() => window.__updateRequests.length),1);
await amdPage.evaluate(() => window.__emitJob({id:'fixture-update',kind:'update',canCancel:false,status:'complete',stage:'complete',result:{title:'Все компоненты актуальны'}}));
await amdPage.locator('#helper-update:not([disabled])').waitFor();
assert.equal(await amdPage.locator('#active-jobs-section').isVisible(),false,'Completed component updates leave the active list');
assert.match(await amdPage.locator('#update-note').textContent(),/актуальны/);
await amdPage.close();
const loginPage = await context.newPage();
await loginPage.addInitScript(() => { window.__requiresLogin = true; window.__denySessionPermission = true; });
await loginPage.goto('https://ui.ytdl.test/downloader.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjNQXAC9IVRw&theme=dark');
await loginPage.locator('#alert:not([hidden])').waitFor();
assert.equal(await loginPage.locator('#helper-status').textContent(), 'Помощник работает');
assert.equal(await loginPage.locator('#video-title').textContent(), 'Не удалось получить качество');
assert.match(await loginPage.locator('#alert').textContent(), /Использовать вход в YouTube/);
assert.ok(!(await loginPage.locator('#alert').textContent()).includes('cookies-from-browser'));
await loginPage.locator('#session-enable').click();
assert.match(await loginPage.locator('#alert').textContent(), /не разрешён/);
await loginPage.evaluate(() => { window.__denySessionPermission = false; });
await loginPage.locator('#session-enable').click();
await loginPage.locator('#download-form').waitFor({state:'visible'});
assert.equal(await loginPage.locator('#session-status').textContent(), 'Вход браузера подключён');
assert.equal(await loginPage.locator('#alert').isVisible(), false);
await loginPage.screenshot({path:path.join(artifacts,'session-connected.png'),animations:'disabled'});
await loginPage.locator('#session-disable').click();
await loginPage.locator('#session-panel[data-state="disabled"]').waitFor();
assert.equal(await loginPage.locator('#session-status').textContent(), 'Вход в YouTube не подключён');
await loginPage.close();
const storagePage = await context.newPage();
await storagePage.goto('https://ui.ytdl.test/downloader.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjNQXAC9IVRw&theme=dark');
await storagePage.locator('#download-form').waitFor({state:'visible'});
await storagePage.locator('#folder').fill('C:\\Finished');
await storagePage.locator('#pick-work-folder').click();
await storagePage.waitForFunction(() => window.__fixtureStorage.preferences.workFolder === 'D:\\Videos\\YouTube');
assert.equal(await storagePage.locator('#folder').inputValue(), 'C:\\Finished');
await storagePage.locator('#download').click();
await storagePage.waitForFunction(() => window.__downloadRequests.length === 1);
const storageRequest = await storagePage.evaluate(() => window.__downloadRequests[0]);
assert.equal(storageRequest.folder, 'C:\\Finished');
assert.equal(storageRequest.workFolder, 'D:\\Videos\\YouTube');
await storagePage.close();
assert.deepEqual(errors, []);
console.log("UI checks passed: direct download + settings, stationary buttons, thumbnails, Shorts, real-state indicators, reconnect, themes and narrow layout.");
} finally { await browser.close(); }