47 lines
9.4 KiB
TypeScript
47 lines
9.4 KiB
TypeScript
import {test,expect,type Page,type Route} from "@playwright/test"
|
|
import {emptyAlerts,emptyOutbox,heldOutbox} from "./fixtures/alerts"
|
|
async function setup(page:Page,reply:()=>{status?:number;json:unknown}){
|
|
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;return r.fulfill(path==="/api/alerts/outbox"?reply():{json:path==="/api/auth/config"?{required:false}:path==="/api/alerts"?emptyAlerts:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})});await page.goto("/alerts")
|
|
}
|
|
test("Queue initial failure is unknown, retry confirms zero",async({page})=>{
|
|
let fail=true;await setup(page,()=>({json:fail?{}:emptyOutbox}));const panel=page.getByRole("region",{name:"Очередь Telegram",exact:true});await expect(panel.getByRole("alert")).toContainText("не подтверждено");await expect(panel).not.toContainText("Ожидают: 0");await expect(page.getByText("Всего: 0",{exact:true})).toBeVisible();fail=false;await panel.getByRole("button",{name:"Обновить очередь Telegram"}).click();await expect(panel).toContainText("Ожидают: 0");await expect(panel.getByRole("alert")).toHaveCount(0)
|
|
})
|
|
test("Queue held states retain confirmed snapshot after failure and fit mobile",async({page})=>{
|
|
await page.setViewportSize({width:320,height:1000});let fail=false;const held={...emptyOutbox,counts:{...emptyOutbox.counts,total:4,pending:1,processing:1,unknown:1,unrecognized:1},items:[{id:"a",resolutionVersion:0,status:"unknown",ruleName:"Held fixture",createdAt:"2026-10-03T00:00:00Z",attemptedAt:"2026-10-03T01:00:00Z"},{id:"b",resolutionVersion:0,status:"processing",ruleName:null,createdAt:"2026-10-03T00:00:00Z",attemptedAt:"2026-10-03T02:00:00Z"}]};await setup(page,()=>fail?{status:503,json:{error:"QUEUE UNAVAILABLE"}}:{json:held});const panel=page.getByRole("region",{name:"Очередь Telegram",exact:true});await expect(panel).toContainText("Held fixture");await expect(panel).toContainText("автоматически не переотправляются");fail=true;await panel.getByRole("button",{name:"Обновить очередь Telegram"}).click();await expect(panel.getByRole("alert")).toContainText("Показан последний успешный снимок");await expect(panel).toContainText("Ожидают: 1");const bounds=await panel.boundingBox();expect(bounds!.x).toBeGreaterThanOrEqual(0);expect(bounds!.x+bounds!.width).toBeLessThanOrEqual(320);await panel.screenshot({path:"test-results/alerts-outbox-320.png"})
|
|
})
|
|
|
|
async function retrySetup(page:Page,write:(r:Route)=>Promise<unknown>,required=false){
|
|
if(required)await page.addInitScript(()=>localStorage.setItem("mmapp_token","fixture."+btoa(JSON.stringify({sub:"fixture",exp:Math.floor(Date.now()/1000)+3600,apps:["mm"],permissions:["mm:alerts:read"]}))+".fixture"))
|
|
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.endsWith("/retry"))return write(r);return r.fulfill({json:path==="/api/auth/config"?{required}:path==="/api/alerts"?emptyAlerts:path==="/api/alerts/outbox"?heldOutbox:path==="/api/filters/gre-tunnels"?{tunnels:[],failures:[]}:[]})});await page.goto("/alerts")
|
|
}
|
|
async function openRetry(page:Page){await page.getByRole("button",{name:"Повторить сообщение Held retry fixture"}).click();const dialog=page.getByRole("alertdialog");await expect(dialog.getByRole("button",{name:"Поставить в очередь повторно"})).toBeDisabled();await dialog.getByRole("checkbox",{name:"Проверил чат и принимаю риск дубликата"}).check();return dialog}
|
|
test("Manual queue retry: explicit risk, one pending POST excludes concurrent operations and confirms only queue receipt",async({page})=>{
|
|
let calls=0,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve);await retrySetup(page,async r=>{calls++;expect(r.request().postDataJSON()).toEqual({expectedVersion:3,acknowledgedDuplicateRisk:true});await gate;await r.fulfill({json:{ok:true,id:"held-fixture",status:"pending",resolutionVersion:4}})});
|
|
try{const dialog=await openRetry(page);await dialog.getByRole("button",{name:"Поставить в очередь повторно"}).click();await expect.poll(()=>calls).toBe(1);await expect(dialog.getByRole("button",{name:"Отмена"})).toBeDisabled();await expect(page.getByRole("button",{name:"Обновить",exact:true,includeHidden:true})).toBeDisabled();release();await expect(dialog).toHaveCount(0);await expect(page.getByText("Сообщение поставлено в очередь. Отправка и доставка ещё не подтверждены.")).toBeVisible();expect(calls).toBe(1)}finally{release()}
|
|
})
|
|
test("Manual queue retry: malformed receipt retains dialog and checkpoint across refresh/reload until explicit review",async({page})=>{
|
|
let calls=0;await retrySetup(page,async r=>{calls++;return r.fulfill({json:{ok:true}})});const dialog=await openRetry(page);await dialog.getByRole("button",{name:"Поставить в очередь повторно"}).click();await expect(dialog.getByRole("alert")).toContainText("не подтверждена");await expect(dialog.getByRole("button",{name:"Поставить в очередь повторно"})).toBeDisabled();await dialog.getByRole("button",{name:"Отмена"}).click();await page.getByRole("button",{name:"Обновить очередь Telegram"}).click();await expect(page.getByRole("button",{name:"Повторить сообщение Held retry fixture"})).toBeDisabled();await page.reload();const panel=page.getByRole("region",{name:"Очередь Telegram",exact:true});await expect(panel).toContainText("Результат повтора неизвестен");await expect(panel.getByRole("button",{name:"Результат повтора проверен"})).toBeDisabled();await panel.getByRole("checkbox",{name:"Проверил очередь и чат"}).check();await panel.getByRole("button",{name:"Результат повтора проверен"}).click();await expect(page.getByRole("button",{name:"Повторить сообщение Held retry fixture"})).toBeEnabled();expect(calls).toBe(1)
|
|
})
|
|
test("Manual queue retry: read permission cannot enqueue",async({page})=>{
|
|
let calls=0;await retrySetup(page,async r=>{calls++;return r.fulfill({status:409,json:{error:"Fixture queue conflict"}})},true);await expect(page.getByRole("button",{name:"Повторить сообщение Held retry fixture"})).toBeDisabled();expect(calls).toBe(0)
|
|
})
|
|
test("Manual queue retry: confirmed conflict keeps form without unknown checkpoint",async({page})=>{
|
|
await retrySetup(page,r=>r.fulfill({status:409,json:{error:"Fixture queue conflict"}}));const dialog=await openRetry(page);await dialog.getByRole("button",{name:"Поставить в очередь повторно"}).click();await expect(dialog.getByRole("alert")).toHaveText("Fixture queue conflict");await expect(dialog.getByRole("checkbox")).toBeChecked();await expect(dialog.getByRole("button",{name:"Поставить в очередь повторно"})).toBeEnabled()
|
|
})
|
|
|
|
test("Manual queue retry: HTTP500 explains unknown outcome inside the dialog and preserves the guard",async({page})=>{
|
|
await page.setViewportSize({width:320,height:1000});let calls=0
|
|
await retrySetup(page,r=>{calls++;return r.fulfill({status:500,json:{error:"Внутренняя ошибка сервера"}})})
|
|
const dialog=await openRetry(page);await dialog.getByRole("button",{name:"Поставить в очередь повторно"}).click()
|
|
await expect(dialog.getByRole("alert")).toContainText("Результат повтора не подтверждён. Автоматический повтор заблокирован. Проверьте очередь и чат перед новой попыткой.")
|
|
await expect(dialog.getByRole("alert")).toContainText("Внутренняя ошибка сервера")
|
|
await expect(dialog.getByRole("checkbox")).toBeChecked();await expect(dialog.getByRole("button",{name:"Поставить в очередь повторно"})).toBeDisabled()
|
|
const bounds=await dialog.boundingBox();expect(bounds!.x).toBeGreaterThanOrEqual(0);expect(bounds!.x+bounds!.width).toBeLessThanOrEqual(320)
|
|
await dialog.screenshot({path:"test-results/alerts-outbox-http500-320.png"})
|
|
await dialog.getByRole("button",{name:"Отмена"}).click();await page.reload()
|
|
const panel=page.getByRole("region",{name:"Очередь Telegram",exact:true});await expect(panel).toContainText("Результат повтора неизвестен")
|
|
await expect(panel.getByRole("button",{name:"Повторить сообщение Held retry fixture"})).toBeDisabled();expect(calls).toBe(1)
|
|
})
|
|
|
|
test("Manual queue retry: mobile confirmation fits, cancel restores focus without POST",async({page})=>{await page.setViewportSize({width:320,height:1000});let calls=0;await retrySetup(page,r=>{calls++;return r.fulfill({json:{ok:true}})});const dialog=await openRetry(page);const bounds=await dialog.boundingBox();expect(bounds!.x).toBeGreaterThanOrEqual(0);expect(bounds!.x+bounds!.width).toBeLessThanOrEqual(320);await dialog.screenshot({path:"test-results/alerts-outbox-dialog-320.png"});await page.keyboard.press("Escape");await expect(dialog).toHaveCount(0);await expect(page.getByRole("button",{name:"Повторить сообщение Held retry fixture"})).toBeFocused();expect(calls).toBe(0)})
|