374 lines
24 KiB
TypeScript
374 lines
24 KiB
TypeScript
import { test, expect, type Page } from "@playwright/test"
|
||
|
||
async function setup(page: Page, url: string, failed = 0, longNames = false, permission?: string, isAdmin = false) {
|
||
const control = { writes: [] as { method: string; path: string; body: unknown }[], failed, posts: [] as { path: string; body: unknown }[] }
|
||
const servers = [{ id: 1, name: "Источник", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "64512", status: "online", latency: 1, os: "7", model: "CHR", wanUplinks: [] }]
|
||
const probes = ["p1", "p/2"].map(id => ({ id, srcServerId: "1", name: "Проверка сети", target: "192.0.2.10", filter: "main", rtt: null, loss: 100, status: "down", series: [0], enabled: true, showOnDashboard: false }))
|
||
if (permission) {
|
||
await page.addInitScript(token => localStorage.setItem("mmapp_token", token), uptimeToken(permission, isAdmin))
|
||
servers.push({ ...servers[0], id: 2, name: "Назначение", host: "192.0.2.2" })
|
||
}
|
||
if (longNames) {
|
||
servers[0].name = "Источник-с-длинным-названием-площадки-для-проверки-переноса"
|
||
probes.forEach(p => { p.name = "Проверка-доступности-длинного-имени-сетевого-узла"; p.target = "router-with-a-long-hostname.example.test"; p.filter = "Фильтр-с-длинным-названием" })
|
||
}
|
||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||
await page.route("**/api/**", route => {
|
||
const request = route.request()
|
||
const path = new URL(request.url()).pathname
|
||
if (!["GET", "HEAD"].includes(request.method())) control.writes.push({ method: request.method(), path, body: request.postDataJSON() })
|
||
if (request.method() === "POST") control.posts.push({ path, body: request.postDataJSON() })
|
||
if (path === "/api/uptime/overview" && control.failed) return route.fulfill({ status: control.failed, json: { error: "Мониторинг недоступен" } })
|
||
const json = path === "/api/auth/config" ? { required: Boolean(permission) }
|
||
: path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false }
|
||
: path === "/api/servers" ? servers
|
||
: path === "/api/uptime/overview" ? { probes, resources: [] }
|
||
: path === "/api/uptime/speed-probes" ? { probes: permission ? [{ id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "", dstInterface: "", protocol: "tcp", direction: "both", durationSec: "10", enabled: true }] : [] }
|
||
: path === "/api/uptime/speed-test/runs" ? { runs: [] }
|
||
: path === "/api/bgp/sessions" ? []
|
||
: path === "/api/filters/gre-tunnels" || path === "/api/vxlan" ? { tunnels: [] }
|
||
: path === "/api/wireguard" ? { interfaces: [], peers: [] }
|
||
: path === "/api/traffic/servers" ? { servers: [] }
|
||
: path === "/api/events" ? { events: [] }
|
||
: path === "/api/internet-path/latest" ? { snapshot: { sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} } }
|
||
: {}
|
||
return route.fulfill({ json })
|
||
})
|
||
await page.goto(url)
|
||
return control
|
||
}
|
||
|
||
test("очередь проверок: точный ID, чтение без ping, ручной ping только выбранной проверки", async ({ page }) => {
|
||
const control = await setup(page, "/dashboard")
|
||
const queue = page.getByRole("region", { name: "Требует внимания", exact: true })
|
||
const link = queue.locator('a[href="/uptime?probe=p%2F2"]')
|
||
await expect(link).toBeVisible()
|
||
await link.click()
|
||
await expect(page).toHaveURL(/\/uptime\?probe=p%2F2$/)
|
||
const context = page.getByRole("status", { name: "Проверка по ссылке" })
|
||
await expect(context).toContainText("Выбрана проверка: Проверка сети")
|
||
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
|
||
await expect(page.locator('[data-probe-id="p/2"]')).toBeVisible()
|
||
expect(control.posts).toEqual([])
|
||
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
|
||
await expect(context).toContainText("Список ограничен")
|
||
expect(control.posts).toEqual([])
|
||
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||
await expect.poll(() => control.posts).toEqual([{ path: "/api/uptime/probes/collect-group", body: { probeIds: ["p/2"] } }])
|
||
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
|
||
await expect(queue).toBeVisible()
|
||
})
|
||
|
||
test("проверка по прямой ссылке: сброс, возврат, перезагрузка и частичный сбой", async ({ page }) => {
|
||
const control = await setup(page, "/uptime?probe=p%2F2&keep=1")
|
||
const context = page.getByRole("status", { name: "Проверка по ссылке" })
|
||
await expect(context).toContainText("Выбрана проверка")
|
||
await context.getByRole("link", { name: "Все проверки", exact: true }).click()
|
||
await expect(page).toHaveURL(/\/uptime\?keep=1$/)
|
||
await expect(page.locator("[data-probe-id]")).toHaveCount(2)
|
||
await page.goBack()
|
||
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
|
||
await page.reload()
|
||
await expect(context).toContainText("Выбрана проверка")
|
||
control.failed = 503
|
||
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
|
||
await expect(context).toContainText("сохранённые данные могут быть устаревшими")
|
||
await expect(page.locator('[data-probe-id="p/2"]')).toBeVisible()
|
||
control.failed = 0
|
||
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
|
||
await expect(context).toContainText("Список ограничен")
|
||
await page.setViewportSize({ width: 390, height: 844 })
|
||
await context.scrollIntoViewIfNeeded()
|
||
const box = await context.boundingBox()
|
||
expect(box!.x + box!.width).toBeLessThanOrEqual(390)
|
||
await page.screenshot({ path: "test-results/uptime-link-mobile.png", animations: "disabled" })
|
||
})
|
||
|
||
for (const failed of [0, 403, 500]) {
|
||
test(`недоступная проверка: ${failed || "не найден ID"}`, async ({ page }) => {
|
||
const control = await setup(page, "/uptime?probe=missing", failed)
|
||
await expect(page.getByRole("status", { name: "Проверка по ссылке" })).toContainText(failed ? "Не удалось обновить данные проверки" : "Проверка не найдена или недоступна")
|
||
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
|
||
expect(control.posts).toEqual([])
|
||
})
|
||
}
|
||
|
||
for (const width of [320, 390, 1440]) {
|
||
test(`Ping-пробы ${width}px: длинные подписи, доступные действия и раскрытие`, async ({ page }) => {
|
||
await page.setViewportSize({ width, height: 1000 })
|
||
const control = await setup(page, "/uptime?probe=p%2F2", 0, true)
|
||
const row = page.locator('[data-probe-id="p/2"]')
|
||
await expect(row).toBeVisible()
|
||
await expect(row.getByText("График RTT", { exact: true })).toBeVisible({ visible: width < 1440 })
|
||
if (width < 1440) expect(await row.getByText("Фильтр-с-длинным-названием", { exact: true }).evaluate(el => el.getBoundingClientRect().right)).toBeLessThanOrEqual(width)
|
||
for (const name of ["Редактировать пробу", "Удалить пробу", "Показать на дашборде"]) {
|
||
const button = row.getByRole("button", { name, exact: true })
|
||
await button.scrollIntoViewIfNeeded()
|
||
const bounds = await button.boundingBox()
|
||
expect(bounds!.x).toBeGreaterThanOrEqual(0)
|
||
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
|
||
}
|
||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width)
|
||
const toggle = row.getByRole("switch", { name: /Включить пробу/ })
|
||
await expect(toggle).toBeChecked()
|
||
const chart = row.getByRole("button", { name: /Подробный график RTT/ })
|
||
await chart.focus()
|
||
await page.keyboard.press("Enter")
|
||
await expect(chart).toHaveAttribute("aria-expanded", "true")
|
||
await page.keyboard.press("Enter")
|
||
await expect(chart).toHaveAttribute("aria-expanded", "false")
|
||
await row.scrollIntoViewIfNeeded()
|
||
await page.screenshot({ path: `test-results/ping-row-${width}.png`, animations: "disabled" })
|
||
expect(control.writes).toEqual([])
|
||
await row.getByRole("button", { name: "Редактировать пробу", exact: true }).click()
|
||
await expect(page.getByRole("dialog")).toBeVisible()
|
||
await page.keyboard.press("Escape")
|
||
await expect(page.getByRole("dialog")).toBeHidden()
|
||
await row.getByRole("button", { name: "Показать на дашборде", exact: true }).click()
|
||
await expect.poll(() => control.writes.some(w => w.method === "PATCH" && w.path === "/api/uptime/probes/p%2F2" && (w.body as { showOnDashboard: boolean }).showOnDashboard === true)).toBe(true)
|
||
await toggle.click()
|
||
await expect.poll(() => control.writes.some(w => w.method === "PUT" && w.path === "/api/uptime/probes")).toBe(true)
|
||
})
|
||
}
|
||
|
||
// UI claim fixtures; JWT verification remains a separate integration check.
|
||
function uptimeToken(permission: string, isAdmin = false) {
|
||
return "fixture." + Buffer.from(JSON.stringify({ apps: ["mm"], permissions: ["mm:servers:read", permission], is_admin: isAdmin, exp: Math.floor(Date.now() / 1000) + 3600 })).toString("base64url") + ".fixture"
|
||
}
|
||
|
||
for (const [permission, isAdmin] of [["mm:uptime:read", false], ["mm:uptime:read", true], ["mm:servers:write", false]] as const) {
|
||
test(`мониторинг только чтение: ${permission}, is_admin=${isAdmin}`, async ({ page }) => {
|
||
const control = await setup(page, "/uptime", 0, false, permission, isAdmin)
|
||
await expect(page.getByText("Только просмотр", { exact: true })).toBeVisible()
|
||
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeDisabled()
|
||
const row = page.locator('[data-probe-id="p1"]')
|
||
for (const name of ["Показать на дашборде", "Редактировать пробу", "Удалить пробу"]) {
|
||
await expect(row.getByRole("button", { name, exact: true })).toBeDisabled()
|
||
}
|
||
await expect(row.getByRole("switch")).toBeDisabled()
|
||
await expect(page.getByRole("button", { name: /Добавить интерфейс для/ })).toBeDisabled()
|
||
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeDisabled()
|
||
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
|
||
await page.getByRole("button", { name: /Ресурсы/ }).click()
|
||
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
|
||
await page.getByRole("button", { name: /Скорость/ }).click()
|
||
await expect(page.getByRole("button", { name: "Новая speed-проба", exact: true })).toBeDisabled()
|
||
for (const name of ["Запустить", "Редактировать speed-пробу", "Удалить speed-пробу"]) {
|
||
await expect(page.getByRole("button", { name, exact: true })).toBeDisabled()
|
||
}
|
||
await expect(page.getByRole("switch")).toBeDisabled()
|
||
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
|
||
expect(control.writes).toEqual([])
|
||
})
|
||
}
|
||
|
||
for (const permission of ["mm:uptime:write", "mm:uptime:admin"]) {
|
||
test(`мониторинг: ${permission} разрешает ручной ping`, async ({ page }) => {
|
||
const control = await setup(page, "/uptime?probe=p1", 0, false, permission)
|
||
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeEnabled()
|
||
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||
await expect.poll(() => control.posts.map(p => p.path)).toEqual(["/api/uptime/probes/collect-group"])
|
||
await page.getByRole("button", { name: /Скорость/ }).click()
|
||
await expect(page.getByRole("button", { name: "Запустить", exact: true })).toBeEnabled()
|
||
})
|
||
}
|
||
|
||
for (const speed of [false, true]) {
|
||
test(`мониторинг: отзыв права перед сохранением ${speed ? "Speed" : "Ping"}`, async ({ page }) => {
|
||
const control = await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
|
||
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
|
||
await expect(page.getByRole("dialog")).toBeVisible()
|
||
await page.evaluate(token => localStorage.setItem("mmapp_token", token), uptimeToken("mm:uptime:read"))
|
||
await page.getByRole("dialog").getByRole("button", { name: "Сохранить", exact: true }).click()
|
||
await expect(page.getByRole("dialog")).toBeVisible()
|
||
await expect(page.getByRole("dialog").getByRole("alert")).toContainText("Недостаточно прав")
|
||
expect(control.writes).toEqual([])
|
||
})
|
||
}
|
||
|
||
for (const speed of [false, true]) {
|
||
for (const status of [400, 403, 500]) {
|
||
test(`сохранение ${speed ? "Speed" : "Ping"}: ${status} сохраняет черновик, повтор подтверждён`, async ({ page }) => {
|
||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
|
||
let fail = true
|
||
const bodies: { probes: Record<string, unknown>[] }[] = []
|
||
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", route => {
|
||
if (route.request().method() !== "PUT") return route.fallback()
|
||
bodies.push(route.request().postDataJSON())
|
||
return route.fulfill({ status: fail ? status : 200, json: fail ? { error: "Сохранение отклонено" } : { ok: true } })
|
||
})
|
||
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
|
||
const dialog = page.getByRole("dialog")
|
||
const input = speed ? dialog.locator('input').filter({ visible: true }).last() : dialog.getByPlaceholder("youtube.com")
|
||
await input.fill(speed ? "17" : "Черновик проверки")
|
||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||
await expect(dialog.getByRole("alert")).toContainText(status === 403 ? "Недостаточно прав" : "Сохранение отклонено")
|
||
await expect(input).toHaveValue(speed ? "17" : "Черновик проверки")
|
||
fail = false
|
||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||
await expect(dialog).toBeHidden()
|
||
expect(bodies).toHaveLength(2)
|
||
expect(bodies[1]).toEqual(bodies[0])
|
||
expect(bodies[1].probes[0][speed ? "durationSec" : "name"]).toBe(speed ? 17 : "Черновик проверки")
|
||
})
|
||
}
|
||
|
||
test(`сохранение ${speed ? "Speed" : "Ping"}: один PUT, Escape и отмена заблокированы до ответа`, async ({ page }) => {
|
||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
|
||
let calls = 0
|
||
let release!: () => void
|
||
const pending = new Promise<void>(resolve => { release = resolve })
|
||
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
|
||
if (route.request().method() !== "PUT") return route.fallback()
|
||
calls++
|
||
await pending
|
||
await route.fulfill({ json: { ok: true } })
|
||
})
|
||
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
|
||
const dialog = page.getByRole("dialog")
|
||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
|
||
await expect(dialog.getByRole("button", { name: "Сохранение…", exact: true })).toBeDisabled()
|
||
await expect(dialog.getByRole("button", { name: "Отмена", exact: true })).toBeDisabled()
|
||
await expect(dialog.locator("fieldset input").last()).toBeDisabled()
|
||
await page.keyboard.press("Escape")
|
||
await expect(dialog).toBeVisible()
|
||
expect(calls).toBe(1)
|
||
release()
|
||
await expect(dialog).toBeHidden()
|
||
})
|
||
}
|
||
|
||
for (const speed of [false, true]) {
|
||
test(`сохранение ${speed ? "Speed" : "Ping"}: таймаут сохраняет форму с неизвестным результатом`, async ({ page }) => {
|
||
await page.addInitScript(() => {
|
||
const timeout = AbortSignal.timeout.bind(AbortSignal)
|
||
AbortSignal.timeout = ms => timeout(ms === 30_000 ? 100 : ms)
|
||
})
|
||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
|
||
let release!: () => void
|
||
const pending = new Promise<void>(resolve => { release = resolve })
|
||
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
|
||
if (route.request().method() !== "PUT") return route.fallback()
|
||
await pending
|
||
await route.fulfill({ json: { ok: true } }).catch(() => {})
|
||
})
|
||
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
|
||
const dialog = page.getByRole("dialog")
|
||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||
await expect(dialog.getByRole("alert")).toContainText("Результат операции неизвестен")
|
||
await expect(dialog.getByRole("button", { name: "Сохранить", exact: true })).toBeEnabled()
|
||
release()
|
||
await expect(dialog).toBeVisible()
|
||
})
|
||
}
|
||
|
||
for (const speed of [false, true]) {
|
||
test(`переключение ${speed ? "Speed" : "Ping"}: ожидание API, отказ, повтор без потери состояния`, async ({ page }) => {
|
||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
|
||
let calls = 0
|
||
let fail = true
|
||
let release!: () => void
|
||
const pending = new Promise<void>(resolve => { release = resolve })
|
||
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
|
||
if (route.request().method() !== "PUT") return route.fallback()
|
||
calls++
|
||
expect(route.request().postDataJSON().probes[0].enabled).toBe(false)
|
||
if (fail) await pending
|
||
await route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: "Forbidden" } : { ok: true } })
|
||
})
|
||
const toggle = page.getByRole("switch")
|
||
await expect(toggle).toBeChecked()
|
||
await toggle.click()
|
||
await expect(toggle).toBeDisabled()
|
||
await expect(toggle).toBeChecked()
|
||
await expect.poll(() => calls).toBe(1)
|
||
release()
|
||
await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toBeVisible()
|
||
await expect(toggle).toBeChecked()
|
||
await expect(toggle).toBeEnabled()
|
||
fail = false
|
||
await toggle.click()
|
||
await expect(toggle).not.toBeChecked()
|
||
await expect(toggle).toBeEnabled()
|
||
await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toHaveCount(0)
|
||
expect(calls).toBe(2)
|
||
})
|
||
}
|
||
|
||
for (const speed of [false, true]) {
|
||
test(`удаление ${speed ? "Speed" : "Ping"}: подтверждение, отказ и повтор`, async ({ page }) => {
|
||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
|
||
let calls = 0
|
||
let fail = true
|
||
let release!: () => void
|
||
const pending = new Promise<void>(resolve => { release = resolve })
|
||
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
|
||
if (route.request().method() !== "PUT") return route.fallback()
|
||
calls++
|
||
const rows = route.request().postDataJSON().probes
|
||
expect(rows.some((p: { id: string }) => p.id === (speed ? "s1" : "p1"))).toBe(false)
|
||
if (fail) await pending
|
||
await route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: "Forbidden" } : { ok: true } })
|
||
})
|
||
const remove = page.getByRole("button", { name: speed ? "Удалить speed-пробу" : "Удалить пробу", exact: true })
|
||
await remove.click()
|
||
const dialog = page.getByRole("alertdialog")
|
||
await expect(dialog).toContainText(speed ? "Источник → Назначение" : "Проверка сети → 192.0.2.10")
|
||
await dialog.getByRole("button", { name: "Отмена", exact: true }).click()
|
||
expect(calls).toBe(0)
|
||
await remove.click()
|
||
await dialog.getByRole("button", { name: "Удалить", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
|
||
await expect(dialog.getByRole("button", { name: "Удаление…", exact: true })).toBeDisabled()
|
||
await page.keyboard.press("Escape")
|
||
await expect(dialog).toBeVisible()
|
||
await expect.poll(() => calls).toBe(1)
|
||
release()
|
||
await expect(dialog.getByRole("alert")).toContainText("Недостаточно прав")
|
||
fail = false
|
||
await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
|
||
await expect(dialog).toBeHidden()
|
||
await expect(remove).toHaveCount(0)
|
||
expect(calls).toBe(2)
|
||
})
|
||
}
|
||
|
||
test("звезда дашборда: состояние меняется после PATCH, ошибка сохраняет прежнее", async ({ page }) => {
|
||
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
|
||
await page.getByRole("button", { name: "Пауза", exact: true }).click()
|
||
let calls = 0
|
||
let fail = true
|
||
let release!: () => void
|
||
const pending = new Promise<void>(resolve => { release = resolve })
|
||
await page.route("**/api/uptime/probes/p1", async route => {
|
||
if (route.request().method() !== "PATCH") return route.fallback()
|
||
calls++
|
||
expect(route.request().postDataJSON()).toEqual({ showOnDashboard: true })
|
||
if (fail) await pending
|
||
await route.fulfill({ status: fail ? 500 : 200, json: fail ? { error: "Звезда не сохранена" } : { ok: true } })
|
||
})
|
||
const star = page.getByRole("button", { name: "Показать на дашборде", exact: true })
|
||
await star.click()
|
||
await expect(star).toBeDisabled()
|
||
await expect(star).toHaveAttribute("aria-pressed", "false")
|
||
await expect(page.getByRole("switch")).toBeDisabled()
|
||
await expect.poll(() => calls).toBe(1)
|
||
release()
|
||
await expect(page.getByRole("alert").filter({ hasText: "Звезда не сохранена" })).toBeVisible()
|
||
await expect(star).toBeEnabled()
|
||
fail = false
|
||
await star.click()
|
||
await expect(page.getByRole("button", { name: "Убрать с дашборда", exact: true })).toHaveAttribute("aria-pressed", "true")
|
||
expect(calls).toBe(2)
|
||
})
|