From 747f54446264786fa0bc10c04cca6363c1a9e48f Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 12:10:38 +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=BF=D0=B8=D1=80=D0=BE=D0=B2=20WireGuard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/wireguard/page.tsx | 5 +- .../data-grids/wireguard-peers-grid.tsx | 35 +++++++++--- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 8 +++ docs/ux-ui-stage-12.md | 9 +++ .../screens/large-wireguard-peers-320.png | Bin 0 -> 42477 bytes tests/e2e/large-wireguard-peers.spec.ts | 54 ++++++++++++++++++ 7 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 docs/ux-ui/screens/large-wireguard-peers-320.png create mode 100644 tests/e2e/large-wireguard-peers.spec.ts diff --git a/app/(main)/wireguard/page.tsx b/app/(main)/wireguard/page.tsx index c2bcb68e..24c36fac 100644 --- a/app/(main)/wireguard/page.tsx +++ b/app/(main)/wireguard/page.tsx @@ -206,6 +206,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf const [exportPeerId, setExportPeerId] = useState(null) const [peerIface, setPeerIface] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) + const pendingPeer = pendingDelete?.kind === "peer" ? pendingDelete.iface.peers.find((peer, index) => peerRowId(pendingDelete.iface, peer, index) === pendingDelete.peerId) : undefined const deleteLock = useRef(false) const [deleteBusy, setDeleteBusy] = useState(false) const [deleteError, setDeleteError] = useState(null) @@ -973,11 +974,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf {pendingDelete?.kind === "peer" ? "Удалить пира?" : "Удалить интерфейс?"} - + {pendingDelete?.kind === "iface" ? `${pendingDelete.iface.name} на ${pendingDelete.iface.serverName}. Вместе с интерфейсом будут удалены связанные пиры на роутере.` : pendingDelete - ? `Пир на ${pendingDelete.iface.name} (${pendingDelete.iface.serverName}).` + ? `Пир «${pendingPeer?.name || pendingPeer?.publicKey || pendingDelete.peerId}» (ID ${pendingDelete.peerId}) на ${pendingDelete.iface.name} (${pendingDelete.iface.serverName}).` : null} diff --git a/components/data-grids/wireguard-peers-grid.tsx b/components/data-grids/wireguard-peers-grid.tsx index f744b278..cd55f6cc 100644 --- a/components/data-grids/wireguard-peers-grid.tsx +++ b/components/data-grids/wireguard-peers-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, @@ -57,6 +57,7 @@ function WireguardPeersGrid({ const cols: ColumnDef[] = [ { id: "peer", + size: 144, accessorKey: "publicKey", header: ({ column }) => ( @@ -80,6 +81,7 @@ function WireguardPeersGrid({ }, { id: "iface", + size: 120, accessorKey: "ifaceName", header: ({ column }) => , cell: ({ row }) => ( @@ -96,6 +98,7 @@ function WireguardPeersGrid({ if (!compactServer) { cols.push({ id: "server", + size: 128, accessorKey: "serverName", header: ({ column }) => , cell: ({ row }) => ( @@ -115,6 +118,7 @@ function WireguardPeersGrid({ cols.push( { id: "allowedIps", + size: 136, accessorFn: (row) => row.allowedIps.join(", "), header: ({ column }) => , cell: ({ row }) => ( @@ -130,6 +134,7 @@ function WireguardPeersGrid({ }, { id: "handshake", + size: 144, accessorKey: "latestHandshake", header: ({ column }) => , cell: ({ row }) => ( @@ -150,6 +155,7 @@ function WireguardPeersGrid({ }, { id: "transfer", + size: 160, header: () => ( RX / TX ), @@ -174,6 +180,7 @@ function WireguardPeersGrid({ }, { id: "endpoint", + size: 144, accessorKey: "endpoint", header: ({ column }) => , cell: ({ row }) => ( @@ -191,7 +198,7 @@ function WireguardPeersGrid({ id: "actions", header: () => Действия, enableSorting: false, - size: 72, + size: 112, cell: ({ row }) => { const peer = row.original return ( @@ -201,7 +208,7 @@ function WireguardPeersGrid({ type="button" variant="ghost" size="icon" - className="size-7" + className="size-11" aria-label="Экспорт peer .conf" onClick={() => onExportPeer(peer)} > @@ -213,7 +220,7 @@ function WireguardPeersGrid({ type="button" variant="ghost" size="icon" - className="size-7 text-destructive" + className="size-11 text-destructive" aria-label="Удалить пира" onClick={() => onDeletePeer(peer)} > @@ -235,12 +242,20 @@ function WireguardPeersGrid({ const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["peer"], end: [] } }, data: peers, columns, getRowId: (row) => JSON.stringify([row.serverId, row.ifaceId, row.id]), }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(peers.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [peers.length, pageIndex, pageSize, table]) + if (peers.length === 0) { return ( 50 || pageSize !== 50} + tableLayout={{ dense: true, columnsPinnable: true }} tableClassNames={{ headerRow: "border-b border-border", bodyRow: cn("group/row"), }} - /> + > + + + + ) } diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 4443223f..4ca0fb12 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 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Б сохраняются. +Этап 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 принят локально:WG peers1000/50 DOM/pinning/точный delete target/29+3 browser/build. Следующий блок12.3.30: большое раскрытие пиров; далее 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 b94337b3..faa04af9 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,14 @@ +## 12.3.29 — большой каталог пиров WireGuard (2026-10-04) + +Peer grid теперь использует client pagination50, clamp и сохранение страницы при refresh. Полный поиск/сортировка выполняются до страницы, composite server/interface/peer ID сохранён. Штатные ReUI ScrollArea/start pinning удерживают идентификатор на320px, действия имеют44px цель. Подтверждение удаления теперь показывает имя и точный ID пира, а не только интерфейс. + +Профиль1000 пиров одного интерфейса/119347 JSON bytes:50 строк/2596 DOM nodes/1369ms ready в локальном dev с HTTP-моками. Проверены page20/refresh, поиск записи1000, сортировка по всему каталогу, выбранные server/interface/keep в URL,503/cache/page, zero/missing RX/TX,320px/axe grid,1000→1 и точный пир в dialog.29 старых browser прошли; после уточнения dialog повтор большого профиля и обоих delete-сценариев3/3. TypeScript без incremental/ESLint и production frontend build прошли. [Снимок320px](ux-ui/screens/large-wireguard-peers-320.png) просмотрен. + +Осталось17 manualPagination:true specialized grids, включая bounded/demo источники. Следующий блок12.3.30: ограничить DOM раскрытия большого списка пиров одного интерфейса. Восстановление WireGuard search/tab/status из URL, аудит подписи «онлайн» по факту handshake и внешняя12.5 остаются; presence старого handshake не доказывает текущую доступность. Общая main/context после delta целиком ещё не повторена. Коммит локальный без 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 цель. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index d8f28496..fde99649 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -407,3 +407,12 @@ UI проверяет Ping-каталог до публикации, сохра Поиск/вкладка/статус 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.29 — большой каталог пиров WireGuard (2026-10-04) + +Peer grid теперь использует client pagination50, clamp и сохранение страницы при refresh. Полный поиск/сортировка выполняются до страницы, composite server/interface/peer ID сохранён. Штатные ReUI ScrollArea/start pinning удерживают идентификатор на320px, действия имеют44px цель. Подтверждение удаления теперь показывает имя и точный ID пира, а не только интерфейс. + +Профиль1000 пиров одного интерфейса/119347 JSON bytes:50 строк/2596 DOM nodes/1369ms ready в локальном dev с HTTP-моками. Проверены page20/refresh, поиск записи1000, сортировка по всему каталогу, выбранные server/interface/keep в URL,503/cache/page, zero/missing RX/TX,320px/axe grid,1000→1 и точный пир в dialog.29 старых browser прошли; после уточнения dialog повтор большого профиля и обоих delete-сценариев3/3. TypeScript без incremental/ESLint и production frontend build прошли. [Снимок320px](ux-ui/screens/large-wireguard-peers-320.png) просмотрен. + +Осталось17 manualPagination:true specialized grids, включая bounded/demo источники. Следующий блок12.3.30: ограничить DOM раскрытия большого списка пиров одного интерфейса. Восстановление WireGuard search/tab/status из URL, аудит подписи «онлайн» по факту handshake и внешняя12.5 остаются; presence старого handshake не доказывает текущую доступность. Общая main/context после delta целиком ещё не повторена. Коммит локальный без push. + diff --git a/docs/ux-ui/screens/large-wireguard-peers-320.png b/docs/ux-ui/screens/large-wireguard-peers-320.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f862fcf4d5764d3b79bf6dc1c8655dfc667618 GIT binary patch literal 42477 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/large-wireguard-peers.spec.ts b/tests/e2e/large-wireguard-peers.spec.ts new file mode 100644 index 00000000..f6e24a93 --- /dev/null +++ b/tests/e2e/large-wireguard-peers.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from "@playwright/test" +import { wgListResponseSchema } from "../../packages/contracts/src/wireguard" +import axe from "axe-core" +test("WireGuard1000 peers: full search/sort/page/cache/target/320px", async ({ page }, info) => { + test.setTimeout(90000) + let peers = Array.from({ length: 1000 }, (_, i) => ({ id: "p" + (i + 1), rosId: "*P" + (i + 1), publicKey: "key-" + String(i + 1).padStart(4, "0"), name: "Пир " + String(i + 1).padStart(4, "0"), allowedIps: ["10." + Math.floor(i/256) + "." + (i%256) + ".1/32"], transferRx: 0 })) + const iface = { id: "*1", rosId: "*1", name: "wg-main", serverId: "1", serverName: "Роутер", serverCountry: "RU", listenPort: 13231, mtu: 1420, comment: "", enabled: true, status: "up" } + 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: "Каталог WireGuard недоступен" } } : { json: wgListResponseSchema.parse({ interfaces: [{ ...iface, peers }] }) }) } + 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?server=1&interface=*1&keep=1") + await page.getByRole("tab", { name: /Пиры/ }).click() + const rows = page.locator("tr[data-row-id]"); await expect(rows).toHaveCount(50) + const profile = { count: peers.length, jsonBytes: Buffer.byteLength(JSON.stringify(peers)), rows: await rows.count(), nodes: await page.locator("*").count(), readyMs: Date.now()-started } + console.log("WG peers 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("Пир 0951") + const before=calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(()=>calls).toBeGreaterThan(before); await expect(rows.first()).toContainText("Пир 0951") + const search=page.getByPlaceholder("Поиск по ключу, IP, endpoint…") + await search.fill("key-1000"); await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText("Пир 1000") + await expect(page).toHaveURL(url=>url.searchParams.get("server")==="1"&&url.searchParams.get("interface")==="*1"&&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("Пир 0001") + await page.getByRole("table").getByRole("button", { name: "Пир", exact: true }).click() + await expect(rows.first()).toContainText("Пир 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.getByRole("status", { name: "WireGuard по ссылке", exact:true })).toContainText("сохранённые данные могут быть устаревшими") + await expect(rows).toHaveCount(50); await expect(rows.first()).toHaveAttribute("data-row-id",previous!) + await expect(rows.first().getByLabel("RX",{exact:true})).toHaveText("0 Б"); await expect(rows.first().getByLabel("TX",{exact:true})).toHaveText("—") + await page.setViewportSize({width:320,height:1000}) + const scroll=page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]') + await rows.first().scrollIntoViewIfNeeded(); 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) + await expect(rows.first().getByRole("button",{name:"Экспорт peer .conf",exact:true})).toBeInViewport() + 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.screenshot({path:info.outputPath("wireguard-peers-320.png"),animations:"disabled"}) + failed=false;peers=[peers[999]];await page.getByRole("button",{name:"Обновить",exact:true}).click() + await expect(rows).toHaveCount(1);await expect(rows.first()).toContainText("Пир 1000") + await rows.first().getByRole("button",{name:"Удалить пира",exact:true}).click() + await expect(page.getByRole("alertdialog")).toContainText("Пир 1000");await page.getByRole("alertdialog").getByRole("button",{name:"Отмена",exact:true}).click() + expect(writes).toEqual([]) +})