diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index b29c55e4..5fc62d04 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -707,7 +707,7 @@ interface SpeedTestRun { durationSec: number txAvgMbps: number | null rxAvgMbps: number | null - status: "running" | "done" | "error" + status: "running" | "done" | "error" | "unknown" command: string lines: string[] afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null } | null @@ -1077,7 +1077,14 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId const [probeRttChartOpen, setProbeRttChartOpen] = useState>({}) const [speedBusy, setSpeedBusy] = useState(false) const [speedError, setSpeedError] = useState(null) + const [speedUnknownRunId, setSpeedUnknownRunId] = useState(null) const [speedRuns, setSpeedRuns] = useState([]) + useEffect(() => { + if (speedUnknownRunId && speedRuns.some(run => run.id === speedUnknownRunId && (run.status === "done" || run.status === "error"))) { + setSpeedUnknownRunId(null) + setSpeedError(null) + } + }, [speedRuns, speedUnknownRunId]) const [speedProbes, setSpeedProbes] = useState([]) const speedReadRevision = useRef(0) const [speedReads, setSpeedReads] = useState>({ probes: { receivedAt: null, error: null }, history: { receivedAt: null, error: null } }) @@ -1182,7 +1189,10 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId srcInterfaceAddress: r.srcInterfaceAddress ?? null, dstInterfaceAddress: r.dstInterfaceAddress ?? null, })) - if (current()) setSpeedRuns(rows) + if (current()) setSpeedRuns(previous => [ + ...previous.filter(run => (run.status === "running" || run.status === "unknown") && !rows.some(row => row.id === run.id)), + ...rows, + ]) }), ]) }, [apiFetch, liveApi]) @@ -1687,6 +1697,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId overviewReqRef.current++ const context = ++saveContext.current setSpeedError(null) + setSpeedUnknownRunId(null) setSpeedBusy(true) const durationSec = Math.max(3, Number.parseInt(probe.durationSec, 10) || 10) const runId = `speed-${Date.now()}` @@ -1806,13 +1817,15 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId } catch (e) { if (context !== saveContext.current) return const message = serverOperationError(e) + const persistenceUnknown = isSpeedPersistenceUnknown(e, runId) + setSpeedUnknownRunId(persistenceUnknown ? runId : null) setSpeedRuns((prev) => prev.map((r) => r.id === runId ? ({ ...r, - status: "error", - lines: [`status: error`, message], + status: persistenceUnknown ? "unknown" : "error", + lines: [`status: ${persistenceUnknown ? "unknown" : "error"}`, message], }) : r)) setSpeedError(message) - if (!isSpeedPersistenceUnknown(e, runId)) setSpeedProbes((prev) => prev.map((p) => p.id === probe.id ? ({ + if (!persistenceUnknown) setSpeedProbes((prev) => prev.map((p) => p.id === probe.id ? ({ ...p, lastRunAt: new Date().toISOString(), lastStatus: "error", @@ -2107,7 +2120,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId {/* ── error ── */} {speedError && (
- + {speedError} @@ -2160,7 +2173,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId const run = speedRuns.find((r) => sameSpeedMeasurement(r, probe), ) - const useRun = run && (run.status === "running" || ( + const useRun = run && run.status !== "unknown" && (run.status === "running" || ( probe.lastStatus != null && probe.lastRunAt != null && run.startedAt >= Date.parse(probe.lastRunAt) )) const viewStatus = useRun ? run.status : (probe.lastStatus === "error" || probe.lastStatus === "done" ? probe.lastStatus : null) diff --git a/components/data-grids/uptime-speed-history-data-grid.tsx b/components/data-grids/uptime-speed-history-data-grid.tsx index 9122e073..cc7a6961 100644 --- a/components/data-grids/uptime-speed-history-data-grid.tsx +++ b/components/data-grids/uptime-speed-history-data-grid.tsx @@ -31,7 +31,7 @@ export interface SpeedTestRunRow { durationSec: number txAvgMbps: number | null rxAvgMbps: number | null - status: "running" | "done" | "error" + status: "running" | "done" | "error" | "unknown" afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null } | null srcInterfaceAddress?: string | null dstInterfaceAddress?: string | null @@ -130,6 +130,14 @@ function UptimeSpeedHistoryDataGrid({ runs, servers }: UptimeSpeedHistoryDataGri if (status === "error") { return error } + if (status === "unknown") { + return ( +
+ Сохранение не подтверждено +
runId: {row.original.id}
+
+ ) + } return done }, meta: { headerTitle: "Статус", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 1885d5b6..12edb10a 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -117,3 +117,11 @@ BGP-prefix evaluator, отдельное live GRE расписание и recove ## Локальный snapshot guard — 2026-10-08 12.5.20 сохраняетlastconfirmedspeed snapshot дляunknown502/совпадающегоrunId.2unit tests/frontendtypecheck passed;browserunknownUX ещёнепроверен,patchнеразвёрнут. Backend4c12c55/frontend673e5ba прежние. Следующий12.5.21 — build/browserunknown/deploymentобоихобразов. + +## Локальный unknown UX — 2026-10-08 + +12.5.21: отдельная строка unknown/runId, сохранение snapshot и сверка по идентификатору при чтении истории. 13 браузерных тестов с HTTP-моками/typecheck/ESLint прошли; это не приёмка real browser/PG. Основной backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка и проверка обновлённых образов. Полный 12.5 открыт. + +## Локальный unknown UX — 2026-10-08 + +12.5.21: отдельная строка unknown/runId, сохранение snapshot и сверка по идентификатору при чтении истории. 13 браузерных тестов с HTTP-моками/typecheck/ESLint прошли; это не приёмка real browser/PG. Основной backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка и проверка обновлённых образов. Полный 12.5 открыт. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 8bc064df..279c3ae7 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-08:12.5.20 завершёнлокально — unknown502/currentrunId не переписываетподтверждённыйsnapshot пробы.2unit tests/frontendtypecheck passed;временнаястроказапросаещёerror,полныйbrowserunknownUX открыт. Runtimebackend4c12c55/frontend673e5ba прежние. Следующий12.5.21 — build/browserunknownspeed/deploymentобоихобразов. +Актуальное продолжение 2026-10-08: 12.5.21 завершён локально — speed unknown отображается отдельно от error, сохраняется при чтении пустой истории и сверяется по runId. Последний подтверждённый snapshot пробы сохранён. 13 браузерных тестов с HTTP-моками, frontend typecheck и ESLint прошли. Основной runtime backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-07. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index fbd33fc2..3b384c63 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-08:12.5.20 завершёнлокально — unknown502/currentrunId не переписываетподтверждённыйsnapshot пробы.2unit tests/frontendtypecheck passed;временнаястроказапросаещёerror,полныйbrowserunknownUX открыт. Runtimebackend4c12c55/frontend673e5ba прежние. Следующий12.5.21 — build/browserunknownspeed/deploymentобоихобразов. +Актуальное продолжение 2026-10-08: 12.5.21 завершён локально — speed unknown отображается отдельно от error, сохраняется при чтении пустой истории и сверяется по runId. Последний подтверждённый snapshot пробы сохранён. 13 браузерных тестов с HTTP-моками, frontend typecheck и ESLint прошли. Основной runtime backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -34,6 +34,22 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в описанных границах:12.3.83,main1186/1186,context221/221. Этап остаётся в работе: стенд12.5.1–3 развёрнут,bootstrap/smoke проверены;остальные сценарии12.5 не выполнены. | +## 12.5.21 — неопределённый исход speed-запуска и сверка с историей (2026-10-08) + +В локальной строке запуска введён status unknown: «Сохранение не подтверждено», с видимым runId; это не новый статус серверного DTO (история API по-прежнему принимает только done/error). При обновлении истории незавершённая или неподтверждённая локальная строка сохраняется, если её идентификатора нет в ответе. Подтверждённая строка с тем же runId заменяет локальную, предупреждение снимается. Unknown не подменяет последний подтверждённый snapshot карточки пробы; предупреждение показано без destructive-оформления. Автоматического повторного сетевого теста нет. Ручной новый запуск остаётся доступен; предупреждение просит проверить историю перед повтором. Состояние локальной строки не сохраняется после перезагрузки страницы. + +Проверки: 13/13 Playwright (uptime-speed-data и uptime-speed-unknown) за 27.7s, включая unknown → пустая история → done/error, сохранение TX42, один POST и адаптацию 320px. Это live-ветка UI с HTTP-моками, не real PG/browser proof. Scoped frontend typecheck (tmp/tsconfig-speed-check.json, исключён старый ignored tmp build context) и ESLint изменённых файлов прошли. Существующие null/zero, malformed DTO, свежесть, независимые источники и Ping-состояния сохранены. + +Основной backend4c12c55/frontend673e5ba не изменён. Новые backend/frontend исправления ещё не развёрнуты. Полный 12.5 открыт. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. Локальный коммит без push. + +## 12.5.21 — неопределённый исход speed-запуска и сверка с историей (2026-10-08) + +В локальной строке запуска введён status unknown: «Сохранение не подтверждено», с видимым runId; это не новый статус серверного DTO (история API по-прежнему принимает только done/error). При обновлении истории незавершённая или неподтверждённая локальная строка сохраняется, если её идентификатора нет в ответе. Подтверждённая строка с тем же runId заменяет локальную, предупреждение снимается. Unknown не подменяет последний подтверждённый snapshot карточки пробы; предупреждение показано без destructive-оформления. Автоматического повторного сетевого теста нет. Ручной новый запуск остаётся доступен; предупреждение просит проверить историю перед повтором. Состояние локальной строки не сохраняется после перезагрузки страницы. + +Проверки: 13/13 Playwright (uptime-speed-data и uptime-speed-unknown) за 27.7s, включая unknown → пустая история → done/error, сохранение TX42, один POST и адаптацию 320px. Это live-ветка UI с HTTP-моками, не real PG/browser proof. Scoped frontend typecheck (tmp/tsconfig-speed-check.json, исключён старый ignored tmp build context) и ESLint изменённых файлов прошли. Существующие null/zero, malformed DTO, свежесть, независимые источники и Ping-состояния сохранены. + +Основной backend4c12c55/frontend673e5ba не изменён. Новые backend/frontend исправления ещё не развёрнуты. Полный 12.5 открыт. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. Локальный коммит без push. + ## 12.5.20 — сохранение подтверждённого speed snapshot при unknown receipt (2026-10-08) Приостатке5%окнавыполненнебольшойлокальныйблокпередdeployment. Frontendcatch раньше ставилlastStatuserror/новыйlastRunAt любойошибкеPOSTspeed-test,включаяunknowncommit. Теперьподтверждённыйsnapshot пробысохраняется,еслиApiClientError502 содержитoutcomeunknown иrunId текущегозапроса. ЧужойrunId/неправильныйpayload/403/обычнаяошибка нераспознаютсякакэтотисход. Существующееservererrorсообщениепоказывается;новыекомпоненты/композицияReUIне менялись. Временнаястрокасамогозапросапо-прежнемуимеетstatuserror; полныйunknown-state/browserUX ещёнужнопроверить. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 700a88a5..d9723783 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-08:12.5.20 завершёнлокально — unknown502/currentrunId не переписываетподтверждённыйsnapshot пробы.2unit tests/frontendtypecheck passed;временнаястроказапросаещёerror,полныйbrowserunknownUX открыт. Runtimebackend4c12c55/frontend673e5ba прежние. Следующий12.5.21 — build/browserunknownspeed/deploymentобоихобразов. +Актуальное продолжение 2026-10-08: 12.5.21 завершён локально — speed unknown отображается отдельно от error, сохраняется при чтении пустой истории и сверяется по runId. Последний подтверждённый snapshot пробы сохранён. 13 браузерных тестов с HTTP-моками, frontend typecheck и ESLint прошли. Основной runtime backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -1154,3 +1154,19 @@ SchedulerSnapshotError раньшеигнорировалruns[].okfalse уuptime Добавленыlib/speed-persistence-outcome.ts и2meaningfultests;подключенguardвuptime/page.tsx. Обаunit tests passed. ПервыйroottscподхватилкопиистарогоCI contextвignoredtmp ивернулисходныеошибкиэтихкопий. Повторныйполныйfrontendtypecheckсвременнымconfig,которыйсохраняетrootoptions/исключения иисключаетtmp,passed; основнойtsconfigне менялся. Diff-check passed. ReUI skillпрочитан: адаптациясуществующегоэкрана,безновойregistryinstallation/редизайна. Browserunknownspeed/productionbuild/deploymentвэтомблокенезапускались. Основнойruntimebackend4c12c55/frontend673e5ba сохраняется;backendpatchf408afb иfrontendguardещёнеразвёрнуты. Следующий12.5.21 — собратьобаобновлённыхобраза,проверитьbrowserunknownspeed/историю иразвернутьпослепроверок. Журналобновлён;локальныйкоммитбезpush. + +## 12.5.21 — неопределённый исход speed-запуска и сверка с историей (2026-10-08) + +В локальной строке запуска введён status unknown: «Сохранение не подтверждено», с видимым runId; это не новый статус серверного DTO (история API по-прежнему принимает только done/error). При обновлении истории незавершённая или неподтверждённая локальная строка сохраняется, если её идентификатора нет в ответе. Подтверждённая строка с тем же runId заменяет локальную, предупреждение снимается. Unknown не подменяет последний подтверждённый snapshot карточки пробы; предупреждение показано без destructive-оформления. Автоматического повторного сетевого теста нет. Ручной новый запуск остаётся доступен; предупреждение просит проверить историю перед повтором. Состояние локальной строки не сохраняется после перезагрузки страницы. + +Проверки: 13/13 Playwright (uptime-speed-data и uptime-speed-unknown) за 27.7s, включая unknown → пустая история → done/error, сохранение TX42, один POST и адаптацию 320px. Это live-ветка UI с HTTP-моками, не real PG/browser proof. Scoped frontend typecheck (tmp/tsconfig-speed-check.json, исключён старый ignored tmp build context) и ESLint изменённых файлов прошли. Существующие null/zero, malformed DTO, свежесть, независимые источники и Ping-состояния сохранены. + +Основной backend4c12c55/frontend673e5ba не изменён. Новые backend/frontend исправления ещё не развёрнуты. Полный 12.5 открыт. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. Локальный коммит без push. + +## 12.5.21 — неопределённый исход speed-запуска и сверка с историей (2026-10-08) + +В локальной строке запуска введён status unknown: «Сохранение не подтверждено», с видимым runId; это не новый статус серверного DTO (история API по-прежнему принимает только done/error). При обновлении истории незавершённая или неподтверждённая локальная строка сохраняется, если её идентификатора нет в ответе. Подтверждённая строка с тем же runId заменяет локальную, предупреждение снимается. Unknown не подменяет последний подтверждённый snapshot карточки пробы; предупреждение показано без destructive-оформления. Автоматического повторного сетевого теста нет. Ручной новый запуск остаётся доступен; предупреждение просит проверить историю перед повтором. Состояние локальной строки не сохраняется после перезагрузки страницы. + +Проверки: 13/13 Playwright (uptime-speed-data и uptime-speed-unknown) за 27.7s, включая unknown → пустая история → done/error, сохранение TX42, один POST и адаптацию 320px. Это live-ветка UI с HTTP-моками, не real PG/browser proof. Scoped frontend typecheck (tmp/tsconfig-speed-check.json, исключён старый ignored tmp build context) и ESLint изменённых файлов прошли. Существующие null/zero, malformed DTO, свежесть, независимые источники и Ping-состояния сохранены. + +Основной backend4c12c55/frontend673e5ba не изменён. Новые backend/frontend исправления ещё не развёрнуты. Полный 12.5 открыт. Следующий 12.5.22 — сборка обновлённых образов и приёмка на стенде перед deployment. Локальный коммит без push. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index b64d9630..b9b0a158 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -125,3 +125,11 @@ ssh -N -L 3000:127.0.0.1:3000 -L 8000:127.0.0.1:8000 root@161.104.46.222 ## Локальный snapshot guard — 2026-10-08 12.5.20 сохраняетlastconfirmedspeed snapshot дляunknown502/совпадающегоrunId.2unit tests/frontendtypecheck passed;browserunknownUX ещёнепроверен,patchнеразвёрнут. Backend4c12c55/frontend673e5ba прежние. Следующий12.5.21 — build/browserunknown/deploymentобоихобразов. + +## Локальный unknown UX — 2026-10-08 + +12.5.21: отдельная строка unknown/runId, сохранение snapshot и сверка по идентификатору при чтении истории. 13 браузерных тестов с HTTP-моками/typecheck/ESLint прошли; это не приёмка real browser/PG. Основной backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка и проверка обновлённых образов. Полный 12.5 открыт. + +## Локальный unknown UX — 2026-10-08 + +12.5.21: отдельная строка unknown/runId, сохранение snapshot и сверка по идентификатору при чтении истории. 13 браузерных тестов с HTTP-моками/typecheck/ESLint прошли; это не приёмка real browser/PG. Основной backend4c12c55/frontend673e5ba прежний. Следующий 12.5.22 — сборка и проверка обновлённых образов. Полный 12.5 открыт. diff --git a/tests/e2e/uptime-speed-unknown.spec.ts b/tests/e2e/uptime-speed-unknown.spec.ts new file mode 100644 index 00000000..f9561a3a --- /dev/null +++ b/tests/e2e/uptime-speed-unknown.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from "@playwright/test" +import { server } from "./fixtures/users" + +for (const confirmedStatus of ["done", "error"] as const) { + test(`Speed: unknown receipt сохраняет snapshot и сверяется по runId/${confirmedStatus}`, async ({ page }) => { + let runId = "" + let confirmed = false + let writes = 0 + const probe = { + id: "probe-unknown", srcServerId: "1", dstServerId: "2", + srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "transmit", + durationSec: "3", enabled: true, lastStatus: "done", lastRunAt: "2026-10-03T00:00:00Z", + lastTxAvgMbps: 42, lastRxAvgMbps: null, lastError: null, + } + await page.route("**/health", route => route.fulfill({ json: { ok: true } })) + await page.route("**/api/**", route => { + const path = new URL(route.request().url()).pathname + if (path === "/api/uptime/speed-test" && route.request().method() === "POST") { + writes++ + runId = route.request().postDataJSON().runId + return route.fulfill({ status: 502, json: { + outcome: "unknown", runId, + error: `Сетевой тест завершён, сохранение результата не подтверждено. Проверьте историю по runId ${runId} перед повтором`, + } }) + } + return route.fulfill({ json: + path === "/api/auth/config" ? { required: false } : + path === "/api/servers" ? [server, { ...server, id: 2, name: "Приёмник", host: "192.0.2.2" }] : + path === "/api/uptime/overview" ? { probes: [], resources: [] } : + path === "/api/uptime/speed-probes" ? { probes: [probe] } : + path === "/api/uptime/speed-test/runs" ? { runs: confirmed ? [{ + id: runId, srcServerId: "1", dstServerId: "2", srcInterface: "ether1", dstInterface: "ether1", + protocol: "tcp", direction: "transmit", durationSec: 3, status: confirmedStatus, + txAvgMbps: confirmedStatus === "done" ? 84 : null, rxAvgMbps: null, + error: confirmedStatus === "error" ? "Подтверждённая ошибка" : null, + createdAt: new Date().toISOString(), + }] : [] } : {}, + }) + }) + await page.goto("/uptime?tab=speed") + await page.getByRole("button", { name: "Пауза", exact: true }).click() + const launch = page.getByRole("button", { name: "Запустить", exact: true }) + const card = launch.locator("xpath=../../..") + await expect(card).toContainText("42 Мбит/с") + await launch.click() + await expect(page.getByText("Сохранение не подтверждено", { exact: true })).toBeVisible() + const row = page.locator(`tr[data-row-id="${runId}"]`) + await expect(row).toContainText(`runId: ${runId}`) + await expect(row).not.toContainText("error") + await expect(card).toContainText("42 Мбит/с") + await page.setViewportSize({ width: 320, height: 1000 }) + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(row).toContainText("Сохранение не подтверждено") + await expect(card).toContainText("42 Мбит/с") + expect(writes).toBe(1) + confirmed = true + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(row).toContainText(confirmedStatus) + await expect(page.getByText("Сохранение не подтверждено", { exact: true })).toHaveCount(0) + await expect(page.getByText(/Проверьте историю по runId/)).toHaveCount(0) + if (confirmedStatus === "done") await expect(row).toContainText("84 Мбит/с") + expect(writes).toBe(1) + }) +}