import { test, expect } from "@playwright/test" import axe from "axe-core" import { writeFileSync } from "node:fs" for (const width of [320, 390, 1440]) for (const count of [1, 7]) { test(`Матрица: измерение целиком рядом с pinned WAN/${width}/${count}`, async ({ page }, info) => { await page.setViewportSize({ width, height: 1000 }) const longName = "WAN-" + "площадка".repeat(15) const common = { country: "RU", enabled: true, status: "online", latency: 10, site: "TEST" } const servers = [ { ...common, id: 1, name: "Home", host: "192.0.2.1", type: "home-router", wanUplinks: [{ id: "wan", name: longName, iface: "ether1", isp: "ISP", ip: "198.51.100.1", maxDl: 100, maxUl: 100 }] }, ...Array.from({ length: count }, (_, i) => ({ ...common, id: i + 2, name: `Jump ${i + 1}`, host: `192.0.2.${i + 2}`, type: "jump-host" })), ] const probes = servers.slice(1).map(s => ({ id: `probe-${s.id}`, srcServerId: "1", dstServerId: String(s.id), srcInterface: "ether1", dstInterface: "", enabled: true, lastPingRttMs: 5, lastPingLossPct: 0, lastTxAvgMbps: 30, lastRxAvgMbps: 80, lastPingAt: "2026-10-07T00:00:00Z" })) const writes: string[] = [] await page.route("**/health", r => r.fulfill({ json: { ok: true } })) await page.route("**/api/**", r => { const path = new URL(r.request().url()).pathname if (r.request().method() !== "GET" && !path.endsWith("/preview")) writes.push(path) return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/preview") ? { serverId: 1, serverName: "Home", pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} }) }) await page.goto("/route-optimizer") const region = page.getByRole("region", { name: "Матрица WAN и Jump Host" }) const row = region.locator("tr[data-row-id]") await expect(row).toHaveCount(1) const viewport = region.locator('[data-slot="scroll-area-viewport"]') const measurements: unknown[] = [] async function inspectColumns() { const cells = row.locator("td").filter({ hasText: "↓80 ↑30 Мбит/с" }) for (let i = 0; i < await cells.count(); i++) { const cell = cells.nth(i) await cell.evaluate(el => { const viewport = el.closest('[data-slot="scroll-area-viewport"]') as HTMLElement const pinned = el.parentElement!.querySelector('td[data-pinned="start"]')! viewport.scrollLeft += el.getBoundingClientRect().left - viewport.getBoundingClientRect().left - pinned.getBoundingClientRect().width }) const geometry = await cell.evaluate(el => { const viewport = el.closest('[data-slot="scroll-area-viewport"]')!, pinned = el.parentElement!.querySelector('td[data-pinned="start"]')! const c = el.getBoundingClientRect(), v = viewport.getBoundingClientRect(), p = pinned.getBoundingClientRect() return { cellLeft: c.left, cellRight: c.right, pinnedRight: p.right, viewportRight: v.right, pinnedWidth: p.width, cellWidth: c.width } }) measurements.push({ width: page.viewportSize()!.width, count, ...geometry }) console.log("Matrix geometry", JSON.stringify({ width: page.viewportSize()!.width, count, ...geometry })) expect(geometry.cellLeft).toBeGreaterThanOrEqual(geometry.pinnedRight - 1) expect(geometry.cellRight).toBeLessThanOrEqual(geometry.viewportRight + 1) const text = await cell.getByText("↓80 ↑30 Мбит/с", { exact: true }).boundingBox() expect(text!.x).toBeGreaterThanOrEqual(geometry.pinnedRight - 1) expect(text!.x + text!.width).toBeLessThanOrEqual(geometry.viewportRight + 1) } } await inspectColumns() if (count > 5) { await region.getByRole("button", { name: "Следующие Jump Host" }).click() await expect(region).toContainText("Столбцы 6–7 из 7") await inspectColumns() } await expect(row.getByText(longName, { exact: true })).toHaveAttribute("title", longName) const bounds = await viewport.boundingBox() expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width) expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width) if (width === 320 && count === 1) { await page.setViewportSize({ width: 1440, height: 1000 }) await expect.poll(() => row.locator('td[data-pinned="start"]').evaluate(el => el.getBoundingClientRect().width)).toBeGreaterThanOrEqual(180) await inspectColumns() await page.setViewportSize({ width: 320, height: 1000 }) await expect.poll(() => row.locator('td[data-pinned="start"]').evaluate(el => el.getBoundingClientRect().width)).toBe(112) await inspectColumns() await region.scrollIntoViewIfNeeded() await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) await page.screenshot({ path: info.outputPath("matrix-readable-320.png"), animations: "disabled" }) writeFileSync(info.outputPath("geometry.json"), JSON.stringify({ width, count, measurements, writes }, null, 2)) } if (width === 320 && count === 1) { await page.addScriptTag({ content: axe.source }) const result = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[aria-label="Матрица WAN и Jump Host"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } })) expect(result.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([]) } expect(writes).toEqual([]) }) }