Fix preview control layers and centred settings icons in YouTubeDL 1.4.6

This commit is contained in:
DepressedCat committed 2026-10-11 21:45:52 +03:00
1 parent 5f2aad00f4
commit a4b8a1c469
13 files changed
+102 -25

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ try {
assert.equal(new URL(page.url()).hash,"#install");
assert.ok(await page.locator(".download-grid").isVisible());
assert.equal(await page.locator('[data-download="extension"]').getAttribute("href"),`https://git.lonarpgproxy.space/OwnerLink/YouTubeDL/releases/download/v${version}/YouTubeDL-${version}.zip`);
assert.equal(await page.locator('[data-download="helper"]').getAttribute("href"),"https://git.lonarpgproxy.space/OwnerLink/YouTubeDL/releases/download/v1.4.5/YouTubeDL-Helper-Setup.exe");
assert.equal(await page.locator('[data-download="helper"]').getAttribute("href"),"https://git.lonarpgproxy.space/OwnerLink/YouTubeDL/releases/download/v1.4.6/YouTubeDL-Helper-Setup.exe");
await page.locator('[data-screenshot="light"]').click();
await page.locator("#menu-screenshot").evaluate(image => image.decode());
assert.match(await page.locator("#menu-screenshot").getAttribute("src"), /menu-light\.png$/);
+57 -2
View File
@@ -8,7 +8,7 @@ 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: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" });
const context = await browser.newContext({ viewport: { width: 1200, height: 800 }, reducedMotion: "reduce" });
const context = await browser.newContext({ viewport: { width: 1200, height: 800 }, reducedMotion: "reduce", deviceScaleFactor: Number(process.env.YTDL_TEST_SCALE || 1) });
await context.addInitScript({ path: path.join(root, "tests/ui-fixture.js") });
const poster = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="330" height="186"><defs><linearGradient id="g"><stop stop-color="#096574"/><stop offset="1" stop-color="#192636"/></linearGradient></defs><path fill="url(#g)" d="M0 0h330v186H0z"/></svg>');
await context.route("https://www.youtube.com/**", route => route.fulfill({ contentType: "text/html", body: `<!doctype html><html dark><head><meta charset="utf-8"><style>
@@ -42,6 +42,42 @@ try {
for (const id of ["short-card", "legacy-card", "modern-card"]) {
await page.locator(`#${id} [data-ytdl-preview], #${id}[data-ytdl-preview]`).waitFor(); await assertInside(id);
}
// YouTube's native notification/account dropdowns are fixed at z-index 2202.
// Both the Shorts portal and regular thumbnail controls must stay beneath them.
await page.evaluate(() => {
const container = document.createElement('ytd-popup-container');
container.style.cssText = 'position:absolute;inset:0 0 auto';
container.innerHTML = '<tp-yt-iron-dropdown id="native-popup" style="position:fixed;left:12px;top:56px;width:1100px;height:300px;z-index:2202;background:#282828;color:white;border-radius:12px;padding:16px;box-sizing:border-box"><h2>Уведомления YouTube</h2><p>Новые видео и трансляции</p></tp-yt-iron-dropdown>';
document.body.append(container);
});
for (const id of ['short-card', 'legacy-card', 'modern-card']) {
const covered = await page.locator(`#${id}`).evaluate(card => {
const owner = card.dataset.ytdlPreview || card.querySelector('[data-ytdl-preview]')?.dataset.ytdlPreview;
const host = document.querySelector(`[data-ytdl-preview-for="${owner}"]`);
return [...host.shadowRoot.querySelectorAll('button')].every(button => {
const rect = button.getBoundingClientRect();
return !!document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2)?.closest('#native-popup');
});
});
assert.equal(covered, true, `Native YouTube popup must cover both controls on ${id}`);
}
await page.screenshot({ path: path.join(artifacts, 'thumbnail-native-popup.png'), animations: 'disabled' });
await page.evaluate(() => document.querySelector('ytd-popup-container').remove());
// Closing the popup must restore interaction without a page refresh.
const legacyOwner = await page.locator('#legacy-card [data-ytdl-preview]').getAttribute('data-ytdl-preview');
await page.locator(`[data-ytdl-preview-for="${legacyOwner}"] .download`).click();
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), 'https://www.youtube.com/watch?v=jNQXAC9IVRw');
// Compare the actual rendered centres, including any SVG transforms.
const gearCentres = await page.locator('.ytdl-thumbnail-download .settings svg').evaluateAll(svgs => svgs.map(svg => {
const outline = svg.querySelector('path'), circle = svg.querySelector('circle'), box = outline.getBBox();
const outer = new DOMPoint(box.x + box.width / 2, box.y + box.height / 2).matrixTransform(outline.getCTM());
const inner = new DOMPoint(circle.cx.baseVal.value, circle.cy.baseVal.value).matrixTransform(circle.getCTM());
const button = svg.closest('button').getBoundingClientRect(), hole = circle.getBoundingClientRect();
return { outer: { x: outer.x, y: outer.y }, inner: { x: inner.x, y: inner.y }, button: { x: button.x + button.width / 2, y: button.y + button.height / 2 }, hole: { x: hole.x + hole.width / 2, y: hole.y + hole.height / 2 } };
}));
assert.equal(gearCentres.length, 3, 'Check regular, modern and Shorts thumbnail gears');
assert.ok(gearCentres.every(gear => Math.abs(gear.outer.x - gear.inner.x) < .01 && Math.abs(gear.outer.y - gear.inner.y) < .01 && Math.abs(gear.button.x - gear.hole.x) < .01 && Math.abs(gear.button.y - gear.hole.y) < .01),
`Settings gear must have concentric outline and hole: ${JSON.stringify(gearCentres)}`);
// The real YouTube app is absolutely positioned: BODY has zero height, and
// its scroll/auto overflow is propagated to the viewport, not a zero-size clip.
await page.evaluate(() => {
@@ -134,6 +170,25 @@ try {
assert.equal(await shortButtons.isVisible(),true);
await page.evaluate(() => document.querySelector('#short-card').remove());
await shortButtons.waitFor({state:'detached'});
// The standalone Shorts page can temporarily lack its native action rail.
// Its fixed fallback controls must follow the same popup layering rule.
await page.setViewportSize({width:1200,height:800});
await page.goto('https://www.youtube.com/shorts/Mf9GCn_LsUI');
await page.addScriptTag({ path: path.join(root, 'extension/controls.js') });
const fallback = page.locator('#ytdl-shorts-download');
await fallback.waitFor({state:'visible'});
await page.evaluate(() => {
const popup = document.createElement('tp-yt-iron-dropdown'); popup.id = 'native-popup';
popup.style.cssText = 'position:fixed;inset:56px 0 0;z-index:2202;background:#282828';
document.body.append(popup);
});
assert.equal(await fallback.evaluate(host => [...host.shadowRoot.querySelectorAll('button')].every(button => {
const rect = button.getBoundingClientRect();
return document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2)?.id === 'native-popup';
})), true, 'Native popup must also cover standalone Shorts fallback controls');
await page.evaluate(() => document.querySelector('#native-popup').remove());
await fallback.locator('.download').click();
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), 'https://www.youtube.com/watch?v=Mf9GCn_LsUI');
assert.deepEqual(errors, []);
console.log('Thumbnail placement passed: real YouTube zero-height BODY, image bounds, title links, padding, borders, scaled layout, replaced/hidden/removed hover previews, responsive/lazy sizing, scroll, clipped shelves and recycled URLs.');
console.log('Thumbnail placement passed: native popup occlusion, centred settings gear, real YouTube zero-height BODY, image bounds, title links, padding, borders, scaled layout, replaced/hidden/removed hover previews, responsive/lazy sizing, scroll, clipped shelves and recycled URLs.');
} finally { await browser.close(); }
+1 -1
View File
@@ -31,7 +31,7 @@
], audios: [{ id: "251", codec: "opus", extension: "webm", bitrate: 160, language: "en", note: "English", size: 5000000 }] };
window.chrome = {
runtime: {
getManifest: () => ({ version: window.__extensionVersion || "1.4.5" }),
getManifest: () => ({ version: window.__extensionVersion || "1.4.6" }),
getURL: file => `https://ui.ytdl.test/${file}`,
sendMessage: async message => {
if (message.type === "QUICK_DOWNLOAD") { window.__quickRequests.push(message.url); return { ok: true, jobId: "fixture-job" }; }
+4 -4
View File
@@ -20,8 +20,8 @@ try {
page.on("pageerror", error => errors.push(error.message));
await page.goto("https://ui.ytdl.test/downloader.html?theme=dark&url=https://www.youtube.com/watch?v=aircAruvnKk");
await page.locator('#helper-panel[data-state="incompatible"]').waitFor();
assert.match(await page.locator('#helper-message').textContent(), /1\.3\.0.*1\.4\.5/);
assert.match(await page.locator('#helper-versions').textContent(), /Расширение 1\.4\.5.*Помощник 1\.3\.0/);
assert.match(await page.locator('#helper-message').textContent(), /1\.3\.0.*1\.4\.6/);
assert.match(await page.locator('#helper-versions').textContent(), /Расширение 1\.4\.6.*Помощник 1\.3\.0/);
assert.equal(await page.locator('#helper-install').textContent(), 'Обновить помощник');
assert.equal(await page.locator('#download').isDisabled(), true);
assert.equal(await page.locator('#download-form').isVisible(), false);
@@ -30,7 +30,7 @@ try {
await page.locator('#setup-helper[data-state="incompatible"]').waitFor();
assert.equal(await page.locator('#setup-next').isDisabled(), true);
assert.equal(await page.locator('#setup-install').textContent(), 'Обновить помощник');
assert.match(await page.locator('#setup-versions').textContent(), /1\.4\.5.*1\.3\.0/);
assert.match(await page.locator('#setup-versions').textContent(), /1\.4\.6.*1\.3\.0/);
await page.evaluate(() => { window.__helperVersion = '1.3.2'; });
await page.locator('#setup-check').click();
await page.locator('#setup-helper[data-state="running"]').waitFor();
@@ -44,7 +44,7 @@ try {
assert.equal(await page.locator('#setup-install-steps').isVisible(), false);
await page.locator('#setup-close').click();
assert.equal(await page.locator('#helper-install').textContent(), 'Обновить расширение');
assert.match(await page.locator('#helper-message').textContent(), /Помощник 1\.3\.3.*расширением 1\.4\.5/);
assert.match(await page.locator('#helper-message').textContent(), /Помощник 1\.3\.3.*расширением 1\.4\.6/);
assert.equal(await page.locator('#download').isDisabled(), true);
await page.evaluate(() => { window.__helperVersion = '1.3.2'; });
await page.locator('#helper-start').click();