51 lines
3.9 KiB
TypeScript
51 lines
3.9 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
import BackupCard from "./components/BackupCard";
|
|
import { I18nProvider } from "./i18n/I18nProvider";
|
|
import { translations } from "./i18n/translations";
|
|
import { api } from "./api";
|
|
|
|
const mockToast = jest.fn(); const mockPrompt = jest.fn();
|
|
jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn() }, getApiErrorMessage: (_error: unknown, fallback: string) => fallback }));
|
|
jest.mock("./toast", () => ({ useToast: () => ({ toast: mockToast }), useOptionalToast: () => null }));
|
|
jest.mock("./prompt", () => ({ usePrompt: () => ({ prompt: mockPrompt }) }));
|
|
const saved={ deletionEnabled: false, deletionStatus: "active", requiredConfirmation: "DELETE owner@example.test", request: null };
|
|
function deferred<T>() { let resolve!: (value: T) => void; const promise=new Promise<T>(yes => { resolve=yes; }); return { promise, resolve }; }
|
|
function mount() { return render(<I18nProvider><BackupCard /></I18nProvider>); }
|
|
beforeEach(() => { jest.clearAllMocks(); window.localStorage.clear(); (api.get as jest.Mock).mockResolvedValue({ data: saved }); (api.post as jest.Mock).mockReset().mockRejectedValue(new Error('Unexpected mutation')); Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: jest.fn(() => 'blob:test') }); Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: jest.fn() }); jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); });
|
|
afterEach(() => jest.restoreAllMocks());
|
|
|
|
test("[P7-09 backup] unknown lifecycle never claims disabled and Retry only reads", async () => {
|
|
(api.get as jest.Mock).mockRejectedValueOnce(new Error("offline")); mount();
|
|
const retry=await screen.findByRole('button', { name: translations.en.retry });
|
|
expect(screen.queryByText(translations.en.accountDeleteDisabled)).not.toBeInTheDocument();
|
|
retry.focus(); fireEvent.click(retry); await screen.findByText(translations.en.accountDeleteDisabled);
|
|
expect(screen.getByRole('group', { name: translations.en.backupTitle })).toHaveFocus();
|
|
expect(api.get).toHaveBeenCalledTimes(2); expect(api.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("[P7-09 backup] synchronous duplicate and competing downloads admit only one request", async () => {
|
|
const held=deferred<unknown>(); (api.post as jest.Mock).mockReturnValue(held.promise); mount();
|
|
await screen.findByText(translations.en.accountDeleteDisabled);
|
|
const readable=screen.getByRole('button', { name: translations.en.accountExportDownload }); const encrypted=screen.getByRole('button', { name: translations.en.backupDownload });
|
|
act(() => { readable.click(); readable.click(); encrypted.click(); });
|
|
expect(api.post).toHaveBeenCalledTimes(1);
|
|
await act(async () => held.resolve({ data: new Blob(['zip']), headers: {} }));
|
|
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("[P7-09 backup] disposal rejects a late readable export without downloading private bytes or notifying", async () => {
|
|
const held=deferred<unknown>(); (api.post as jest.Mock).mockReturnValue(held.promise); const view=mount();
|
|
fireEvent.click(screen.getByRole('button', { name: translations.en.accountExportDownload })); view.unmount();
|
|
await act(async () => held.resolve({ data: new Blob(['private zip']), headers: {} }));
|
|
expect(URL.createObjectURL).not.toHaveBeenCalled(); expect(mockToast).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("[P7-09 backup] disposal during the existing deletion prompt cannot issue a later delete", async () => {
|
|
(api.get as jest.Mock).mockResolvedValue({ data: { ...saved, deletionEnabled: true } });
|
|
const held=deferred<string>(); mockPrompt.mockReturnValue(held.promise); const view=mount();
|
|
fireEvent.click(await screen.findByRole('button', { name: translations.en.accountDeleteButton })); view.unmount();
|
|
await act(async () => held.resolve(saved.requiredConfirmation)); expect(api.post).not.toHaveBeenCalled();
|
|
});
|