From a62150d92640d32192280f17ffb25fa591a40734 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sun, 18 Oct 2026 20:59:13 -0400 Subject: [PATCH 1/5] fix(import): empty the root files when another import folder is chosen (#446) SET_IMPORT_FOLDER now empties rootFiles. Until the new folder's scan landed, or for good when that scan failed, the warning about files placed directly in the import folder named the previous folder's files under the new folder's name. scannedSources is left as it was: the source list of the last scan stays until the new one, as before. Reducer tests next to "a scan error leaves the root files of the last scan in place": a folder change empties the root files and keeps the sources until the next scan, and a scan error after a folder change names no root file of the previous folder. Co-Authored-By: Claude Opus 5.5 --- src/hooks/useImportWizard.test.ts | 28 ++++++++++++++++++++++++++++ src/hooks/useImportWizard.ts | 16 +++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/hooks/useImportWizard.test.ts b/src/hooks/useImportWizard.test.ts index 0640038..11f66cc 100644 --- a/src/hooks/useImportWizard.test.ts +++ b/src/hooks/useImportWizard.test.ts @@ -222,6 +222,34 @@ describe("wizardReducer — rootFiles (#427)", () => { const after = wizardReducer(before, { type: "SET_ERROR", payload: "folder not found" }); expect(after.rootFiles).toBe(before.rootFiles); }); + + it("choosing another folder empties the root files until its own scan lands (#446)", () => { + const before = reduce([ + { type: "SET_IMPORT_FOLDER", payload: "/releves" }, + { type: "SET_SCANNED_SOURCES", payload: scan(["a.csv"]) }, + ]); + const changed = wizardReducer(before, { type: "SET_IMPORT_FOLDER", payload: "/autre" }); + // The warning would name "a.csv" of /releves under the name of /autre. + expect(changed.importFolder).toBe("/autre"); + expect(changed.rootFiles).toEqual([]); + // Prior behaviour, kept: the sources of the last scan stay until the new one. + expect(changed.scannedSources).toBe(before.scannedSources); + + const scanned = wizardReducer(changed, { type: "SET_SCANNED_SOURCES", payload: scan(["b.csv"], []) }); + expect(scanned.rootFiles).toEqual(["b.csv"]); + expect(scanned.scannedSources).toEqual([]); + }); + + it("a scan error after a folder change names no root file of the previous folder (#446)", () => { + const after = reduce([ + { type: "SET_IMPORT_FOLDER", payload: "/releves" }, + { type: "SET_SCANNED_SOURCES", payload: scan(["a.csv"]) }, + { type: "SET_IMPORT_FOLDER", payload: "/autre" }, + { type: "SET_LOADING", payload: true }, + { type: "SET_ERROR", payload: "folder not found" }, + ]); + expect(after.rootFiles).toEqual([]); + }); }); describe("sampleFolderError — the refusals of create_sample_import_folders (#384, D2)", () => { diff --git a/src/hooks/useImportWizard.ts b/src/hooks/useImportWizard.ts index 75d22c0..8a5ea69 100644 --- a/src/hooks/useImportWizard.ts +++ b/src/hooks/useImportWizard.ts @@ -110,9 +110,11 @@ export interface WizardState { /** * Names of the CSV/TXT files placed directly at the root of the import * folder, which no source reads (#427). Set with `scannedSources` by the - * same action, so the warning and the source list always come from the same - * scan; kept by `RESET` like them. Shown on screen only, never logged: a - * statement name often carries an account number. + * same action, so the warning and the source list come from the same scan; + * kept by `RESET` like them. Emptied by `SET_IMPORT_FOLDER`, which leaves + * `scannedSources` alone (#446): another folder shows no warning until its + * own scan. Shown on screen only, never logged: a statement name often + * carries an account number. */ rootFiles: string[]; selectedSource: ScannedSource | null; @@ -252,8 +254,12 @@ export function wizardReducer(state: WizardState, action: WizardAction): WizardS case "SET_STEP": return { ...state, step: action.payload }; case "SET_IMPORT_FOLDER": - // The sample-folder banner describes the previous folder: drop it. - return { ...state, importFolder: action.payload, sampleCreated: null }; + // The sample-folder banner describes the previous folder: drop it. So do + // the root files (#446): until the new folder's scan lands, or for good + // if it fails, the warning would name the previous folder's files under + // the new folder's name. The scanned sources stay until that scan, as + // they always did. + return { ...state, importFolder: action.payload, rootFiles: [], sampleCreated: null }; case "SET_IMPORT_FOLDER_LOADED": return { ...state, importFolderLoaded: true }; case "SET_SCANNED_SOURCES": -- 2.45.2 From d551c4b0d49163801d9d4454a39ac1197bc7bbd4 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sun, 18 Oct 2026 20:59:17 -0400 Subject: [PATCH 2/5] test: pin the collapsed sub-group order and the banner's dot (#446) reorderRows: a collapsed sub-group (a subtotal with no child row) followed by a sibling leaf gives Vide, z, A, the order read by running the case first; then the same block followed by a sibling root, a group (Vide, z, A, b, B) or a leaf (Vide, z, A, R). OnboardingBanner: the decorative h-1 w-1 dot is absent while the visit steps are not initialized and while the Budget entitlement is unknown, and present, between the counter and the next step, once the total is known. Co-Authored-By: Claude Opus 5.5 --- .../OnboardingBanner.render.test.tsx | 31 +++++++++++++++++-- src/utils/reorderRows.test.ts | 24 ++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/components/onboarding/OnboardingBanner.render.test.tsx b/src/components/onboarding/OnboardingBanner.render.test.tsx index ece4e51..a369166 100644 --- a/src/components/onboarding/OnboardingBanner.render.test.tsx +++ b/src/components/onboarding/OnboardingBanner.render.test.tsx @@ -96,6 +96,13 @@ const buttonNamed = (node: ReactNode, key: string) => { return button; }; +/** The dot between the counter and the next step: a decorative `h-1 w-1` span. */ +const isDot = (e: AnyElement) => { + const classes = String(e.props.className).split(/\s+/); + return e.type === "span" && classes.includes("h-1") && classes.includes("w-1"); +}; +const dots = (props: OnboardingBannerProps) => find(tree(props), isDot); + describe("OnboardingBanner — banner mode (#397)", () => { it("is a region named for the path, with the counter, the next step and its link", () => { const html = markup(bannerProps()); @@ -119,15 +126,35 @@ describe("OnboardingBanner — banner mode (#397)", () => { }); it("the counter waits for the Budget entitlement, the next step does not", () => { - const html = markup(bannerProps({ budgetReady: false })); + const props = bannerProps({ budgetReady: false }); + const html = markup(props); expect(html).not.toContain("onboarding.banner.progress"); expect(html).toMatch(/href="\/import"/); + // The dot waits with the counter it follows (#446). + expect(dots(props)).toHaveLength(0); }); it("the counter waits for the visit steps' initialization too, the next step does not (#425)", () => { - const html = markup(bannerProps({ visitStepsInitialized: false })); + const props = bannerProps({ visitStepsInitialized: false }); + const html = markup(props); expect(html).not.toContain("onboarding.banner.progress"); expect(html).toMatch(/href="\/import"/); + // No dot either: it would stand alone before the next step (#446). + expect(dots(props)).toHaveLength(0); + }); + + it("once the total is known, a decorative dot separates the counter from the next step (#446)", () => { + const props = bannerProps(); + const [dot] = dots(props); + expect(dots(props)).toHaveLength(1); + expect(dot.props["aria-hidden"]).toBe("true"); + const html = markup(props); + const counter = html.indexOf("t(onboarding.banner.progress"); + const dotAt = html.search(/