fix(ui): ограничить каталог бэкапов и вернуть фокус действий

This commit is contained in:
Denozordec committed 2026-10-04 05:01:29 +07:00
1 parent 5d9a6f3757
commit 611e542ea7
11 files changed
+123 -35

No files matched your search

+3 -3
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"
import { backup, setupBackups } from "./fixtures/backups"
for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Бэкапы: длинный файл и ошибки диалогов/" + name, async ({ page }) => {
for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Бэкапы: длинный файл и ошибки диалогов/" + name, async ({ page }, info) => {
await page.setViewportSize({ width, height: 900 }); await setupBackups(page)
const filename = "a".repeat(130) + ".rsc"
await page.route("**/api/backups", r => r.fulfill({ json: [{ ...backup, filename }] }))
@@ -9,11 +9,11 @@ for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]
await page.goto("/backups"); await page.getByRole("button", { name: "Удалить " + filename, exact: true }).click()
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Удалить", exact: true }).click(); await expect(dialog).toContainText("Запись резервной копии сохранена")
expect(await dialog.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true)
await page.screenshot({ path: "docs/ux-ui/screens/backups-delete-error-" + name + ".png", fullPage: true })
await page.screenshot({ path: info.outputPath("backups-delete-error-" + name + ".png"), fullPage: true })
await dialog.getByRole("button", { name: "Отмена", exact: true }).click(); await page.getByRole("button", { name: "Восстановить " + filename, exact: true }).click()
await dialog.getByRole("button", { name: "Восстановить", exact: true }).click(); await expect(dialog).toContainText("Результат импорта неизвестен"); await expect(dialog.getByRole("button", { name: "Восстановить", exact: true })).toBeDisabled()
expect(await dialog.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true)
const box = await dialog.boundingBox(); expect(box && box.x >= 0 && box.y >= 0 && box.x + box.width <= width + 1 && box.y + box.height <= 901).toBe(true)
await page.screenshot({ path: "docs/ux-ui/screens/backups-restore-unknown-" + name + ".png", fullPage: true })
await page.screenshot({ path: info.outputPath("backups-restore-unknown-" + name + ".png"), fullPage: true })
await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0)
})
+4 -4
View File
@@ -1,24 +1,24 @@
import { test, expect } from "@playwright/test"
import { backup, server, setupBackups } from "./fixtures/backups"
for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Бэкапы: клавиатура, Frame и адаптивность/" + name, async ({ page }) => {
for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Бэкапы: клавиатура, Frame и адаптивность/" + name, async ({ page }, info) => {
await page.setViewportSize({ width, height: 900 }); await setupBackups(page)
await page.route("**/api/servers", r => r.fulfill({ json: [{ ...server, status: null }] }))
const long = { ...backup, filename: "backup-of-router-with-a-very-long-name-for-mobile-screens.rsc", serverName: "Очень длинное имя роутера для проверки адаптивности", notes: "Длинная заметка: проверка подтверждённых резервных копий и адаптивной таблицы" }
await page.route("**/api/backups", r => r.fulfill({ json: [long] }))
await page.goto("/backups"); await expect(page.getByText(long.filename, { exact: true })).toBeVisible(); await expect(page.getByRole("heading", { level: 1, name: "Резервные копии" })).toBeVisible()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true)
await page.screenshot({ path: "docs/ux-ui/screens/backups-history-" + name + ".png", fullPage: true })
await page.screenshot({ path: info.outputPath("backups-history-" + name + ".png"), fullPage: true })
const settings = page.getByRole("tab", { name: "Настройки", exact: true }); await settings.focus(); await page.keyboard.press("Enter")
await expect(page.getByText("Неизвестно", { exact: true })).toBeVisible(); await expect(page.getByText("Оффлайн", { exact: true })).toHaveCount(0)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true)
await page.getByText("Автоматический бэкап", { exact: true }).scrollIntoViewIfNeeded()
expect(await page.locator("fieldset").evaluateAll(elements => elements.every(element => element.scrollWidth <= element.clientWidth + 1))).toBe(true)
for (const label of ["Ежедневно", "Еженедельно", "Ежемесячно"]) { const box = await page.getByRole("button", { name: label, exact: true }).boundingBox(); expect(box && box.x >= 0 && box.x + box.width <= width + 1).toBe(true) }
await page.screenshot({ path: "docs/ux-ui/screens/backups-settings-" + name + ".png", fullPage: true })
await page.screenshot({ path: info.outputPath("backups-settings-" + name + ".png"), fullPage: true })
const create = page.getByRole("button", { name: "Новый бэкап", exact: true }).first(); await create.focus(); await page.keyboard.press("Enter")
const dialog = page.getByRole("dialog", { name: "Новый бэкап", exact: true }); await expect(dialog).toBeVisible(); await expect(dialog.getByText("Неизвестно", { exact: true })).toBeVisible(); await expect(dialog.getByText("недоступен", { exact: true })).toHaveCount(0)
await expect.poll(async () => { const box = await dialog.boundingBox(); return !!box && box.x >= 0 && box.x + box.width <= width + 1 && (width >= 640 || box.width >= width - 2) }).toBe(true)
expect(await dialog.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true)
await page.screenshot({ path: "docs/ux-ui/screens/backups-create-" + name + ".png", fullPage: true })
await page.screenshot({ path: info.outputPath("backups-create-" + name + ".png"), fullPage: true })
await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); await expect(create).toBeFocused()
})
+42
View File
@@ -0,0 +1,42 @@
import { test, expect } from "@playwright/test"
import { backupItemDtoSchema } from "../../packages/contracts/src/backups"
import { backup, setupBackups } from "./fixtures/backups"
import axe from "axe-core"
import { mkdirSync } from "node:fs"
test("Backups1000: весь поиск, страница при refresh, отказ и правильный файл", async ({ page }, info) => {
test.setTimeout(90000)
let catalog = Array.from({ length: 1000 }, (_, i) => backupItemDtoSchema.parse({ ...backup, id: "backup-" + (i + 1), filename: "config-" + String(i + 1).padStart(4, "0") + ".rsc", sizeBytes: (i + 1) * 1024 }))
let failed = false, calls = 0; const writes: string[] = []
await setupBackups(page)
await page.route("**/api/**", r => { if(r.request().method() !== "GET") writes.push(r.request().method() + " " + new URL(r.request().url()).pathname); return r.fallback() })
await page.route("**/api/backups", r => { calls++; return r.fulfill(failed ? { status: 503, json: { error: "История недоступна" } } : { json: catalog }) })
const start = Date.now(); await page.goto("/backups")
const rows = page.locator("tr[data-row-id]"); await expect(page.getByText("config-0001.rsc", { exact: true })).toBeVisible()
const sample = { fixtureRows: 1000, fixtureBytes: Buffer.byteLength(JSON.stringify(catalog)), renderedRows: await rows.count(), domNodes: await page.locator("*").count(), readyMs: Date.now() - start }
console.log(JSON.stringify(sample)); await info.attach("large-backups-metrics", { body: JSON.stringify(sample), contentType: "application/json" })
expect(sample.renderedRows).toBe(50); expect(sample.domNodes).toBeLessThan(5000)
for(let i=0;i<3;i++) await page.getByRole("button", { name: "…", exact: true }).last().click()
await page.getByRole("button", { name: "Перейти на страницу 20", exact: true }).click()
const refresh = page.getByRole("button", { name: "Обновить", exact: true }); const before = calls; await refresh.click(); await expect.poll(() => calls).toBeGreaterThan(before); await expect(refresh).toBeEnabled()
await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible()
failed = true; await refresh.click(); await expect(page.getByRole("alert", { name: "Данные резервных копий: backups", exact: true })).toContainText("Последние данные сохранены"); await expect(rows).toHaveCount(50)
failed = false; await refresh.click(); await expect(page.getByRole("alert", { name: "Данные резервных копий: backups", exact: true })).toHaveCount(0)
const search = page.getByPlaceholder("Поиск по файлу, серверу, заметке…"); await search.fill("config-1000"); await expect(rows).toHaveCount(1); await expect(page).toHaveURL(/q=config-1000/)
await page.reload(); await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "backup-1000"); await search.fill(""); await expect(rows).toHaveCount(50)
await page.getByRole("button", { name: "Размер", exact: true }).click(); await expect(rows.first()).toHaveAttribute("data-row-id", "backup-1000")
await page.setViewportSize({ width: 320, height: 1000 })
const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]'); await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth })
for (const action of ["Скачать", "Восстановить", "Удалить"]) {
const button = rows.first().getByRole("button", { name: action + " config-1000.rsc", exact: true })
expect(await button.evaluate(el => { const box=el.getBoundingClientRect(), viewport=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned=el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return box.width >= 44 && box.height >= 44 && box.left >= pinned.right-1 && box.right <= viewport.right+1 })).toBe(true)
}
const restore = rows.first().getByRole("button", { name: "Восстановить config-1000.rsc", exact: true }); await restore.focus(); await page.keyboard.press("Enter"); await expect(page.getByRole("alertdialog")).toContainText("config-1000.rsc"); await page.getByRole("alertdialog").getByRole("button", { name: "Отмена", exact: true }).click(); await expect(restore).toBeFocused()
const remove = rows.first().getByRole("button", { name: "Удалить config-1000.rsc", exact: true })
expect(await remove.evaluate(el => { const value=el.getBoundingClientRect(), viewport=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned=el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return value.left >= pinned.right-1 && value.right <= viewport.right+1 })).toBe(true)
await remove.focus(); await page.keyboard.press("Enter"); await expect(page.getByRole("alertdialog")).toContainText("config-1000.rsc"); await page.getByRole("alertdialog").getByRole("button", { name: "Отмена", exact: true }).click(); await expect(remove).toBeFocused()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.addScriptTag({ content: axe.source }); const audit=await page.evaluate(async () => (window as unknown as {axe:typeof axe}).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type:"tag", values:["wcag2a","wcag2aa","wcag21a","wcag21aa"] } })); expect(audit.violations).toEqual([])
await page.addStyleTag({ content:"nextjs-portal{display:none!important}" }); mkdirSync("docs/ux-ui/screens",{recursive:true}); await page.getByRole("table").scrollIntoViewIfNeeded(); await page.screenshot({path:"docs/ux-ui/screens/large-backups-320.png",animations:"disabled"})
await page.getByRole("button",{name:"Следующая страница",exact:true}).click(); catalog=[catalog[999]]; const previous=calls; await refresh.click(); await expect.poll(() => calls).toBeGreaterThan(previous); await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id","backup-1000"); expect(writes).toEqual([])
})