262 lines
22 KiB
TypeScript
262 lines
22 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
|
|
import { api } from "./api";
|
|
import { I18nProvider } from "./i18n/I18nProvider";
|
|
import OperationsPage from "./views/OperationsPage";
|
|
import { translations } from "./i18n/translations";
|
|
|
|
jest.mock("./api", () => ({
|
|
api: {
|
|
get: jest.fn(),
|
|
post: jest.fn(),
|
|
delete: jest.fn(),
|
|
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
|
|
},
|
|
getApiErrorMessage: (_error: unknown, fallback?: string) => fallback || "Request failed.",
|
|
}));
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
function renderPage() {
|
|
return render(<I18nProvider><OperationsPage /></I18nProvider>);
|
|
}
|
|
|
|
const operation = {
|
|
id: "11111111-1111-1111-1111-111111111111",
|
|
taskType: "strategy_snapshot",
|
|
status: "running",
|
|
createdAtUtc: "2026-08-02T12:00:00Z",
|
|
progressStage: "Analysing role",
|
|
progressPercent: 40,
|
|
canCancel: true,
|
|
canRetry: false,
|
|
};
|
|
|
|
const notification = {
|
|
id: "22222222-2222-2222-2222-222222222222",
|
|
operationId: operation.id,
|
|
kind: "operation_failed",
|
|
title: "Operation failed",
|
|
message: "A background operation failed. Review it for details.",
|
|
createdAtUtc: "2026-08-02T12:05:00Z",
|
|
readAtUtc: null,
|
|
};
|
|
|
|
let serverOperations: any[];
|
|
let serverNotifications: any[];
|
|
function deferred<T = any>() {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (cause: unknown) => void;
|
|
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
async function language(value: "en" | "nb") {
|
|
await act(async () => window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: value })));
|
|
}
|
|
function response(url: string) { return { data: url.startsWith("/operations") ? serverOperations : serverNotifications } as any; }
|
|
function activity() { return screen.getByRole("region", { name: "Recent activity" }); }
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
window.localStorage.clear();
|
|
document.documentElement.lang = "en";
|
|
serverOperations = [{ ...operation }]; serverNotifications = [{ ...notification }];
|
|
mockedApi.get.mockReset().mockImplementation((url: string) => Promise.resolve(response(url)));
|
|
mockedApi.post.mockReset().mockImplementation((url: string) => {
|
|
if (url.startsWith("/operations")) {
|
|
serverOperations = serverOperations.map(item => ({ ...item, status: url.endsWith("/cancel") ? "cancelled" : "queued", canCancel: url.endsWith("/retry"), canRetry: url.endsWith("/cancel") }));
|
|
return Promise.resolve({ data: serverOperations[0] } as any);
|
|
}
|
|
serverNotifications = serverNotifications.map(item => ({ ...item, readAtUtc: "2026-08-02T12:06:00Z" }));
|
|
return Promise.resolve({ data: undefined } as any);
|
|
});
|
|
mockedApi.delete.mockReset().mockImplementation(() => { serverNotifications = []; return Promise.resolve({ data: undefined } as any); });
|
|
});
|
|
afterEach(() => { jest.useRealTimers(); jest.restoreAllMocks(); });
|
|
|
|
test("shows persistent operation and notification states with accessible actions", async () => {
|
|
renderPage();
|
|
|
|
expect(await screen.findByText("strategy_snapshot")).toBeInTheDocument();
|
|
const task = screen.getByRole("article", { name: operation.taskType });
|
|
expect(task).toHaveAccessibleName(operation.taskType);
|
|
expect(task).toHaveAttribute("data-operation-id", operation.id);
|
|
expect(screen.getByText("Analysing role")).toBeInTheDocument();
|
|
expect(screen.getByRole("progressbar", { name: "strategy_snapshot progress" })).toHaveAttribute("aria-valuenow", "40");
|
|
expect(screen.getByText("Operation failed")).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(`/operations/${operation.id}/cancel`));
|
|
|
|
const markRead = screen.getByRole("button", { name: "Mark read" });
|
|
await waitFor(() => expect(markRead).toBeEnabled());
|
|
fireEvent.click(markRead);
|
|
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(`/notifications/${notification.id}/read`));
|
|
|
|
const dismiss = screen.getByRole("button", { name: "Dismiss" });
|
|
await waitFor(() => expect(dismiss).toBeEnabled());
|
|
fireEvent.click(dismiss);
|
|
await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith(`/notifications/${notification.id}`));
|
|
});
|
|
|
|
test("shows honest empty and failure states", async () => {
|
|
mockedApi.get.mockResolvedValueOnce({ data: [] } as any).mockResolvedValueOnce({ data: [] } as any);
|
|
const { unmount } = renderPage();
|
|
expect(await screen.findByText("No notifications.")).toBeInTheDocument();
|
|
expect(screen.getByText("No background operations yet.")).toBeInTheDocument();
|
|
unmount();
|
|
|
|
mockedApi.get.mockRejectedValue(new Error("offline"));
|
|
renderPage();
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded.");
|
|
expect(screen.queryByText("No notifications.")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("No background operations yet.")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("localizes operation controls and empty states in Norwegian Bokmål", async () => {
|
|
window.localStorage.setItem("uiLanguage", "nb");
|
|
mockedApi.get.mockResolvedValue({ data: [] } as any);
|
|
|
|
renderPage();
|
|
|
|
expect(await screen.findByText("Ingen varsler.")).toBeInTheDocument();
|
|
expect(screen.getByText("Ingen bakgrunnsoperasjoner ennå.")).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Oppdater" })).toBeInTheDocument();
|
|
});
|
|
|
|
test("[P7-08 operations] initial failure offers single-flight GET Retry without false empty or writes", async () => {
|
|
mockedApi.get.mockRejectedValue(new Error("offline")); renderPage();
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded.");
|
|
expect((mockedApi.get.mock.calls[0][1]?.signal as AbortSignal).aborted).toBe(true);
|
|
expect(screen.queryByText("No notifications.")).not.toBeInTheDocument(); expect(screen.queryByText("No background operations yet.")).not.toBeInTheDocument();
|
|
const pending = deferred(); mockedApi.get.mockImplementation(() => pending.promise);
|
|
const retry = screen.getByRole("button", { name: "Retry" }); act(() => { retry.focus(); fireEvent.click(retry); fireEvent.click(retry); });
|
|
expect(mockedApi.get).toHaveBeenCalledTimes(4); expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled();
|
|
await act(async () => pending.resolve({ data: [] })); expect(await screen.findByText("No notifications.")).toBeVisible();
|
|
expect(screen.getByRole("heading", { name: "Recent activity", level: 2 })).toHaveFocus(); expect(activity()).toHaveAttribute("aria-busy", "false");
|
|
expect(mockedApi.get).toHaveBeenCalledWith("/operations?limit=50", expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
expect(mockedApi.get).toHaveBeenCalledWith("/notifications?limit=50", expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
});
|
|
|
|
test("[P7-08 operations] cached refresh failure preserves known rows and does not fabricate empty results", async () => {
|
|
renderPage(); expect(await screen.findByText(operation.taskType)).toBeVisible(); mockedApi.get.mockRejectedValue(new Error("offline"));
|
|
fireEvent.click(screen.getByRole("button", { name: "Refresh" })); expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded.");
|
|
expect(screen.getByText(operation.taskType)).toBeVisible(); expect(screen.getByText(notification.title)).toBeVisible();
|
|
expect(screen.queryByText("No notifications.")).not.toBeInTheDocument(); expect(screen.queryByText("No background operations yet.")).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test.each(["Cancel", "Retry", "Mark read", "Dismiss"])("[P7-08 operations] same-render duplicate %s reserves exactly one mutation and blocks competing work", async label => {
|
|
if (label === "Retry") serverOperations = [{ ...operation, status: "cancelled", canCancel: false, canRetry: true }];
|
|
const pending = deferred(); mockedApi.post.mockImplementation(() => pending.promise); mockedApi.delete.mockImplementation(() => pending.promise);
|
|
renderPage(); const button = await screen.findByRole("button", { name: label });
|
|
act(() => { fireEvent.click(button); fireEvent.click(button); fireEvent.click(screen.getByRole("button", { name: label === "Dismiss" ? "Mark read" : "Dismiss" })); });
|
|
expect(mockedApi.post.mock.calls.length + mockedApi.delete.mock.calls.length).toBe(1); expect(screen.getByRole("button", { name: "Refresh" })).toBeDisabled();
|
|
await act(async () => pending.resolve({ data: label === "Cancel" || label === "Retry" ? { ...serverOperations[0], canCancel: false, canRetry: false } : undefined }));
|
|
await waitFor(() => expect(screen.getByRole("button", { name: "Refresh" })).toBeEnabled());
|
|
});
|
|
|
|
test("[P7-08 operations] language and ordinary rerenders reformat cached data without refetch or polling restart", async () => {
|
|
const intervals = jest.spyOn(window, "setInterval"); const cleared = jest.spyOn(window, "clearInterval");
|
|
serverOperations = [{ ...operation, taskType: "strategy.snapshot", status: "waiting_for_retry" }]; const { rerender } = renderPage();
|
|
expect(await screen.findByText("Strategy snapshot")).toBeVisible(); expect(screen.getByText("Waiting to retry")).toBeVisible(); expect(mockedApi.get).toHaveBeenCalledTimes(2);
|
|
const starts = intervals.mock.calls.length; const pollId = intervals.mock.results[intervals.mock.calls.findIndex(([, delay]) => delay === 15000)].value; await language("nb");
|
|
expect(screen.getByText("Strategioversikt")).toBeVisible(); expect(screen.getByText("Venter på nytt forsøk")).toBeVisible();
|
|
expect(screen.getByText(`Opprettet ${new Date(operation.createdAtUtc).toLocaleString("nb-NO")}`)).toBeVisible();
|
|
rerender(<I18nProvider><OperationsPage /></I18nProvider>); expect(mockedApi.get).toHaveBeenCalledTimes(2); expect(intervals).toHaveBeenCalledTimes(starts); expect(cleared).not.toHaveBeenCalledWith(pollId);
|
|
});
|
|
|
|
test("[P7-08 operations] 15-second polling is single-flight and resumes after the held pair completes", async () => {
|
|
jest.useFakeTimers(); renderPage(); await act(async () => {}); expect(mockedApi.get).toHaveBeenCalledTimes(2);
|
|
const pending = deferred(); mockedApi.get.mockImplementation(() => pending.promise);
|
|
await act(async () => jest.advanceTimersByTime(15000)); expect(mockedApi.get).toHaveBeenCalledTimes(4);
|
|
await act(async () => jest.advanceTimersByTime(30000)); expect(mockedApi.get).toHaveBeenCalledTimes(4);
|
|
await act(async () => pending.resolve({ data: [] })); mockedApi.get.mockImplementation((url: string) => Promise.resolve(response(url)));
|
|
await act(async () => jest.advanceTimersByTime(15000)); expect(mockedApi.get).toHaveBeenCalledTimes(6); expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("[P7-08 operations] mutation invalidates a held old poll before adopting the returned same-ID DTO", async () => {
|
|
jest.useFakeTimers(); renderPage(); await act(async () => {}); const oldOperations = deferred(); const oldNotices = deferred();
|
|
mockedApi.get.mockImplementationOnce(() => oldOperations.promise).mockImplementationOnce(() => oldNotices.promise);
|
|
await act(async () => jest.advanceTimersByTime(15000)); const signal = mockedApi.get.mock.calls[2][1]?.signal as AbortSignal; expect(signal).toBeInstanceOf(AbortSignal);
|
|
const post = deferred(); const refresh = deferred(); mockedApi.post.mockImplementationOnce(() => post.promise); mockedApi.get.mockImplementation(() => refresh.promise);
|
|
fireEvent.click(screen.getByRole("button", { name: "Cancel" })); expect(signal.aborted).toBe(true);
|
|
const cancelled = { ...operation, status: "cancelled", canCancel: false, canRetry: true };
|
|
await act(async () => post.resolve({ data: cancelled })); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(screen.getByRole("article", { name: operation.taskType })).toHaveTextContent("Cancelled");
|
|
await act(async () => { oldOperations.resolve({ data: [{ ...operation, progressStage: "Stale old poll" }] }); oldNotices.resolve({ data: [notification] }); });
|
|
expect(screen.queryByText("Stale old poll")).not.toBeInTheDocument();
|
|
await act(async () => refresh.resolve({ data: [] })); expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("[P7-08 operations] committed cancel plus failed refresh announces truth and GET Retry cannot replay or unlock stale writes", async () => {
|
|
const changed = jest.fn(); window.addEventListener("notifications-changed", changed);
|
|
try { renderPage(); const cancel = await screen.findByRole("button", { name: "Cancel" }); const cancelled = { ...operation, status: "cancelled", canCancel: false, canRetry: true };
|
|
mockedApi.post.mockResolvedValueOnce({ data: cancelled } as any); mockedApi.get.mockRejectedValue(new Error("read offline")); act(() => cancel.focus()); fireEvent.click(cancel);
|
|
const failure = await screen.findByRole("alert"); expect(failure).toHaveTextContent("The action completed, but recent activity could not be refreshed. Refresh before taking another action.");
|
|
const item = screen.getByRole("article", { name: operation.taskType }); expect(within(item).getByRole("button", { name: "Retry" })).toBeDisabled(); expect(item).toHaveTextContent("Cancelled"); expect(changed).toHaveBeenCalledTimes(1);
|
|
expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus();
|
|
serverOperations = [cancelled]; mockedApi.get.mockImplementation((url: string) => Promise.resolve(response(url)));
|
|
fireEvent.click(within(failure).getByRole("button", { name: "Retry" })); await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument());
|
|
expect(mockedApi.post).toHaveBeenCalledTimes(1); expect(mockedApi.delete).not.toHaveBeenCalled(); expect(changed).toHaveBeenCalledTimes(1); expect(within(item).getByRole("button", { name: "Retry" })).toBeEnabled();
|
|
} finally { window.removeEventListener("notifications-changed", changed); }
|
|
});
|
|
|
|
test.each(["Mark read", "Dismiss"])("[P7-08 operations] committed204 %s updates notices locally even when refresh fails without completing a task", async label => {
|
|
const changed = jest.fn(); window.addEventListener("notifications-changed", changed);
|
|
try { renderPage(); const action = await screen.findByRole("button", { name: label }); mockedApi.get.mockRejectedValue(new Error("offline")); fireEvent.click(action);
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("The action completed, but recent activity could not be refreshed.");
|
|
expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument(); if (label === "Dismiss") expect(screen.queryByText(notification.title)).not.toBeInTheDocument(); else expect(screen.getByText(notification.title)).toBeVisible();
|
|
expect(screen.getByRole("article", { name: operation.taskType })).toHaveTextContent("Running"); expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
|
|
expect(mockedApi.post.mock.calls.filter(([url]) => String(url).startsWith("/operations"))).toEqual([]); expect(changed).toHaveBeenCalledTimes(1);
|
|
} finally { window.removeEventListener("notifications-changed", changed); }
|
|
});
|
|
|
|
test("[P7-08 operations] failed POST preserves eligibility and current-locale recovery without false commit or new GET", async () => {
|
|
const pending = deferred(); mockedApi.post.mockImplementationOnce(() => pending.promise); const changed = jest.fn(); window.addEventListener("notifications-changed", changed);
|
|
try { renderPage(); const cancel = await screen.findByRole("button", { name: "Cancel" }); act(() => cancel.focus()); fireEvent.click(cancel); await language("nb"); expect(mockedApi.get).toHaveBeenCalledTimes(2);
|
|
await act(async () => pending.reject(new Error("offline"))); expect(await screen.findByRole("alert")).toHaveTextContent(translations.nb.operationsActionFailed); expect(screen.getByRole("button", { name: translations.nb.cancel })).toBeEnabled(); expect(screen.getByRole("button", { name: translations.nb.cancel })).toHaveFocus(); expect(changed).not.toHaveBeenCalled(); expect(mockedApi.get).toHaveBeenCalledTimes(2);
|
|
await language("en"); expect(screen.getByRole("alert")).toHaveTextContent(translations.en.operationsActionFailed); expect(mockedApi.get).toHaveBeenCalledTimes(2);
|
|
} finally { window.removeEventListener("notifications-changed", changed); }
|
|
});
|
|
|
|
test("[P7-08 operations] server action admission overrides displayed status and unknown metadata remains honest", async () => {
|
|
serverOperations = [{ ...operation, taskType: "Unknown_Task_Type", status: "succeeded", progressStage: "arbitrary-stage", failureCategory: "unmapped_category", canCancel: true, canRetry: false }, { ...operation, id: "foreign-looking-local-fixture", taskType: "cv.process", status: "running", canCancel: false, canRetry: true }, { ...operation, id: "unmapped-prototype-code", taskType: "toString", status: "constructor", canCancel: false, canRetry: false }];
|
|
renderPage(); expect(await screen.findByText("Unknown_Task_Type")).toBeVisible(); const first = screen.getByRole("article", { name: "Unknown_Task_Type" }); expect(first).toHaveAttribute("data-operation-id", operation.id); expect(within(first).getByRole("button", { name: "Cancel" })).toBeEnabled(); expect(within(first).queryByRole("button", { name: "Retry" })).not.toBeInTheDocument(); expect(first).toHaveTextContent("arbitrary-stage"); expect(first).toHaveTextContent("unmapped category");
|
|
const second = screen.getByRole("article", { name: "CV processing" }); expect(second).toHaveAttribute("data-operation-id", "foreign-looking-local-fixture"); expect(within(second).queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(within(second).getByRole("button", { name: "Retry" })).toBeEnabled();
|
|
const unknown = screen.getByRole("article", { name: "toString" }); expect(unknown).toHaveAttribute("data-operation-id", "unmapped-prototype-code"); expect(unknown).toHaveTextContent("constructor"); expect(within(unknown).queryAllByRole("button")).toHaveLength(0);
|
|
});
|
|
|
|
test("[P7-08 operations] unmount aborts a pending read and late completion cannot fetch or publish", async () => {
|
|
const pending = deferred(); mockedApi.get.mockImplementation(() => pending.promise); const { unmount } = renderPage(); const signal = mockedApi.get.mock.calls[0][1]?.signal as AbortSignal; expect(signal).toBeInstanceOf(AbortSignal); unmount(); expect(signal.aborted).toBe(true); await act(async () => pending.resolve({ data: [operation] })); expect(mockedApi.get).toHaveBeenCalledTimes(2); expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("[P7-08 operations] unmount during a write prevents its late follow-up GET, event and focus effects", async () => {
|
|
const changed = jest.fn(); window.addEventListener("notifications-changed", changed); const pending = deferred(); mockedApi.post.mockImplementationOnce(() => pending.promise);
|
|
try { const { unmount } = renderPage(); fireEvent.click(await screen.findByRole("button", { name: "Cancel" })); unmount(); await act(async () => pending.resolve({ data: { ...operation, status: "cancelled" } })); expect(mockedApi.get).toHaveBeenCalledTimes(2); expect(changed).not.toHaveBeenCalled(); } finally { window.removeEventListener("notifications-changed", changed); }
|
|
});
|
|
|
|
test("[P7-08 operations] removing the initiating control restores meaningful focus while deliberate focus elsewhere stays put", async () => {
|
|
const pending = deferred(); mockedApi.post.mockImplementationOnce(() => pending.promise); renderPage(); const mark = await screen.findByRole("button", { name: "Mark read" }); act(() => mark.focus()); fireEvent.click(mark); serverNotifications = [{ ...notification, readAtUtc: "2026-08-02T12:06:00Z" }]; await act(async () => pending.resolve({ data: undefined })); expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus();
|
|
const next = deferred(); mockedApi.delete.mockImplementationOnce(() => next.promise); const dismiss = screen.getByRole("button", { name: "Dismiss" }); act(() => dismiss.focus()); fireEvent.click(dismiss); const external = document.createElement("button"); external.textContent = "Deliberate external target"; document.body.appendChild(external); act(() => external.focus()); serverNotifications = []; await act(async () => next.resolve({ data: undefined })); expect(external).toHaveFocus(); external.remove();
|
|
});
|
|
|
|
test("[P7-08 operations] accepted background polls preserve surviving and deliberate focus and recover removed focused controls", async () => {
|
|
jest.useFakeTimers(); renderPage(); await act(async () => {});
|
|
const cancel = screen.getByRole("button", { name: "Cancel" }); act(() => cancel.focus());
|
|
serverOperations = [{ ...operation, progressStage: "Fresh progress with the same action" }];
|
|
await act(async () => jest.advanceTimersByTime(15000)); expect(cancel).toHaveFocus(); expect(cancel.isConnected).toBe(true);
|
|
const external = document.createElement("button"); external.textContent = "Deliberate target outside recent activity"; document.body.appendChild(external);
|
|
try {
|
|
act(() => external.focus()); serverOperations = [{ ...operation, status: "cancelled", canCancel: false, canRetry: true }];
|
|
await act(async () => jest.advanceTimersByTime(15000)); expect(external).toHaveFocus(); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument();
|
|
serverOperations = [{ ...operation }]; await act(async () => jest.advanceTimersByTime(15000)); expect(external).toHaveFocus();
|
|
act(() => screen.getByRole("button", { name: "Cancel" }).focus()); serverOperations = [{ ...operation, status: "cancelled", canCancel: false, canRetry: true }];
|
|
await act(async () => jest.advanceTimersByTime(15000)); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus();
|
|
act(() => screen.getByRole("button", { name: "Mark read" }).focus()); serverNotifications = [];
|
|
await act(async () => jest.advanceTimersByTime(15000)); expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus();
|
|
expect(mockedApi.get).toHaveBeenCalledTimes(12); expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled();
|
|
} finally { external.remove(); }
|
|
});
|