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: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" });
const context = await browser.newContext({ viewport: { width: 1200, height: 800 }, reducedMotion: "reduce" });
await context.addInitScript({ path: path.join(root, "tests/ui-fixture.js") });
const poster = 'data:image/svg+xml,' + encodeURIComponent('');
await context.route("https://www.youtube.com/**", route => route.fulfill({ contentType: "text/html", body: `
ТЫ НЕ ВСТРЕЧАЛ этого Левиафана в Subnautica 2...1,6 млн просмотров

S.T.A.L.K.E.R. — видео и описание
Название внутри ссылкиИнформация под превью
` }));
const page = await context.newPage();
const errors = []; page.on("pageerror", error => errors.push(error.message));
async function assertInside(id) {
const geometry = 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}"]`);
const media = card.querySelector('video, img');
const box = value => ({ left:value.left, top:value.top, right:value.right, bottom:value.bottom });
return { media: box(media.getBoundingClientRect()), buttons: [...host.shadowRoot.querySelectorAll('button')].map(button => box(button.getBoundingClientRect())), count: document.querySelectorAll(`[data-ytdl-preview-for="${owner}"]`).length };
});
assert.equal(geometry.count, 1, `Only one control pair in ${id}`);
for (const button of geometry.buttons) {
assert.ok(button.left >= geometry.media.left + 3 && button.right <= geometry.media.right - 3 && button.top >= geometry.media.top + 3 && button.bottom <= geometry.media.bottom - 3,
`Controls must stay inside ${id}'s actual picture: ${JSON.stringify(geometry)}`);
}
}
try {
await page.goto("https://www.youtube.com/");
await page.addScriptTag({ path: path.join(root, "extension/controls.js") });
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);
}
// 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(() => {
document.querySelector('main').style.position='absolute';
document.body.style.overflowY='scroll'; document.body.style.overflowX='auto';
});
await page.waitForTimeout(100);
const shortOwner=await page.locator('#short-card').getAttribute('data-ytdl-preview');
assert.equal(await page.locator(`[data-ytdl-preview-for="${shortOwner}"]`).isVisible(),true,'Viewport overflow must not hide buttons when BODY has zero height');
await assertInside('short-card');
await page.screenshot({ path: path.join(artifacts, "thumbnail-placement.png"), animations: "disabled" });
await page.evaluate(() => {
const card = document.querySelector('#short-card');
card.addEventListener('mouseenter', () => {
const replacement = document.createElement('a'); replacement.href = '/shorts/Mf9GCn_LsUI'; replacement.className = 'short-image';
replacement.innerHTML = '';
card.querySelector('.short-image').replaceWith(replacement);
const player = document.createElement('div'); player.className = 'hover-player';
player.style.cssText = 'position:absolute;inset:0 0 auto;height:186px;background:#123b47;z-index:30;pointer-events:auto;border-radius:12px'; card.append(player);
}, { once:true });
});
await page.locator('#short-card').hover();
await page.locator('#short-card .hover-player').waitFor();
await page.waitForTimeout(250);
await assertInside('short-card');
const shortKey = await page.locator('#short-card').getAttribute('data-ytdl-preview');
const shortButtons = page.locator(`[data-ytdl-preview-for="${shortKey}"]`);
await shortButtons.locator('.download').click();
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), 'https://www.youtube.com/watch?v=Mf9GCn_LsUI');
assert.equal(page.url(), 'https://www.youtube.com/');
await page.screenshot({ path: path.join(artifacts, 'thumbnail-hover-placement.png'), animations:'disabled' });
// YouTube can hide the original card while rendering its hover player in a
// separate overlay. Controls must be outside that opacity/visibility subtree.
await page.evaluate(() => document.querySelector('#short-card').style.opacity = '0');
assert.equal(await shortButtons.evaluate(host => host.parentElement === document.documentElement), true);
assert.equal(await shortButtons.locator('.download').evaluate(button => Number(getComputedStyle(button).opacity)), 1);
await shortButtons.locator('.download').click();
await page.evaluate(() => {
document.querySelector('#short-card').style.opacity = '';
document.querySelector('#short-card .short-image').style.visibility = 'hidden';
});
await page.waitForTimeout(250);
assert.equal(await shortButtons.isVisible(), true);
await shortButtons.locator('.download').click();
await page.evaluate(() => document.querySelector('#short-card .short-image').style.visibility = '');
// Hover playback can remove the entire image link, leaving only metadata and
// a separate player. That metadata must not move the controls into the title.
const beforeRemoval = await shortButtons.boundingBox();
await page.evaluate(() => {
const link = document.querySelector('#short-card .short-image');
window.__removedThumbnail = link; link.remove();
});
await page.waitForTimeout(250);
assert.deepEqual(await shortButtons.boundingBox(), beforeRemoval);
await shortButtons.locator('.download').click();
await page.evaluate(() => document.querySelector('#short-card').prepend(window.__removedThumbnail));
// Layout changes without a window resize: columns narrower, title grows,
// and a lazy image gets its final height after initial attachment.
await page.evaluate(() => {
for (const card of document.querySelectorAll('.card')) card.style.width = '260px';
document.querySelector('#short-card .title').style.minHeight = '110px';
document.querySelector('#short-card .hover-player').style.height = '146.5px';
document.querySelector('#legacy-card img').style.aspectRatio = '4/3';
});
await page.waitForTimeout(300);
for (const id of ['short-card','legacy-card','modern-card']) await assertInside(id);
await page.evaluate(() => {
document.querySelector('#short-card .short-image').href = '/shorts/jNQXAC9IVRw';
document.querySelector('#short-card .title').href = '/shorts/jNQXAC9IVRw';
});
await shortButtons.locator('.download').click();
assert.equal(await page.evaluate(() => window.__quickRequests.at(-1)), 'https://www.youtube.com/watch?v=jNQXAC9IVRw');
await page.evaluate(() => { document.body.style.minHeight = '1500px'; });
await page.setViewportSize({width:1200,height:300});
await page.evaluate(() => window.scrollTo(0,60));
await page.waitForTimeout(100);
await assertInside('short-card');
// A horizontally clipped Shorts shelf must not leave buttons floating over
// content outside the visible shelf.
await page.evaluate(() => {
const card = document.querySelector('#short-card');
const shelf = document.createElement('div'); shelf.id = 'short-shelf';
shelf.style.cssText = 'width:260px;overflow:hidden;flex-shrink:0';
card.before(shelf); shelf.append(card); card.style.marginLeft = '300px';
});
await page.waitForTimeout(100);
assert.equal(await shortButtons.isVisible(),false);
await page.evaluate(() => document.querySelector('#short-card').style.marginLeft='0');
await page.waitForTimeout(100);
assert.equal(await shortButtons.isVisible(),true);
await page.evaluate(() => document.querySelector('#short-card').remove());
await shortButtons.waitFor({state:'detached'});
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.');
} finally { await browser.close(); }