diff --git a/app/(main)/alerts/page.tsx b/app/(main)/alerts/page.tsx index db9d601f..19cb1c2b 100644 --- a/app/(main)/alerts/page.tsx +++ b/app/(main)/alerts/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react" import {can} from "@/lib/auth" +import {useAlertsTelegramPreview} from "@/hooks/use-alerts-telegram-preview" import {useAlertsConfigWrite} from "@/hooks/use-alerts-config-write" import {AlertsGroupForm} from "@/components/alerts-group-form" import {useAlertsTargets} from "@/hooks/use-alerts-targets" @@ -46,8 +47,6 @@ import { import { Flag, countryName } from "@/components/flag" import { cn } from "@/lib/utils" import {getAlerts} from "@/shared/api/alerts" -import { requestJson } from "@/shared/api/http-client" -import { toast } from "sonner" // ─── types ──────────────────────────────────────────────────────────────────── @@ -177,11 +176,7 @@ interface AlertsMeta { greTunnelDetail?: AlertsGreTunnelRow[] } -function makeApiFetch(backendUrl: string) { - return async function apiFetch(path: string, init?: RequestInit): Promise { - return requestJson(backendUrl, path, init) - } -} + function formatRelativeRu(iso: string): string { const t = Date.parse(iso) @@ -1539,7 +1534,7 @@ function AddRuleSheet({ initialForm, editRuleId, onTestRuleTelegram, - testTelegramDisabled, saving,saveDisabled,saveError,review, + testTelegramDisabled, saving,saveDisabled,saveError,review,testOutcome,testError,testReview, }: { open: boolean onClose: () => void @@ -1549,6 +1544,9 @@ function AddRuleSheet({ saveDisabled:boolean saveError:string|null review:React.ReactNode + testOutcome:string|null + testError:string|null + testReview:React.ReactNode typeTargets: Record isLive: boolean serversCatalog: AlertsServerRow[] @@ -1557,7 +1555,7 @@ function AddRuleSheet({ initialForm: RuleFormSnapshot | null editRuleId: string | null /** Live: отправка тестового сообщения с текстом как у сохранённого правила (без записи в БД). */ - onTestRuleTelegram?: (payload: RuleTelegramTestPayload) => Promise + onTestRuleTelegram?: (payload: RuleTelegramTestPayload) => Promise /** true — кнопка теста неактивна (например, бэкенд недоступен). */ testTelegramDisabled?: boolean }) { @@ -1682,9 +1680,6 @@ function AddRuleSheet({ cooldown: form.cooldown, ruleChatId: form.chatId.trim(), }) - toast.success("Тестовое сообщение отправлено в Telegram.") - } catch (e) { - toast.error(e instanceof Error ? e.message : "Не удалось отправить тест") } finally { setTestTelegramBusy(false) } @@ -2194,7 +2189,7 @@ function AddRuleSheet({ disabled={!canSave || testTelegramBusy || testTelegramDisabled||saving} title={ testTelegramDisabled - ? "Тест из формы будет доступен после подключения защиты повторной отправки" + ? "Проверьте права, доступность, сохранённый токен и результат предыдущего запроса" : !canSave ? "Заполните название, объект, условие и при необходимости порог" : undefined @@ -2209,6 +2204,9 @@ function AddRuleSheet({ Тест в Telegram ) : null} + {testError?

{testError}

:null} + {testOutcome?

{testOutcome}

:null} + {testReview} {review} {saveError?

{saveError}

:null} :null} /> {if(!open&&!operationLock.current)setPendingDelete(null)}}> diff --git a/components/alerts-telegram-form.tsx b/components/alerts-telegram-form.tsx index 8401710c..ce192178 100644 --- a/components/alerts-telegram-form.tsx +++ b/components/alerts-telegram-form.tsx @@ -1,8 +1,9 @@ "use client" -import {useEffect,useRef,useState} from "react" +import {useEffect,useRef,useState,useSyncExternalStore} from "react" import {EyeIcon,EyeOffIcon} from "lucide-react" import type {PutTelegramBody} from "@mmapp/contracts/alerts" +import {telegramPending,subscribeTelegramPending,setTelegramPending as checkpoint} from "@/lib/alerts-telegram-checkpoint" import {can} from "@/lib/auth" import {ApiClientError} from "@/shared/api/http-client" import {saveAlertsTelegram,testAlertsTelegram} from "@/shared/api/alerts" @@ -11,24 +12,21 @@ import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" export type TelegramDraft={token:string;chatId:string;messageThreadId:string;connected:boolean} -const pending=new Set() -const storageKey=(source:string)=>"mm:alerts-telegram-pending:"+source -function unresolved(source:string){try{return pending.has(source)||sessionStorage.getItem(storageKey(source))==="true"}catch{return pending.has(source)}} -function checkpoint(source:string,value:boolean){if(value)pending.add(source);else pending.delete(source);try{if(value)sessionStorage.setItem(storageKey(source),"true");else sessionStorage.removeItem(storageKey(source))}catch{/* The memory guard remains active during this browser lifetime. */}} function thread(value:string){if(!value.trim())return null;if(!/^\d+$/.test(value.trim()))throw Error("ID темы: укажите положительное целое число или оставьте поле пустым");const n=Number(value);if(!Number.isSafeInteger(n)||n<1||n>2147483647)throw Error("ID темы должен быть от 1 до 2147483647");return n} export function AlertsTelegramForm({initial,tokenConfigured,base,live,available,onBegin,onEnd,onSaved}:{initial:TelegramDraft;tokenConfigured:boolean;base:string;live:boolean;available:boolean;onBegin:()=>boolean;onEnd:()=>void;onSaved:(value:TelegramDraft,configured:boolean)=>void}){ const source=JSON.stringify([live,base]) const [draft,setDraft]=useState>>({}) - const [showToken,setShowToken]=useState(false),[busy,setBusy]=useState<"save"|"test"|null>(null),[error,setError]=useState(null),[outcome,setOutcome]=useState(null),[unknown,setUnknown]=useState(false),[ready,setReady]=useState(false) + const [showToken,setShowToken]=useState(false),[busy,setBusy]=useState<"save"|"test"|null>(null),[error,setError]=useState(null),[outcome,setOutcome]=useState(null),[ready,setReady]=useState(false) + const unknown=useSyncExternalStore(subscribeTelegramPending,()=>telegramPending(source),()=>false) const active=useRef(true),lock=useRef(false),controller=useRef(null) - useEffect(()=>{active.current=true;queueMicrotask(()=>{if(active.current){setUnknown(unresolved(source));setReady(true)}});return()=>{active.current=false;controller.current?.abort()}},[source]) + useEffect(()=>{active.current=true;queueMicrotask(()=>{if(active.current){setReady(true)}});return()=>{active.current=false;controller.current?.abort()}},[source]) const value={...initial,...draft},allowed=can("mm:alerts:write"),enabled=ready&&live&&available&&allowed const dirty=Object.keys(draft).some(key=>value[key as keyof typeof draft]!==initial[key as keyof typeof draft]) function current(){return active.current&&can("mm:alerts:write")} function change(key:keyof typeof draft,value:string){setDraft(previous=>({...previous,[key]:value}));setError(null);setOutcome(null)} async function execute(kind:"save"|"test"){ - if(!enabled||lock.current||unknown||!current())return + if(!enabled||lock.current||unknown||telegramPending(source)||!current())return const payload:PutTelegramBody={} try{ const messageThreadId=thread(value.messageThreadId) @@ -45,7 +43,7 @@ export function AlertsTelegramForm({initial,tokenConfigured,base,live,available, }catch(e){setError(e instanceof Error?e.message:"Проверьте параметры Telegram");return} if(!onBegin())return lock.current=true;controller.current=new AbortController();setBusy(kind);setError(null);setOutcome(null) - checkpoint(source,true);setUnknown(true) + checkpoint(source,true) try{ if(kind==="save"){ const result=await saveAlertsTelegram(base,payload,{signal:controller.current.signal});if(!current())return @@ -57,11 +55,11 @@ export function AlertsTelegramForm({initial,tokenConfigured,base,live,available, await testAlertsTelegram(base,{chatId:value.chatId.trim(),...(value.token.trim()?{token:value.token.trim()}:{}),...(messageThreadId!==null?{messageThreadId}:{})},{signal:controller.current.signal});if(!current())return setOutcome("Telegram API подтвердил приём тестового сообщения. Проверьте его в чате.") } - checkpoint(source,false);setUnknown(false) + checkpoint(source,false) }catch(e){ const rejected=e instanceof ApiClientError&&[400,401,403,409,422].includes(e.status) if(rejected&¤t())checkpoint(source,false) - if(current()){setUnknown(!rejected);setError(e instanceof Error?e.message:"Результат Telegram не получен")} + if(current()){setError(e instanceof Error?e.message:"Результат Telegram не получен")} }finally{lock.current=false;controller.current=null;if(active.current){setBusy(null);onEnd()}} } return
@@ -73,7 +71,7 @@ export function AlertsTelegramForm({initial,tokenConfigured,base,live,available, {error?

{error}

:null} {outcome?

{outcome}

:null} {busy?

Ожидаем подтверждение сервера…

:null} - {unknown&&!busy?

Результат запроса неизвестен. Он мог выполниться. Перед повтором проверьте параметры в БД или сообщение в Telegram; обновление данных не подтверждает отправку.

:null} + {unknown&&!busy?

Результат запроса неизвестен. Он мог выполниться. Перед повтором проверьте параметры в БД или сообщение в Telegram; обновление данных не подтверждает отправку.

:null} diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 9eb20bb8..2ee2a498 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-03 -Этап 11Б в работе; последний проверенный блок 11Б.4.3.3; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.3.3.1: защищённый тест Telegram из редактора правила с общим checkpoint и замком; затем согласованность GET/revision/outbox, общий dashboard permission reader, Releases/probe scheduling и приёмка 11Б/12. +Этап 11Б в работе; последний проверенный блок 11Б.4.3.3.1; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.4.1: согласованный read-only GET Alerts и достоверные metadata/history; затем optimistic revision/outbox audit, общий dashboard permission reader, адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index bd380707..d041423a 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -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Б.4.3.3; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.3.3.1; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,14 @@ +## Блок 11Б.4.3.3.1 — общий guard Telegram preview (2026-10-03) + +- Rule preview снова доступен: strict testAlertsTelegram, caller abort/source lifetime, synchronous lock со всеми Alerts mutations, alerts:write/live/health/known token. Отправляется snapshot черновика без записи rule и без токена из сохранённых параметров в body. +- Общий Telegram checkpoint/external store связывает карточку и editor: unknown не обходится через другую форму. Explicit review доступен в editor без потери draft; late ABA receipt не снимает guard. API acceptance показана inline, прежний ложный toast «отправлено» удалён. +- 3 preview browser, 2 preview source ABA и регрессия 10 Telegram + 4 Telegram source пройдены; typecheck без incremental/lint. Первоначальный typecheck поймал оставшийся setUnknown после store extraction; исправлен, повторная проверка прошла. Реальные Telegram/PG не запускались. +- Аудит следующего блока обнаружил в backend fabricated rttLossTargets/probeTargets из имён серверов и synthetic live history с sent:true. Их нужно исправить вместе с multi-query GET. Это пока ограничения Alerts, не принятый результат. +- Следующий точный шаг 11Б.4.4.1: согласованный read-only GET Alerts и достоверные metadata/history; затем optimistic revision/outbox audit, общий dashboard permission reader, адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. + ## Блок 11Б.4.3.3 — запись правил и групп по подтверждению API (2026-10-03) - Strict saveAlertRules подключён вместо generic PUT/оптимистичной подмены. Pending/failed/unknown оставляет последний подтверждённый список; editor ждёт receipt и закрывается только после success, error/unknown сохраняет draft. Исправлена потеря recoveryMode/recoveryStabilitySec при edit — форма передаёт сохранённую политику. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index 290ab76e..8116b06a 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -263,3 +263,8 @@ Dirty/selective save, strict receipt, общий lock, bounded source lifetime, ### 11Б.4.3.3 — правила и группы (2026-10-03) Strict receipt вместо optimistic overwrite, draft editor/group формы, права/common lock/source/unknown, подтверждение delete и сохранение recovery policy. 8 browser + 2 source, Telegram regression, typecheck/lint/build пройдены. Preview Telegram временно disabled; picker и backend consistency ещё не приняты. Далее 11Б.4.3.3.1: защищённый тест Telegram из редактора правила с общим checkpoint и замком; затем согласованность GET/revision/outbox, общий dashboard permission reader, Releases/probe scheduling и приёмка 11Б/12. + + +### 11Б.4.3.3.1 — Telegram preview (2026-10-03) + +Strict preview и общий checkpoint карточки/editor без секретов, common lock/source/rights, inline API acceptance. 3 browser + 2 source, Telegram regression, typecheck/lint прошли. Доставка и реальные PG/Telegram не приняты. Далее 11Б.4.4.1: согласованный read-only GET Alerts и достоверные metadata/history; затем optimistic revision/outbox audit, общий dashboard permission reader, адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. diff --git a/hooks/use-alerts-telegram-preview.ts b/hooks/use-alerts-telegram-preview.ts new file mode 100644 index 00000000..9f902862 --- /dev/null +++ b/hooks/use-alerts-telegram-preview.ts @@ -0,0 +1,34 @@ +"use client" +import {useEffect,useRef,useState,useSyncExternalStore} from "react" +import {testTelegramSchema,type TestTelegramBody} from "@mmapp/contracts/alerts" +import {can} from "@/lib/auth" +import {telegramPending,subscribeTelegramPending,setTelegramPending} from "@/lib/alerts-telegram-checkpoint" +import {ApiClientError} from "@/shared/api/http-client" +import {testAlertsTelegram} from "@/shared/api/alerts" + +export function useAlertsTelegramPreview(base:string,live:boolean,available:boolean,onBegin:()=>boolean,onEnd:()=>void){ + const source=JSON.stringify([live,base]) + const unknown=useSyncExternalStore(subscribeTelegramPending,()=>telegramPending(source),()=>false) + const [busy,setBusy]=useState(false),[error,setError]=useState(null),[outcome,setOutcome]=useState(null) + const active=useRef(true),lock=useRef(false),controller=useRef(null) + useEffect(()=>{active.current=true;return()=>{active.current=false;controller.current?.abort()}},[source]) + const enabled=live&&available&&can("mm:alerts:write") + async function test(payload:TestTelegramBody){ + if(!enabled||lock.current||telegramPending(source)||!active.current||!can("mm:alerts:write"))return false + const parsed=testTelegramSchema.strict().safeParse(payload) + if(!parsed.success){setError("Проверьте параметры тестового сообщения");return false} + if(!onBegin())return false + lock.current=true;controller.current=new AbortController();setBusy(true);setError(null);setOutcome(null);setTelegramPending(source,true) + const current=()=>active.current&&can("mm:alerts:write") + try{ + await testAlertsTelegram(base,parsed.data,{signal:controller.current.signal});if(!current())return false + setTelegramPending(source,false);setOutcome("Telegram API подтвердил приём тестового правила. Проверьте его в чате.");return true + }catch(e){ + if(e instanceof ApiClientError&&[400,401,403,409,422].includes(e.status)&¤t())setTelegramPending(source,false) + if(current())setError(e instanceof Error?e.message:"Результат отправки не получен");return false + }finally{lock.current=false;controller.current=null;if(active.current){setBusy(false);onEnd()}} + } + function review(){if(!enabled||busy)return;setTelegramPending(source,false)} + function clear(){setError(null);setOutcome(null)} + return {test,busy,error,outcome,unknown,enabled,review,clear} +} diff --git a/lib/alerts-telegram-checkpoint.ts b/lib/alerts-telegram-checkpoint.ts new file mode 100644 index 00000000..9468a428 --- /dev/null +++ b/lib/alerts-telegram-checkpoint.ts @@ -0,0 +1,6 @@ +// Shared between Telegram settings and rule preview; never stores credentials. +const pending=new Set(),listeners=new Set<()=>void>() +const key=(source:string)=>"mm:alerts-telegram-pending:"+source +export function telegramPending(source:string){try{return pending.has(source)||sessionStorage.getItem(key(source))==="true"}catch{return pending.has(source)}} +export function subscribeTelegramPending(listener:()=>void){listeners.add(listener);return()=>{listeners.delete(listener)}} +export function setTelegramPending(source:string,value:boolean){if(value)pending.add(source);else pending.delete(source);try{if(value)sessionStorage.setItem(key(source),"true");else sessionStorage.removeItem(key(source))}catch{/* Memory guard survives section navigation. */}for(const listener of listeners)listener()} diff --git a/tests/e2e/alerts-context.spec.ts b/tests/e2e/alerts-context.spec.ts index 0cec7546..d69fb5be 100644 --- a/tests/e2e/alerts-context.spec.ts +++ b/tests/e2e/alerts-context.spec.ts @@ -31,3 +31,10 @@ for(const failed of [false,true])test("Rules source: ABA ignores old save and re const toggle=()=>page.getByRole("switch",{name:"Включить правило Fixture rule",exact:true}) try{await page.goto("/alerts");await expect(toggle()).toBeEnabled();await toggle().click();await expect.poll(()=>started).toBe(true);await page.goto("/settings");await source(page,"http://localhost:8101");await page.goto("/alerts");await expect(toggle()).toBeEnabled();await page.goto("/settings");await source(page,"http://localhost:8000");await page.goto("/alerts");await expect(page.getByText(/Результат сохранения правил неизвестен/)).toBeVisible();release();await expect.poll(()=>completed).toBe(true);await expect(toggle()).toBeChecked();await expect(toggle()).toBeDisabled();await expect(page.getByText("Правила и группы сохранены в БД",{exact:true})).toHaveCount(0);await expect(page.getByText("OLD RULES FAILED",{exact:true})).toHaveCount(0);expect(calls).toBe(1)}finally{release()} }) + +for(const failed of [false,true])test("Preview source: late ABA receipt cannot clear Telegram checkpoint/"+failed,async({page})=>{ + await page.route("**/health",r=>r.fulfill({headers,json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;return r.fulfill({headers,json:path==="/api/auth/config"?{required:false}:path==="/api/alerts"?{...alertsWithRule,telegram:configuredTelegram}:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})}) + let started=false,completed=false,calls=0,release!:()=>void;const gate=new Promise(resolve=>release=resolve) + await page.route("http://localhost:8000/api/alerts/telegram/test",async r=>{calls++;started=true;await gate;try{await r.fulfill(failed?{headers,status:503,json:{error:"OLD PREVIEW FAILED"}}:{headers,json:{ok:true}})}catch{}finally{completed=true}}) + try{await page.goto("/alerts");await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click();await page.getByRole("dialog").getByRole("button",{name:"Тест в Telegram",exact:true}).click();await expect.poll(()=>started).toBe(true);await page.goto("/settings");await source(page,"http://localhost:8101");await source(page,"http://localhost:8000");await page.goto("/alerts");await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click();await expect(page.getByRole("dialog")).toContainText("Результат Telegram неизвестен");release();await expect.poll(()=>completed).toBe(true);await expect(page.getByRole("dialog").getByRole("button",{name:"Тест в Telegram",exact:true})).toBeDisabled();await expect(page.getByRole("dialog")).not.toContainText("API подтвердил приём");await expect(page.getByRole("dialog")).not.toContainText("OLD PREVIEW FAILED");expect(calls).toBe(1)}finally{release()} +}) diff --git a/tests/e2e/alerts-preview.spec.ts b/tests/e2e/alerts-preview.spec.ts new file mode 100644 index 00000000..2dbe16e6 --- /dev/null +++ b/tests/e2e/alerts-preview.spec.ts @@ -0,0 +1,14 @@ +import {test,expect,type Page,type Route} from "@playwright/test" +import {alertsWithRule} from "./fixtures/alerts" +const telegram={chatId:"-100fixture",tokenConfigured:true,connected:true,messageThreadId:9} +async function setup(page:Page,send:(route:Route)=>Promise){await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));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/alerts")return r.fulfill({json:{...alertsWithRule,telegram}});if(path==="/api/alerts/telegram/test")return send(r);return r.fulfill({json:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})});await page.goto("/alerts");await expect(page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true})).toBeEnabled();await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click()} +test("Rule preview: strict acceptance keeps unsaved form and excludes concurrent save",async({page})=>{ + let calls=0,payload:unknown,release!:()=>void;const gate=new Promise(resolve=>release=resolve) + try{await setup(page,async r=>{calls++;payload=r.request().postDataJSON();await gate;return r.fulfill({json:{ok:true}})});const dialog=page.getByRole("dialog");await dialog.getByRole("textbox",{name:"Название правила",exact:true}).fill("Unsaved preview");await dialog.getByRole("button",{name:"Тест в Telegram",exact:true}).click();await expect.poll(()=>calls).toBe(1);await expect(dialog.getByRole("button",{name:"Сохранить изменения",exact:true})).toBeDisabled();await expect(dialog.getByRole("button",{name:"Тест в Telegram",exact:true})).toBeDisabled();release();await expect(dialog).toContainText("Telegram API подтвердил приём тестового правила. Проверьте его в чате.");await expect(dialog.getByRole("textbox",{name:"Название правила",exact:true})).toHaveValue("Unsaved preview");expect(payload).toMatchObject({chatId:telegram.chatId,messageThreadId:9,rulePreview:{name:"Unsaved preview",targets:["router-fixture"],conditionLine:"перешёл в offline"}});expect(payload).not.toHaveProperty("token");expect(calls).toBe(1)}finally{release()} +}) +test("Rule preview: unknown send protects both preview and Telegram form until explicit review",async({page})=>{ + let calls=0;await setup(page,r=>{calls++;return r.fulfill({json:{ok:false}})});const dialog=page.getByRole("dialog");await dialog.getByRole("button",{name:"Тест в Telegram",exact:true}).click();await expect(dialog.getByRole("alert")).toContainText("не подтверждена");await expect(dialog.getByRole("button",{name:"Тест в Telegram",exact:true})).toBeDisabled();await dialog.getByRole("button",{name:"Отмена",exact:true}).click();const form=page.getByRole("region",{name:"Параметры Telegram",exact:true});await expect(form).toContainText("Результат запроса неизвестен");await expect(form.getByRole("button",{name:"Отправить тестовое сообщение",exact:true})).toBeDisabled();await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click();await expect(page.getByRole("dialog").getByRole("button",{name:"Тест в Telegram",exact:true})).toBeDisabled();await page.getByRole("dialog").getByRole("button",{name:"Я проверил Telegram — разрешить новый запрос",exact:true}).click();await expect(page.getByRole("dialog").getByRole("button",{name:"Тест в Telegram",exact:true})).toBeEnabled();expect(calls).toBe(1) +}) +test("Rule preview: Telegram form unknown send cannot be bypassed through editor",async({page})=>{ + let calls=0;await setup(page,r=>{calls++;return r.fulfill({status:502,json:{error:"Fixture upstream unknown"}})});await page.getByRole("dialog").getByRole("button",{name:"Отмена",exact:true}).click();const form=page.getByRole("region",{name:"Параметры Telegram",exact:true});await form.getByRole("button",{name:"Отправить тестовое сообщение",exact:true}).click();await expect(form.getByRole("alert")).toHaveText("Fixture upstream unknown");await page.getByRole("button",{name:"Изменить правило Fixture rule",exact:true}).click();await expect(page.getByRole("dialog").getByRole("button",{name:"Тест в Telegram",exact:true})).toBeDisabled();expect(calls).toBe(1) +})