fix(scheduler): завершить локальную приёмку контекста и адаптивности

This commit is contained in:
Denozordec committed 2026-10-03 12:03:38 +07:00
1 parent 5df337f6d7
commit 65aeedd3b0
17 files changed
+108 -16

No files matched your search

+8 -7
View File
@@ -876,9 +876,9 @@ function DataCollectionContent() {
sub: settingsReads.traffic.error ? "Последний сбор требует обновления" : trafficCollector?.lastError ? trafficCollector.lastError : trafficCollector?.lastDurationMs != null ? `${trafficCollector.lastDurationMs} мс` : "нет данных",
icon: trafficCollector?.lastError ? (
<XCircleIcon className="size-4" />
) : (
) : trafficCollector?.lastCollectedAt ? (
<CheckCircleIcon className="size-4" />
),
) : (<CalendarClockIcon className="size-4" />),
},
{
label: "Журнал (в списке)",
@@ -946,7 +946,7 @@ function DataCollectionContent() {
{isLive && schedulerStatusReceivedAt ? <p className="text-xs text-muted-foreground">Статус получен: {new Date(schedulerStatusReceivedAt).toLocaleString("ru-RU")}</p> : null}
{isLive && Object.entries(settingsReads).map(([key, value]) => (
<div key={key} className="text-xs text-muted-foreground" data-testid={"settings-" + key}>
<p role="status">{SETTINGS_LABELS[key as SettingsKey]} — {value.known ? "настройки получены" : "настройки не получены"}{value.receivedAt ? " (получены): " + new Date(value.receivedAt).toLocaleString("ru-RU") : ""}</p>
<p role="status">{SETTINGS_LABELS[key as SettingsKey]} — {value.known ? "настройки получены" : "настройки не получены"}{value.receivedAt ? " · Получено: " + new Date(value.receivedAt).toLocaleString("ru-RU") : ""}</p>
{value.error ? <Alert variant="warning" className="mt-1"><AlertCircleIcon aria-hidden="true" /><AlertDescription>{value.error}{value.known ? ". Сохранены последние подтверждённые данные." : ""}</AlertDescription></Alert> : null}
</div>
))}
@@ -965,9 +965,9 @@ function DataCollectionContent() {
aria-label="Сводка сбора данных"
items={stats.map((s, i) => ({
id: `dc-${i}`,
label: s.label,
label: <span className="whitespace-normal">{s.label}</span>,
value: s.value,
hint: s.sub,
hint: <span className="text-xs text-muted-foreground whitespace-normal">{s.sub}</span>,
icon: s.icon,
iconClassName:
s.label === "Трафик — последний сбор" && trafficCollector?.lastError
@@ -976,7 +976,7 @@ function DataCollectionContent() {
? "text-warning"
: s.label === "Журнал (в списке)" && errorRunsInView
? "text-destructive"
: s.label === "Трафик — последний сбор"
: s.label === "Трафик — последний сбор" && trafficCollector?.lastCollectedAt
? "text-success"
: "text-muted-foreground",
variant:
@@ -1042,11 +1042,12 @@ function DataCollectionContent() {
</p>
<Button
size="sm"
aria-label="Сохранить настройки планировщика"
disabled={schedulerSaveBusy || dirtyCount === 0}
onClick={() => { void persistSchedulerDrafts() }}
>
{schedulerSaveBusy ? <LoaderCircleIcon className="size-4 animate-spin mr-2" /> : null}
Сохранить настройки планировщика
Сохранить изменения
</Button>
<div className="text-xs text-muted-foreground flex flex-col gap-0.5 pt-1 border-t border-border/60">
<p>
@@ -274,7 +274,7 @@ function NetflowSettingsPanel({
<>
<OpsPanel
title="Traffic Flow / NetFlow (IPFIX)"
description="Дополнение к сбору счётчиков REST. Приём только через WireGuard на хосте Docker MM. Preview: https://reui.io/preview/base/settings-16"
description="Дополнение к сбору счётчиков REST. Приём только через WireGuard на хосте Docker MM."
headerRight={
<div className="flex items-center gap-2">
{settings?.listenerBound ? (
+1 -1
View File
@@ -226,7 +226,7 @@ REST запрос → ожидание → подтверждённый output /
| Пользователи | [Профиль](ux-ui/screens/users-profile-desktop.png), [ошибка](ux-ui/screens/users-error-desktop.png) | [390](ux-ui/screens/users-profile-mobile.png), [320](ux-ui/screens/users-profile-narrow.png) |
| Контейнеры | [Обзор](ux-ui/screens/containers-overview-desktop.png), [снимок](ux-ui/screens/containers-export-desktop.png) | [390](ux-ui/screens/containers-export-mobile.png), [320](ux-ui/screens/containers-export-narrow.png) |
Каталог Evolution защищён от неполного HTTP-ответа в 11Б.3.3.4.1; Подтверждение testConnection исправлено в 11Б.3.3.4.2; состояния интеграции остаются в объёме 11Б. Актуальная точка продолжения: 11Б.3.3.7 — backend source/визуальная приёмка scheduler; параллельных агентов не использовать. Ручные run-now/refresh, общий lock/права и сообщения skipped/partial/event/status приняты в 11Б.3.3.6.3. Точечные операции настроек, ввод и подтверждённый сброс dirty приняты в 11Б.3.3.6.2. Независимые strict settings readers и защита черновиков подключены в 11Б.3.3.5; реальные интеграции остаются внешней приёмкой. Независимые scheduler status/runs и достоверные KPI приняты в 11Б.3.3.3. Strict history/unknown/свежесть приняты в 11Б.3.3.2. Контекст/abort/revision и исходный lint исправлены в 11Б.3.3.1; далее независимые readers/dirty states и операции. GeoIP API принят в 11Б.3.2. Scheduler API основа проверена в 11Б.3.1, пока не подключена к странице. Совместимость JSONB/legacy snapshot и foreign guard проверены в 11Б.2; исходные lint/context ограничения страницы описаны в журнале. Общий runner исправлен в 11Б.1; карта оставшегося этапа — [11Б](ux-ui-stage-11b.md). Этап 11А принят локально в 11А.33: backups 71 основных + 28 source E2E, 27 frontend unit, backend профиль и production build. Backup scheduler cleanup проверен в 11А.32; общий dispatcher/journal status исправлен в 11Б.1, реальные интеграции остаются внешней приёмкой. S3/partial job/initial errors приняты в 11А.31.2.2. Диалоги ошибок приняты в 11А.31.2.1. Frame/первые 9 экранов приняты в 11А.31.1. URL/Filters контекст принят в 11А.30. Клиентский checkpoint restore принят в 11А.29.5. Backend outcome неизвестного импорта принят в 11А.29.4. Идентичность receipt принята в 11А.29.3. Backend cleanup удаления принят в 11А.29.2. Frontend delete/restore/download принят в 11А.29.1, затем URL/Frame/визуальная приёмка и scheduler cleanup. Frontend checkpoint/создание/serial polling приняты локально в 11А.28.2. API/основа наблюдения реализованы в 11А.28.1. Настройки расписания/S3 реализованы в 11А.27.1–11А.27.2. Каталоги/source/права реализованы в 11А.25, backend результат ручных операций исправлен в 11А.26. Сертификаты приняты локально в 11А.24, backups API стал строгим. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Резервные копии и весь 11А приняты локально в 11А.33; внешние ограничения — журнал.
Каталог Evolution защищён от неполного HTTP-ответа в 11Б.3.3.4.1; Подтверждение testConnection исправлено в 11Б.3.3.4.2; состояния интеграции остаются в объёме 11Б. Актуальная точка продолжения: 11Б.3.4 — GeoIP/NetFlow формы и операции. Scheduler принят локально в 11Б.3.3.7 (46 основных + 6 backend source E2E, ПК/390/320); внешние интеграции не доказаны. Параллельных агентов не использовать. Ручные run-now/refresh, общий lock/права и сообщения skipped/partial/event/status приняты в 11Б.3.3.6.3. Точечные операции настроек, ввод и подтверждённый сброс dirty приняты в 11Б.3.3.6.2. Независимые strict settings readers и защита черновиков подключены в 11Б.3.3.5; реальные интеграции остаются внешней приёмкой. Независимые scheduler status/runs и достоверные KPI приняты в 11Б.3.3.3. Strict history/unknown/свежесть приняты в 11Б.3.3.2. Контекст/abort/revision и исходный lint исправлены в 11Б.3.3.1; далее независимые readers/dirty states и операции. GeoIP API принят в 11Б.3.2. Scheduler API основа проверена в 11Б.3.1, пока не подключена к странице. Совместимость JSONB/legacy snapshot и foreign guard проверены в 11Б.2; исходные lint/context ограничения страницы описаны в журнале. Общий runner исправлен в 11Б.1; карта оставшегося этапа — [11Б](ux-ui-stage-11b.md). Этап 11А принят локально в 11А.33: backups 71 основных + 28 source E2E, 27 frontend unit, backend профиль и production build. Backup scheduler cleanup проверен в 11А.32; общий dispatcher/journal status исправлен в 11Б.1, реальные интеграции остаются внешней приёмкой. S3/partial job/initial errors приняты в 11А.31.2.2. Диалоги ошибок приняты в 11А.31.2.1. Frame/первые 9 экранов приняты в 11А.31.1. URL/Filters контекст принят в 11А.30. Клиентский checkpoint restore принят в 11А.29.5. Backend outcome неизвестного импорта принят в 11А.29.4. Идентичность receipt принята в 11А.29.3. Backend cleanup удаления принят в 11А.29.2. Frontend delete/restore/download принят в 11А.29.1, затем URL/Frame/визуальная приёмка и scheduler cleanup. Frontend checkpoint/создание/serial polling приняты локально в 11А.28.2. API/основа наблюдения реализованы в 11А.28.1. Настройки расписания/S3 реализованы в 11А.27.1–11А.27.2. Каталоги/source/права реализованы в 11А.25, backend результат ручных операций исправлен в 11А.26. Сертификаты приняты локально в 11А.24, backups API стал строгим. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Резервные копии и весь 11А приняты локально в 11А.33; внешние ограничения — журнал.
| Экран | ПК | Телефон |
| --- | --- | --- |
+9 -1
View File
@@ -28,7 +28,7 @@
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.3.6.3; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.3.7; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,14 @@
## Блок 11Б.3.3.7 — локальная приёмка scheduler (2026-10-03)
- Проверены обычные HTTP-пути со сменой backend: 6 source E2E для позднего GET/PUT/run-now, успех и отказ старого источника не меняют новый. Source тесты вынесены в отдельную конфигурацию :3101/local; общий профиль остаётся :3100/same-origin.
- 46 основных профильных E2E проверены: 39 прошли сразу, 7 KPI assertions требовали обновления селектора после переноса подписей; 9 history/status повторно прошли с устойчивой привязкой к установленному FramePanel. Отдельно 6 source E2E прошли. Новая структура не меняет unknown/zero значения.
- ПК/390/320 проходят без горизонтального переполнения body; сохранены 6 снимков docs/ux-ui/screens/data-collection*.png, визуально просмотрены 320 и ПК. KPI тексты переносятся, unknown сбор не помечается зелёным успехом. Кнопка «Сохранить изменения» помещается на 320, доступное имя сохранено. Удалена техническая ссылка Preview из текста NetFlow.
- ESLint новых/изменённых файлов и production frontend/contracts build прошли; две последние правки текста дополнительно проверены 3 responsive E2E с обновлением снимков. ReUI композиция/тема сохранены, labels и async объявления есть. Это приёмка scheduler/settings/history, а не всей страницы интеграций.
- Следующий блок 11Б.3.4: GeoIP/NetFlow формы — known/error/dirty/source/partial и операции. Evolution provider, alerts/releases/probe scheduling и итоговая приёмка 11Б остаются впереди. PostgreSQL, RouterOS, Portal, MaxMind download и JWT не доказаны HTTP-фикстурами.
## Блок 11Б.3.3.6.3 — подтверждённые ручные запуски (2026-10-03)
- Строгие scheduler run-now/refresh подключены вместо raw POST. Показаны подтверждённый запуск, skipped/not-due и partial; event/status failure отделены от результата задачи. Неверный/чужой receipt не становится успехом и не повторяет POST, сообщает неизвестность результата и необходимость обновить журнал.
+8
View File
@@ -146,3 +146,11 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
- Run-now/refresh используют общий синхронный lock с PUT, права mm:settings:admin и сохранённые настройки без отправки dirty draft. Неизвестная/running задача не запускается из UI. Поздние callbacks после unmount/source change не публикуются; backend применение уже начатой операции не отменяется.
- Прошли 26 E2E (10 operations + 16 settings), ESLint и production frontend/contracts build. Проверены partial/event/status, false receipt/foreign snapshot, lock и mode change; смена режима также прекращает оставшийся batch PUT. Предыдущие 11 status/history/race прошли в 11Б.3.3.6.2; новый блок не меняет readers.
- Далее 11Б.3.3.7: backend source E2E и визуальная/адаптивная приёмка scheduler. GeoIP/NetFlow формы и Evolution provider, alerts/releases/probe scheduling остаются в объёме этапа 11Б; их общий интерфейс пока не принят.
## Блок 11Б.3.3.7 — локальная приёмка scheduler (2026-10-03)
- Проверены обычные HTTP-пути со сменой backend: 6 source E2E для позднего GET/PUT/run-now, успех и отказ старого источника не меняют новый. Source тесты вынесены в отдельную конфигурацию :3101/local; общий профиль остаётся :3100/same-origin.
- 46 основных профильных E2E проверены: 39 прошли сразу, 7 KPI assertions требовали обновления селектора после переноса подписей; 9 history/status повторно прошли с устойчивой привязкой к установленному FramePanel. Отдельно 6 source E2E прошли. Новая структура не меняет unknown/zero значения.
- ПК/390/320 проходят без горизонтального переполнения body; сохранены 6 снимков docs/ux-ui/screens/data-collection*.png, визуально просмотрены 320 и ПК. KPI тексты переносятся, unknown сбор не помечается зелёным успехом. Кнопка «Сохранить изменения» помещается на 320, доступное имя сохранено. Удалена техническая ссылка Preview из текста NetFlow.
- ESLint новых/изменённых файлов и production frontend/contracts build прошли; две последние правки текста дополнительно проверены 3 responsive E2E с обновлением снимков. ReUI композиция/тема сохранены, labels и async объявления есть. Это приёмка scheduler/settings/history, а не всей страницы интеграций.
- Следующий блок 11Б.3.4: GeoIP/NetFlow формы — known/error/dirty/source/partial и операции. Evolution provider, alerts/releases/probe scheduling и итоговая приёмка 11Б остаются впереди. PostgreSQL, RouterOS, Portal, MaxMind download и JWT не доказаны HTTP-фикстурами.
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test"
export default defineConfig({
testDir: "./tests/e2e",
testIgnore: ["**/backups-context.spec.ts", "**/certificates-context.spec.ts", "**/containers-context.spec.ts", "**/users-context.spec.ts", "**/probes-context.spec.ts", "**/terminal-context.spec.ts", "**/network-map-context.spec.ts", "**/route-optimizer-context.spec.ts", "**/recursive-routes-context.spec.ts", "**/ospf-context.spec.ts", "**/bgp-context.spec.ts", "**/firewall-context.spec.ts", "**/filters-context.spec.ts", "**/ipsec-context.spec.ts", "**/gre-context.spec.ts", "**/vxlan-context.spec.ts"],
testIgnore: ["**/data-collection-context.spec.ts", "**/backups-context.spec.ts", "**/certificates-context.spec.ts", "**/containers-context.spec.ts", "**/users-context.spec.ts", "**/probes-context.spec.ts", "**/terminal-context.spec.ts", "**/network-map-context.spec.ts", "**/route-optimizer-context.spec.ts", "**/recursive-routes-context.spec.ts", "**/ospf-context.spec.ts", "**/bgp-context.spec.ts", "**/firewall-context.spec.ts", "**/filters-context.spec.ts", "**/ipsec-context.spec.ts", "**/gre-context.spec.ts", "**/vxlan-context.spec.ts"],
fullyParallel: false,
workers: 1,
retries: 0,
@@ -0,0 +1,5 @@
import { defineConfig } from "@playwright/test"
import base from "./playwright.config"
const server = base.webServer
if (!server || Array.isArray(server)) throw new Error("Ожидается один локальный сервер")
export default defineConfig({ ...base, use: { ...base.use, baseURL: "http://127.0.0.1:3101" }, testIgnore: [], testMatch: "data-collection-context.spec.ts", webServer: { ...server, command: "node node_modules/next/dist/bin/next dev --hostname 127.0.0.1 --port 3101", url: "http://127.0.0.1:3101", env: { ...server.env, NEXT_PUBLIC_BACKEND_URL: "local" } } })
+35
View File
@@ -0,0 +1,35 @@
import { test, expect, type Page } from "@playwright/test"
import { geoip } from "./fixtures/geoip"
const headers = { "access-control-allow-origin": "*", "access-control-allow-methods": "GET,POST,PUT,OPTIONS", "access-control-allow-headers": "Content-Type,Authorization" }
const common = { enabled: true, intervalSec: 45, retentionDays: 14, lastCollectedAt: null, lastDurationMs: 0, lastError: null }
const job = { jobKey: "backups", enabled: true, intervalSec: 60, running: false, lastFinishedAt: null, lastStatus: null, lastDurationMs: 0, lastError: null }
const snapshot = { v: 1, job: "backups", sampledAt: "2026-10-03T00:00:00Z", due: true, created: 1, failures: 0, pruned: 0 }
async function setup(page: Page) {
await page.route("**/health", r => r.fulfill({ headers, json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
if (path === "/api/auth/config") return r.fulfill({ headers, json: { required: false } })
if (path === "/api/geoip") return r.fulfill({ headers, json: geoip })
if (path === "/api/scheduler/status") return r.fulfill({ headers, json: { jobs: [job] } })
if (path === "/api/scheduler/runs") return r.fulfill({ headers, json: { runs: [] } })
if (path === "/api/traffic/settings") return r.fulfill({ headers, json: { ...common, intervalSec: new URL(r.request().url()).port === "8101" ? 77 : 45 } })
return r.fulfill({ headers, status: 503, json: { error: "outside context fixture" } })
})
}
for (const action of ["read", "put", "run"] as const) for (const fails of [false, true]) test("Сбор данных: поздний ответ после смены backend/" + action + "/" + fails, async ({ page }) => {
await setup(page); let started = false, completed = false, release!: () => void; const gate = new Promise<void>(resolve => release = resolve)
const path = action === "run" ? "/api/scheduler/jobs/backups/run-now" : "/api/traffic/settings"
await page.route("**" + path, async r => {
if (new URL(r.request().url()).port !== "8000" || (action === "read" ? r.request().method() !== "GET" : action === "put" ? r.request().method() !== "PUT" : false)) return r.fallback()
started = true; await gate
try { await r.fulfill(fails ? { headers, status: 503, json: { error: "СТАРАЯ ОШИБКА" } } : { headers, json: action === "run" ? { ok: true, result: { status: "ok", error: null, result: snapshot }, status: { jobs: [job] } } : action === "put" ? { ok: true, settings: { ...common, intervalSec: 99 } } : { ...common, intervalSec: 99 } }) } catch {} finally { completed = true }
})
try {
await page.goto("/data-collection")
if (action === "put") { const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99"); await page.getByRole("button", { name: "Сохранить настройки планировщика" }).click() }
if (action === "run") { const run = page.getByRole("button", { name: "Запустить: Бэкапы", exact: true }); await expect(run).toBeEnabled(); await run.click() }
await expect.poll(() => started).toBe(true); await page.goto("/settings"); const url = page.getByPlaceholder("http://localhost:8000"); await url.fill("http://localhost:8101"); await url.press("Enter"); await page.goto("/data-collection")
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toHaveValue("77"); release(); await expect.poll(() => completed).toBe(true)
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toHaveValue("77"); await expect(page.getByText(/СТАРАЯ ОШИБКА/)).toHaveCount(0); await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toHaveCount(0); await expect(page.getByText("Запуск подтверждён: Бэкапы", { exact: true })).toHaveCount(0)
} finally { release() }
})
+2 -2
View File
@@ -15,7 +15,7 @@ for (const malformed of [true, false]) test("Журнал: первоначал
await setup(page); await page.route("**/api/scheduler/runs?**", r => r.fulfill(malformed ? { json: {} } : { status: 503, json: { error: "history unavailable" } }))
await page.goto("/data-collection"); await expect(page.getByText("История не получена", { exact: true }).first()).toBeVisible()
await expect(page.getByText("Пока нет прогонов", { exact: true })).toHaveCount(0)
await expect(page.getByText("Журнал (в списке)", { exact: true }).locator("../..")).toContainText("—")
await expect(page.getByText("Журнал (в списке)", { exact: true }).locator("xpath=ancestor::*[@data-slot='frame-panel'][1]")).toContainText("—")
})
test("Журнал: background error сохраняет строки и время последнего чтения", async ({ page }) => {
await setup(page); let fails = false
@@ -28,7 +28,7 @@ test("Журнал: background error сохраняет строки и врем
test("Журнал: подтверждённый пустой список имеет ноль и время получения", async ({ page }) => {
await setup(page); await page.route("**/api/scheduler/runs?**", r => r.fulfill({ json: { runs: [] } }))
await page.goto("/data-collection"); await expect(page.getByText("Пока нет прогонов", { exact: true })).toBeVisible(); await expect(page.getByText(/^История получена:/)).toBeVisible()
await expect(page.getByText("Журнал (в списке)", { exact: true }).locator("../..")).toContainText("0")
await expect(page.getByText("Журнал (в списке)", { exact: true }).locator("xpath=ancestor::*[@data-slot='frame-panel'][1]")).toContainText("0")
})
test("Журнал: смена среза не выдаёт прежние строки за новый фильтр", async ({ page }) => {
@@ -0,0 +1,35 @@
import { test, expect, type Page } from "@playwright/test"
import { geoip } from "./fixtures/geoip"
const common = { enabled: true, intervalSec: 300, retentionDays: 14, lastCollectedAt: null, lastDurationMs: 0, lastError: null }
const values: Record<string, object> = {
"/api/traffic/settings": { ...common, intervalSec: 45 },
"/api/servers-api-ping/settings": { ...common, enabled: false, intervalSec: 120 },
"/api/uptime/settings": { ...common, resourcesEnabled: true, pingEnabled: false, speedEnabled: false, probeIntervalSec: 15, speedIntervalSec: 60 },
"/api/internet-path/settings": common,
"/api/certificates/renew-settings": { enabled: false, intervalSec: 21600, renewBeforeDays: 30 },
}
async function setup(page: Page) {
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
if (path === "/api/auth/config") return r.fulfill({ json: { required: false } })
if (path === "/api/geoip") return r.fulfill({ json: geoip })
if (path === "/api/scheduler/status") return r.fulfill({ json: { jobs: [] } })
if (path === "/api/scheduler/runs") return r.fulfill({ json: { runs: [] } })
if (path === "/api/traffic/flow/settings") return r.fulfill({ json: { enabled: false, collectorIp: "10.255.254.1", flowListenPort: 4739, wgListenPort: 51821, prefix: "10.255.254.0/24", publicEndpoint: "", hostPublicKey: "", hasHostPrivateKey: false, hubServerId: null, retentionHours: 24, topN: 200, mapServiceMinSharePct: 5, lastDatagramAt: null, lastExporterIp: null, lastError: null, packetsReceived: 0, listenerBound: false, listenerAddress: null, peers: [] } })
if (values[path]) return r.fulfill({ json: values[path] })
return r.fulfill({ status: 503, json: { error: "outside settings fixture" } })
})
}
for (const width of [1440, 390, 320]) test("Сбор данных: адаптивность/" + width, async ({ page }) => {
await page.setViewportSize({ width, height: 1000 }); await setup(page)
await page.goto("/data-collection"); await expect(page.getByTestId("settings-traffic")).toContainText("настройки получены")
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toHaveValue("45")
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
await page.getByRole("button", { name: "Обновить", exact: true }).scrollIntoViewIfNeeded()
await page.screenshot({ path: "docs/ux-ui/screens/data-collection-" + width + ".png", fullPage: true })
await page.getByRole("button", { name: "Сохранить настройки планировщика" }).scrollIntoViewIfNeeded()
await expect(page.getByRole("textbox", { name: "Хранение трафика", exact: true })).toBeVisible()
await page.screenshot({ path: "docs/ux-ui/screens/data-collection-settings-" + width + ".png", fullPage: true })
})
+3 -3
View File
@@ -15,18 +15,18 @@ async function setup(page: import("@playwright/test").Page) {
for (const malformed of [true, false]) test("Статус: initial failure не подменяется drafts/" + malformed, async ({ page }) => {
await setup(page); await page.route("**/api/scheduler/status", r => r.fulfill(malformed ? { json: {} } : { status: 503, json: { error: "status unavailable" } }))
await page.goto("/data-collection"); await expect(page.getByText("Статус планировщика не получен", { exact: true })).toBeVisible()
for (const label of ["Включено задач", "Сейчас выполняется"]) await expect(page.getByText(label, { exact: true }).locator("../..")).toContainText("—")
for (const label of ["Включено задач", "Сейчас выполняется"]) await expect(page.getByText(label, { exact: true }).locator("xpath=ancestor::*[@data-slot='frame-panel'][1]")).toContainText("—")
})
test("Статус и история появляются до медленной зависимости uptime", async ({ page }) => {
await setup(page); let started = false, release!: () => void; const gate = new Promise<void>(resolve => release = resolve)
await page.route("**/api/uptime/settings", async r => { started = true; await gate; try { await r.fulfill({ status: 503, json: { error: "uptime unavailable" } }) } catch {} })
await page.route("**/api/scheduler/status", r => r.fulfill({ json: { jobs: [job] } }))
try { await page.goto("/data-collection"); await expect.poll(() => started).toBe(true); await expect(page.getByText("Включено задач", { exact: true }).locator("../..")).toContainText("0 / 1"); await expect(page.getByText("Пока нет прогонов", { exact: true })).toBeVisible(); await expect(page.getByText(/^Статус получен:/)).toBeVisible() } finally { release() }
try { await page.goto("/data-collection"); await expect.poll(() => started).toBe(true); await expect(page.getByText("Включено задач", { exact: true }).locator("xpath=ancestor::*[@data-slot='frame-panel'][1]")).toContainText("0 / 1"); await expect(page.getByText("Пока нет прогонов", { exact: true })).toBeVisible(); await expect(page.getByText(/^Статус получен:/)).toBeVisible() } finally { release() }
})
test("Статус: background failure сохраняет последний статус и время", async ({ page }) => {
await setup(page); let fails = false
await page.route("**/api/scheduler/status", r => r.fulfill(fails ? { status: 503, json: { error: "status refresh unavailable" } } : { json: { jobs: [job] } }))
await page.goto("/data-collection"); await expect(page.getByText(/^Статус получен:/)).toBeVisible(); const received = await page.getByText(/^Статус получен:/).textContent(); fails = true
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled(); await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByText("Статус планировщика не обновлён", { exact: true })).toBeVisible(); await expect(page.getByText("Включено задач", { exact: true }).locator("../..")).toContainText("0 / 1"); await expect(page.getByText(/^Статус получен:/)).toHaveText(received!)
await expect(page.getByText("Статус планировщика не обновлён", { exact: true })).toBeVisible(); await expect(page.getByText("Включено задач", { exact: true }).locator("xpath=ancestor::*[@data-slot='frame-panel'][1]")).toContainText("0 / 1"); await expect(page.getByText(/^Статус получен:/)).toHaveText(received!)
})