/** Verify the report-page lightbox + attachments tab with a LARGE screenshot. */
import { chromium } from "playwright-core";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
const root = dirname(fileURLToPath(import.meta.url));
const SERVER = "http://localhost:8001";
const WEB = "http://localhost:5173";
const stamp = Date.now();
const EMAIL = `lb-verify-${stamp}@example.com`;
const PASSWORD = "lb-verify-pass";
const executablePath = join(homedir(), ".cache/ms-playwright/chromium-1234/chrome-linux64/chrome");
const browser = await chromium.launch({ executablePath, headless: true });
async function api(path, options = {}) {
const r = await fetch(SERVER + path, options);
if (!r.ok) throw new Error(`API ${path} -> ${r.status}`);
return r.json();
}
const user = await api("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nickname: `lb-verify-${stamp}`, email: EMAIL, password: PASSWORD }),
});
const login = await api("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Client": "extension" },
body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
});
const auth = { Authorization: `Bearer ${login.token}` };
const project = await api("/api/projects", {
method: "POST",
headers: { "Content-Type": "application/json", ...auth },
body: JSON.stringify({ name: "Lightbox Verify" }),
});
// big screenshot: 1400x900 viewport @2x DPR → 2800x1800 natural
const maker = await browser.newPage({ viewport: { width: 1400, height: 900 }, deviceScaleFactor: 2 });
await maker.setContent(`<body style="margin:0;background:linear-gradient(135deg,#1f2335,#7aa2f7)">
<h1 style="color:#c0caf5;font:700 60px monospace;padding:80px">Big screenshot 2800×1800</h1></body>`);
const png = await maker.screenshot({ path: "/tmp/ltt-shots/big.png" });
await maker.close();
const form = new FormData();
form.append("file", new Blob([png], { type: "image/png" }), "big.png");
const up = await api("/api/uploads", { method: "POST", headers: auth, body: form });
const note = await api("/api/reports", {
method: "POST",
headers: { "Content-Type": "application/json", ...auth },
body: JSON.stringify({
project_id: project.id,
type: "element_note",
title: "Lightbox verify",
environment: { user_agent: "verify", browser: "Chromium", os: "Linux", viewport: { w: 1400, h: 900 }, dpr: 2, language: "en" },
attachment_ids: [up.file_id],
}),
});
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(`${WEB}/r/${note.share_token}`);
await page.waitForSelector(".screenshot-expand", { timeout: 15000 });
await page.waitForTimeout(800);
await page.screenshot({ path: "/tmp/ltt-shots/08-report.png" });
// attachments tab preview (Правка 2)
await page.click("text=Вложения");
await page.waitForTimeout(800);
console.log("widths:", await page.evaluate(() => {
const r = (el) => (el ? (({ x, width }) => `${Math.round(x)}+${Math.round(width)}`)(el.getBoundingClientRect()) : "none");
const tabs = document.querySelector(".tabs");
const panel = document.querySelector(".tab-panel-active");
const cs = panel ? getComputedStyle(panel) : null;
return {
tabs: r(tabs),
tabsMaxWidth: tabs ? getComputedStyle(tabs).maxWidth : null,
panel: r(panel),
panelWidth: cs?.width,
panelParent: r(panel?.parentElement),
panelParentTag: panel?.parentElement?.className,
card: r(document.querySelector(".tab-panel-active .card")),
};
}));
await page.screenshot({ path: "/tmp/ltt-shots/09-attachments.png" });
await page.click("text=Детали");
// open the lightbox (Правка 3)
await page.click(".screenshot-expand");
await page.waitForSelector(".lightbox", { timeout: 5000 });
await page.waitForTimeout(1000);
const zoom = page.locator(".lightbox-zoom");
console.log("fit zoom (expect <100% for 2800px image):", (await zoom.textContent()).trim());
await page.screenshot({ path: "/tmp/ltt-shots/10-lightbox-fit.png" });
// wheel zoom toward the cursor
await page.mouse.move(700, 400);
for (let i = 0; i < 3; i++) await page.mouse.wheel(0, -120);
await page.waitForTimeout(300);
console.log("after wheel zoom-in:", (await zoom.textContent()).trim());
// pan by dragging (drag ends over the backdrop — must NOT close)
const before = await page.locator(".lightbox-img").getAttribute("style");
await page.mouse.move(700, 450);
await page.mouse.down();
await page.mouse.move(400, 250, { steps: 8 });
await page.mouse.up();
const after = await page.locator(".lightbox-img").getAttribute("style");
console.log("pan moved transform:", before !== after ? "YES" : "NO");
console.log("lightbox still open after drag:", (await page.locator(".lightbox").count()) === 1);
await page.screenshot({ path: "/tmp/ltt-shots/11-lightbox-panned.png" });
// zoom cap: hammer zoom-in, must never exceed 100% (natural resolution)
for (let i = 0; i < 12; i++) await page.click('.lightbox-btn[title="Zoom in"]');
console.log("after 12 more zoom-ins (cap):", (await zoom.textContent()).trim());
// toolbar zoom-out works
await page.click('.lightbox-btn[title="Zoom out"]');
await page.click('.lightbox-btn[title="Zoom out"]');
console.log("after 2x zoom-out:", (await zoom.textContent()).trim());
// Esc closes
await page.keyboard.press("Escape");
await page.waitForTimeout(300);
console.log("lightbox closed on Esc:", (await page.locator(".lightbox").count()) === 0);
await browser.close();
console.log("VERIFY OK");