import { test, expect } from "@playwright/test" import axe from "axe-core" import { serverReadSchema } from "../../packages/contracts/src/servers" for (const theme of ["light", "dark"]) test(`Server type badges: all types have readable contrast/${theme}`, async ({ page }) => { await page.addInitScript(value => localStorage.setItem("rl-theme", value), theme) await page.route("**/health", route => route.fulfill({ json: { ok: true } })) const servers = ["home-router", "exit-node", "jump-host"].map((type, i) => serverReadSchema.parse({ id: i + 1, name: `Контраст ${i + 1}`, host: `192.0.2.${i + 1}`, port: 443, username: "fixture", password: "fixture", useSsl: true, verifySsl: true, type, site: "TEST", country: "RU", asn: "", comment: "", enabled: true, lanSubnet: "", wanUplinks: [], createdAt: "2026-10-15T00:00:00Z", updatedAt: "2026-10-15T00:00:00Z", status: "online", latency: 1, os: "7.24.5", model: "CHR", uptime: null, cpuLoad: null, freeMemory: null, totalMemory: null, identityName: null, sessions: 0, polledAt: null, })) await page.route("**/api/**", route => route.fulfill({ json: new URL(route.request().url()).pathname === "/api/servers" ? servers : new URL(route.request().url()).pathname === "/api/auth/config" ? { required: false } : {} })) await page.goto("/servers") for (const name of ["Home Router", "Exit Node", "JumpHost"]) await expect(page.getByRole("cell", { name, exact: true })).toBeVisible() await page.addScriptTag({ content: axe.source }) const violations = await page.evaluate(async () => (await (window as unknown as { axe: typeof axe }).axe.run("#main-content", { runOnly: { type: "rule", values: ["color-contrast"] }, })).violations.map(v => ({ id: v.id, targets: v.nodes.map(n => n.target) }))) expect(violations).toEqual([]) }) for (const theme of ["light", "dark"]) test(`Server rail: keyboard selection across pages and target sizes/${theme}`, async ({ page }) => { await page.addInitScript(value => localStorage.setItem("rl-theme", value), theme) await page.route("**/health", route => route.fulfill({ json: { ok: true } })) await page.route("**/api/**", route => { const path = new URL(route.request().url()).pathname return route.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? Array.from({ length: 61 }, (_, i) => ({ id: i + 1, name: `Роутер ${String(i + 1).padStart(3, "0")}`, host: `192.0.2.${i + 1}`, enabled: true, status: "online", type: "home-router", country: "RU", })) : path === "/api/gre/tunnels" ? { tunnels: [], failures: [] } : {} }) }) await page.goto("/gre") const rail = page.getByRole("listbox", { name: "Серверы", exact: true }) const all = rail.getByRole("option", { name: /Все серверы/ }) await expect(all).toHaveAttribute("aria-selected", "true") await all.focus() await page.keyboard.press("ArrowDown") const first = rail.getByRole("option", { name: /Роутер 001/ }) await expect(first).toBeFocused() await expect(first).toHaveAttribute("aria-selected", "true") await expect(page).toHaveURL(/server=1(?:&|$)/) await page.keyboard.press("End") const last = rail.getByRole("option", { name: /Роутер 061/ }) await expect(last).toBeFocused() await expect(last).toHaveAttribute("aria-selected", "true") await expect(last).toBeInViewport() await expect(page).toHaveURL(/server=61(?:&|$)/) await page.keyboard.press("ArrowUp") await expect(rail.getByRole("option", { name: /Роутер 060/ })).toBeFocused() await page.keyboard.press("Home") await expect(all).toBeFocused() await expect(all).toHaveAttribute("aria-selected", "true") await page.keyboard.press("Tab") await expect.poll(() => rail.evaluate(el => el.contains(document.activeElement))).toBe(false) await page.addScriptTag({ content: axe.source }) const violations = await page.evaluate(async () => { const engine = (window as unknown as { axe: typeof axe }).axe return (await engine.run(document.querySelector('[role="listbox"][aria-label="Серверы"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] }, })).violations.map(v => ({ id: v.id, targets: v.nodes.map(n => n.target) })) }) expect(violations).toEqual([]) })