diff --git a/app/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.ts b/app/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.ts index b0271608a..a61589ca5 100644 --- a/app/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.ts +++ b/app/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.ts @@ -3,15 +3,13 @@ import { eigentumDone, eigentumZusammenfassungDone } from "./navStatesEigentum"; import { einkommenDone as einkommenDoneGuard } from "./guards"; import type { BeratungshilfeFinanzielleAngaben } from "./context"; -export type SubflowState = "Done" | "Open"; - export type FinanzielleAngabenGuard = GenericGuard; export const einkommenDone: FinanzielleAngabenGuard = ({ context }) => einkommenDoneGuard({ context }); -const partnerDone: FinanzielleAngabenGuard = ({ context }) => +export const partnerDone: FinanzielleAngabenGuard = ({ context }) => (context.staatlicheLeistungen != undefined && hasStaatlicheLeistungen({ context })) || ["no", "widowed"].includes(context.partnerschaft ?? "") || @@ -25,7 +23,7 @@ const hasStaatlicheLeistungen: FinanzielleAngabenGuard = ({ context }) => context.staatlicheLeistungen == "buergergeld" || context.staatlicheLeistungen == "grundsicherung"; -const kinderDone: FinanzielleAngabenGuard = ({ context }) => +export const kinderDone: FinanzielleAngabenGuard = ({ context }) => context.hasKinder == "no" || context.kinder !== undefined; const wohnungAloneDone: FinanzielleAngabenGuard = ({ context }) => @@ -39,18 +37,21 @@ const wohnungWithOthersDone: FinanzielleAngabenGuard = ({ context }) => context.apartmentCostOwnShare !== undefined && context.apartmentCostFull !== undefined; -const wohnungDone: FinanzielleAngabenGuard = ({ context }) => +export const wohnungDone: FinanzielleAngabenGuard = ({ context }) => context.livingSituation !== undefined && context.apartmentSizeSqm !== undefined && (wohnungAloneDone({ context }) || wohnungWithOthersDone({ context })); -const andereUnterhaltszahlungenDone: FinanzielleAngabenGuard = ({ context }) => +export const andereUnterhaltszahlungenDone: FinanzielleAngabenGuard = ({ + context, +}) => (context.staatlicheLeistungen != undefined && hasStaatlicheLeistungen({ context })) || context.hasWeitereUnterhaltszahlungen == "no" || (context.unterhaltszahlungen !== undefined && context.unterhaltszahlungen.length > 0); -const ausgabenDone: FinanzielleAngabenGuard = ({ context }) => { + +export const ausgabenDone: FinanzielleAngabenGuard = ({ context }) => { return ( context.hasAusgaben === "no" || (context.hasAusgaben === "yes" && @@ -59,30 +60,6 @@ const ausgabenDone: FinanzielleAngabenGuard = ({ context }) => { ); }; -const subflowDoneConfig: Record = { - einkommen: einkommenDone, - partner: partnerDone, - kinder: kinderDone, - eigentum: eigentumDone, - "eigentum-zusammenfassung": eigentumZusammenfassungDone, - wohnung: wohnungDone, - "andere-unterhaltszahlungen": andereUnterhaltszahlungenDone, - ausgaben: ausgabenDone, -}; - -export const beratungshilfeFinanzielleAngabenSubflowState = ( - context: BeratungshilfeFinanzielleAngaben, - subflowId: string, -): SubflowState => { - if ( - subflowId in subflowDoneConfig && - subflowDoneConfig[subflowId]({ context }) - ) { - return "Done"; - } - return "Open"; -}; - export const beratungshilfeFinanzielleAngabeDone: GenericGuard< BeratungshilfeFinanzielleAngaben > = ({ context }) => { diff --git a/app/models/flows/beratungshilfeFormular/index.ts b/app/models/flows/beratungshilfeFormular/index.ts index 250e7fd7d..b1515c08b 100644 --- a/app/models/flows/beratungshilfeFormular/index.ts +++ b/app/models/flows/beratungshilfeFormular/index.ts @@ -21,8 +21,12 @@ import { beratungshilfeAbgabeGuards } from "./abgabe/guards"; import abgabeFlow from "./abgabe/flow.json"; import { type BeratungshilfeFinanzielleAngaben } from "./finanzielleAngaben/context"; import { - beratungshilfeFinanzielleAngabeDone, - beratungshilfeFinanzielleAngabenSubflowState, + andereUnterhaltszahlungenDone, + ausgabenDone, + einkommenDone, + kinderDone, + partnerDone, + wohnungDone, } from "./finanzielleAngaben/navStates"; import { type BeratungshilfePersoenlicheDaten, @@ -41,6 +45,10 @@ import { eigentumZusammenfassungShowWarnings, } from "./stringReplacements"; import { finanzielleAngabenArrayConfig } from "./finanzielleAngaben/arrayConfiguration"; +import { + eigentumDone, + eigentumZusammenfassungDone, +} from "./finanzielleAngaben/navStatesEigentum"; export const beratungshilfeFormular = { cmsSlug: "form-flow-pages", @@ -60,15 +68,27 @@ export const beratungshilfeFormular = { meta: { done: rechtsproblemDone }, }), "finanzielle-angaben": _.merge(finanzielleAngabenFlow, { - meta: { - done: beratungshilfeFinanzielleAngabeDone, - subflowState: beratungshilfeFinanzielleAngabenSubflowState, + states: { + einkommen: { meta: { done: einkommenDone } }, + partner: { meta: { done: partnerDone } }, + kinder: { meta: { done: kinderDone } }, + "andere-unterhaltszahlungen": { + meta: { done: andereUnterhaltszahlungenDone }, + }, + eigentum: { meta: { done: eigentumDone } }, + eigentumZusammenfassung: { + meta: { done: eigentumZusammenfassungDone }, + }, + wohnung: { meta: { done: wohnungDone } }, + ausgaben: { meta: { done: ausgabenDone } }, }, }), "persoenliche-daten": _.merge(persoenlicheDatenFlow, { meta: { done: beratungshilfePersoenlicheDatenDone }, }), - abgabe: abgabeFlow, + abgabe: _.merge(abgabeFlow, { + meta: { done: () => false }, + }), }, }), guards: { diff --git a/app/models/flows/flows.server.ts b/app/models/flows/flows.server.ts index 036a6627c..c0418f8b1 100644 --- a/app/models/flows/flows.server.ts +++ b/app/models/flows/flows.server.ts @@ -30,8 +30,3 @@ export const flows = { "fluggastrechte/vorabcheck": fluggastrechteVorabcheck, "fluggastrechte/formular": fluggastrechtFlow, } as const satisfies Record; - -export function getSubflowsEntries(config: Config) { - if (!config.states) return []; - return Object.entries(config.states).filter(([, state]) => "states" in state); -} diff --git a/app/routes/shared/formular.server.ts b/app/routes/shared/formular.server.ts index 01bb55b57..4b1e46fbb 100644 --- a/app/routes/shared/formular.server.ts +++ b/app/routes/shared/formular.server.ts @@ -20,7 +20,7 @@ import { validatedSession } from "~/services/security/csrf.server"; import { throw404IfFeatureFlagEnabled } from "~/services/errorPages/throw404"; import { logError } from "~/services/logging"; import { getMigrationData } from "~/services/session.server/crossFlowMigration"; -import { navItemsFromFlowSpecifics } from "~/services/flowNavigation.server"; +import { navItemsFromStepStates } from "~/services/flowNavigation.server"; import type { z } from "zod"; import type { CollectionSchemas } from "~/services/cms/schemas"; import { getButtonNavigationProps } from "~/util/buttonProps"; @@ -161,11 +161,12 @@ export const loader = async ({ backDestination: backDestinationWithArrayIndexes, }); - const navItems = navItemsFromFlowSpecifics( - stepId, - flowController, - navigationStrings, - ); + const navItems = + navItemsFromStepStates( + stepId, + flowController.stepStates(), + navigationStrings, + ) ?? []; const migrationData = await getMigrationData( stepId, diff --git a/app/services/flow/server/buildFlowController.ts b/app/services/flow/server/buildFlowController.ts index 4f28a1121..ee87e76ec 100644 --- a/app/services/flow/server/buildFlowController.ts +++ b/app/services/flow/server/buildFlowController.ts @@ -11,7 +11,6 @@ import { stepIdToPath, } from "~/services/flow/stepIdConverter"; import type { Context } from "~/models/flows/contexts"; -import type { SubflowState } from "~/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates"; import type { GenericGuard, Guards } from "~/models/flows/guards.server"; import _ from "lodash"; import type { ArrayConfig } from "~/services/array"; @@ -44,8 +43,6 @@ type Meta = { progressPosition: number | undefined; isUneditable: boolean | undefined; done: GenericGuard; - subflowState: (context: Context, subflowId: string) => SubflowState; - subflowDone: (context: Context, subflowId: string) => boolean | undefined; arrays?: Record; }; @@ -127,6 +124,63 @@ function getInitial(machine: FlowStateMachine) { return stateValueToStepIds(initialSnapshot.value).pop(); } +export type StepState = { + stepId: string; + isDone: boolean; + isReachable: boolean; + isUneditable: boolean; + url: string; + subStates?: StepState[]; +}; + +function stepStates( + stateNode: FlowStateMachine["states"][string], + reachableSteps: string[], +): StepState[] { + // Recurse a statenode until encountering a done function or no more substates are left + // For each encountered statenode a StepState object is returned, containing whether the state is reachable, done and its URL + const context = (stateNode.machine.config.context ?? {}) as Context; + + const statesWithDoneFunctionOrSubstates = Object.values( + stateNode.states ?? {}, + ).filter((state) => state.meta?.done || Object.keys(state.states).length > 0); + + return statesWithDoneFunctionOrSubstates.map((state) => { + const stepId = stateValueToStepIds(pathToStateValue(state.path))[0]; + const meta = state.meta as Meta | undefined; + const hasDoneFunction = meta?.done !== undefined; + const isUneditable = Boolean(meta?.isUneditable); + const reachableSubStates = stepStates(state, reachableSteps).filter( + (state) => state.isReachable, + ); + + // Ignore subflows if empty or parent state has done function + if (hasDoneFunction || reachableSubStates.length === 0) { + const initial = state.config.initial as string | undefined; + const initialStepId = initial ? `${stepId}/${initial}` : stepId; + + return { + url: `${state.machine.id}${initialStepId}`, + isDone: hasDoneFunction ? meta.done({ context }) : false, + stepId, + isUneditable, + isReachable: reachableSteps.includes(initialStepId), + }; + } + + return { + url: `${state.machine.id}${stepId}`, + isDone: reachableSubStates.every((state) => state.isDone), + stepId, + isUneditable, + isReachable: reachableSubStates.length > 0, + subStates: reachableSubStates, + }; + }); +} + +export type FlowController = ReturnType; + export const buildFlowController = ({ config, data: context = {}, @@ -141,22 +195,20 @@ export const buildFlowController = ({ guards, }).createMachine({ ...config, context }); const baseUrl = config.id ?? ""; + const reachableSteps = getSteps(machine); // depends on context return { getMeta: (currentStepId: string) => metaFromStepId(machine, currentStepId), getRootMeta: () => rootMeta(machine), + stepStates: () => stepStates(machine.root, reachableSteps), isDone: (currentStepId: string) => Boolean(metaFromStepId(machine, currentStepId)?.done({ context })), - getSubflowState: (currentStepId: string, subflowId: string) => - metaFromStepId(machine, currentStepId)?.subflowState(context, subflowId), isUneditable: (currentStepId: string) => Boolean(metaFromStepId(machine, currentStepId)?.isUneditable), getConfig: () => config, isFinal: (currentStepId: string) => isFinalStep(machine, currentStepId), isReachable: (currentStepId: string) => { - // depends on context - const steps = getSteps(machine); - return steps.includes(currentStepId); + return reachableSteps.includes(currentStepId); }, getPrevious: (stepId: string) => { const backArray = transitionDestinations( diff --git a/app/services/flowNavigation.server.ts b/app/services/flowNavigation.server.ts index af0ddd392..0ebd3a288 100644 --- a/app/services/flowNavigation.server.ts +++ b/app/services/flowNavigation.server.ts @@ -1,106 +1,40 @@ import { NavState, type NavItem } from "~/components/FlowNavigation"; -import { getSubflowsEntries } from "~/models/flows/flows.server"; -import { type buildFlowController } from "./flow/server/buildFlowController"; -import _ from "lodash"; +import { type StepState } from "./flow/server/buildFlowController"; import { type Translations } from "./cms/index.server"; -export function navItemsFromFlowSpecifics( - currentStepId: string, - flowController: ReturnType, - translation: Translations = {}, -): NavItem[] { - const currentFlow = flowController.getConfig(); - const flowId = currentFlow.id ?? ""; +function isStepStateIdCurrent(stepStateId: string, stepId: string) { + // subflows might start with the same name, need to check the following char + return stepId.includes(stepStateId + "/"); +} - return getSubflowsEntries(currentFlow).map(([rootStateName, flowEntry]) => { - const subflows = - "states" in flowEntry - ? Object.entries(flowEntry.states ?? {}).filter( - ([, state]) => "states" in state, - ) - : []; +function isSubflowCurrent(subflows: NavItem[]) { + return subflows.some((state) => state.state === NavState.Current); +} - const firstSubflowEntry = subflows.find( - (entry) => entry[0] === flowEntry.initial, - ); +export function navItemsFromStepStates( + stepId: string, + stepStates: StepState[] | undefined, + translations: Translations = {}, +): NavItem[] | undefined { + if (!stepStates) return undefined; - const destinationStepId = `${rootStateName}/${ - typeof flowEntry.initial === "string" ? flowEntry.initial : "" - }${ - firstSubflowEntry - ? typeof firstSubflowEntry[1].initial === "string" - ? `/${firstSubflowEntry[1].initial}` - : "" - : "" - }`; + return stepStates.map((stepState) => { + const { isDone, isReachable, isUneditable, subStates } = stepState; - const rootLabel = translation[rootStateName] ?? "No key"; - const subflowSpecifics = - subflows.length > 0 - ? _.flatten( - subflows.map((entry) => { - const subflow = entry[1] ?? {}; - const subflowKey = entry[0] ?? "No key"; - return getSubflowSpecifics( - rootStateName, - subflow?.id ?? "", - typeof subflow?.initial === "string" ? subflow?.initial : "", - translation, - subflowKey, - flowId, - currentStepId, - flowController, - ); - }), - ) - : []; + const subNavItems = navItemsFromStepStates(stepId, subStates, translations); + const isCurrent = subNavItems + ? isSubflowCurrent(subNavItems) + : isStepStateIdCurrent(stepState.stepId, stepId); return { - destination: flowId + destinationStepId, - label: rootLabel, - state: navState({ - isCurrent: currentStepId.startsWith(rootStateName), - isReachable: flowController.isReachable(destinationStepId), - isDone: flowController.isDone(rootStateName), - isUneditable: flowController.isUneditable(rootStateName), - }), - subflows: subflowSpecifics, + destination: stepState.url, + label: translations[stepState.stepId ?? ""] ?? stepState.stepId, + subflows: subNavItems, + state: navState({ isCurrent, isDone, isReachable, isUneditable }), }; }); } -function getSubflowSpecifics( - rootStateName: string, - id: string, - initial: string, - translation: Translations, - subflowKey: string, - flowId: string, - currentStepId: string, - flowController: ReturnType, -) { - const subflowRoot = `${rootStateName}/${id ?? ""}`; - const subflowDestionationStepId = `${subflowRoot}/${initial ?? ""}`; - - const subflowState = flowController.getSubflowState( - rootStateName, - subflowKey, - ); - - return { - destination: flowId + subflowDestionationStepId, - label: translation[subflowRoot] ?? subflowKey, - state: navState({ - isCurrent: - currentStepId.startsWith(subflowRoot) && - currentStepId.at(subflowRoot.length) === "/", // subflows might start with the same name, this ensures the following characters - isReachable: flowController.isReachable(subflowDestionationStepId), - isDone: subflowState == "Done", - isUneditable: flowController.isUneditable(subflowRoot), - }), - }; -} - export function navState({ isCurrent, isReachable, diff --git a/tests/unit/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.test.ts b/tests/unit/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.test.ts index a8104c0db..521465b62 100644 --- a/tests/unit/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.test.ts +++ b/tests/unit/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates.test.ts @@ -1,7 +1,4 @@ -import { - beratungshilfeFinanzielleAngabeDone, - beratungshilfeFinanzielleAngabenSubflowState, -} from "~/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates"; +import { beratungshilfeFinanzielleAngabeDone } from "~/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates"; import * as navStatesEigentum from "~/models/flows/beratungshilfeFormular/finanzielleAngaben/navStatesEigentum"; describe("navStates", () => { @@ -125,48 +122,4 @@ describe("navStates", () => { ).toBeFalsy(); }); }); - - describe("beratungshilfeFinanzielleAngabenSubflowState", () => { - it("should return done for the subflow andere-unterhaltszahlungen when hasWeitereUnterhaltszahlungen is no", () => { - const actual = beratungshilfeFinanzielleAngabenSubflowState( - { - hasWeitereUnterhaltszahlungen: "no", - }, - "andere-unterhaltszahlungen", - ); - - expect(actual).toEqual("Done"); - }); - - it("should return open for the subflow andere-unterhaltszahlungen when hasWeitereUnterhaltszahlungen is yes and missing unterhaltszahlungen data", () => { - const actual = beratungshilfeFinanzielleAngabenSubflowState( - { - hasWeitereUnterhaltszahlungen: "yes", - }, - "andere-unterhaltszahlungen", - ); - - expect(actual).toEqual("Open"); - }); - - it("should return done for the subflow andere-unterhaltszahlungen when hasWeitereUnterhaltszahlungen is yes and unterhaltszahlungen data", () => { - const actual = beratungshilfeFinanzielleAngabenSubflowState( - { - hasWeitereUnterhaltszahlungen: "yes", - unterhaltszahlungen: [ - { - birthday: "10.10.2020", - familyRelationship: "mother", - firstName: "firstName", - monthlyPayment: "100", - surname: "surname", - }, - ], - }, - "andere-unterhaltszahlungen", - ); - - expect(actual).toEqual("Done"); - }); - }); }); diff --git a/tests/unit/services/flow/buildFlowController.test.ts b/tests/unit/services/flow/buildFlowController.test.ts index c5e30e7da..f716b5426 100644 --- a/tests/unit/services/flow/buildFlowController.test.ts +++ b/tests/unit/services/flow/buildFlowController.test.ts @@ -291,4 +291,209 @@ describe("buildFlowController", () => { }); }); }); + + describe(".stepStates()", () => { + it("ignores states without substates or done function", () => { + expect( + buildFlowController({ + config: { + id: "/test/", + initial: "start", + states: { start: {} }, + }, + }).stepStates(), + ).toEqual([]); + }); + + it("builds single step state", () => { + expect( + buildFlowController({ + config: { + id: "/test/", + initial: "start", + states: { + start: { meta: { done: () => true } }, + }, + }, + }).stepStates(), + ).toEqual([ + { + isDone: true, + isReachable: true, + isUneditable: false, + stepId: "start", + url: "/test/start", + }, + ]); + }); + + it("builds nested step states", () => { + expect( + buildFlowController({ + config: { + id: "/test/", + initial: "parent1", + states: { + parent1: { + initial: "child1", + states: { + child1: { + initial: "start", + states: { + start: { on: { SUBMIT: "#/test/.parent1.child2" } }, + }, + }, + child2: { initial: "start", states: { start: {} } }, + }, + }, + }, + }, + }).stepStates(), + ).toEqual([ + { + isDone: false, + isReachable: true, + isUneditable: false, + stepId: "parent1", + url: "/test/parent1", + subStates: [ + { + isDone: false, + isReachable: true, + isUneditable: false, + stepId: "parent1/child1", + url: "/test/parent1/child1/start", + }, + { + isDone: false, + isReachable: true, + isUneditable: false, + stepId: "parent1/child2", + url: "/test/parent1/child2/start", + }, + ], + }, + ]); + }); + + it("handles unreachable nested steps", () => { + expect( + buildFlowController({ + config: { + id: "/test/", + initial: "child1", + states: { + child1: { meta: { done: () => true }, on: { SUBMIT: "child3" } }, + child2: { meta: { done: () => true } }, + child3: { initial: "start", states: { start: {} } }, + child4: { initial: "start", states: { start: {} } }, + }, + }, + }).stepStates(), + ).toEqual([ + { + isDone: true, + isReachable: true, + isUneditable: false, + stepId: "child1", + url: "/test/child1", + }, + { + isDone: true, + isReachable: false, + isUneditable: false, + stepId: "child2", + url: "/test/child2", + }, + { + isDone: false, + isReachable: true, + isUneditable: false, + stepId: "child3", + url: "/test/child3/start", + }, + { + isDone: false, + isReachable: false, + isUneditable: false, + stepId: "child4", + url: "/test/child4/start", + }, + ]); + }); + }); + + it("all children must be done for parent to be done", () => { + const stepStates = buildFlowController({ + config: { + id: "/test/", + initial: "parent1", + states: { + parent1: { + initial: "child1", + states: { + child1: { + initial: "start", + meta: { done: () => true }, + states: { + start: { on: { SUBMIT: "#/test/.parent1.child2" } }, + }, + }, + child2: { + initial: "start", + meta: { done: () => false }, + states: { + start: { on: { SUBMIT: "#/test/.parent2" } }, + }, + }, + }, + }, + parent2: { + initial: "child1", + states: { + child1: { + initial: "start", + meta: { done: () => true }, + states: { + start: { on: { SUBMIT: "#/test/.parent2.child2" } }, + }, + }, + child2: { + initial: "start", + meta: { done: () => true }, + states: { start: {} }, + }, + }, + }, + }, + }, + }).stepStates(); + expect(stepStates[0].isDone).toBe(false); + expect(stepStates[1].isDone).toBe(true); + }); + + it("any child must be reachable for parent to be reachable", () => { + const stepStates = buildFlowController({ + config: { + id: "/test/", + initial: "parent1", + states: { + parent1: { + initial: "child1", + states: { child1: { on: { SUBMIT: "#/test/.parent2" } } }, + }, + parent2: { + initial: "child1", + states: { child1: {} }, + }, + parent3: { + initial: "child1", + states: { child1: {} }, + }, + }, + }, + }).stepStates(); + expect(stepStates[1].isReachable).toBe(true); + expect(stepStates[2].isReachable).toBe(false); + }); }); diff --git a/tests/unit/services/flowNavigation.test.ts b/tests/unit/services/flowNavigation.test.ts index 0edc35d0c..b0e66f05d 100644 --- a/tests/unit/services/flowNavigation.test.ts +++ b/tests/unit/services/flowNavigation.test.ts @@ -1,10 +1,9 @@ import { - navItemsFromFlowSpecifics, + navItemsFromStepStates, navState, } from "~/services/flowNavigation.server"; -import { buildFlowController } from "~/services/flow/server/buildFlowController"; -import _ from "lodash"; import { NavState } from "~/components/FlowNavigation"; +import invariant from "tiny-invariant"; describe("flowNavigation", () => { describe("navState", () => { @@ -28,80 +27,92 @@ describe("flowNavigation", () => { }); }); - describe("flow to NavState", () => { - const config = { - id: "/test/flow/", - initial: "step1", - states: { - step1: { - initial: "step1a", - states: { step1a: {} }, - }, - step2: {}, - }, + describe("navItemsFromStepStates()", () => { + const parentStepState = { + url: "/step1", + isDone: false, + stepId: "step1", + isUneditable: false, + isReachable: true, + }; + const childStepState = { + url: "/step1/a", + isDone: false, + stepId: "step1/a", + isUneditable: false, + isReachable: true, }; - describe("checks open state", () => { - const flowController = buildFlowController({ config }); - expect( - navItemsFromFlowSpecifics("step1.step1a", flowController)[0].state, - ).toBe(NavState.Current); - - expect(navItemsFromFlowSpecifics("step2", flowController)[0].state).toBe( - NavState.Open, - ); - }); - - describe("checks done state", () => { - const flowController = buildFlowController({ - config: _.merge(_.cloneDeep(config), { - states: { step1: { meta: { done: () => true } } }, - }), - }); + const stepStatesNested = [ + { ...parentStepState, subStates: [childStepState] }, + ]; + it("parents' IsCurrent true if in child state", () => { expect( - navItemsFromFlowSpecifics("step1.step1a", flowController)[0].state, - ).toBe(NavState.Current); - - expect(navItemsFromFlowSpecifics("step2", flowController)[0].state).toBe( - NavState.Done, - ); + navItemsFromStepStates("step1/start", [parentStepState]), + ).toStrictEqual([ + { + destination: parentStepState.url, + label: parentStepState.stepId, + state: NavState.Current, + subflows: undefined, + }, + ]); }); - describe("works with isUneditable", () => { - const flowController = buildFlowController({ - config: _.merge(_.cloneDeep(config), { - states: { step1: { meta: { isUneditable: true, done: () => true } } }, - }), - }); - + it("nested parents IsCurrent true if in nested child state", () => { expect( - navItemsFromFlowSpecifics("step1.step1a", flowController)[0].state, - ).toBe(NavState.Current); - - expect(navItemsFromFlowSpecifics("step2", flowController)[0].state).toBe( - NavState.DoneDisabled, - ); + navItemsFromStepStates("step1/a/start", stepStatesNested), + ).toStrictEqual([ + { + destination: parentStepState.url, + label: parentStepState.stepId, + state: NavState.Current, + subflows: [ + { + label: childStepState.stepId, + destination: childStepState.url, + state: NavState.Current, + subflows: undefined, + }, + ], + }, + ]); }); - describe("checks open state", () => { - const flowController = buildFlowController({ - config: _.merge(_.cloneDeep(config), { - states: { - step2: { - initial: "step2a", - states: { step2a: {} }, + it("nested parents IsCurrent false if not in nested child state", () => { + expect( + navItemsFromStepStates("step1/b/start", stepStatesNested), + ).toStrictEqual([ + { + destination: parentStepState.url, + label: parentStepState.stepId, + state: NavState.Open, + subflows: [ + { + label: childStepState.stepId, + destination: childStepState.url, + state: NavState.Open, + subflows: undefined, }, - }, - }), - }); + ], + }, + ]); + }); - expect( - navItemsFromFlowSpecifics("step1.step1a", flowController)[0].state, - ).toBe(NavState.Current); - expect( - navItemsFromFlowSpecifics("step1.step1a", flowController)[1].state, - ).toBe(NavState.OpenDisabled); + it("does label lookup", () => { + const navItems = navItemsFromStepStates( + "step1/a/start", + stepStatesNested, + { + step1: "Parent", + "step1/a": "Child", + }, + ); + invariant(navItems); + invariant(navItems[0].subflows); + expect(navItems[0].label).toBe("Parent"); + expect(navItems[0].subflows[0].label).toBe("Child"); }); }); });