fix(ui): ограничить отображение каталога OSPF-маршрутов
This commit is contained in:
1 parent
908974302e
commit
586da55c24
7 files changed
+98
-7
No files matched your search
@@ -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 } from "react"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import {
|
||||
type ColumnDef,
|
||||
useTable,
|
||||
@@ -49,11 +49,12 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
() => [
|
||||
{
|
||||
id: "destination",
|
||||
size: 150,
|
||||
accessorKey: "destination",
|
||||
header: ({ column }) => (
|
||||
<DataGridSortHeader column={column} title="Назначение" className="ml-1" />
|
||||
),
|
||||
cell: ({ row }) => <span className="font-mono">{row.original.destination}</span>,
|
||||
cell: ({ row }) => <span className="font-mono whitespace-normal break-all">{row.original.destination}</span>,
|
||||
meta: {
|
||||
headerTitle: "Назначение",
|
||||
headerClassName: DATA_GRID_CELL_PAD_FIRST,
|
||||
@@ -62,6 +63,7 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
size: 116,
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Тип" />,
|
||||
cell: ({ row }) => (
|
||||
@@ -78,6 +80,7 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
},
|
||||
{
|
||||
id: "cost",
|
||||
size: 80,
|
||||
accessorKey: "cost",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Cost" />,
|
||||
cell: ({ row }) => (
|
||||
@@ -104,6 +107,7 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
},
|
||||
{
|
||||
id: "via",
|
||||
size: 120,
|
||||
accessorKey: "via",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Интерфейс" />,
|
||||
cell: ({ row }) => (
|
||||
@@ -128,6 +132,7 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
},
|
||||
{
|
||||
id: "area",
|
||||
size: 120,
|
||||
accessorKey: "area",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Область" />,
|
||||
cell: ({ row }) => (
|
||||
@@ -145,12 +150,20 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
manualPagination: true,
|
||||
manualPagination: false,
|
||||
autoResetPageIndex: false,
|
||||
initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["destination"], end: [] } },
|
||||
data: routes,
|
||||
columns,
|
||||
getRowId: (row) => row.id,
|
||||
})
|
||||
|
||||
const { pageIndex, pageSize } = table.state.pagination
|
||||
useEffect(() => {
|
||||
const lastPage = Math.max(0, Math.ceil(routes.length / pageSize) - 1)
|
||||
if (pageIndex > lastPage) table.setPageIndex(lastPage)
|
||||
}, [routes.length, pageIndex, pageSize, table])
|
||||
|
||||
if (routes.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
@@ -165,9 +178,14 @@ function OspfRoutesDataGrid({ routes, emptyTitle }: OspfRoutesDataGridProps) {
|
||||
<DataGridShell
|
||||
table={table}
|
||||
recordCount={routes.length}
|
||||
pagination={routes.length > 50 || pageSize !== 50}
|
||||
tableClassNames={{ headerRow: "border-b border-border", bodyRow: "group/row hover:bg-muted/30" }}
|
||||
tableLayout={{ dense: true }}
|
||||
/>
|
||||
tableLayout={{ dense: true, columnsPinnable: true }}
|
||||
>
|
||||
<DataGridContainer border={false} className="w-full rounded-none border-0">
|
||||
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
|
||||
</DataGridContainer>
|
||||
</DataGridShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,3 +74,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич
|
||||
12.3.19:backups1000/171812 JSON bytes —1000→50 rows/43580→2462 DOM nodes/898ms dev ready. Whole-catalog search/Filters/URL/reload/page20/refresh/503 cache/1000→1/правильный filename/возврат focus/320px/axe приняты;39+4 browser/type/lint/build. Полный DB список ещё загружается, производительность PG не измерялась.
|
||||
|
||||
12.3.21: history200 остаётся50 DOM rows/2367 nodes/1150ms dev ready. Strict DTO и null rates приняты от pure DB mapper до grid/KPI/card;60+5 browser и unit4/builds. Это не проверка реального PostgreSQL/RouterOS.
|
||||
|
||||
12.3.24:OSPF routes1000/200347 JSON bytes —50 DOM rows/1356 page nodes/930ms dev ready. Источник /ospf/catalog объединяет полный RouterOS ipRoutes без page/limit. Whole-catalog search/URL/reload/global Cost sort/page20/refresh/503 cache/1000→1/320px/видимость области/axe приняты;22+1 browser/type/lint/build. Полный DTO остаётся в памяти; скорость RouterOS не измерена. Осталось20 manualPagination:true specialized grids, включая demo/bounded источники; это не20 доказанных дефектов.
|
||||
+1
-1
@@ -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:строгая транзакционная запись speed-probes; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние оLine truncated
|
||||
Этап 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. СледующиLine truncated
|
||||
|
||||
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
|
||||
|
||||
|
||||
## 12.3.24 — большой каталог OSPF-маршрутов (2026-10-04)
|
||||
|
||||
Аудит источника: /ospf/catalog опрашивает полный RouterOS ipRoutes и объединяет маршруты устройств без page/limit. Grid переведён на штатную TanStack v9 client pagination50; сортировка применяется ко всему отфильтрованному каталогу. Страница сохраняется при refresh и ограничивается при сокращении списка/поиске. Назначение закреплено штатным columnPinning start/end, горизонтальная прокрутка использует DataGridScrollArea. Подобраны ширины, чтобы значение области не перекрывалось на320px; длинный префикс переносится.
|
||||
|
||||
1000 routes/200347 JSON bytes:50 DOM rows/1356 page nodes/930ms dev ready. Проверены page20, refresh, поиск дальнего маршрута/URL/reload, глобальная Cost сортировка,503 cache/unknown/freshness,1000→1 recovery,320px/no page overflow/полная видимость области/axe.22 профильных OSPF browser +1 после окончательной правки ширин прошли; lint/typecheck и production build прошли, мобильный снимок просмотрен. Ранние прогоны выявили неверное left API pinning (исправлено на v9 start/end) и две неверные предпосылки нового теста (ID и поле поиска); итоговые проверки прошли без ослабления assertions. Полный DTO остаётся в памяти, реальная задержка RouterOS не измерена.
|
||||
|
||||
Следующий блок12.3.25: BFD sessions большого каталога OSPF,50 DOM rows/поиск/sort/cache/320px. После него оставшиеся network grids и общая матрица12.4; внешняя12.5 отдельно. Коммит локальный без push.
|
||||
|
||||
## 12.3.23 — атомарная запись speed-проб (2026-10-04)
|
||||
|
||||
Общий write DTO требует явный probes массив, уникальные ID, безопасные положительные ID устройств, разные источник/назначение, целую длительность от3 секунд, protocol/direction/enabled. Missing/null/malformed каталог отклоняется400 целиком до обращения к хранилищу; явный probes:[] сохраняет намеренное удаление последней записи. Измерения из запроса исключены и не перезаписывают подтверждённые наблюдения.
|
||||
|
||||
@@ -344,3 +344,12 @@ Backend заменяет каталог в одной Drizzle transaction с pg_
|
||||
|
||||
Следующий блок12.3.24: большой OSPF route catalog — аудит фактического источника, пагинация50, поиск/сортировка по полному списку, refresh/error/recovery и320px. Затем остальные network grids и общая матрица12.4. Коммит локальный без push.
|
||||
|
||||
|
||||
## 12.3.24 — большой каталог OSPF-маршрутов (2026-10-04)
|
||||
|
||||
Аудит источника: /ospf/catalog опрашивает полный RouterOS ipRoutes и объединяет маршруты устройств без page/limit. Grid переведён на штатную TanStack v9 client pagination50; сортировка применяется ко всему отфильтрованному каталогу. Страница сохраняется при refresh и ограничивается при сокращении списка/поиске. Назначение закреплено штатным columnPinning start/end, горизонтальная прокрутка использует DataGridScrollArea. Подобраны ширины, чтобы значение области не перекрывалось на320px; длинный префикс переносится.
|
||||
|
||||
1000 routes/200347 JSON bytes:50 DOM rows/1356 page nodes/930ms dev ready. Проверены page20, refresh, поиск дальнего маршрута/URL/reload, глобальная Cost сортировка,503 cache/unknown/freshness,1000→1 recovery,320px/no page overflow/полная видимость области/axe.22 профильных OSPF browser +1 после окончательной правки ширин прошли; lint/typecheck и production build прошли, мобильный снимок просмотрен. Ранние прогоны выявили неверное left API pinning (исправлено на v9 start/end) и две неверные предпосылки нового теста (ID и поле поиска); итоговые проверки прошли без ослабления assertions. Полный DTO остаётся в памяти, реальная задержка RouterOS не измерена.
|
||||
|
||||
Следующий блок12.3.25: BFD sessions большого каталога OSPF,50 DOM rows/поиск/sort/cache/320px. После него оставшиеся network grids и общая матрица12.4; внешняя12.5 отдельно. Коммит локальный без push.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,54 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
import { ospfCatalogSchema } from "../../lib/ospf-response"
|
||||
import axe from "axe-core"
|
||||
|
||||
test("OSPF routes1000: full search/sort, page20, refresh/cache/shrink and320px", async ({ page }, info) => {
|
||||
test.setTimeout(90000)
|
||||
const server = { id: 1, name: "Роутер 1", host: "192.0.2.1", country: "RU", enabled: true, status: null }
|
||||
let routes = Array.from({ length: 1000 }, (_, i) => ({ id: "*" + (i + 1), serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU", destination: "10." + Math.floor(i / 256) + "." + (i % 256) + ".0/24", type: "O" as const, cost: i + 1, nextHop: "192.0.2.2", via: "ether1", area: "backbone" }))
|
||||
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/ospf/catalog") { calls++; return r.fulfill(failed ? { status: 503, json: { error: "OSPF catalog unavailable" } } : { json: ospfCatalogSchema.parse({ neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes, failures: [] }) }) }
|
||||
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [server] : {} })
|
||||
})
|
||||
const started = Date.now(); await page.goto("/ospf?tab=routes&server=1&keep=1")
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(50)
|
||||
const profile = { count: routes.length, jsonBytes: Buffer.byteLength(JSON.stringify(routes)), renderedRows: await rows.count(), nodes: await page.locator("*").count(), readyMs: Date.now() - started }
|
||||
console.log("OSPF routes 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()).toHaveAttribute("data-row-id", "1-*951")
|
||||
const before = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||
await expect.poll(() => calls).toBeGreaterThan(before); await expect(rows.first()).toHaveAttribute("data-row-id", "1-*951")
|
||||
const search = page.getByPlaceholder("Поиск в текущем разделе OSPF…")
|
||||
await search.fill(routes[999].destination); await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText(routes[999].destination)
|
||||
await expect(page).toHaveURL(url => url.searchParams.get("q") === routes[999].destination && url.searchParams.get("keep") === "1")
|
||||
await page.reload(); await expect(rows).toHaveCount(1)
|
||||
await search.fill(""); await expect(rows).toHaveCount(50)
|
||||
const costSort = page.getByRole("table").getByRole("button", { name: "Cost", exact: true }); await costSort.click()
|
||||
await expect(rows.first()).toContainText(routes[999].destination)
|
||||
await next.click(); const previousRow = await rows.first().getAttribute("data-row-id")
|
||||
failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||
const error = page.getByRole("alert").filter({ hasText: "Не удалось загрузить OSPF" })
|
||||
await expect(error).toContainText("Показаны последние полученные данные"); await expect(rows).toHaveCount(50); await expect(rows.first()).toHaveAttribute("data-row-id", previousRow!)
|
||||
await expect(rows.first()).toContainText("Неизвестно")
|
||||
await page.setViewportSize({ width: 320, height: 1000 })
|
||||
const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]')
|
||||
await expect(scroll).toHaveCount(1); expect(await scroll.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true)
|
||||
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(),b=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect();return a.left>=b.left-1&&a.right<=b.right+1 })).toBe(true)
|
||||
expect(await rows.first().getByText("backbone", { exact: true }).evaluate(el => { const a=el.getBoundingClientRect(),b=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(),p=el.closest("tr")!.querySelector("td")!.getBoundingClientRect();return a.left>=p.right-1&&a.right<=b.right+1 })).toBe(true)
|
||||
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").getByRole("button", { name: "Назначение", exact: true }).scrollIntoViewIfNeeded()
|
||||
await page.screenshot({ path: info.outputPath("ospf-routes-320.png"), animations: "disabled" })
|
||||
failed = false; routes = [routes[999]]; await error.getByRole("button", { name: "Повторить загрузку OSPF" }).click()
|
||||
await expect(rows).toHaveCount(1); await expect(error).toHaveCount(0); await expect(rows.first()).toContainText(routes[0].destination)
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
Reference in new issue
Block a user