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

This commit is contained in:
Denozordec committed 2026-10-03 22:32:53 +07:00
1 parent 271291a910
commit 1a9966ec95
48 files changed
+74 -12

No files matched your search

+11 -7
View File
@@ -2577,10 +2577,10 @@ function AlertsContent() {
</span> </span>
</div> </div>
{/* ── main layout ── */} </>:null}
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_300px] gap-5 items-start"> <div className={cn("grid gap-5 items-start",known?"grid-cols-1 xl:grid-cols-[minmax(0,1fr)_300px]":"grid-cols-1")}>
<div className="min-w-0 flex flex-col gap-5">
{/* ── rules card ── */} {known?<>
<OpsPanel <OpsPanel
title={ title={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@@ -2629,14 +2629,20 @@ function AlertsContent() {
</div> </div>
</OpsPanel> </OpsPanel>
{/* ── right sidebar ── */} </>:null}
{isLive?<AlertsOutboxPanel base={backendUrl} available={backendStatus===true} operationsBusy={operationBusy} beginOperation={beginOperation} endOperation={endOperation}/>:null}
{isLive?<AlertsTargetsStatus catalogs={catalogs}/>:null}
</div>
{known?
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<AlertsTelegramForm initial={tg} tokenConfigured={tokenConfigured} base={backendUrl} live={isLive} available={backendStatus===true&&!operationBusy} <AlertsTelegramForm initial={tg} tokenConfigured={tokenConfigured} base={backendUrl} live={isLive} available={backendStatus===true&&!operationBusy}
onBegin={beginOperation} onEnd={endOperation} onBegin={beginOperation} onEnd={endOperation}
onSaved={(value,configured)=>{setTg(value);setTokenConfigured(configured)}}/> onSaved={(value,configured)=>{setTg(value);setTokenConfigured(configured)}}/>
<HistoryCard entries={history} /> <HistoryCard entries={history} />
</div> </div>
:null}
</div> </div>
{known?<>
{/* ── presets + groups (свёрнуто по умолчанию) ── */} {/* ── presets + groups (свёрнуто по умолчанию) ── */}
<Collapsible open={extrasExpanded} onOpenChange={setExtrasExpanded} className="rounded-lg border border-border bg-card shadow-sm"> <Collapsible open={extrasExpanded} onOpenChange={setExtrasExpanded} className="rounded-lg border border-border bg-card shadow-sm">
<CollapsibleTrigger <CollapsibleTrigger
@@ -2765,8 +2771,6 @@ function AlertsContent() {
</Collapsible> </Collapsible>
</>:null} </>:null}
{isLive?<AlertsTargetsStatus catalogs={catalogs}/>:null}
{isLive?<AlertsOutboxPanel base={backendUrl} available={backendStatus===true} operationsBusy={operationBusy} beginOperation={beginOperation} endOperation={endOperation}/>:null}
</div> </div>
</div> </div>
+1 -1
View File
@@ -4,7 +4,7 @@
## Актуальная точка продолжения — 2026-10-03 ## Актуальная точка продолжения — 2026-10-03
Этап 11Б в работе; последний проверенный блок 11Б.6; Releases/probe schedule приняты локально. Следующий блок 11Б.7: сводная локальная регрессия оповещений, scheduler/integrations/settings/DB/releases/probe schedule, итоговый список внешних ограничений и production build; затем начать этап 12. Processing recovery без доказуемой остановки всех отправителей остаётся недоступен. Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. Следующий блок 12.1: общий shell — автоматический аудит доступности/клавиатура/фокус на ПК и320px в обеих темах, согласовать health popover с shared health; затем strict/partial источники dashboard, производительность и сквозная регрессия. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются.
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
+9 -1
View File
@@ -28,7 +28,7 @@
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 | | 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.6; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,14 @@
## Блок 11Б.7 — сводная локальная приёмка администрирования (2026-10-03)
- 189 browser, 68 context/source, 61 unit/contracts/API и7 named backend profiles прошли. После финальной композиции Alerts дополнительно16 reader/targets/queue и25 form/preview/layout,14 Alerts source прошли. Frontend/contracts production build и backend build прошли; финальная frontend build после композиции тоже прошла. Lint/typecheck профильных изменений прошли.
- Очередь и каталоги теперь идут сразу под правилами в основной колонке, до Telegram на телефоне; перестройка не зависит от known основной загрузки. Новый delayed-primary тест доказывает один GET очереди при unknown→known без remount. Desktop/mobile screenshots просмотрены.
- Добавлен единый administration-context профиль;39 снимков сохранены/обновлены в репозитории. Таблица результатов/референсы/схемы в docs/ux-ui-stage-11b.md; подготовлен план приёмки12. Установленные ReUI primitives/base-nova сохранены, checklist применён; полного WCAG соответствия не заявляем.
- Локальная приёмка11Б завершена с явными исключениями: BGP-prefix evaluator/live GRE schedule/processing recovery не реализованы, UI не выдаёт их за работающие. CAS retry разрешён толькоunknown с принятием риска; processing удерживается. Реальные migrations0008/0009, PG isolation/rollback/CAS, restore/UDP workers, RouterOS/Telegram/MaxMind/Portal/Evolution/CI требуют внешней приёмки. Backend legacy speed DTO normalization не перепринята.
- Следующий блок 12.1: общий shell — автоматический аудит доступности/клавиатура/фокус на ПК и320px в обеих темах, согласовать health popover с shared health; затем strict/partial источники dashboard, производительность и сквозная регрессия. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются.
## Блок 11Б.6 — достоверное расписание проб скорости (2026-10-03) ## Блок 11Б.6 — достоверное расписание проб скорости (2026-10-03)
- Live schedule reader выделен из диагностики, ждёт auth config и проверяет uptime:read до запроса/публикации. Strict required DTO/unique IDs/nullable finite metrics, no-store, 30s deadline, abort/lifetime/revision и source-key parent. Первоначальный сбой остаётся unknown, повтор явный; фоновая ошибка сохраняет подтверждённый список/время. - Live schedule reader выделен из диагностики, ждёт auth config и проверяет uptime:read до запроса/публикации. Strict required DTO/unique IDs/nullable finite metrics, no-store, 30s deadline, abort/lifetime/revision и source-key parent. Первоначальный сбой остаётся unknown, повтор явный; фоновая ошибка сохраняет подтверждённый список/время.
+21
View File
@@ -332,3 +332,24 @@ Releases strict metadata/unknown/local/безопасные ссылки и curr
### Продолжение 2026-10-03 — 11Б.6 ### Продолжение 2026-10-03 — 11Б.6
Live probe schedule strict read/own permissions/last good/retry/deadline/source и unknown TX/RX принят локально; demo tunnel selection исправлен. Unit2/browser36+8/context8/typecheck/lint/ReUI audit прошли, screenshot320 просмотрен. Реальный collector/legacy backend normalization не переприняты. Следующий блок 11Б.7: сводная локальная регрессия оповещений, scheduler/integrations/settings/DB/releases/probe schedule, итоговый список внешних ограничений и production build; затем начать этап 12. Processing recovery без доказуемой остановки всех отправителей остаётся недоступен. Live probe schedule strict read/own permissions/last good/retry/deadline/source и unknown TX/RX принят локально; demo tunnel selection исправлен. Unit2/browser36+8/context8/typecheck/lint/ReUI audit прошли, screenshot320 просмотрен. Реальный collector/legacy backend normalization не переприняты. Следующий блок 11Б.7: сводная локальная регрессия оповещений, scheduler/integrations/settings/DB/releases/probe schedule, итоговый список внешних ограничений и production build; затем начать этап 12. Processing recovery без доказуемой остановки всех отправителей остаётся недоступен.
## Сводная локальная приёмка — 11Б.7 (2026-10-03)
| Группа | Локальный результат | Осталось вне этой проверки |
| --- | --- | --- |
| Scheduler / сбор данных | Независимые known/error/time, dirty/selective save, общий lock, receipt partial/fatal, source isolation | Реальное выполнение collectors, PG journal/rollback и UDP lifecycle |
| GeoIP / NetFlow / Evolution | Reader/permissions, формы и секреты, unknown checkpoints, подтверждённый результат | MaxMind/RIPE, IPFIX/WireGuard export, Portal и upstream provider |
| System DB | Выбор файла отделён от restore, backup bytes/metadata, один запрос, предупреждения и неизвестный результат | Реальные pg_dump/pg_restore, rollback и остановка всех workers |
| Alerts | Rules/groups revision/If-Match, форма/права, независимые каталоги/очередь, Telegram API acceptance, CAS retry только unknown | PG migrations 0008/0009, isolation/CAS/rollback, реальные Telegram; processing recovery и BGP-prefix не реализованы |
| Releases | Strict manifest, unknown/local/mismatch, безопасные ссылки, только current production announcement | Реальная публикация CI/Gitea |
| Probe schedule | Strict read, uptime:read, retry/time/source, nullable TX/RX | Реальное выполнение uptime speed worker; backend legacy normalization не перепринята |
Схема финального Alerts: сводка → правила → очередь → каталоги; справа Telegram/журнал, затем пресеты/подсказки. На телефоне очередь идёт перед Telegram; независимые readers остаются смонтированы при unknown → known основной загрузки.
| Экран | ПК | Телефон | Референс |
| --- | --- | --- | --- |
| Alerts | [Экран](ux-ui/screens/alerts-overview-1440.png) | [Экран](ux-ui/screens/alerts-overview-320.png) | [List 9](https://reui.io/preview/base/list-9?ref=mcp), [Sheet 8](https://reui.io/preview/base/sheet-8?ref=mcp) |
| Releases | [Экран](ux-ui/screens/releases-local-1440.png) | [Экран](ux-ui/screens/releases-local-320.png) | [List 9](https://reui.io/preview/base/list-9?ref=mcp) |
| Probe schedule | [Экран](ux-ui/screens/probes-speed-schedule-1440.png) | [Экран](ux-ui/screens/probes-speed-schedule-320.png) | [Data Grid](https://reui.io/components/data-grid?ref=mcp) |
Сводная регрессия: 189 browser, 68 source/context, 61 unit/contracts/API и 7 named backend profiles прошли. После заключительной компоновки Alerts дополнительные16 reader/targets/queue,25 forms/layout/preview,14 source и финальная production build прошли. Backend build тоже прошла.39 снимков сохранены в репозитории; итог фиксируется в начале журнала. Браузерные тесты подменяют HTTP; backend DI/HTTP используют тестовые зависимости. Это не доказательство работы внешних систем или полного соответствия WCAG. SQL миграции локально не применялись, push/deploy не выполнялись.
+17
View File
@@ -0,0 +1,17 @@
# Этап 12: итоговая приёмка UX/UI
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
| Блок | Проверка и результат | Критерий остановки |
| --- | --- | --- |
| 12.1 | Общая оболочка, автоматический аудит доступности, клавиатура/фокус, 320px/ПК и обе темы | Исправлены доказанные нарушения, непроверенные состояния перечислены |
| 12.2 | Достоверность источников дашборда: неполный BGP, nullable counters/traffic, неверные DTO | Unknown/partial не становятся zero/down/empty; last good сохраняется |
| 12.3 | Производительность больших списков и таблиц на фиксированных локальных данных | Записаны размер fixture, измерения и практические ограничения |
| 12.4 | Сквозная регрессия интерфейса, права, context; production/contracts/backend builds и ReUI checklist | Итоговая матрица с результатами и точными исключениями |
| 12.5 | Внешняя приёмка PG/RouterOS/Telegram/интеграций | Протокол тестового окружения; локальные моки не заменяют эту проверку |
Сначала воспроизвести предполагаемые проблемы общей оболочки: health popover принимает только status:ok, тогда как shared health допускает подтверждённый legacy ok:true; проверить согласованность. Проверить реальные accessible names header triggers и skip link focus. Использовать установленный axe-core для автоматической части; отсутствие violations не означает полного соответствия или замену screen reader/manual проверки.
Клавиатурный сценарий должен предоставлять эквивалент действий мышью ([W3C SC 2.1.1](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html)); программные имена/роли/состояния проверяются отдельно ([W3C SC 4.1.2](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html)). Проверка reflow включает 320 CSS px; широкие таблицы проверяются как отдельные области прокрутки, а не как разрешение переполнения страницы ([W3C SC 1.4.10](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html)).
Внешние ограничения 11Б сохраняются: migrations0008/0009, PG isolation/CAS/rollback, реальный restore/UDP workers, RouterOS, Telegram, MaxMind/RIPE, Portal/Evolution, CI/Gitea. Processing не разрешается повторять без доказуемой остановки всех отправителей; timeout или checkbox не являются таким доказательством. BGP-prefix evaluator отсутствует и честно обозначен UI; отдельного live GRE расписания нет. Производственный выпуск не разрешён одним успешным browser/DI прогоном.
Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

@@ -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:["alerts-context.spec.ts","data-collection-context.spec.ts","evobgp-context.spec.ts","geoip-context.spec.ts","netflow-context.spec.ts","settings-context.spec.ts","system-database-context.spec.ts","probes-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"}}})
+8 -1
View File
@@ -15,7 +15,14 @@ for(const width of [320,1440])test("Alerts rules priority, accessible filters an
const name=page.getByText("Fixture rule",{exact:true});await expect(name).toBeVisible();expect((await name.boundingBox())!.width).toBeGreaterThan(100) const name=page.getByText("Fixture rule",{exact:true});await expect(name).toBeVisible();expect((await name.boundingBox())!.width).toBeGreaterThan(100)
const position=await search.boundingBox();const catalogs=await page.getByText("Каталоги целей",{exact:true}).boundingBox();const queue=await page.getByRole("region",{name:"Очередь Telegram"}).boundingBox() const position=await search.boundingBox();const catalogs=await page.getByText("Каталоги целей",{exact:true}).boundingBox();const queue=await page.getByRole("region",{name:"Очередь Telegram"}).boundingBox()
expect(position!.y).toBeLessThan(catalogs!.y);expect(position!.y).toBeLessThan(queue!.y) expect(position!.y).toBeLessThan(catalogs!.y);expect(position!.y).toBeLessThan(queue!.y)
if(width===320){const telegram=await page.getByRole("region",{name:"Параметры Telegram"}).boundingBox();expect(queue!.y).toBeLessThan(telegram!.y)}else expect(queue!.y).toBeLessThan(900)
await search.scrollIntoViewIfNeeded();await expect(search).toBeInViewport() await search.scrollIntoViewIfNeeded();await expect(search).toBeInViewport()
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true) expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true)
await page.screenshot({path:"test-results/alerts-layout-"+width+".png",fullPage:true}) await page.screenshot({path:"docs/ux-ui/screens/alerts-overview-"+width+".png",fullPage:true})
})
test("Alerts queue remains mounted when delayed primary snapshot becomes known",async({page})=>{
let started=false,calls=0,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve)
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",async r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/alerts"){started=true;await gate;return r.fulfill({json:alertsWithRule})}if(path==="/api/alerts/outbox")calls++;return r.fulfill({json:path==="/api/auth/config"?{required:false}:path==="/api/alerts/outbox"?emptyOutbox:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})})
try{await page.goto("/alerts");await expect.poll(()=>started).toBe(true);await expect(page.getByRole("region",{name:"Очередь Telegram"})).toBeVisible();await expect.poll(()=>calls).toBe(1);release();await expect(page.getByRole("textbox",{name:"Поиск правил",exact:true})).toBeVisible();expect(calls).toBe(1)}finally{release()}
}) })
+1 -1
View File
@@ -7,6 +7,6 @@ test("Probe schedule refresh preserves confirmed rows and missing RX does not be
test("Probe schedule timeout is retryable without automatic request",async({page})=>{await page.clock.install();await setup(page);let calls=0;await page.route("**/api/uptime/speed-probes",()=>{calls++});const panel=await open(page);await expect.poll(()=>calls).toBe(1);await page.clock.fastForward(30100);await expect(panel.getByRole("alert")).toContainText("Время ожидания");await expect(panel.getByRole("button",{name:"Обновить пробы скорости"})).toBeEnabled();expect(calls).toBe(1)}) test("Probe schedule timeout is retryable without automatic request",async({page})=>{await page.clock.install();await setup(page);let calls=0;await page.route("**/api/uptime/speed-probes",()=>{calls++});const panel=await open(page);await expect.poll(()=>calls).toBe(1);await page.clock.fastForward(30100);await expect(panel.getByRole("alert")).toContainText("Время ожидания");await expect(panel.getByRole("button",{name:"Обновить пробы скорости"})).toBeEnabled();expect(calls).toBe(1)})
test("Probe schedule respects its own uptime permission",async({page})=>{await setup(page);const claims={sub:"fixture",apps:["mm"],permissions:["mm:servers:read"],exp:Math.floor(Date.now()/1000)+3600};const token="e30."+Buffer.from(JSON.stringify(claims)).toString("base64url")+".fixture";await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));let calls=0;await page.route("**/api/uptime/speed-probes",r=>{calls++;return r.fulfill({json:{probes:[]}})});const panel=await open(page);await expect(panel.getByRole("alert")).toContainText("Нет права читать пробы скорости");expect(calls).toBe(0)}) test("Probe schedule respects its own uptime permission",async({page})=>{await setup(page);const claims={sub:"fixture",apps:["mm"],permissions:["mm:servers:read"],exp:Math.floor(Date.now()/1000)+3600};const token="e30."+Buffer.from(JSON.stringify(claims)).toString("base64url")+".fixture";await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));let calls=0;await page.route("**/api/uptime/speed-probes",r=>{calls++;return r.fulfill({json:{probes:[]}})});const panel=await open(page);await expect(panel.getByRole("alert")).toContainText("Нет права читать пробы скорости");expect(calls).toBe(0)})
for(const width of [320,1440])test("Probe schedule known rows fit/"+width,async({page})=>{await setup(page);await page.setViewportSize({width,height:900});await page.route("**/api/uptime/speed-probes",r=>r.fulfill({json:{probes:[{...probe,lastRunAt:"2026-10-03T00:00:00Z"}]}}));const panel=await open(page);await expect(panel.getByText(/RX: неизвестно/)).toBeVisible();await panel.scrollIntoViewIfNeeded();await expect(panel.getByRole("button",{name:"Обновить пробы скорости"})).toBeInViewport();expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true);await panel.screenshot({path:"test-results/probes-schedule-"+width+".png"})}) for(const width of [320,1440])test("Probe schedule known rows fit/"+width,async({page})=>{await setup(page);await page.setViewportSize({width,height:900});await page.route("**/api/uptime/speed-probes",r=>r.fulfill({json:{probes:[{...probe,lastRunAt:"2026-10-03T00:00:00Z"}]}}));const panel=await open(page);await expect(panel.getByText(/RX: неизвестно/)).toBeVisible();await panel.scrollIntoViewIfNeeded();await expect(panel.getByRole("button",{name:"Обновить пробы скорости"})).toBeInViewport();expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true);await panel.screenshot({path:"docs/ux-ui/screens/probes-speed-schedule-"+width+".png"})})
test("Demo schedule clears a tunnel from another device and selects a valid one",async({page})=>{await setup(page);await page.addInitScript(()=>localStorage.setItem("routerlists:data-source","mock"));await page.goto("/probes?server=srv1");await page.getByRole("button",{name:"Расписание",exact:true}).click();await page.getByRole("button",{name:"Добавить правило",exact:true}).click();const server=page.getByRole("combobox",{name:"Устройство расписания"}),tunnel=page.getByRole("combobox",{name:"Туннель расписания"});await expect(tunnel).toHaveValue("gre1");await server.selectOption("srv2");await expect(tunnel).toHaveValue("");await expect(page.getByRole("button",{name:"Добавить",exact:true})).toBeDisabled();await server.selectOption("srv7");await expect(tunnel).toHaveValue("gre5");await expect(page.getByRole("button",{name:"Добавить",exact:true})).toBeEnabled()}) test("Demo schedule clears a tunnel from another device and selects a valid one",async({page})=>{await setup(page);await page.addInitScript(()=>localStorage.setItem("routerlists:data-source","mock"));await page.goto("/probes?server=srv1");await page.getByRole("button",{name:"Расписание",exact:true}).click();await page.getByRole("button",{name:"Добавить правило",exact:true}).click();const server=page.getByRole("combobox",{name:"Устройство расписания"}),tunnel=page.getByRole("combobox",{name:"Туннель расписания"});await expect(tunnel).toHaveValue("gre1");await server.selectOption("srv2");await expect(tunnel).toHaveValue("");await expect(page.getByRole("button",{name:"Добавить",exact:true})).toBeDisabled();await server.selectOption("srv7");await expect(tunnel).toHaveValue("gre5");await expect(page.getByRole("button",{name:"Добавить",exact:true})).toBeEnabled()})
+1 -1
View File
@@ -9,7 +9,7 @@ for(const width of [320,1440])test("Releases local build is not presented as pub
await expect(page.getByText("Сводка коммитов",{exact:true})).toBeVisible() await expect(page.getByText("Сводка коммитов",{exact:true})).toBeVisible()
await expect(page.getByRole("link",{name:"Открыть релиз",exact:true})).toHaveCount(0) await expect(page.getByRole("link",{name:"Открыть релиз",exact:true})).toHaveCount(0)
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true) expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true)
await page.screenshot({path:"test-results/releases-local-"+width+".png",fullPage:true}) await page.screenshot({path:"docs/ux-ui/screens/releases-local-"+width+".png",fullPage:true})
}) })
// Server component reads a local manifest. Restore the original bytes even on assertion failure. // Server component reads a local manifest. Restore the original bytes even on assertion failure.