fix(ui): подтверждать удаление проб и изменение звезды
This commit is contained in:
1 parent
4f235058be
commit
ce14f5de3e
4 files changed
+136
-47
No files matched your search
+57
-45
@@ -13,6 +13,7 @@ import { FormField, FormToggle, SegmentedControl } from "@/components/form-kit"
|
|||||||
import { Frame, FramePanel } from "@/components/reui/frame"
|
import { Frame, FramePanel } from "@/components/reui/frame"
|
||||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter } from "@/components/ui/alert-dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { Flag } from "@/components/flag"
|
import { Flag } from "@/components/flag"
|
||||||
@@ -1056,6 +1057,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
|
|||||||
|
|
||||||
function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId: string | null; allProbesHref: string }) {
|
function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId: string | null; allProbesHref: string }) {
|
||||||
const { mode, backendUrl, backendStatus, checkBackend } = useDataSource()
|
const { mode, backendUrl, backendStatus, checkBackend } = useDataSource()
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<{ kind: "ping" | "speed"; id: string; label: string } | null>(null)
|
||||||
const draftProbeId = useRef("")
|
const draftProbeId = useRef("")
|
||||||
const [probeSaving, setProbeSaving] = useState(false)
|
const [probeSaving, setProbeSaving] = useState(false)
|
||||||
const [probeSaveError, setProbeSaveError] = useState<string | null>(null)
|
const [probeSaveError, setProbeSaveError] = useState<string | null>(null)
|
||||||
@@ -1498,40 +1500,26 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
|||||||
})
|
})
|
||||||
}, [apiFetch, liveApi])
|
}, [apiFetch, liveApi])
|
||||||
|
|
||||||
const toggleDashboardStar = useCallback((id: string) => {
|
const toggleDashboardStar = (id: string) => {
|
||||||
if (!requireWrite()) return
|
if (!requireWrite()) return
|
||||||
const cur = probes.find((p) => p.id === id)
|
const cur = probes.find(p => p.id === id)
|
||||||
if (!cur) return
|
if (!cur) return
|
||||||
const nextVal = !cur.showOnDashboard
|
const nextVal = !cur.showOnDashboard
|
||||||
if (liveApi) {
|
void saveProbeForm(async () => {
|
||||||
overviewReqRef.current += 1
|
if (liveApi) await apiFetch("/api/uptime/probes/" + encodeURIComponent(id), {
|
||||||
}
|
method: "PATCH", signal: AbortSignal.timeout(30_000),
|
||||||
setProbes((prev) => prev.map((p) => (p.id === id ? { ...p, showOnDashboard: nextVal } : p)))
|
|
||||||
if (liveApi) {
|
|
||||||
void apiFetch(`/api/uptime/probes/${encodeURIComponent(id)}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ showOnDashboard: nextVal }),
|
body: JSON.stringify({ showOnDashboard: nextVal }),
|
||||||
})
|
})
|
||||||
.then(() => loadLiveOverview())
|
}, () => {
|
||||||
.then(() => {
|
setProbes(prev => prev.map(p => p.id === id ? { ...p, showOnDashboard: nextVal } : p))
|
||||||
if (typeof window !== "undefined") {
|
if (!liveApi) {
|
||||||
window.dispatchEvent(new Event(UPTIME_PROBES_CHANGED))
|
const ids = readMockDashboardStarIds()
|
||||||
}
|
if (nextVal) ids.add(id); else ids.delete(id)
|
||||||
})
|
writeMockDashboardStarIds(ids)
|
||||||
.catch(() => {
|
|
||||||
setProbes((prev) => prev.map((p) => (p.id === id ? { ...p, showOnDashboard: cur.showOnDashboard } : p)))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const s = readMockDashboardStarIds()
|
|
||||||
if (nextVal) s.add(id)
|
|
||||||
else s.delete(id)
|
|
||||||
writeMockDashboardStarIds(s)
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.dispatchEvent(new Event(UPTIME_PROBES_CHANGED))
|
|
||||||
}
|
}
|
||||||
}
|
window.dispatchEvent(new Event(UPTIME_PROBES_CHANGED))
|
||||||
}, [probes, liveApi, apiFetch, loadLiveOverview, requireWrite])
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const persistSpeedProbes = useCallback(async (rows: SpeedProbeRow[]) => {
|
const persistSpeedProbes = useCallback(async (rows: SpeedProbeRow[]) => {
|
||||||
if (!liveApi) return
|
if (!liveApi) return
|
||||||
@@ -1584,18 +1572,12 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
|||||||
void saveProbeForm(() => persistProbes(next), () => setProbes(next))
|
void saveProbeForm(() => persistProbes(next), () => setProbes(next))
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteProbe = (id: string) => {
|
const deleteProbe = (id: string) => {
|
||||||
if (!requireWrite()) return
|
if (!requireWrite()) return
|
||||||
if (!liveApi) {
|
const probe = probes.find(p => p.id === id)
|
||||||
const s = readMockDashboardStarIds()
|
if (!probe) return
|
||||||
s.delete(id)
|
setProbeSaveError(null)
|
||||||
writeMockDashboardStarIds(s)
|
setDeleteTarget({ kind: "ping", id, label: probe.name + " → " + probe.target })
|
||||||
}
|
|
||||||
setProbes((prev) => {
|
|
||||||
const next = prev.filter((x) => x.id !== id)
|
|
||||||
void persistProbes(next).catch(error => setOpError(serverOperationError(error)))
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCollapse = (serverId: string) =>
|
const toggleCollapse = (serverId: string) =>
|
||||||
@@ -1943,10 +1925,27 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
|||||||
|
|
||||||
const deleteSpeedProbe = (id: string) => {
|
const deleteSpeedProbe = (id: string) => {
|
||||||
if (!requireWrite()) return
|
if (!requireWrite()) return
|
||||||
setSpeedProbes((prev) => {
|
const probe = speedProbes.find(p => p.id === id)
|
||||||
const next = prev.filter((p) => p.id !== id)
|
if (!probe) return
|
||||||
void persistSpeedProbes(next).catch(error => setSpeedError(serverOperationError(error)))
|
const name = (id: string) => allServers.find(s => s.id === id)?.name ?? id
|
||||||
return next
|
setProbeSaveError(null)
|
||||||
|
setDeleteTarget({ kind: "speed", id, label: name(probe.srcServerId) + " → " + name(probe.dstServerId) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmProbeDelete = () => {
|
||||||
|
if (!deleteTarget || !requireWrite()) return
|
||||||
|
const { kind, id } = deleteTarget
|
||||||
|
const nextPing = probes.filter(p => p.id !== id)
|
||||||
|
const nextSpeed = speedProbes.filter(p => p.id !== id)
|
||||||
|
void saveProbeForm(() => kind === "ping" ? persistProbes(nextPing) : persistSpeedProbes(nextSpeed), () => {
|
||||||
|
if (kind === "ping") {
|
||||||
|
setProbes(nextPing)
|
||||||
|
if (!liveApi) {
|
||||||
|
const ids = readMockDashboardStarIds(); ids.delete(id); writeMockDashboardStarIds(ids)
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new Event(UPTIME_PROBES_CHANGED))
|
||||||
|
} else setSpeedProbes(nextSpeed)
|
||||||
|
setDeleteTarget(null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2002,9 +2001,9 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{!canWrite && <ContextAlert variant="info" role="status"><ContextTitle>Только просмотр</ContextTitle><ContextDescription>Изменение проб и запуск измерений требуют разрешения на запись. Обновление данных загружает сохранённые результаты.</ContextDescription></ContextAlert>}
|
{!canWrite && <ContextAlert variant="info" role="status"><ContextTitle>Только просмотр</ContextTitle><ContextDescription>Изменение проб и запуск измерений требуют разрешения на запись. Обновление данных загружает сохранённые результаты.</ContextDescription></ContextAlert>}
|
||||||
{permissionError && !sheetOpen && !speedSheetOpen && <ContextAlert variant="destructive" role="alert"><ContextDescription>{permissionError}</ContextDescription></ContextAlert>}
|
{permissionError && !sheetOpen && !speedSheetOpen && !deleteTarget && <ContextAlert variant="destructive" role="alert"><ContextDescription>{permissionError}</ContextDescription></ContextAlert>}
|
||||||
|
|
||||||
{probeSaveError && !sheetOpen && !speedSheetOpen && <ContextAlert variant="destructive" role="alert"><ContextDescription>{probeSaveError}</ContextDescription></ContextAlert>}
|
{probeSaveError && !sheetOpen && !speedSheetOpen && !deleteTarget && <ContextAlert variant="destructive" role="alert"><ContextDescription>{probeSaveError}</ContextDescription></ContextAlert>}
|
||||||
|
|
||||||
{/* ── Live indicator ── */}
|
{/* ── Live indicator ── */}
|
||||||
{!isPaused && (
|
{!isPaused && (
|
||||||
@@ -2957,6 +2956,19 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
|||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
<AlertDialog open={deleteTarget !== null} onOpenChange={open => { if (!open && !savingRef.current) setDeleteTarget(null) }}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Удалить пробу?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>Проба «{deleteTarget?.label}» будет удалена из мониторинга.</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
{(probeSaveError || permissionError) && <p role="alert" className="text-sm text-destructive">{probeSaveError || permissionError}</p>}
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button type="button" variant="outline" disabled={probeSaving} onClick={() => setDeleteTarget(null)}>Отмена</Button>
|
||||||
|
<Button type="button" variant="destructive" disabled={probeSaving || !canWrite} onClick={confirmProbeDelete}>{probeSaving ? "Удаление…" : "Удалить"}</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -21,7 +21,7 @@
|
|||||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
||||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; переключатели подтверждаются API; далее удаление и звезда |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; переключатели подтверждаются API; удаление и звезда подтверждаются API; далее конкуренция измерений |
|
||||||
| 8 | Трафик и статистика | Не начат |
|
| 8 | Трафик и статистика | Не начат |
|
||||||
| 9 | VPN, Firewall и Filters | Не начат |
|
| 9 | VPN, Firewall и Filters | Не начат |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||||
@@ -29,6 +29,15 @@
|
|||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 7.4Б2 — удаление и звезда (2026-09-21)
|
||||||
|
|
||||||
|
- После сброса пятичасовой квоты создан ранее отложенный коммит 4f23505. Предыдущий git add не выполнялся: auto-review не смог завершиться из-за лимита; повтор выполнен после восстановления квоты.
|
||||||
|
- Ping/Speed удаляются через существующий AlertDialog с названием/целью. Отмена не пишет данные; подтверждение выполняет PUT через общий gate. Во время запроса закрытие/повтор заблокированы; 403 оставляет диалог и строку, успех удаляет строку. Повторная проверка прав выполняется в обработчике.
|
||||||
|
- Звезда дашборда теперь применяет состояние после PATCH и сообщает ошибку, сохраняя прежнее значение. PATCH имеет timeout 30 секунд и использует тот же gate, что формы/переключатели/удаление; обновление mock-звёзд и событие дашборда отправляются после успеха.
|
||||||
|
- Проверки: все 30 E2E uptime-links прошли, включая 3 новых сценария удаления/звезды, прежние формы/права/mobile/ссылки. TypeScript прошёл; lint теста чистый, uptime — прежние 1 set-state-in-effect и 6 unused warnings. ReUI audit: переиспользован AlertDialog из серверного сценария, структура установленных компонентов и тема не изменены. Production build прошёл: 34 статические страницы.
|
||||||
|
- Осталось 7.4Б3: общий порядок записи и запущенных измерений (runSpeedTest, collect-now, collect-group), блокировка повторного запуска, поздние ответы после смены контекста. PUT/PATCH CRUD теперь сериализованы между собой, но запуск измерения пока не занимает общий gate. Далее URL-контекст мониторинга и приёмка 7.5.
|
||||||
|
- Проверки — HTTP-моки, реальные JWT/RouterOS не проверялись. Контроль квоты: 12% пятичасовой / 94% недельной израсходовано. Коммит локальный без push.
|
||||||
|
|
||||||
## Блок 7.4Б1 — подтверждённые переключатели (2026-09-21)
|
## Блок 7.4Б1 — подтверждённые переключатели (2026-09-21)
|
||||||
|
|
||||||
- По прямому поручению пользователя работа продолжена ниже резерва. Ping enabled и Speed update теперь используют общий saveProbeForm: синхронная блокировка, timeout, защита от устаревшего чтения, применение строки только после PUT. Ошибка видна вне Sheet и не сбрасывается фоновым GET.
|
- По прямому поручению пользователя работа продолжена ниже резерва. Ping enabled и Speed update теперь используют общий saveProbeForm: синхронная блокировка, timeout, защита от устаревшего чтения, применение строки только после PUT. Ошибка видна вне Sheet и не сбрасывается фоновым GET.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ URL — источник сохранённого состояния. Back/Forwa
|
|||||||
3. **7.4 — формы, детали и полный контекст мониторинга.** Восстановление вкладки/фильтров из URL, понятные единицы и подписи, возврат к списку после действия. Не дублировать сохранённые исправления этапа 1; проверить загрузку, отмену, 400/403/500, timeout и повторное нажатие.
|
3. **7.4 — формы, детали и полный контекст мониторинга.** Восстановление вкладки/фильтров из URL, понятные единицы и подписи, возврат к списку после действия. Не дублировать сохранённые исправления этапа 1; проверить загрузку, отмену, 400/403/500, timeout и повторное нажатие.
|
||||||
4. **7.5 — приёмка этапа.** Сценарий «проблема → сервер/проверка → действие → возврат» с сохранением контекста; профильные E2E, права в HTTP-моках, TypeScript, lint и build. Реальные JWT/RouterOS отдельно в тестовом окружении.
|
4. **7.5 — приёмка этапа.** Сценарий «проблема → сервер/проверка → действие → возврат» с сохранением контекста; профильные E2E, права в HTTP-моках, TypeScript, lint и build. Реальные JWT/RouterOS отдельно в тестовом окружении.
|
||||||
|
|
||||||
Следующий исполняемый шаг: блок 7.4Б2 — подтверждение удаления, звезда и конкурирующие записи Ping/Speed. Переключатели (7.4Б1) уже применяются только после API. Ограничения прав и сохранение обеих форм после API выполнены; ошибки 400/403/500 и timeout сохраняют черновик. Оптимистичные row-handlers ещё требуют доработки. Серверная часть 7.3А выполнена: can(mm:servers:write), ограничения кнопок, проверки обработчиков и 5 E2E прав. Codec URL серверов, hook и подключение к ServersPageContent выполнены. Полную регрессию всей системы здесь не повторять без изменений общей основы или найденной регрессии.
|
Следующий исполняемый шаг: блок 7.4Б3 — конкуренция CRUD с измерениями Speed/Ping, повторные запуски и поздние ответы. Переключатели, удаление с подтверждением и звезда (7.4Б1–Б2) уже применяются после API через общий gate. Ограничения прав и сохранение обеих форм после API выполнены; ошибки 400/403/500 и timeout сохраняют черновик. CRUD строк подтверждается API; запуск измерений требует отдельной защиты от конкуренции. Серверная часть 7.3А выполнена: can(mm:servers:write), ограничения кнопок, проверки обработчиков и 5 E2E прав. Codec URL серверов, hook и подключение к ServersPageContent выполнены. Полную регрессию всей системы здесь не повторять без изменений общей основы или найденной регрессии.
|
||||||
|
|
||||||
## Решение по синхронизации URL (7.1)
|
## Решение по синхронизации URL (7.1)
|
||||||
|
|
||||||
|
|||||||
@@ -303,3 +303,71 @@ for (const speed of [false, true]) {
|
|||||||
expect(calls).toBe(2)
|
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)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user