Simpl-Resultat/src/hooks/useImportWizard.mount.test.ts
le king fu d024f5aeb9 fix(import): mark the import folder read once its mount read is over
The wizard starts with `importFolder` null for lack of reading, which the
Import page took for "no folder": the "No import folder" empty state
flashed before the stored folder came back (#384).

`importFolderLoaded` is set in the `finally` of the mount read, after the
first scan, so neither variant of the empty state flashes, and it is kept
by RESET, which rebuilds from the initial state while the mount read
never runs again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:34:37 -04:00

128 lines
4.5 KiB
TypeScript

// useImportWizard — the folder read at mount marks itself done (#384, #398).
//
// No renderer in this project, and the reducer alone cannot reach the mount
// read: it is an effect of the hook. React's hooks used by `useImportWizard`
// are stubbed (technique of useImportWizard.executeImport.test.ts): the hook
// runs as a plain function, its effect is captured and run by hand, and the
// services it calls are mocked. What is pinned here is the `finally`: the
// folder counts as read once the read and its scan are over, success or not.
import { describe, it, expect, vi, beforeEach } from "vitest";
const harness = vi.hoisted(() => ({
state: null as unknown,
dispatch: vi.fn(),
effects: [] as Array<() => void>,
}));
vi.mock("react", async (importOriginal) => {
const actual = await importOriginal<typeof import("react")>();
return {
...actual,
useReducer: () => [harness.state, harness.dispatch],
useCallback: (fn: unknown) => fn,
useEffect: (fn: () => void) => {
harness.effects.push(fn);
},
};
});
vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn() }));
vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() }));
vi.mock("../services/userPreferenceService", () => ({
getImportFolder: vi.fn(),
setImportFolder: vi.fn(),
}));
vi.mock("../services/importSourceService", () => ({
getAllSources: vi.fn(async () => []),
getSourceByName: vi.fn(async () => null),
createSource: vi.fn(),
updateSource: vi.fn(),
}));
vi.mock("../services/importedFileService", () => ({
existsByHash: vi.fn(),
createImportedFile: vi.fn(),
updateFileStatus: vi.fn(),
getFilesBySourceId: vi.fn(async () => []),
}));
vi.mock("../services/importConfigTemplateService", () => ({
getAllTemplates: vi.fn(async () => []),
createTemplate: vi.fn(),
updateTemplate: vi.fn(),
deleteTemplate: vi.fn(),
}));
import { invoke } from "@tauri-apps/api/core";
import { useImportWizard, initialWizardState } from "./useImportWizard";
import { getImportFolder } from "../services/userPreferenceService";
import type { ScannedSource } from "../shared/types";
const SOURCES: ScannedSource[] = [
{ folder_name: "Desjardins", folder_path: "/releves/Desjardins", files: [] },
];
/** Lets the mount read's promises settle. */
const flush = async () => {
for (let i = 0; i < 20; i++) await Promise.resolve();
};
/** Mounts the hook and runs its mount effect, the first one it registers. */
async function mount() {
useImportWizard();
const [readFolder] = harness.effects;
readFolder();
await flush();
}
const types = () => harness.dispatch.mock.calls.map((c) => (c[0] as { type: string }).type);
beforeEach(() => {
harness.state = initialWizardState;
harness.effects = [];
harness.dispatch.mockReset();
vi.mocked(getImportFolder).mockReset();
vi.mocked(invoke).mockReset();
});
describe("useImportWizard — the mount read marks the folder read (#384, #398)", () => {
it("with a folder: after the folder and its first scan, as the last dispatch", async () => {
vi.mocked(getImportFolder).mockResolvedValue("/releves");
vi.mocked(invoke).mockResolvedValue(SOURCES);
await mount();
expect(invoke).toHaveBeenCalledWith("scan_import_folder", { folderPath: "/releves" });
const order = types();
expect(order[0]).toBe("SET_IMPORT_FOLDER");
expect(order.indexOf("SET_SCANNED_SOURCES")).toBeGreaterThan(0);
expect(order[order.length - 1]).toBe("SET_IMPORT_FOLDER_LOADED");
expect(order.filter((t) => t === "SET_IMPORT_FOLDER_LOADED")).toHaveLength(1);
});
it("without a folder: marked read, nothing scanned", async () => {
vi.mocked(getImportFolder).mockResolvedValue(null);
await mount();
expect(invoke).not.toHaveBeenCalled();
expect(harness.dispatch.mock.calls.map((c) => c[0])).toEqual([
{ type: "SET_IMPORT_FOLDER", payload: null },
{ type: "SET_IMPORT_FOLDER_LOADED" },
]);
});
it("when the preference cannot be read: still marked read (the empty state then offers a folder)", async () => {
vi.mocked(getImportFolder).mockRejectedValue(new Error("database is locked"));
await mount();
expect(types()).toEqual(["SET_IMPORT_FOLDER_LOADED"]);
});
it("when the scan fails: marked read after the error", async () => {
vi.mocked(getImportFolder).mockResolvedValue("/releves");
vi.mocked(invoke).mockRejectedValue(new Error("folder not found"));
await mount();
const order = types();
expect(order).toContain("SET_ERROR");
expect(order[order.length - 1]).toBe("SET_IMPORT_FOLDER_LOADED");
});
});