From 5fbfc752fbe759c9bb7a75831235f70489f46183 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 04:29:30 +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=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=20=D1=81=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=B8=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data-grids/certificates-data-grid.tsx | 24 ++++++-- docs/ux-ui-performance.md | 5 +- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 8 +++ docs/ux-ui-stage-12.md | 8 +++ docs/ux-ui/screens/large-certificates-320.png | Bin 0 -> 28152 bytes tests/e2e/large-certificates.spec.ts | 58 ++++++++++++++++++ 7 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 docs/ux-ui/screens/large-certificates-320.png create mode 100644 tests/e2e/large-certificates.spec.ts diff --git a/components/data-grids/certificates-data-grid.tsx b/components/data-grids/certificates-data-grid.tsx index bfddb32b..71247363 100644 --- a/components/data-grids/certificates-data-grid.tsx +++ b/components/data-grids/certificates-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 { useMemo, useEffect, type ReactNode } from "react" import { type ColumnDef, useTable, @@ -223,13 +223,22 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle = const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + autoResetExpanded: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 } }, data: certificates, columns, getRowId: (row) => row.id, getRowCanExpand: () => true, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(certificates.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [certificates.length, pageSize, pageIndex, table]) + if (!isLoading && certificates.length === 0) { return ( 50 || pageSize !== 50} isLoading={isLoading} loadingMode="skeleton" onRowClick={(row) => table.getRow(row.id).toggleExpanded()} - /> + > + + + + + + ) } diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 3a8d68b7..a4859a63 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -20,7 +20,6 @@ | Файл | Следующая проверка | | --- | --- | | [backups-data-grid.tsx](../components/data-grids/backups-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | -| [certificates-data-grid.tsx](../components/data-grids/certificates-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [communities-data-grid.tsx](../components/data-grids/communities-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [data-collection-scheduler-data-grid.tsx](../components/data-grids/data-collection-scheduler-data-grid.tsx) | UI строится из11 SCHEDULER_JOB_KEYS; fixture1000 не соответствует источнику | | [filters-data-grid.tsx](../components/data-grids/filters-data-grid.tsx) | Порядок/drag и операции до ограничения DOM | @@ -44,7 +43,7 @@ | [wireguard-data-grid.tsx](../components/data-grids/wireguard-data-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | | [wireguard-peers-grid.tsx](../components/data-grids/wireguard-peers-grid.tsx) | Потребитель API, контекст/детали и fixture1000 | -Осталось24 специализированных grids; ни один из оставшегося перечня не принят этим документом на больших данных. CompactDataGrid дополнительно проверен на повторном получении100 active probes: страница сохраняется; сокращение100→1 ограничивает индекс. Speed probes и uptime resources приняты в блоках12.3.7–12.3.9 и исключены из оставшегося перечня. Следующие большие профили: probes-schedule, затем сетевые/admin grids с учётом границ API. Общий журнал: [ux-ui-progress.md](ux-ui-progress.md). +Осталось23 специализированных grids; ни один из оставшегося перечня не принят этим документом на больших данных. CompactDataGrid дополнительно проверен на повторном получении100 active probes: страница сохраняется; сокращение100→1 ограничивает индекс. Speed probes и uptime resources приняты в блоках12.3.7–12.3.9 и исключены из оставшегося перечня. Следующие большие профили: probes-schedule, затем сетевые/admin grids с учётом границ API. Общий журнал: [ux-ui-progress.md](ux-ui-progress.md). 12.3.6:1000 speed probes baseline —1000 rendered rows/23590 DOM nodes/3741ms dev ready; профиль collector-probes4 cases и ESLint прошли. Далее12.3.7 ограничение DOM speed grid; этот baseline не означает принятую производительность. @@ -67,3 +66,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.14: history200/73075 JSON bytes —200→50 rows,7583→2361 DOM nodes; final dev ready1271ms. Global TX sort/page4/refresh page2/200→1/320px/ScrollArea/axe проверены. Это DOM приёмка, не принятие legacy null→0 rates и общей очистки двух GET при ошибке reloadSpeedData. 12.3.15: live расписание на1000 rows дополнительно проверено при503 на page20, freshness/cache/recovery1/no POST;50 rows/739 section nodes/1778 page nodes. Demo ProbesScheduleDataGrid не использует API и начинается с3 INIT_RULES; исключать его из live migration, но не считать произвольный большой demo-list проверенным. + +12.3.16:certificates1000/329034 JSON bytes —1000→50 rows/29666→2145 DOM nodes,1098ms final dev ready. Whole-catalog search/URL/reload/sort/page20/SAN/refresh/1000→1/320px/axe проверены. Осталось23 manualPagination:true файла; закрепление идентификатора строки при горизонтальной прокрутке — следующий UX блок. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 5e63b57c..adb446e7 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, затем users/backups/network grids и отдельная достоверность history;12.4/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:имя строки при horizontal scroll, затем users/backups/network grids и отдельная достоверность history;12.4/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 6123f499..72864443 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,14 @@ +## 12.3.16 — каталог1000 сертификатов и refresh деталей (2026-10-04) + +Fixture strict per-device certificatesStateSchema,1000 сертификатов одного устройства/329034 JSON bytes. Baseline1000 rows/29666 DOM nodes/2689ms dev ready; после client pagination50 и горизонтального ReUI ScrollArea:50 rows/2145 nodes/1098ms. autoResetPageIndex=false/clamp сохраняют страницу и корректно сокращают1000→1. Воспроизведено закрытие SAN при refresh: autoResetExpanded=false сохраняет детали по стабильному составному ID устройства/RouterOS. + +Проверены page20/Enter/SAN, refresh страницы и раскрытия, поиск записи1000 по полному каталогу с URL/server/reload, глобальная числовая сортировка срока, ScrollArea/reflow320, tagged axe grid, сокращение и отсутствие POST. После последней production правки19 из20 browser прошли; narrow layout прерван разовой UNKNOWN open PNG ошибкой Windows. Повтор всех3 layout прошёл без изменения UI. Typecheck/ESLint и production frontend/contracts build34 страницы прошли. Снимок просмотрен; из-за горизонтальной прокрутки имя строки уходит из видимой области — следующий UX блок сохраняет идентификацию строк. + +Следующий блок12.3.17: штатное закрепление колонки имени сертификата и измерения pivot при horizontal scroll, проверить доступ к остальным ячейкам на320px/ПК. Затем users/backups/network grids. Осталось23 manualPagination:true файла, включая demo-only и bounded источники; это не23 доказанных дефекта. Достоверность speed history, итоговая матрица12.4 и внешняя приёмка12.5 остаются. Коммит локальный, без push. + ## 12.3.15 — live расписание1000 и отказ на последней странице (2026-10-04) Уточнён consumer: ProbesScheduleDataGrid только demo/INIT_RULES3; live /probes использует ProbesSpeedSchedule→ProbesSpeedProbesDataGrid, уже принятый на1000 в12.3.7. Существующий большой профиль усилен проверкой503 на page20:50 строк и951–1000 из1000 остаются, полученное время не обновляется, отображается последний успешный список. Recovery1000→1 очищает ошибку и clamp страницы, POST не отправляется. Не добавлялось вымышленное live GRE расписание. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index d552bc8b..0babee93 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -265,3 +265,11 @@ Fixture17311 JSON bytes,50 строк в DOM; после исправления2 15 browser collector-probes/probes-schedule прошли: включает initial unknown/malformed/timeout/permissions/320px, demo device/tunnel selection и Rail1001 обе темы/границу площадок.1000 live rows:50 DOM rows/739 nodes секции/1778 page nodes/938ms dev ready. Typecheck и ESLint теста прошли. Production код не менялся с e5cf057; build34 страницы принят в предыдущем блоке. Исторические generated screenshots восстановлены. Следующий блок12.3.16:certificates1000 через strict per-device state, pagination/поиск всего каталога/числовая сортировка/детали/refresh/уменьшение/320px. Далее users/backups/network grids; отдельная достоверность speed history остаётся в12.3.14.12.4 итоговая матрица и12.5 внешняя приёмка не завершены. Локальный коммит без push. + +## 12.3.16 — каталог1000 сертификатов и refresh деталей (2026-10-04) + +Fixture strict per-device certificatesStateSchema,1000 сертификатов одного устройства/329034 JSON bytes. Baseline1000 rows/29666 DOM nodes/2689ms dev ready; после client pagination50 и горизонтального ReUI ScrollArea:50 rows/2145 nodes/1098ms. autoResetPageIndex=false/clamp сохраняют страницу и корректно сокращают1000→1. Воспроизведено закрытие SAN при refresh: autoResetExpanded=false сохраняет детали по стабильному составному ID устройства/RouterOS. + +Проверены page20/Enter/SAN, refresh страницы и раскрытия, поиск записи1000 по полному каталогу с URL/server/reload, глобальная числовая сортировка срока, ScrollArea/reflow320, tagged axe grid, сокращение и отсутствие POST. После последней production правки19 из20 browser прошли; narrow layout прерван разовой UNKNOWN open PNG ошибкой Windows. Повтор всех3 layout прошёл без изменения UI. Typecheck/ESLint и production frontend/contracts build34 страницы прошли. Снимок просмотрен; из-за горизонтальной прокрутки имя строки уходит из видимой области — следующий UX блок сохраняет идентификацию строк. + +Следующий блок12.3.17: штатное закрепление колонки имени сертификата и измерения pivot при horizontal scroll, проверить доступ к остальным ячейкам на320px/ПК. Затем users/backups/network grids. Осталось23 manualPagination:true файла, включая demo-only и bounded источники; это не23 доказанных дефекта. Достоверность speed history, итоговая матрица12.4 и внешняя приёмка12.5 остаются. Коммит локальный, без push. diff --git a/docs/ux-ui/screens/large-certificates-320.png b/docs/ux-ui/screens/large-certificates-320.png new file mode 100644 index 0000000000000000000000000000000000000000..d2d0aa68496dca7051cde696f52ce62b23b38a35 GIT binary patch literal 28152 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/large-certificates.spec.ts b/tests/e2e/large-certificates.spec.ts new file mode 100644 index 00000000..bc1d4152 --- /dev/null +++ b/tests/e2e/large-certificates.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from "@playwright/test" +import { certificatesStateSchema } from "../../packages/contracts/src/certificates" +import { certificate, certificateState, server } from "./fixtures/certificates" +import axe from "axe-core" +import { mkdirSync } from "node:fs" + +test("Certificates1000: DOM, страницы, детали, поиск, refresh и320px", async ({ page }, info) => { + test.setTimeout(90000) + let certificates = Array.from({ length: 1000 }, (_, i) => ({ ...certificate, id: "1:*" + (i + 1), rosId: "*" + (i + 1), name: "cert-" + (i + 1), sans: ["alt" + (i + 1) + ".example.test"], daysLeft: 300 + i, validFrom: "2026-09-01T00:00:00Z", validUntil: new Date(Date.parse("2026-10-01T00:00:00Z") + (300 + i) * 86400000).toISOString(), trusted: true, status: "valid", keySize: 2048 })) + let 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/certificates/state") calls++ + return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [server] : path === "/api/certificates/state" ? certificatesStateSchema.parse(certificateState(certificates)) : path === "/api/certificates/acme-settings" ? { directoryUrl: "https://acme.example.test/directory", tokenConfigured: false } : path === "/api/certificates/renew-settings" ? { enabled: false, intervalSec: 300, renewBeforeDays: 30 } : {} }) + }) + const start = Date.now(); await page.goto("/certificates?server=1") + const rows = page.locator("tr[data-row-id]") + await expect(page.getByText("cert-1", { exact: true })).toBeVisible() + const sample = { fixtureRows: 1000, fixtureBytes: Buffer.byteLength(JSON.stringify(certificateState(certificates))), renderedRows: await rows.count(), domNodes: await page.locator("*").count(), readyMs: Date.now() - start } + console.log(JSON.stringify(sample)); await info.attach("large-certificates-metrics", { body: JSON.stringify(sample), contentType: "application/json" }) + expect(sample.renderedRows).toBe(50); expect(sample.domNodes).toBeLessThan(5000) + 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() + const detail = page.getByRole("button", { name: "Детали сертификата cert-1000", exact: true }) + await detail.focus(); await page.keyboard.press("Enter") + await expect(detail).toHaveAttribute("aria-expanded", "true") + await expect(page.getByText("alt1000.example.test", { exact: true })).toBeVisible() + const before = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => calls).toBeGreaterThan(before) + await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled() + await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible() + await expect(detail).toHaveAttribute("aria-expanded", "true") + const search = page.getByRole("textbox", { name: "Поиск по имени, CN, эмитенту…" }) + await search.fill("cert-1000"); await expect(rows).toHaveCount(1) + await expect(page).toHaveURL(url => url.searchParams.get("q") === "cert-1000" && url.searchParams.get("server") === "1") + await page.reload(); await expect(rows).toHaveCount(1); await expect(search).toHaveValue("cert-1000") + await search.fill(""); await expect(rows).toHaveCount(50) + await page.getByRole("table").getByRole("button", { name: "Срок", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "1:*1000") + 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 }) + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addScriptTag({ content: axe.source }) + const audit = 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"] } })) + expect(audit.violations).toEqual([]) + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); mkdirSync("docs/ux-ui/screens", { recursive: true }) + await page.getByRole("table").scrollIntoViewIfNeeded(); await page.screenshot({ path: "docs/ux-ui/screens/large-certificates-320.png", animations: "disabled" }) + await page.getByRole("button", { name: "Следующая страница", exact: true }).click() + certificates = [certificates[999]]; const previous = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => calls).toBeGreaterThan(previous); await expect(rows).toHaveCount(1) + await expect(rows.first()).toHaveAttribute("data-row-id", "1:*1000") + expect(writes).toEqual([]) +})