import { test, expect, type Page, type Route } from "@playwright/test" import { serverReadSchema, type ServerRead } from "../../packages/contracts/src/servers" function server(id: number): ServerRead { return serverReadSchema.parse({ id, name: `Проверка ${id}`, host: `192.0.2.${id}`, port: 443, username: "test-user", password: "fixture-only", useSsl: true, verifySsl: true, type: "exit-node", site: "TEST", country: id === 2 ? "DE" : "RU", asn: "", comment: "", enabled: true, lanSubnet: "", wanUplinks: [], createdAt: "2026-09-13T00:00:00Z", updatedAt: "2026-09-13T00:00:00Z", status: "offline", latency: null, os: "7.20", model: "Test router", uptime: null, cpuLoad: null, freeMemory: null, totalMemory: null, identityName: null, sessions: 0, polledAt: null, }) } async function setup(page: Page, intercept?: (route: Route, path: string) => Promise, count = 2) { const rows = Array.from({ length: count }, (_, i) => server(i + 1)) await page.route("**/health", route => route.fulfill({ json: { ok: true } })) await page.route("**/api/**", async route => { const path = new URL(route.request().url()).pathname if (await intercept?.(route, path)) return const method = route.request().method() let body: unknown = {} if (path === "/api/auth/config") body = { required: false } else if (path === "/api/servers") { if (method === "POST") { const added = { ...server(rows.length + 1), ...route.request().postDataJSON() } rows.push(added); body = added } else body = rows } else if (/^\/api\/servers\/\d+$/.test(path)) { const index = rows.findIndex(s => s.id === Number(path.split("/").pop())) if (method === "DELETE") { rows.splice(index, 1); await route.fulfill({ status: 204 }); return } if (method === "PUT") rows[index] = { ...rows[index], ...route.request().postDataJSON() } body = rows[index] } await route.fulfill({ json: body }) }) await page.goto("/servers") await expect(page.getByText("Проверка 1", { exact: true })).toBeVisible() } async function edit(page: Page, id = 1) { await page.getByRole("button", { name: `Действия: Проверка ${id}`, exact: true }).click() await page.getByRole("menuitem", { name: "Редактировать", exact: true }).click() } test("таблица v9 сохраняет все строки, сортировку и раскрытие деталей", async ({ page }) => { await setup(page, undefined, 12) 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") await page.getByText("Проверка 12", { exact: true }).click() await expect(page.getByText("Модель: Test router", { exact: true })).toBeVisible() await page.getByText("Проверка 12", { exact: true }).click() await expect(page.getByText("Модель: Test router", { exact: true })).toBeHidden() }) async function finalStep(page: Page) { for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "Далее", exact: true }).click() } test("фильтры сервера: применение, AND, изменение и сброс", async ({ page }) => { const errors: string[] = [] page.on("pageerror", error => errors.push(error.message)) await setup(page) await page.getByRole("button", { name: "Фильтры", exact: true }).click() await page.getByRole("option", { name: "Статус", exact: true }).click() await page.getByRole("option", { name: "равно", exact: true }).click() await page.getByRole("option", { name: "Online", exact: true }).click() await expect(page.getByText("Нет совпадений", { exact: true })).toBeVisible() await page.getByRole("button", { name: "Online", exact: true }).click() await page.getByRole("option", { name: "Offline", exact: true }).click() await expect(page.locator('tr[data-row-id]')).toHaveCount(2) await page.getByRole("button", { name: "Фильтры", exact: true }).click() await page.getByRole("option", { name: "Страна", exact: true }).click() await page.getByRole("option", { name: "равно", exact: true }).click() await page.getByRole("option", { name: "RU", exact: true }).click() await expect(page.locator('tr[data-row-id]')).toHaveCount(1) await expect(page.getByText("Проверка 1", { exact: true })).toBeVisible() await page.screenshot({ path: "test-results/servers-desktop.png", fullPage: true, animations: "disabled" }) await page.getByRole("button", { name: "Очистить", exact: true }).click() await expect(page.locator('tr[data-row-id]')).toHaveCount(2) expect(errors).toEqual([]) }) test("мобильный эталон: клавиатура, Escape и раскрытие", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }) await setup(page) const filters = page.getByRole("button", { name: "Фильтры", exact: true }) await filters.focus() await page.keyboard.press("Enter") await expect(page.getByRole("combobox", { name: "Поиск поля…" })).toBeFocused() await page.keyboard.press("Escape") await expect(filters).toBeFocused() const expand = page.getByRole("button", { name: "Раскрыть: Проверка 1", exact: true }) await expand.focus() await page.keyboard.press("Enter") await expect(page.getByText("Модель: Test router", { exact: true })).toBeVisible() await page.getByRole("button", { name: "Свернуть: Проверка 1", exact: true }).press("Enter") await expect(page.getByText("Модель: Test router", { exact: true })).toBeHidden() await page.screenshot({ path: "test-results/servers-mobile.png", fullPage: true, animations: "disabled" }) expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true) }) for (const status of [400, 403, 500]) { test(`ошибка сохранения ${status} сохраняет форму; повтор завершается успешно`, async ({ page }) => { let fail = true await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "PUT" && fail) { await route.fulfill({ status, json: { error: "Сохранение отклонено" } }); return true } return false }) await edit(page) await expect(page.getByRole("button", { name: "Далее", exact: true })).toBeEnabled() await finalStep(page) await page.getByRole("button", { name: "Сохранить", exact: true }).click() await expect(page.getByRole("alert")).toContainText(status === 403 ? "Недостаточно прав" : "Сохранение отклонено") await expect(page.getByRole("dialog")).toBeVisible() fail = false await page.getByRole("button", { name: "Сохранить", exact: true }).click() await expect(page.getByRole("dialog")).toBeHidden() await expect(page.getByText("Сервер сохранён.", { exact: true })).toBeVisible() }) } test("двойное сохранение отправляет один запрос и блокирует форму", async ({ page }) => { let requests = 0 let finish!: () => void const pending = new Promise(resolve => { finish = resolve }) await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "PUT") { requests++; await pending await route.fulfill({ json: server(1) }); return true } return false }) await edit(page); await finalStep(page) await page.getByRole("button", { name: "Сохранить", exact: true }).evaluate((button: HTMLButtonElement) => { button.click(); button.click() }) await expect(page.getByRole("button", { name: "Сохранение…", exact: true })).toBeDisabled() await expect(page.getByRole("button", { name: "Отмена", exact: true })).toBeDisabled() await expect.poll(() => requests).toBe(1) finish() await expect(page.getByRole("dialog")).toBeHidden() expect(requests).toBe(1) }) test("ошибка удаления оставляет строку и подтверждение; повтор удаляет", async ({ page }) => { let fail = true await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "DELETE" && fail) { await route.fulfill({ status: 500, json: { error: "Удаление отклонено" } }); return true } return false }) await page.getByRole("button", { name: "Действия: Проверка 1", exact: true }).click() await page.getByRole("menuitem", { name: "Удалить", exact: true }).click() await expect(page.getByRole("alertdialog")).toContainText("Проверка 1") await page.getByRole("button", { name: "Удалить", exact: true }).click() await expect(page.getByRole("alertdialog")).toContainText("Удаление отклонено") await expect(page.getByText("Проверка 1", { exact: true })).toBeVisible() fail = false await page.getByRole("button", { name: "Удалить", exact: true }).click() await expect(page.getByRole("alertdialog")).toBeHidden() await expect(page.getByText("Проверка 1", { exact: true })).toHaveCount(0) }) test("отключение сохраняет offline-статус и отправляет enabled через API", async ({ page }) => { let payload: unknown await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "PUT") payload = route.request().postDataJSON() return false }) await page.getByRole("button", { name: "Действия: Проверка 1", exact: true }).click() await page.getByRole("menuitem", { name: "Отключить", exact: true }).click() await expect(page.getByText("Проверка 1: отключён.", { exact: true })).toBeVisible() expect(payload).toEqual({ enabled: false }) await expect(page.getByText("Оффлайн", { exact: true })).toHaveCount(2) }) test("ошибка загрузки деталей блокирует сохранение и позволяет повтор", async ({ page }) => { let fail = true await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "GET" && fail) { await route.fulfill({ status: 403, json: { error: "Forbidden" } }); return true } return false }) await edit(page) await expect(page.getByRole("alert")).toContainText("Недостаточно прав") await expect(page.getByRole("button", { name: "Далее", exact: true })).toBeDisabled() fail = false await page.getByRole("button", { name: "Повторить загрузку", exact: true }).click() await expect(page.getByRole("button", { name: "Далее", exact: true })).toBeEnabled() }) test("поздний ответ первого сервера не перезаписывает второй", async ({ page }) => { let finish!: () => void const pending = new Promise(resolve => { finish = resolve }) await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "GET") { await pending; await route.fulfill({ json: { ...server(1), username: "wrong-user" } }); return true } return false }) await edit(page) await expect(page.getByText("Загрузка параметров сервера…")).toBeVisible() await page.getByRole("button", { name: "Отмена", exact: true }).click() await edit(page, 2) await expect(page.getByRole("button", { name: "Далее", exact: true })).toBeEnabled() finish() await page.getByRole("button", { name: "Далее", exact: true }).click() await page.getByRole("button", { name: "Далее", exact: true }).click() await expect(page.getByPlaceholder("api-user", { exact: true })).toHaveValue("test-user") }) test("массовый опрос ограничен четырьмя запросами и повторяет только ошибки", async ({ page }) => { let active = 0, maximum = 0 const calls: string[] = [] let fail = true await setup(page, async (route, path) => { if (path.endsWith("/poll")) { calls.push(path); maximum = Math.max(maximum, ++active) await new Promise(resolve => setTimeout(resolve, 100)) active-- await route.fulfill({ status: fail && path === "/api/servers/2/poll" ? 500 : 200, json: { error: "Опрос недоступен" } }) return true } return false }, 6) await page.getByRole("button", { name: "Проверить все", exact: true }).click() await expect(page.getByText("Опрос: успешно 5 из 6", { exact: true })).toBeVisible() expect(maximum).toBeLessThanOrEqual(4) expect(calls).toHaveLength(6) fail = false await page.getByRole("button", { name: "Повторить неуспешные", exact: true }).click() await expect(page.getByText("Опрос: успешно 1 из 1", { exact: true })).toBeVisible() expect(calls.slice(6)).toEqual(["/api/servers/2/poll"]) }) test("таймаут сохранения сохраняет форму и показывает неопределённый результат", async ({ page }) => { // Speed up only the browser's server-operation deadline, keeping real fetch/AbortSignal. await page.addInitScript(() => { const timeout = AbortSignal.timeout.bind(AbortSignal) AbortSignal.timeout = (ms: number) => timeout(ms === 30_000 ? 500 : ms) }) await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "PUT") { await new Promise(resolve => setTimeout(resolve, 1500)) await route.fulfill({ json: server(1) }).catch(() => {}) return true } return false }) await edit(page); await finalStep(page) await page.getByRole("button", { name: "Сохранить", exact: true }).click() await expect(page.getByRole("alert")).toContainText("Результат операции неизвестен") await expect(page.getByRole("button", { name: "Сохранить", exact: true })).toBeEnabled() }) test("создание сервера проходит через API и добавляет подтверждённую запись", async ({ page }) => { await setup(page) await page.getByRole("button", { name: "Добавить сервер", exact: true }).click() await page.getByRole("button", { name: "Далее", exact: true }).click() await page.getByRole("button", { name: "Далее", exact: true }).click() await page.getByPlaceholder("203.0.113.1", { exact: true }).fill("192.0.2.99") await page.getByPlaceholder("api-user", { exact: true }).fill("fixture-user") await page.getByRole("button", { name: "Далее", exact: true }).click() await page.getByRole("dialog").getByRole("button", { name: "Добавить сервер", exact: true }).click() await expect(page.getByRole("dialog")).toBeHidden() await expect(page.getByRole("button", { name: "Действия: 192.0.2.99", exact: true })).toHaveCount(1) }) test("успешный опрос и ошибка обновления списка имеют раздельные результаты", async ({ page }) => { let polled = false await setup(page, async (route, path) => { if (path.endsWith("/poll")) { polled = true; await route.fulfill({ json: {} }); return true } if (path === "/api/servers" && polled) { await route.fulfill({ status: 500, json: { error: "Список недоступен" } }); return true } return false }) await page.getByRole("button", { name: "Действия: Проверка 1", exact: true }).click() await page.getByRole("menuitem", { name: "Опросить", exact: true }).click() await expect(page.getByText("Опрос: успешно 1 из 1", { exact: true })).toBeVisible() await expect(page.getByRole("alert").filter({ hasText: "Действие не завершено" })).toContainText("Опрос завершён, но список не обновлён") await expect(page.getByText("Проверка 1", { exact: true })).toBeVisible() }) test("ошибка отключения не меняет enabled", async ({ page }) => { await setup(page, async (route, path) => { if (path === "/api/servers/1" && route.request().method() === "PUT") { await route.fulfill({ status: 403, json: { error: "Forbidden" } }); return true } return false }) await page.getByRole("button", { name: "Действия: Проверка 1", exact: true }).click() await page.getByRole("menuitem", { name: "Отключить", exact: true }).click() await expect(page.getByRole("alert").filter({ hasText: "Действие не завершено" })).toContainText("Недостаточно прав") await page.getByRole("button", { name: "Действия: Проверка 1", exact: true }).click() await expect(page.getByRole("menuitem", { name: "Отключить", exact: true })).toBeVisible() }) test("изменение подключения сбрасывает результат его проверки", async ({ page }) => { await setup(page, async (route, path) => { if (path.endsWith("/test-connection")) { await route.fulfill({ json: { success: true, message: "Проверено тестовым API" } }); return true } return false }) await edit(page) await page.getByRole("button", { name: "Далее", exact: true }).click() await page.getByRole("button", { name: "Далее", exact: true }).click() await page.getByRole("button", { name: "Проверить подключение", exact: true }).click() await expect(page.getByText("Проверено тестовым API", { exact: true })).toBeVisible() await page.getByPlaceholder("api-user", { exact: true }).fill("changed-user") await expect(page.getByText("Проверено тестовым API", { exact: true })).toHaveCount(0) })