From a6be18c1d60339a5dd5a13cd7584b4e8028c252f Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 12:05:08 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D0=BE=D0=B2=20WireGuard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/data-grids/wireguard-data-grid.tsx | 44 ++++++++---- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 8 +++ docs/ux-ui-stage-12.md | 9 +++ .../large-wireguard-interfaces-320.png | Bin 0 -> 47775 bytes tests/e2e/large-wireguard-interfaces.spec.ts | 63 ++++++++++++++++++ 6 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 docs/ux-ui/screens/large-wireguard-interfaces-320.png create mode 100644 tests/e2e/large-wireguard-interfaces.spec.ts diff --git a/components/data-grids/wireguard-data-grid.tsx b/components/data-grids/wireguard-data-grid.tsx index 631f7a97..6937f890 100644 --- a/components/data-grids/wireguard-data-grid.tsx +++ b/components/data-grids/wireguard-data-grid.tsx @@ -1,8 +1,8 @@ "use client" -import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" +import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo, type ReactNode } from "react" +import { useEffect, useMemo, type ReactNode } from "react" import { type ColumnDef, useTable, @@ -74,6 +74,7 @@ function WireguardDataGrid({ () => [ { id: "name", + size: 176, accessorKey: "name", header: ({ column }) => ( !!p.latestHandshake).length return (
- {expanded ? ( - - ) : ( - - )} +
- {iface.name} + {iface.name}
{!compactServer ? (
@@ -131,6 +130,7 @@ function WireguardDataGrid({ }, { id: "listenPort", + size: 100, accessorKey: "listenPort", header: ({ column }) => , cell: ({ row }) => ( @@ -144,6 +144,7 @@ function WireguardDataGrid({ }, { id: "mtu", + size: 80, accessorKey: "mtu", header: ({ column }) => , cell: ({ row }) => {row.original.mtu}, @@ -155,6 +156,7 @@ function WireguardDataGrid({ }, { id: "peers", + size: 80, header: () => ( Пиры ), @@ -177,6 +179,7 @@ function WireguardDataGrid({ }, { id: "status", + size: 104, accessorKey: "status", header: ({ column }) => , cell: ({ row }) => ( @@ -213,8 +216,8 @@ function WireguardDataGrid({ variant="outline" size="icon" className={cn( - "size-8 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none", - "opacity-0 transition-[opacity,background-color,color,border-color]", + "size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none", + "transition-[background-color,color,border-color]", "group-hover/row:opacity-100 focus-visible:opacity-100", "data-popup-open:opacity-100 data-popup-open:bg-muted", )} @@ -260,7 +263,7 @@ function WireguardDataGrid({ ) }, enableSorting: false, - size: 56, + size: 72, meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST, @@ -272,13 +275,22 @@ function WireguardDataGrid({ const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + autoResetExpanded: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["name"], end: [] } }, data: interfaces, columns, getRowId: (row) => JSON.stringify([row.serverId, row.id]), getRowCanExpand: () => true, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(interfaces.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [interfaces.length, pageIndex, pageSize, table]) + if (interfaces.length === 0) { return ( 50 || pageSize !== 50} + tableLayout={{ dense: true, columnsPinnable: true }} onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()} tableClassNames={{ headerRow: "border-b border-border", bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"), }} - /> + > + + + + ) } diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index d864af10..4443223f 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-04 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. 12.3.27 принят локально:WireGuard zero/missing counters/UI/backend mapper/29 browser/unit/builds. Следующий блок12.3.28: большой каталог WireGuard interfaces; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. 12.3.27 принят локально:WireGuard zero/missing counters/UI/backend mapper/29 browser/unit/builds. 12.3.28 принят локально:WireGuard interfaces1000/50 DOM/pinning/actions/cache/29+1 browser/build. Следующий блок12.3.29: большой peer grid; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 37058d1c..b94337b3 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,14 @@ +## 12.3.28 — большой каталог интерфейсов WireGuard (2026-10-04) + +Полный API /wireguard не ограничивает каталог страницей. Таблица теперь использует client pagination50, глобальную сортировку, сохранение page/expanded при обновлении и clamp после сокращения списка. Composite ID server/interface сохранён. ReUI DataGridScrollArea и штатное start pinning удерживают имя на320px; длинные имена переносятся. Кнопка раскрытия доступна с клавиатуры, меню действий видно без hover и имеет44px цель. + +Профиль1000 интерфейсов/316026 JSON bytes:50 строк/2136 DOM nodes/1573ms до ready в локальном dev с HTTP-моками (не оценка RouterOS или production latency). Проверены page20, Enter/раскрытие и refresh, поиск записи1000, глобальная числовая сортировка порта,503/cache/page, горизонтальная прокрутка с закреплением имени/меню, axe grid и1000→1 с правильным объектом в диалоге удаления.29 прежних browser прошли; большой профиль после уточнения локального поиска и вертикального scroll1/1. TypeScript без incremental/ESLint и frontend production build прошли. [Снимок320px](ux-ui/screens/large-wireguard-interfaces-320.png) просмотрен. + +Поиск/вкладка/статус WireGuard пока локальные, сервер/interface контекст в URL сохраняется; восстановление остальных фильтров записано отдельным остатком. Осталось18 manualPagination:true specialized grids, включая bounded/demo источники; это не18 доказанных дефектов. Следующий блок12.3.29: полный peer grid1000/50 DOM/действия/320px. Unbounded раскрытие peers одного интерфейса и внешняя12.5 остаются. Коммит локальный без push. + ## 12.3.27 — достоверные счётчики WireGuard (2026-10-04) Нулевые RX/TX отображаются как 0 Б, отсутствующие значения — как — в таблице пиров и раскрытии интерфейса. Направления подписаны для accessibility. Backend принимает только целые неотрицательные десятичные счётчики в safe integer диапазоне: строки вроде12junk, дроби/минус/unsafe больше не превращаются в показания. Существующие правила сетевых операций не изменены. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index ecc9bcf2..d8f28496 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -398,3 +398,12 @@ UI проверяет Ping-каталог до публикации, сохра Проверки: wireguard-links/tables29/29, включая обе композиции zero/missing и старые сценарии операций/ссылок; pure mapper1/1; TypeScript без incremental, ESLint изменённых файлов и backend/frontend production build прошли. Реальные RouterOS ответы не проверены. Следующий блок12.3.28: большой каталог интерфейсов WireGuard, пагинация50/refresh/search/320px; полный peer grid и раскрытие большого списка пиров остаются отдельным объёмом. Коммит локальный без push. + +## 12.3.28 — большой каталог интерфейсов WireGuard (2026-10-04) + +Полный API /wireguard не ограничивает каталог страницей. Таблица теперь использует client pagination50, глобальную сортировку, сохранение page/expanded при обновлении и clamp после сокращения списка. Composite ID server/interface сохранён. ReUI DataGridScrollArea и штатное start pinning удерживают имя на320px; длинные имена переносятся. Кнопка раскрытия доступна с клавиатуры, меню действий видно без hover и имеет44px цель. + +Профиль1000 интерфейсов/316026 JSON bytes:50 строк/2136 DOM nodes/1573ms до ready в локальном dev с HTTP-моками (не оценка RouterOS или production latency). Проверены page20, Enter/раскрытие и refresh, поиск записи1000, глобальная числовая сортировка порта,503/cache/page, горизонтальная прокрутка с закреплением имени/меню, axe grid и1000→1 с правильным объектом в диалоге удаления.29 прежних browser прошли; большой профиль после уточнения локального поиска и вертикального scroll1/1. TypeScript без incremental/ESLint и frontend production build прошли. [Снимок320px](ux-ui/screens/large-wireguard-interfaces-320.png) просмотрен. + +Поиск/вкладка/статус WireGuard пока локальные, сервер/interface контекст в URL сохраняется; восстановление остальных фильтров записано отдельным остатком. Осталось18 manualPagination:true specialized grids, включая bounded/demo источники; это не18 доказанных дефектов. Следующий блок12.3.29: полный peer grid1000/50 DOM/действия/320px. Unbounded раскрытие peers одного интерфейса и внешняя12.5 остаются. Коммит локальный без push. + diff --git a/docs/ux-ui/screens/large-wireguard-interfaces-320.png b/docs/ux-ui/screens/large-wireguard-interfaces-320.png new file mode 100644 index 0000000000000000000000000000000000000000..44560dd2f2fe7e70a16b05686602efac411639df GIT binary patch literal 47775 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 F{{?5XCT#!! literal 0 HcmV?d00001 diff --git a/tests/e2e/large-wireguard-interfaces.spec.ts b/tests/e2e/large-wireguard-interfaces.spec.ts new file mode 100644 index 00000000..2b25eda8 --- /dev/null +++ b/tests/e2e/large-wireguard-interfaces.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from "@playwright/test" +import { wgListResponseSchema } from "../../packages/contracts/src/wireguard" +import axe from "axe-core" + +test("WireGuard1000 interfaces: page/refresh/expansion/search/sort/cache/actions/320px", async ({ page }, info) => { + test.setTimeout(90000) + let interfaces = Array.from({ length: 1000 }, (_, i) => ({ + id: "*" + (i + 1), rosId: "*" + (i + 1), name: "wg-test-" + String(i + 1).padStart(4, "0"), + serverId: "1", serverName: "Тестовый роутер", serverCountry: "RU", listenPort: 13231 + i, mtu: 1420, + comment: "", enabled: true, status: "up", peers: [{ id: "p" + (i + 1), rosId: "*P" + (i + 1), + publicKey: "test-key-" + (i + 1), allowedIps: ["10." + Math.floor(i / 256) + "." + (i % 256) + ".1/32"], transferRx: 0 }], + })) + let failed = false, calls = 0 + 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") writes.push(path) + if (path === "/api/wireguard") { calls++; return r.fulfill(failed ? { status: 503, json: { error: "Unavailable" } } : { json: wgListResponseSchema.parse({ interfaces }) }) } + return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [{ id: 1, name: "Тестовый роутер", host: "192.0.2.1", country: "RU", enabled: true, status: null }] : {} }) + }) + const started = Date.now(); await page.goto("/wireguard?keep=1") + const rows = page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(50) + const profile = { count: interfaces.length, jsonBytes: Buffer.byteLength(JSON.stringify(interfaces)), rows: await rows.count(), nodes: await page.locator("*").count(), readyMs: Date.now() - started } + console.log("WG interface profile", JSON.stringify(profile)); await info.attach("profile", { body: JSON.stringify(profile), contentType: "application/json" }) + const next = page.getByRole("button", { name: "Следующая страница", exact: true }) + for (let i=0;i<19;i++) await next.click() + await expect(next).toBeDisabled(); await expect(rows.first()).toContainText("wg-test-0951") + const expand = rows.first().getByRole("button", { name: "Пиры: wg-test-0951", exact: true }) + await expand.focus(); await page.keyboard.press("Enter"); await expect(expand).toHaveAttribute("aria-expanded", "true") + await expect(page.getByText(interfaces[950].peers[0].allowedIps[0], { exact: true })).toBeVisible() + const before = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => calls).toBeGreaterThan(before); await expect(rows.first()).toContainText("wg-test-0951"); await expect(expand).toHaveAttribute("aria-expanded", "true") + await expand.click() + const search = page.getByPlaceholder("Поиск по имени, серверу, IP…") + await search.fill("wg-test-1000"); await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText("wg-test-1000") + await expect(page).toHaveURL(url => url.searchParams.get("keep") === "1") + await search.fill(""); await expect(rows).toHaveCount(50) + await page.getByRole("table").getByRole("button", { name: "Порт", exact: true }).click() + await expect(rows.first()).toContainText("wg-test-1000") + await next.click(); const previous = await rows.first().getAttribute("data-row-id") + failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(page.getByText(/Показаны последние полученные данные/)).toBeVisible() + await expect(rows).toHaveCount(50); await expect(rows.first()).toHaveAttribute("data-row-id", previous!) + await page.setViewportSize({ width: 320, height: 1000 }) + const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]') + await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) + await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0) + expect(await rows.first().getByRole("cell").first().evaluate(el => {const a=el.getBoundingClientRect(),v=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return a.left>=v.left-1&&a.right<=v.right+1})).toBe(true) + const action = rows.first().getByRole("button", { name: /Действия:/ }) + await action.scrollIntoViewIfNeeded(); await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }); await expect(action).toBeInViewport(); await action.click(); await expect(page.getByRole("menuitem", { name: "Экспорт", exact: true })).toBeVisible(); await page.keyboard.press("Escape") + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addScriptTag({ content: axe.source }) + expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations).toEqual([]) + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); await page.getByRole("table").scrollIntoViewIfNeeded() + await page.screenshot({ path: info.outputPath("wireguard-interfaces-320.png"), animations: "disabled" }) + failed = false; interfaces = [interfaces[999]]; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText("wg-test-1000") + await action.click(); await page.getByRole("menuitem", { name: "Удалить", exact: true }).click() + await expect(page.getByRole("alertdialog")).toContainText("wg-test-1000"); await page.getByRole("alertdialog").getByRole("button", { name: "Отмена", exact: true }).click() + expect(writes).toEqual([]) +})