import { SCHEDULER_JOB_KEYS } from "../../lib/scheduler-settings" import { test, expect, type Page } from "@playwright/test" import axe from "axe-core" import type { SpeedProbeApiRow } from "../../components/data-grids/probes-speed-probes-data-grid" async function setup(page: Page, path: string, count = 12, grouped = false) { const probes: SpeedProbeApiRow[] = Array.from({ length: count }, (_, i) => ({ id: String(i + 1), srcServerId: String(i + 1), dstServerId: String(count + 1), srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null, })) const jobs = SCHEDULER_JOB_KEYS.map(jobKey => ({ jobKey, enabled: true, intervalSec: 300, running: false, lastFinishedAt: null, lastStatus: null, lastDurationMs: 0, lastError: null })) const calls: string[] = [] await page.route("**/health", route => route.fulfill({ json: { ok: true } })) await page.route("**/api/**", route => { const path = new URL(route.request().url()).pathname if (route.request().method() === "POST") calls.push(path) if (path === "/api/scheduler/status") return route.fulfill({ json: { jobs } }) if (path === "/api/scheduler/jobs/alert_engine/run-now") return route.fulfill({ json: { ok: true, result: { status: "ok", error: null, result: { v: 1, job: "alert_engine", sampledAt: "2026-10-03T00:00:00Z", rulesChecked: 0, standaloneFires: 0, groupFires: 0, skippedNoTelegram: false } }, status: { jobs } } }) return route.fulfill({ json: path === "/api/uptime/speed-probes" ? { probes } : path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? Array.from({length:count+1},(_,i)=>({id:i+1,name:i { await setup(page, "/probes") await page.getByRole("button", { name: "Расписание", exact: true }).click() const rows = page.locator("tr[data-row-id]") await expect(rows).toHaveCount(12) await page.getByRole("button", { name: "Источник", exact: true }).click() await expect(rows.first()).toContainText("Источник 1") await page.getByRole("button", { name: "Источник", exact: true }).click() await expect(rows.first()).toContainText("Источник 12") }) test("Сбор данных: все 11 задач и запуск выбранной задачи через API", async ({ page }) => { const calls = await setup(page, "/data-collection") const rows = page.locator("tr[data-row-id]") await expect(rows).toHaveCount(11) await expect(rows.last()).toContainText("Оповещения") await page.getByRole("button", { name: "Запустить: Оповещения", exact: true }).click() await expect.poll(() => calls).toContain("/api/scheduler/jobs/alert_engine/run-now") await expect(rows).toHaveCount(11) await expect(page.getByText("Запуск подтверждён: Оповещения", { exact: true })).toBeVisible() }) test("Демо-расписание: сортировка интервала и удаление выбранного правила", async ({ page }) => { await page.addInitScript(() => localStorage.setItem("routerlists:data-source", "mock")) await setup(page, "/probes") await page.getByRole("button", { name: "Расписание", exact: true }).click() const rows = page.locator("tr[data-row-id]") await expect(rows).toHaveCount(3) await page.getByRole("button", { name: "Интервал", exact: true }).click() await expect(rows.first()).toHaveAttribute("data-row-id", "r3") await rows.first().getByRole("button", { name: "Удалить правило", exact: true }).click() await expect(rows).toHaveCount(2) await expect(page.locator('tr[data-row-id="r3"]')).toHaveCount(0) }) test("Large speed probes: 50 строк, глобальная сортировка и контекст обновления", async ({ page }, info) => { test.setTimeout(90000) const start = Date.now() const writes = await setup(page, "/probes", 1000) await page.getByRole("button", { name: "Расписание", exact: true }).click() const rows = page.locator("tr[data-row-id]") await expect(rows).toHaveCount(50) const sample = { fixtureRows: 1000, renderedRows: await rows.count(), domNodes: await page.locator("*").count(), scheduleNodes: await page.getByRole("region", { name: "Пробы скорости", exact: true }).locator("*").count(), readyMs: Date.now() - start } console.log(JSON.stringify(sample)) await info.attach("large-speed-probes-metrics", { body: JSON.stringify(sample), contentType: "application/json" }) expect(sample.scheduleNodes).toBeLessThan(2000) expect(sample.domNodes).toBeLessThan(2500) await page.getByRole("button", { name: "Следующая страница", exact: true }).click() await expect(rows.first()).toHaveAttribute("data-row-id", "51") await page.getByRole("button", { name: "Обновить пробы скорости", exact: true }).click() await expect(page.getByRole("button", { name: "Обновить пробы скорости", exact: true })).toBeEnabled() await expect(page.getByText("51–100 из 1000", { exact: true })).toBeVisible() await page.getByRole("button", { name: "Источник", exact: true }).click() await page.getByRole("button", { name: "Источник", exact: true }).click() await page.getByRole("button", { name: "Перейти на страницу 1", exact: true }).click() await expect(rows.first()).toHaveAttribute("data-row-id", "1000") for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "…", exact: true }).last().click() await page.getByRole("button", { name: "Перейти на страницу 20", exact: true }).click() await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible() const panel = page.getByRole("region", { name: "Пробы скорости", exact: true }) const freshness = await panel.getByRole("status").textContent() await page.route("**/api/uptime/speed-probes", route => route.fulfill({ status: 503, json: { error: "Большой каталог недоступен" } })) await panel.getByRole("button", { name: "Обновить пробы скорости", exact: true }).click() await expect(panel.getByRole("alert")).toContainText("Показан последний успешный список") await expect(panel.getByRole("status")).toHaveText(freshness!) await expect(rows).toHaveCount(50) await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible() await page.setViewportSize({ width: 320, height: 900 }) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await page.route("**/api/uptime/speed-probes", route => route.fulfill({ json: { probes: [{ id: "1", srcServerId: "1", dstServerId: "1001", srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null }] } })) await page.getByRole("button", { name: "Обновить пробы скорости", exact: true }).click() await expect(rows).toHaveCount(1) await expect(rows.first()).toHaveAttribute("data-row-id", "1") await expect(panel.getByRole("alert")).toHaveCount(0) expect(writes).toEqual([]) }) for (const theme of ["dark", "light"]) test(`Large server rail: URL, страницы, поиск и клавиатура на1001 устройстве/${theme}`, async ({ page }, info) => { await page.addInitScript(value => localStorage.setItem("rl-theme", value), theme) await setup(page, "/probes?server=1000&keep=1", 1000) const rail = page.locator("aside").filter({ has: page.getByRole("listbox", { name: "Серверы", exact: true }) }) const list = rail.getByRole("listbox", { name: "Серверы", exact: true }) await expect(list.getByRole("option")).toHaveCount(50) await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1000") await expect(rail.getByText("951–1000 из 1001", { exact: true })).toBeVisible() expect(await rail.locator("*").count()).toBeLessThan(1200) await info.attach("large-rail-metrics", { body: JSON.stringify({ fixtureDevices: 1001, renderedOptions: await list.getByRole("option").count(), railNodes: await rail.locator("*").count(), domNodes: await page.locator("*").count() }), contentType: "application/json" }) await list.getByRole("option", { selected: true }).focus() await page.keyboard.press("End") await expect(page).toHaveURL(url => url.searchParams.get("server") === "1001" && url.searchParams.get("keep") === "1") await expect(list.getByRole("option")).toHaveCount(1) await expect(list.getByRole("option", { selected: true })).toContainText("Цель") await page.keyboard.press("Home") await expect(page).toHaveURL(url => url.searchParams.get("server") === "1" && url.searchParams.get("keep") === "1") await expect(list.getByRole("option")).toHaveCount(50) await page.keyboard.press("ArrowDown") await expect(page).toHaveURL(url => url.searchParams.get("server") === "2" && url.searchParams.get("keep") === "1") await expect(list.getByRole("option", { selected: true })).toContainText("Источник 2") await rail.getByRole("button", { name: "Следующие устройства", exact: true }).click() await expect(list.getByRole("option").first()).toContainText("Источник 51") await page.getByRole("button", { name: "Обновить устройства диагностики", exact: true }).click() await expect(page.getByRole("button", { name: "Обновить устройства диагностики", exact: true })).toBeEnabled() await expect(rail.getByText("51–100 из 1001", { exact: true })).toBeVisible() const search = rail.getByRole("textbox", { name: "Поиск сервера", exact: true }) await search.fill("Источник 1000") await expect(list.getByRole("option")).toHaveCount(1) await list.getByRole("option").click() await expect(page).toHaveURL(url => url.searchParams.get("server") === "1000" && url.searchParams.get("keep") === "1") await search.fill("") await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1000") await page.setViewportSize({ width: 320, height: 900 }) await page.getByRole("button", { name: "Серверы", exact: true }).click() const sheet = page.getByRole("dialog") await expect(sheet.getByRole("option")).toHaveCount(50) await expect(sheet.getByRole("option", { selected: true })).toContainText("Источник 1000") expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) expect(await sheet.getByRole("button", { name: "Следующие устройства", exact: true }).evaluate(el => el.getBoundingClientRect().right <= innerWidth)).toBe(true) await sheet.getByRole("option", { selected: true }).focus(); await expect(sheet.getByRole("option", { selected: true })).toBeFocused() await page.addScriptTag({ content: axe.source }) const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[role="dialog"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] } })) await info.attach("large-rail-axe", { body: JSON.stringify(audit), contentType: "application/json" }) expect(audit.violations.map(violation => ({ id: violation.id, nodes: violation.nodes.map(node => node.target) }))).toEqual([]) await page.screenshot({ path: `test-results/large-server-rail-mobile-${theme}.png`, animations: "disabled" }) await page.keyboard.press("Escape") await expect(page.getByRole("button", { name: "Серверы", exact: true })).toBeFocused() }) test("Large server rail: выбранное устройство и граница площадок", async ({ page }) => { await setup(page, "/probes?server=1000", 1000, true) const rail = page.locator("aside").filter({ has: page.getByRole("listbox", { name: "Серверы", exact: true }) }) const list = rail.getByRole("listbox", { name: "Серверы", exact: true }) await expect(list.getByRole("option")).toHaveCount(1) await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1000") await expect(rail.getByText("Площадка B", { exact: true })).toBeVisible() await list.getByRole("option", { selected: true }).focus() await page.keyboard.press("End") await expect(list.getByRole("option", { selected: true })).toContainText("Цель") await expect(list.getByRole("option")).toHaveCount(50) await expect(rail.getByText("Площадка A", { exact: true })).toBeVisible() await expect(rail.getByText("Площадка B", { exact: true })).toBeVisible() await page.keyboard.press("Home") await expect(list.getByRole("option", { selected: true })).toContainText("Источник 1") await expect(list.getByRole("option")).toHaveCount(50) })