fix(ui): сбрасывать снимок при изменении параметров speed-пробы
This commit is contained in:
1 parent
090206c893
commit
7f18fcb6db
11 files changed
+106
-22
No files matched your search
@@ -5,7 +5,7 @@ import { parseSpeedRunResponse } from "@/lib/speed-run-response"
|
||||
|
||||
import { dashboardProbeOverviewSchema } from "@/lib/dashboard-probes"
|
||||
|
||||
import { speedHistoryResponseSchema, speedProbesResponseSchema, speedProbesWriteSchema, speedProbesSavedSchema, pingProbesWriteSchema, probeCatalogSavedSchema } from "@mmapp/contracts"
|
||||
import { sameSpeedMeasurement, speedHistoryResponseSchema, speedProbesResponseSchema, speedProbesWriteSchema, speedProbesSavedSchema, pingProbesWriteSchema, probeCatalogSavedSchema } from "@mmapp/contracts"
|
||||
|
||||
import probeStyles from "./probe-rows.module.css"
|
||||
|
||||
@@ -2155,10 +2155,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
{srvProbes.map((probe) => {
|
||||
const dst = allServers.find((s) => s.id === probe.dstServerId)
|
||||
const run = speedRuns.find((r) =>
|
||||
r.srcServerId === probe.srcServerId &&
|
||||
r.dstServerId === probe.dstServerId &&
|
||||
(r.srcInterface ?? "") === probe.srcInterface &&
|
||||
(r.dstInterface ?? "") === probe.dstInterface,
|
||||
sameSpeedMeasurement(r, probe),
|
||||
)
|
||||
const useRun = run && (run.status === "running" || (
|
||||
probe.lastStatus != null && probe.lastRunAt != null && run.startedAt >= Date.parse(probe.lastRunAt)
|
||||
|
||||
@@ -36,7 +36,15 @@ const saved = await put({ probes: [probe] }); assert.equal(saved.statusCode, 200
|
||||
assert.equal(rows.has("old"), false); assert.equal(rows.get("s1")?.enabled, false); assert.equal(rows.get("s1")?.srcInterface, "ether1")
|
||||
rows.get("s1")!.lastTxAvgMbps = 42
|
||||
await put({ probes: [{ ...probe, durationSec: 20, lastTxAvgMbps: 0 }] })
|
||||
assert.equal(rows.get("s1")?.lastTxAvgMbps, 42); assert.equal(rows.get("s1")?.durationSec, 20)
|
||||
assert.equal(rows.get("s1")?.lastTxAvgMbps, null); assert.equal(rows.get("s1")?.durationSec, 20)
|
||||
for (const edit of [{ srcInterface: "ether3" }, { dstInterface: "ether3" }, { protocol: "udp" }, { direction: "receive" }]) {
|
||||
rows.get("s1")!.lastTxAvgMbps = 42
|
||||
await put({ probes: [{ ...probe, ...edit }] })
|
||||
assert.equal(rows.get("s1")?.lastTxAvgMbps, null)
|
||||
rows.get("s1")!.lastTxAvgMbps = 0
|
||||
await put({ probes: [{ ...probe, ...edit, enabled: true }] })
|
||||
assert.equal(rows.get("s1")?.lastTxAvgMbps, 0)
|
||||
}
|
||||
const beforePairChange = rows.get("s1")!.srcServerId
|
||||
await put({ probes: [{ ...probe, srcServerId: 2, dstServerId: 1 }] })
|
||||
assert.notEqual(rows.get("s1")?.srcServerId, beforePairChange)
|
||||
|
||||
@@ -208,7 +208,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
// Serialize complete replacements from all backend processes, including an empty catalog.
|
||||
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('uptime-speed-probes'))`)
|
||||
await run({
|
||||
listProbes: async () => tx.select({ id: uptimeSpeedProbes.id, srcServerId: uptimeSpeedProbes.srcServerId, dstServerId: uptimeSpeedProbes.dstServerId }).from(uptimeSpeedProbes).for("update"),
|
||||
listProbes: async () => tx.select().from(uptimeSpeedProbes).for("update"),
|
||||
remove: async id => { await tx.delete(uptimeSpeedProbes).where(eq(uptimeSpeedProbes.id, id)) },
|
||||
upsert: async (row, createdAt, resetObservations) => {
|
||||
const { id, ...patch } = row
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { matchesSpeedProbeEndpoints, type SpeedProbeEndpoints } from "./speed-run-persistence.js"
|
||||
import { sameSpeedMeasurement } from "@mmapp/contracts"
|
||||
import type { SpeedProbeWrite } from "@mmapp/contracts"
|
||||
export type SpeedProbePatch = SpeedProbeWrite & { sortOrder: number; updatedAt: string }
|
||||
export type SpeedProbeTransaction = {
|
||||
listProbes: () => Promise<SpeedProbeEndpoints[]>
|
||||
listProbes: () => Promise<SpeedProbeWrite[]>
|
||||
remove: (id: string) => Promise<void>
|
||||
upsert: (row: SpeedProbePatch, createdAt: string, resetObservations: boolean) => Promise<void>
|
||||
}
|
||||
export type SpeedProbeStore = { transaction: (run: (tx: SpeedProbeTransaction) => Promise<void>) => Promise<void> }
|
||||
/** The adapter must lock the catalog and commit all configuration changes atomically. Observations are preserved for the same pair and reset when its endpoints change. */
|
||||
/** The adapter must lock the catalog and commit all configuration changes atomically. Observations are preserved only for the same measurement parameters. */
|
||||
export async function replaceSpeedProbes(store: SpeedProbeStore, probes: SpeedProbeWrite[], now = new Date().toISOString()): Promise<void> {
|
||||
await store.transaction(async tx => {
|
||||
const nextIds = new Set(probes.map(row => row.id))
|
||||
const previous = new Map((await tx.listProbes()).map(row => [row.id, row]))
|
||||
for (const id of previous.keys()) if (!nextIds.has(id)) await tx.remove(id)
|
||||
for (const [sortOrder, row] of probes.entries()) {
|
||||
const reset = !matchesSpeedProbeEndpoints(previous.get(row.id), row)
|
||||
const old = previous.get(row.id)
|
||||
const reset = !old || !sameSpeedMeasurement(old, row)
|
||||
await tx.upsert({ ...row, sortOrder, updatedAt: now }, now, reset)
|
||||
}
|
||||
})
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.80 завершён: preflight и conditional UPDATE сверяют полные параметры speed-пробы; 23 pure, HTTP409/RBAC и backend build прошли. Следующий12.3.81: сброс observations при изменении параметров в каталоге/UI. Общие main1176/context221 относятся к прежним указанным HEAD. Внешняя12.5 не выполнена.
|
||||
Актуальное продолжение 2026-10-07: 12.3.81 завершён: смена любых параметров speed-пробы сбрасывает9 observations, история сохранена; 74browser/8context,10pure,HTTP/RBAC,types/lint и contracts/backend/frontend builds прошли. Следующий12.3.82: server timestamps в POST receipt и отсутствие выдуманного Ping при неполном ответе. Общие main1176/context221 относятся к указанным прежним HEAD. Внешняя12.5 не выполнена.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-07. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-04
|
||||
## Актуальная точка продолжения — 2026-10-07
|
||||
|
||||
Этапы0–11 завершены в описанных локальных границах. Этап12 продолжается: доступность12.1, достоверность12.2.1–12.2.10 и профили12.3.1–12.3.63 записаны в журнале. Последняя общая main регрессия12.4.6 на991f2b3:1161 из1162 passed,1 focus race терминала, исправленный в12.4.6.1 с20+15 профильными проверками; context12.4.7 наb1f8326 дала221/221passed. Полный источник данных сохраняется, большие DOM ограничены страницами/окнами. Внешняя12.5 и реальные интеграции не приняты. Подробная история и точные ограничения: docs/ux-ui-stage-12.md.
|
||||
Этапы0–11 завершены в описанных локальных границах. Этап12 продолжается; блоки до12.3.81 отражены в журнале. Общие main12.4.8 на579e816:1176/1176 и context12.4.9 наa3fcba0:221/221 passed. Последующие изменения speed-test проверены профильными тестами; эти общие прогоны не относятся к новому HEAD. Внешняя12.5 и реальные интеграции не приняты. Точные результаты и ограничения: [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
||||
|
||||
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.80 завершён: preflight и conditional UPDATE сверяют полные параметры speed-пробы; 23 pure, HTTP409/RBAC и backend build прошли. Следующий12.3.81: сброс observations при изменении параметров в каталоге/UI. Общие main1176/context221 относятся к прежним указанным HEAD. Внешняя12.5 не выполнена.
|
||||
Актуальное продолжение 2026-10-07: 12.3.81 завершён: смена любых параметров speed-пробы сбрасывает9 observations, история сохранена; 74browser/8context,10pure,HTTP/RBAC,types/lint и contracts/backend/frontend builds прошли. Следующий12.3.82: server timestamps в POST receipt и отсутствие выдуманного Ping при неполном ответе. Общие main1176/context221 относятся к указанным прежним HEAD. Внешняя12.5 не выполнена.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -32,7 +32,15 @@
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе. Локальные блоки до12.3.80 завершены. Общая main1176/1176 на579e816 и context221/221 наa3fcba0 прошли; последующий функциональный delta проверен73browser/8context. Следующий12.3.81; внешняя12.5 не выполнена. |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе. Локальные блоки до12.3.81 завершены. Общая main1176/1176 на579e816 и context221/221 наa3fcba0 прошли; последующий функциональный delta проверен73browser/8context. Следующий12.3.82; внешняя12.5 не выполнена. |
|
||||
|
||||
## 12.3.81 — снимки и история при изменении параметров измерения (2026-10-07)
|
||||
|
||||
Общий sameSpeedMeasurement сравнивает пару, интерфейсы, протокол, направление и длительность в валидных read/write формах. Каталог читает полный конфигурационный снимок под прежним FOR UPDATE; смена любого параметра сбрасывает9 observations. Frontend публикует это правило только после API confirmation и выбирает историю по тем же параметрам. Enable/reorder сохраняют последние наблюдения, в том числе0; history не удаляется. ReUI компоненты и сетевые команды сохранены.
|
||||
|
||||
10 pure snapshot checks, HTTP/DI/RBAC каталога (все изменяемые параметры и сохранение0 при enable),74browser uptime/large history,8uptime context, targeted lint0errors/0warnings, non-incremental TypeScript и contracts/backend/frontend production builds прошли. Новый браузерный сценарий: save500 сохраняет80; успешная смена длительности20 и возврат10 оставляют snapshot unknown, история80 и KPI1run остаются. Перегенерированные3 исторических PNG восстановлены. Реальные PG locks/native tests требуют12.5.
|
||||
|
||||
Следующий12.3.82: POST receipt пока не возвращает серверное время, live publish использует время браузера и ставит Ping date даже без afterBtPing; исправить совместимо и проверить неполный/невалидный receipt. Общие main/context результаты не переименованы в проверки нового HEAD.
|
||||
|
||||
## 12.3.80 — принадлежность результата параметрам speed-пробы (2026-10-07)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.81 завершён: смена любых параметров speed-пробы сбрасывает9 observations, история сохранена; 74browser/8context,10pure,HTTP/RBAC,types/lint и contracts/backend/frontend builds прошли. Следующий12.3.82: server timestamps в POST receipt и отсутствие выдуманного Ping при неполном ответе. Общие main1176/context221 относятся к указанным прежним HEAD. Внешняя12.5 не выполнена.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
| Блок | Проверка и результат | Критерий остановки |
|
||||
@@ -910,3 +912,11 @@ Preflight перед созданием RouterOS client проверяет id,
|
||||
23 pure rate/status/persistence проверки прошли: 7 новых проверяют каждый изменённый параметр для late success/error, неизменный контекст с явным0 и preflight. HTTP/DI проверил409 как для пары, так и для параметров, прежние26 invalid inputs/defaults/RBAC; backend/contracts build passed. Реального PostgreSQL/RouterOS не было: source review условного SQL и DI не заменяют12.5. Это сравнение параметров, не версия строки и не защита от ABA изменения с возвратом тех же значений.
|
||||
|
||||
Следующий12.3.81: каталог и frontend пока сбрасывают observations только при смене пары; распространить правило на все параметры измерения, сохраняя историю и enable/reorder.
|
||||
|
||||
## 12.3.81 — снимки и история при изменении параметров измерения (2026-10-07)
|
||||
|
||||
Общий sameSpeedMeasurement сравнивает пару, интерфейсы, протокол, направление и длительность в валидных read/write формах. Каталог читает полный конфигурационный снимок под прежним FOR UPDATE; смена любого параметра сбрасывает9 observations. Frontend публикует это правило только после API confirmation и выбирает историю по тем же параметрам. Enable/reorder сохраняют последние наблюдения, в том числе0; history не удаляется. ReUI компоненты и сетевые команды сохранены.
|
||||
|
||||
10 pure snapshot checks, HTTP/DI/RBAC каталога (все изменяемые параметры и сохранение0 при enable),74browser uptime/large history,8uptime context, targeted lint0errors/0warnings, non-incremental TypeScript и contracts/backend/frontend production builds прошли. Новый браузерный сценарий: save500 сохраняет80; успешная смена длительности20 и возврат10 оставляют snapshot unknown, история80 и KPI1run остаются. Перегенерированные3 исторических PNG восстановлены. Реальные PG locks/native tests требуют12.5.
|
||||
|
||||
Следующий12.3.82: POST receipt пока не возвращает серверное время, live publish использует время браузера и ставит Ping date даже без afterBtPing; исправить совместимо и проверить неполный/невалидный receipt. Общие main/context результаты не переименованы в проверки нового HEAD.
|
||||
@@ -2,7 +2,7 @@ import { test } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { emptySpeedProbeObservations } from "@mmapp/contracts"
|
||||
import { publishSpeedProbeSnapshots } from "./speed-probe-snapshot"
|
||||
const probe = { id: "p", srcServerId: "1", dstServerId: "2", enabled: true, ...emptySpeedProbeObservations, lastTxAvgMbps: 0 as number | null, lastRxAvgMbps: 80 as number | null, lastStatus: "done" as "done" | "error" | null, lastRunAt: "2026-10-07T00:00:00Z" as string | null }
|
||||
const probe = { id: "p", srcServerId: "1", dstServerId: "2", srcInterface: "ether1", dstInterface: "ether2", protocol: "tcp" as const, direction: "both" as "both" | "transmit" | "receive", durationSec: "10", enabled: true, ...emptySpeedProbeObservations, lastTxAvgMbps: 0 as number | null, lastRxAvgMbps: 80 as number | null, lastStatus: "done" as "done" | "error" | null, lastRunAt: "2026-10-07T00:00:00Z" as string | null }
|
||||
test("same pair preserves latest zero and snapshot instead of readonly values copied into a stale draft", () => {
|
||||
const next = { ...probe, enabled: false, lastTxAvgMbps: 999, lastRxAvgMbps: 999 }
|
||||
const [saved] = publishSpeedProbeSnapshots([probe], [next])
|
||||
@@ -25,3 +25,9 @@ test("new row and reordered existing rows retain correct ownership without mutat
|
||||
assert.deepEqual(saved.map(p => p.id), ["new", "p"])
|
||||
assert.equal(next.lastRxAvgMbps, 80)
|
||||
})
|
||||
|
||||
for (const edit of [{ srcInterface: "ether3" }, { dstInterface: "ether3" }, { protocol: "udp" as const }, { direction: "transmit" as const }, { durationSec: "20" }]) test("changed measurement " + Object.keys(edit)[0] + " resets all observations without altering history", () => {
|
||||
const [saved] = publishSpeedProbeSnapshots([probe], [{ ...probe, ...edit }])
|
||||
for (const key of Object.keys(emptySpeedProbeObservations)) assert.equal(saved[key as keyof typeof emptySpeedProbeObservations], null)
|
||||
assert.equal(probe.lastRxAvgMbps, 80)
|
||||
})
|
||||
@@ -1,12 +1,12 @@
|
||||
import { emptySpeedProbeObservations, type SpeedProbeRead } from "@mmapp/contracts"
|
||||
import { emptySpeedProbeObservations, sameSpeedMeasurement, type SpeedMeasurementParameters, type SpeedProbeRead } from "@mmapp/contracts"
|
||||
type Observations = Pick<SpeedProbeRead, keyof typeof emptySpeedProbeObservations>
|
||||
type Pair = { id: string; srcServerId: string; dstServerId: string }
|
||||
/** Publish confirmed configuration while preserving the latest snapshot only for the same pair. */
|
||||
type Pair = { id: string } & SpeedMeasurementParameters
|
||||
/** Publish confirmed configuration while preserving the latest snapshot only for the same measurement parameters. */
|
||||
export function publishSpeedProbeSnapshots<T extends Pair & Partial<Observations>>(previous: readonly T[], next: readonly T[]) {
|
||||
const byId = new Map(previous.map(row => [row.id, row]))
|
||||
return next.map(row => {
|
||||
const old = byId.get(row.id)
|
||||
if (!old || old.srcServerId !== row.srcServerId || old.dstServerId !== row.dstServerId) {
|
||||
if (!old || !sameSpeedMeasurement(old, row)) {
|
||||
return { ...row, ...emptySpeedProbeObservations }
|
||||
}
|
||||
const observations = Object.fromEntries(Object.keys(emptySpeedProbeObservations).map(key => [key, old[key as keyof Observations] ?? null])) as Observations
|
||||
|
||||
@@ -56,3 +56,15 @@ export const speedTestCommandSchema = z.object({
|
||||
durationSec: speedPositiveInteger.pipe(z.number().min(3).max(2147483647)).default(10),
|
||||
}).refine(row => row.srcServerId !== row.dstServerId, "Источник и назначение должны отличаться")
|
||||
export type SpeedTestCommand = z.infer<typeof speedTestCommandSchema>
|
||||
|
||||
export type SpeedMeasurementParameters = {
|
||||
srcServerId: string | number; dstServerId: string | number; srcInterface?: string; dstInterface?: string;
|
||||
protocol: "tcp" | "udp"; direction: "both" | "transmit" | "receive"; durationSec: string | number
|
||||
}
|
||||
/** Compare validated configuration across HTTP read/write forms; enable/order are not measurement parameters. */
|
||||
export function sameSpeedMeasurement(a: SpeedMeasurementParameters, b: SpeedMeasurementParameters): boolean {
|
||||
return String(a.srcServerId) === String(b.srcServerId) && String(a.dstServerId) === String(b.dstServerId)
|
||||
&& (a.srcInterface ?? "").trim() === (b.srcInterface ?? "").trim()
|
||||
&& (a.dstInterface ?? "").trim() === (b.dstInterface ?? "").trim()
|
||||
&& a.protocol === b.protocol && a.direction === b.direction && Number(a.durationSec) === Number(b.durationSec)
|
||||
}
|
||||
@@ -861,3 +861,45 @@ test("Speed snapshot: смена пары сбрасывает снимок по
|
||||
await expect(history).toContainText("80")
|
||||
expect(writes).toBe(3)
|
||||
})
|
||||
|
||||
test("Speed snapshot: смена длительности сбрасывает снимок, прежняя история не накладывается", async ({ page }) => {
|
||||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write", false, true)
|
||||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||||
await page.getByRole("button", { name: /Скорость/ }).click()
|
||||
await page.route("**/api/uptime/speed-test", route => route.fulfill({ json: { ok: true, result: { runId: route.request().postDataJSON().runId, txAvgMbps: 30, rxAvgMbps: 80 } } }))
|
||||
let fail = true, writes = 0
|
||||
await page.route("**/api/uptime/speed-probes", route => {
|
||||
if (route.request().method() !== "PUT") return route.fallback()
|
||||
writes++
|
||||
return route.fulfill(fail ? { status: 500, json: { error: "Пара не сохранена" } } : { json: { ok: true } })
|
||||
})
|
||||
const run = page.getByRole("button", { name: "Запустить", exact: true })
|
||||
const row = () => page.getByRole("button", { name: "Запустить", exact: true, includeHidden: true }).locator("xpath=../../..")
|
||||
await run.click()
|
||||
await expect(row().getByText("80 Мбит/с", { exact: true })).toBeVisible()
|
||||
const history = page.getByRole("table")
|
||||
await expect(history).toContainText("80")
|
||||
const summary = page.locator('[aria-label="Сводка speed-проб"]')
|
||||
await expect(summary).toContainText("1 run")
|
||||
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
|
||||
const dialog = page.getByRole("dialog")
|
||||
const duration = () => dialog.locator('[data-slot="field"]').filter({ has: page.locator('[data-slot="field-label"]').filter({ hasText: /^Сек$/ }) }).getByRole("textbox")
|
||||
await duration().fill("20")
|
||||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||||
await expect(dialog.getByRole("alert")).toContainText("Пара не сохранена")
|
||||
await expect(row()).toContainText("80 Мбит/с")
|
||||
fail = false
|
||||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(row()).toContainText("20")
|
||||
await expect(row()).not.toContainText("80 Мбит/с")
|
||||
await expect(summary).toContainText("1 run")
|
||||
await expect(history).toContainText("80")
|
||||
await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click()
|
||||
await duration().fill("10")
|
||||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(row()).not.toContainText("80 Мбит/с")
|
||||
await expect(history).toContainText("80")
|
||||
expect(writes).toBe(3)
|
||||
})
|
||||
Reference in new issue
Block a user