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: `
Deep learning · chapter 1

But what is a neural network?

3Blue1BrownНаглядная математика

Тест интерфейса YouTubeDL

` }); 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 = 'Test video'; 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 = 'Short'; preview.addEventListener('mouseenter', () => { const replacement=document.createElement('a'); replacement.href='/shorts/Mf9GCn_LsUI'; replacement.style.cssText='display:block;height:200px'; replacement.innerHTML=''; 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 = '
'; 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(); }