fix(containers): подтверждать остановку и удаление контейнера

This commit is contained in:
Denozordec committed 2026-10-14 19:03:58 +07:00
1 parent b3d11276dd
commit 74151176d7
20 files changed
+414 -28

No files matched your search

+8 -3
View File
@@ -1,8 +1,13 @@
import {test,expect,type Page} from "@playwright/test"
import {container,server,state} from "./fixtures/containers"
async function routes(page:Page){await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>r.fulfill({json:new URL(r.request().url()).pathname==="/api/auth/config"?{required:false}:new URL(r.request().url()).pathname==="/api/servers"?[server]:state()}))}
test.beforeEach(async({page})=>routes(page))
test.beforeEach(async({page}, info)=>{await routes(page); if(info.title.startsWith("Удаление контейнера:")) await page.route("**/api/containers/state",r=>r.fulfill({json:state([{...container,status:"stopped"}])}))})
async function action(page:Page,name:string){await page.getByRole("button",{name:"Действия контейнера "+container.name}).click();await page.getByRole("menuitem",{name,exact:true}).click()}
for(const status of ["running","unknown","error"] as const)test("Удаление требует остановки: "+status,async({page})=>{
let calls=0;await page.route("**/api/containers/state",r=>r.fulfill({json:state([{...container,status}])}));await page.route("**/api/servers/1/containers/remove",r=>{calls++;return r.fulfill({json:{ok:true}})})
await page.goto("/containers");await action(page,"Удалить");const dialog=page.getByRole("alertdialog");await expect(dialog).toContainText("после подтверждённой остановки");await expect(dialog.getByRole("button",{name:"Удалить",exact:true})).toBeDisabled();expect(calls).toBe(0)
await page.route("**/api/containers/state",r=>r.fulfill({json:state([{...container,status:"stopped"}])}));await dialog.getByRole("button",{name:"Обновить каталог",exact:true}).click();await expect(dialog.getByRole("button",{name:"Удалить",exact:true})).toBeEnabled();expect(calls).toBe(0)
})
for(const kind of ["403","502","malformed"] as const)test("Удаление контейнера: ошибка сохраняет target "+kind,async({page})=>{
let calls=0;await page.route("**/api/servers/1/containers/remove",r=>{calls++;return r.fulfill(kind==="malformed"?{json:{ok:false}}:{status:Number(kind),json:{error:"УДАЛЕНИЕ ОТКАЗ"}})})
await page.goto("/containers");await action(page,"Удалить");const dialog=page.getByRole("alertdialog");await dialog.getByRole("button",{name:"Удалить",exact:true}).click();await expect(dialog.getByRole("alert",{name:"Удаление контейнера"})).toBeVisible();await expect(dialog).toContainText(container.name);expect(calls).toBe(1)
@@ -10,7 +15,7 @@ for(const kind of ["403","502","malformed"] as const)test("Удаление ко
})
test("Удаление контейнера: lock, Escape, один запрос и refresh failure",async({page})=>{
let release!:()=>void,calls=0,removed=false;const gate=new Promise<void>(r=>release=r)
await page.route("**/api/containers/state",r=>r.fulfill(removed?{status:503,json:{error:"READ AFTER DELETE FAIL"}}:{json:state()}));await page.route("**/api/servers/1/containers/remove",async r=>{calls++;await gate;removed=true;await r.fulfill({json:{ok:true}})})
await page.route("**/api/containers/state",r=>r.fulfill(removed?{status:503,json:{error:"READ AFTER DELETE FAIL"}}:{json:state([{...container,status:"stopped"}])}));await page.route("**/api/servers/1/containers/remove",async r=>{calls++;await gate;removed=true;await r.fulfill({json:{ok:true}})})
await page.goto("/containers");await action(page,"Удалить");const dialog=page.getByRole("alertdialog");const remove=dialog.getByRole("button",{name:"Удалить",exact:true});await remove.click();await expect(dialog.getByRole("button",{name:"Удаление…",exact:true})).toBeDisabled();await expect(dialog.getByRole("button",{name:"Отмена",exact:true})).toBeDisabled();await page.keyboard.press("Escape");await expect(dialog).toBeVisible();expect(calls).toBe(1);release();await expect(dialog).toHaveCount(0);await expect(page.getByRole("status").filter({hasText:"API подтвердил команду"})).toContainText("Обновление каталогов не удалось");await expect(page.getByText(container.name,{exact:true})).toHaveCount(0)
})
for(const [name,path] of [["Остановить","stop"],["Запустить","start"],["Перезапустить","restart"]] as const)test("Команда контейнера: подтверждение отдельно от состояния "+path,async({page})=>{
@@ -27,6 +32,6 @@ test("Команда контейнера: общий lock блокирует д
await page.goto("/containers");await action(page,"Остановить");await expect(page.getByRole("button",{name:"Действия контейнера "+other.name})).toBeDisabled();await expect(page.getByRole("button",{name:"Обновить",exact:true})).toBeDisabled();expect(calls).toBe(1);release();await expect(page.getByRole("button",{name:"Действия контейнера "+other.name})).toBeEnabled()
})
test("Удаление контейнера: исчезнувший target не отправляется повторно",async({page})=>{
let exists=true;await page.route("**/api/containers/state",r=>r.fulfill({json:state(exists?[container]:[])}));await page.route("**/api/servers/1/containers/remove",r=>{exists=false;return r.fulfill({status:502,json:{error:"Связь потеряна"}})})
let exists=true;await page.route("**/api/containers/state",r=>r.fulfill({json:state(exists?[{...container,status:"stopped"}]:[])}));await page.route("**/api/servers/1/containers/remove",r=>{exists=false;return r.fulfill({status:502,json:{error:"Связь потеряна"}})})
await page.goto("/containers");await action(page,"Удалить");const dialog=page.getByRole("alertdialog");await dialog.getByRole("button",{name:"Удалить",exact:true}).click();await dialog.getByRole("button",{name:"Обновить каталог",exact:true}).click();await expect(dialog.getByRole("button",{name:"Удалить",exact:true})).toBeDisabled()
})