import { test, expect } from "@playwright/test" for (const section of ["gre", "vxlan"]) for (const device of ["desktop", "mobile"] as const) { test(`${section}: клавиатура и layout ${device}`, async ({ page }, info) => { await page.setViewportSize(device === "desktop" ? { width: 1440, height: 1000 } : { width: 390, height: 844 }) await page.clock.install({ time: new Date("2026-09-30T05:00:00Z") }) const devices = [1, 2].map(id => ({ id, name: id === 1 ? "Основной роутер" : "Резервный роутер", host: "192.0.2." + id, enabled: true, country: "RU", status: "online", type: "exit-node" })) const gre = ["1", "2"].map(id => ({ id: "*" + id, serverId: id, name: "gre-" + id, localAddress: "192.0.2." + id, remoteAddress: "198.51.100." + id, localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "live", ipsec: null, mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: true, comment: "", enabled: true, status: "up" })) const vxlan = ["1", "2"].map(id => ({ id: "*" + id, serverId: id, name: "vxlan-" + id, vni: Number(id) * 100, port: 0, dstPort: 8472, vtepIp: "192.0.2." + id, remoteVteps: ["198.51.100." + id], l2mtu: 1500, arpProxy: false, macLearning: true, comment: "", enabled: true, status: "up" })) let writes = 0 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() === "PATCH") { writes++ expect(path).toBe("/api/gre/tunnels") expect(r.request().postDataJSON()).toMatchObject({ serverId: "2", rosId: "*2", comment: "Проверенный контекст" }) gre[1].comment = "Проверенный контекст" return r.fulfill({ json: { ok: true } }) } return r.fulfill({ json: path === "/api/servers" ? devices : path === "/api/gre/tunnels" ? { tunnels: gre } : path === "/api/vxlan" ? { tunnels: vxlan } : path === "/api/auth/config" ? { required: false } : {} }) }) await page.goto("/" + section + "?server=2") await expect(page.locator("tr[data-row-id]")).toHaveCount(1) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }) await page.screenshot({ path: info.outputPath(`${section}-${device}.png`), animations: "disabled" }) const menu = page.getByRole("button", { name: "Действия: " + section + "-2", exact: true }) await menu.focus() await page.keyboard.press("Enter") await page.getByRole("menuitem", { name: section === "gre" ? "Редактировать" : "Экспорт .rsc", exact: true }).click() const dialog = page.getByRole("dialog") await expect.poll(() => dialog.evaluate(el => el.getBoundingClientRect().left >= 0 && el.getBoundingClientRect().right <= innerWidth)).toBe(true) if (device === "mobile") { await expect.poll(() => dialog.evaluate(el => el.getBoundingClientRect().width >= innerWidth - 1)).toBe(true) const footer = dialog.locator('[data-slot="sheet-footer"]') expect(await footer.evaluate(el => [...el.querySelectorAll("button")].every(button => button.getBoundingClientRect().right <= innerWidth && button.getBoundingClientRect().left >= 0))).toBe(true) } if (section === "gre") { await expect(dialog.getByLabel("Сервер (MikroTik)")).toHaveValue("2") await dialog.getByLabel("Комментарий").fill("Проверенный контекст") } else { await expect(dialog).toContainText("Резервный роутер") await expect(dialog).toContainText("vni=200") } await page.screenshot({ path: info.outputPath(`${section}-${section === "gre" ? "form" : "export"}-${device}.png`), animations: "disabled" }) if (section === "gre") { await dialog.getByRole("button", { name: "Сохранить", exact: true }).focus() await page.keyboard.press("Enter") await expect(dialog).toHaveCount(0) expect(writes).toBe(1) } else { await page.keyboard.press("Escape"); expect(writes).toBe(0) } expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) }) }