91 lines
11 KiB
TypeScript
91 lines
11 KiB
TypeScript
import { test, expect } from "@playwright/test"
|
|
import { backup, setupBackups } from "./fixtures/backups"
|
|
|
|
test("Бэкапы: single-flight удаление, отказ refresh не отменяет 204", async ({ page }) => {
|
|
await setupBackups(page); let deletes = 0, deleted = false, release!: () => void; const gate = new Promise<void>(r => release = r)
|
|
await page.route("**/api/backups", r => r.fulfill(deleted ? { status: 503, json: { error: "История после удаления отказала" } } : { json: [backup] }))
|
|
await page.route("**/api/backups/backup-1", async r => { deletes++; await gate; deleted = true; await r.fulfill({ status: 204 }).catch(() => {}) })
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Удалить " + backup.filename, exact: true }).click()
|
|
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Удалить", exact: true }).dblclick()
|
|
await expect(dialog.getByRole("button", { name: "Отмена", exact: true })).toBeDisabled(); await page.keyboard.press("Escape"); await expect(dialog).toBeVisible()
|
|
release(); await expect(dialog).toBeHidden(); await expect(page.getByText(backup.filename, { exact: true })).toHaveCount(0)
|
|
await expect(page.getByText("API подтвердил удаление резервной копии", { exact: true })).toBeVisible(); expect(deletes).toBe(1)
|
|
await expect(page.getByRole("alert", { name: "Данные резервных копий: backups" })).toContainText("История после удаления отказала")
|
|
})
|
|
for (const status of [403, 503, 200]) test("Бэкапы: удаление " + status + " сохраняет цель и диалог", async ({ page }) => {
|
|
await setupBackups(page); await page.route("**/api/backups/backup-1", r => r.fulfill({ status, json: { error: "Удаление не подтверждено" } }))
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Удалить " + backup.filename, exact: true }).click()
|
|
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
|
|
await expect(dialog.getByRole("alert")).toBeVisible(); await expect(dialog).toContainText(backup.filename)
|
|
await expect(dialog.getByRole("button", { name: "Удалить", exact: true })).toBeEnabled()
|
|
await expect(page.getByText("API подтвердил удаление резервной копии", { exact: true })).toHaveCount(0)
|
|
})
|
|
test("Бэкапы: ошибка restore и чужой filename сохраняют диалог", async ({ page }) => {
|
|
await setupBackups(page); let failure = true
|
|
await page.route("**/api/backups/backup-1/restore", r => r.fulfill(failure ? { status: 400, json: { error: "Импорт отказал", outcome: "not-applied" } } : { json: { backupId: "backup-1", serverId: 1, filename: "other.rsc", serverName: "router" } }))
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }).click()
|
|
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Восстановить", exact: true }).click()
|
|
await expect(dialog.getByRole("alert").filter({ hasText: "Импорт отказал" })).toBeVisible()
|
|
failure = false; await dialog.getByRole("button", { name: "Восстановить", exact: true }).click()
|
|
await expect(dialog.getByRole("alert").filter({ hasText: "другому файлу" })).toBeVisible()
|
|
await expect(dialog).toContainText(backup.filename)
|
|
})
|
|
test("Бэкапы: download получает файл, HTML 200 показывает ошибку", async ({ page }) => {
|
|
await setupBackups(page); let html = false
|
|
await page.route("**/api/backups/backup-1/download", r => r.fulfill({ contentType: html ? "text/html" : "text/plain; charset=utf-8", body: html ? "<html>Error</html>" : "config" }))
|
|
await page.goto("/backups"); const button = page.getByRole("button", { name: "Скачать " + backup.filename, exact: true })
|
|
const download = page.waitForEvent("download"); await button.click(); expect((await download).suggestedFilename()).toBe(backup.filename)
|
|
html = true; await button.click(); await expect(page.getByRole("alert", { name: "Операция резервных копий" })).toContainText("не подтверждён API")
|
|
})
|
|
|
|
test("Бэкапы: restore требует правильный serverId и принимает новое имя сервера", async ({ page }) => {
|
|
await setupBackups(page); let serverId = 2, calls = 0
|
|
await page.route("**/api/backups/backup-1/restore", async r => { calls++; await r.fulfill({ json: { backupId: backup.id, serverId, filename: backup.filename, serverName: "Новое имя роутера" } }) })
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }).click()
|
|
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Восстановить", exact: true }).click()
|
|
await expect(dialog.getByRole("alert").filter({ hasText: "другой резервной копии или серверу" })).toBeVisible()
|
|
await expect(dialog.getByRole("button", { name: "Восстановить", exact: true })).toBeDisabled()
|
|
await dialog.getByRole("button", { name: "Закрыть", exact: true }).click()
|
|
await page.getByRole("checkbox", { name: /Я проверил состояние роутера/ }).check(); await page.getByRole("button", { name: "Разрешить новое восстановление", exact: true }).click()
|
|
serverId = 1; await page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }).click(); await dialog.getByRole("button", { name: "Восстановить", exact: true }).dblclick()
|
|
await expect(dialog).toBeHidden(); await expect(page.getByText("API подтвердил импорт " + backup.filename + " на Новое имя роутера", { exact: true })).toBeVisible(); expect(calls).toBe(2)
|
|
})
|
|
|
|
for (const outcome of ["unknown", "legacy-error"]) test("Бэкапы: неизвестный restore переживает reload/" + outcome, async ({ page }) => {
|
|
await setupBackups(page); let calls = 0
|
|
await page.route("**/api/backups/backup-1/restore", async r => { calls++; await r.fulfill({ status: outcome === "unknown" ? 502 : 400, json: { error: "Ответ импорта потерян", ...(outcome === "unknown" ? { outcome: "unknown" } : {}) } }) })
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }).click()
|
|
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Восстановить", exact: true }).click()
|
|
await expect(dialog.getByRole("button", { name: "Восстановить", exact: true })).toBeDisabled(); await expect(dialog).toContainText("Результат импорта неизвестен")
|
|
await page.reload(); const alert = page.getByRole("alert", { name: "Подтверждение восстановления", exact: true })
|
|
await expect(alert).toContainText(backup.filename); await expect(page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true })).toBeDisabled()
|
|
await expect(alert.getByRole("button", { name: "Разрешить новое восстановление", exact: true })).toBeDisabled(); expect(calls).toBe(1)
|
|
await alert.getByRole("checkbox", { name: /Я проверил состояние роутера/ }).check(); await alert.getByRole("button", { name: "Разрешить новое восстановление", exact: true }).click()
|
|
await expect(alert).toBeHidden(); await expect(page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true })).toBeEnabled(); expect(calls).toBe(1)
|
|
})
|
|
test("Бэкапы: запрет записи checkpoint не отправляет restore", async ({ page }) => {
|
|
await setupBackups(page); let calls = 0
|
|
await page.addInitScript(() => { const original = Storage.prototype.setItem; Storage.prototype.setItem = function(key, value) { if (key.startsWith("mmapp:backup-restore:")) throw new Error("storage denied"); return original.call(this, key, value) } })
|
|
await page.route("**/api/backups/backup-1/restore", async r => { calls++; await r.fulfill({ json: {} }) })
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }).click()
|
|
await page.getByRole("alertdialog").getByRole("button", { name: "Восстановить", exact: true }).click()
|
|
await expect(page.getByRole("alert", { name: "Подтверждение восстановления", exact: true })).toContainText("Запрос не отправлен"); expect(calls).toBe(0)
|
|
})
|
|
test("Бэкапы: повреждённый checkpoint запрещает новый restore", async ({ page }) => {
|
|
await setupBackups(page); await page.addInitScript(() => sessionStorage.setItem("mmapp:backup-restore:" + location.origin, "broken"))
|
|
await page.goto("/backups"); await expect(page.getByRole("alert", { name: "Подтверждение восстановления", exact: true })).toContainText("Не удалось прочитать отметку")
|
|
await expect(page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true })).toBeDisabled()
|
|
})
|
|
|
|
test("Бэкапы: подтверждённый restore с отказом очистки checkpoint не допускает новый импорт", async ({ page }) => {
|
|
await setupBackups(page); let calls = 0
|
|
await page.addInitScript(() => { const original = Storage.prototype.removeItem; Storage.prototype.removeItem = function(key) { if (key.startsWith("mmapp:backup-restore:")) throw new Error("storage denied"); return original.call(this, key) } })
|
|
await page.route("**/api/backups/backup-1/restore", async r => { calls++; await r.fulfill({ json: { backupId: backup.id, serverId: backup.serverId, filename: backup.filename, serverName: "router" } }) })
|
|
await page.goto("/backups"); await page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }).click()
|
|
await page.getByRole("alertdialog").getByRole("button", { name: "Восстановить", exact: true }).click()
|
|
await expect(page.getByRole("alertdialog")).toBeHidden(); await expect(page.getByText("API подтвердил импорт " + backup.filename + " на router", { exact: true })).toBeVisible()
|
|
const alert = page.getByRole("alert", { name: "Подтверждение восстановления", exact: true }); await expect(alert).toContainText("Не удалось очистить отметку")
|
|
await alert.getByRole("checkbox", { name: /Я проверил состояние роутера/ }).check(); await alert.getByRole("button", { name: "Разрешить новое восстановление", exact: true }).click()
|
|
await expect(page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true })).toBeDisabled(); expect(calls).toBe(1)
|
|
})
|