diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 7cf7d65..a4c4940 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -19,6 +19,7 @@ - Budget : une catégorie dont le parent a été supprimé ou est introuvable ne disparaît plus de la grille Budget ni du rapport Budget vs réel : elle s'affiche au premier niveau, comme dans l'arbre des catégories (#393). - Import : les messages de « Créer un dossier d'exemple » suivent désormais la langue de l'application : un dossier d'exemple déjà présent dans le dossier d'import (nommé, avec la marche à suivre : actualiser la liste, ou le renommer s'il n'y apparaît pas), un dossier d'import introuvable, une date du jour illisible. Toute autre erreur reste affichée telle que le système l'a écrite. Une fois les dossiers d'exemple écrits, le message de réussite ne s'affiche que si le dossier d'import a pu être relu ; sinon le message d'erreur s'affiche seul, au lieu des deux à la fois (#384). - Import : après le choix d'un autre dossier d'import, l'avertissement des fichiers posés directement dans le dossier d'import ne nomme plus les fichiers de l'ancien dossier sous le nom du nouveau, pendant la lecture du nouveau dossier ou quand il ne peut pas être lu (#446). +- Import : un message d'erreur ne reste plus affiché une fois le dossier d'import relu, après « Actualiser » ou après le choix d'un autre dossier par « Parcourir » ; le message d'un dossier d'exemple déjà présent dans le dossier d'import demandait justement d'actualiser. Annuler le choix du dossier garde le message (#384). ## [0.17.2] - 2026-10-18 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d73785..c240086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Budget: a category whose parent was deleted or cannot be found no longer disappears from the Budget grid and the Budget vs Actual report: it now shows at the top level, as in the category tree (#393). - Import: the messages of “Create a sample folder” now follow the app's language: a sample folder already in the import folder (named, with what to do: refresh the list, or rename it if it does not show up), an import folder that cannot be found, today's date unreadable. Any other error still shows as the system wrote it. Once the sample folders are written, the success message only shows when the import folder could be read again; otherwise the error message shows alone, instead of both at once (#384). - Import: after you choose another import folder, the warning about the files placed directly in the import folder no longer names the previous folder's files under the new folder's name, while the new folder is being read or when it cannot be read (#446). +- Import: an error message no longer stays on screen once the import folder is read again, after “Refresh” or after choosing another folder with “Browse”; the message of a sample folder already in the import folder asked for that very refresh. Cancelling the folder choice keeps the message (#384). ## [0.17.2] - 2026-10-18 diff --git a/src/hooks/useImportWizard.samples.test.ts b/src/hooks/useImportWizard.samples.test.ts index 5b10706..42b64b5 100644 --- a/src/hooks/useImportWizard.samples.test.ts +++ b/src/hooks/useImportWizard.samples.test.ts @@ -10,14 +10,21 @@ // (a failed rescan leaves its error alone on screen), a refusal of the Rust // command shown by its translation key and values, any other message raw, and // the Journal line of a refusal, which never carries the import folder's path -// or name (the Journal travels with a feedback report). +// or name (the Journal travels with a feedback report). Then the banner of a +// refusal once the folder is read again, by « Actualiser » (`refreshFolder`) +// or by another folder (`browseFolder`, which sets the folder before its +// scan, so the root files shown are the new folder's). import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("react", async (orig) => (await import("../test/hookHarness")).fakeReact(await orig())); const ipc = vi.hoisted(() => ({ /** The stored import folder: its name must never reach the Journal. */ importFolder: "/home/jean/Relevés bancaires", - /** What each command answers, in call order: a value, or an Error or a string to reject with. */ + /** + * What each command answers, in call order: a value, or an Error or a string + * to reject with; `{ resolves: "…" }` for a command that resolves a string + * (`pick_folder`). + */ answers: {} as Record, })); vi.mock("@tauri-apps/api/core", () => ({ @@ -26,6 +33,7 @@ vi.mock("@tauri-apps/api/core", () => ({ if (queue.length === 0) throw new Error(`unexpected command ${command}`); const answer = queue.shift(); if (answer instanceof Error || typeof answer === "string") throw answer; + if (typeof answer === "object" && answer !== null && "resolves" in answer) return answer.resolves; return answer; }), })); @@ -174,3 +182,62 @@ describe("createSampleFolders — a refusal (#384, D2)", () => { expect(journal().join("\n")).not.toContain("/home/jean"); }); }); + +// The banner of a refusal asks to refresh the list: « Actualiser », or a new +// folder, reads the folder again and takes the banner away, unless that +// reading fails and puts up its own (#384 review). `browseFolder` sets the +// folder before its scan, so the scan's root files are the new folder's +// (#446 review). +describe("the error banner when the folder is read again (#384)", () => { + const OTHER_FOLDER = "/home/jean/Autres relevés"; + + /** A refused creation, then the next render of the wizard. */ + async function refused() { + ipc.answers.create_sample_import_folders = ["sample_folder_exists:Exemple Desjardins"]; + const state = await createSamples(); + expect(state.error).toBe("import.empty.sampleErrors.folderExists"); + return harness.render(useImportWizard); + } + + it("« Actualiser » that goes through takes the banner away", async () => { + const wizard = await refused(); + ipc.answers.scan_import_folder.push(SAMPLE_SCAN); + await wizard.refreshFolder(); + const state = harness.render(useImportWizard).state; + expect(state.error).toBeNull(); + expect(state.errorValues).toBeNull(); + expect(state.scannedSources).toBe(SAMPLE_SCAN.sources); + }); + + it("« Actualiser » that fails puts up its own error instead", async () => { + const wizard = await refused(); + ipc.answers.scan_import_folder.push(new Error(`Folder does not exist: ${IMPORT_FOLDER}`)); + await wizard.refreshFolder(); + const state = harness.render(useImportWizard).state; + expect(state.error).toBe(`Folder does not exist: ${IMPORT_FOLDER}`); + expect(state.errorValues).toBeNull(); + }); + + it("another folder takes the banner away, and shows its own root files", async () => { + const wizard = await refused(); + ipc.answers.pick_folder = [{ resolves: OTHER_FOLDER }]; + ipc.answers.scan_import_folder.push({ sources: [], root_files: ["releve-4510.csv"] }); + await wizard.browseFolder(); + const state = harness.render(useImportWizard).state; + expect(state.importFolder).toBe(OTHER_FOLDER); + expect(state.error).toBeNull(); + expect(state.errorValues).toBeNull(); + expect(state.rootFiles).toEqual(["releve-4510.csv"]); + expect(vi.mocked(invoke)).toHaveBeenCalledWith("scan_import_folder", { folderPath: OTHER_FOLDER }); + }); + + it("a cancelled picker keeps the folder and its banner", async () => { + const wizard = await refused(); + ipc.answers.pick_folder = [null]; + await wizard.browseFolder(); + const state = harness.render(useImportWizard).state; + expect(state.importFolder).toBe(IMPORT_FOLDER); + expect(state.error).toBe("import.empty.sampleErrors.folderExists"); + expect(state.errorValues).toEqual({ name: "Exemple Desjardins" }); + }); +}); diff --git a/src/hooks/useImportWizard.ts b/src/hooks/useImportWizard.ts index 8a5ea69..0ac2920 100644 --- a/src/hooks/useImportWizard.ts +++ b/src/hooks/useImportWizard.ts @@ -460,7 +460,13 @@ export function useImportWizard() { try { const folder = await invoke("pick_folder"); if (folder) { + // Another folder: the error banner of the previous one goes, as in + // `selectSource` (#384 review); a failed scan puts up its own. A + // cancelled picker changes nothing, so its banner stays. + dispatch({ type: "SET_ERROR", payload: null }); await setImportFolder(folder); + // Before the scan (#446): `SET_IMPORT_FOLDER` empties the root files, + // which the scan of the new folder then fills. dispatch({ type: "SET_IMPORT_FOLDER", payload: folder }); await scanFolderInternal(folder); } @@ -474,6 +480,10 @@ export function useImportWizard() { const refreshFolder = useCallback(async () => { if (state.importFolder) { + // A new reading of the folder: the error banner of an earlier action + // goes (a refused sample folder asks for this very refresh, #384 + // review), and a failed scan puts up its own. + dispatch({ type: "SET_ERROR", payload: null }); await scanFolderInternal(state.importFolder); } }, [state.importFolder, scanFolderInternal]);