From 7bba5487593ec515c2022701cf7577eda4c7482b Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 28 Oct 2024 14:27:33 +0100 Subject: [PATCH] blanks and Calculate tests --- .../@planx/components/Calculate/logic.test.ts | 24 +- .../components/Question/Public/Question.tsx | 2 - .../lib/__tests__/automations.blanks.test.ts | 501 +++++++++++++++++- .../__tests__/automations.parentChild.test.ts | 1 + .../src/pages/FlowEditor/lib/store/preview.ts | 9 +- 5 files changed, 513 insertions(+), 24 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/Calculate/logic.test.ts b/editor.planx.uk/src/@planx/components/Calculate/logic.test.ts index d8d96d0260..3194ae5a77 100644 --- a/editor.planx.uk/src/@planx/components/Calculate/logic.test.ts +++ b/editor.planx.uk/src/@planx/components/Calculate/logic.test.ts @@ -1,11 +1,9 @@ import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; +import { clickContinue, visitedNodes } from "pages/FlowEditor/lib/__tests__/utils"; import { Store, useStore } from "pages/FlowEditor/lib/store"; const { getState, setState } = useStore; -const { upcomingCardIds, resetPreview, record } = getState(); - -// Helper method -const visitedNodes = () => Object.keys(getState().breadcrumbs); +const { upcomingCardIds, resetPreview, autoAnswerableOptions } = getState(); beforeEach(() => { resetPreview(); @@ -17,13 +15,12 @@ test("When formatOutputForAutomations is true, Calculate writes an array and fut expect(upcomingCardIds()).toEqual(["Calculate", "Question"]); // Step forwards through the Calculate - record("Calculate", { data: { testGroup: ["2"] }, auto: true }); - upcomingCardIds(); - - // The Question has been auto-answered - expect(visitedNodes()).toEqual(["Calculate", "Question"]); + clickContinue("Calculate", { data: { testGroup: ["2"] }, auto: true }); - expect(upcomingCardIds()).toEqual(["Group2Notice"]); + // The Question can be auto-answered + expect(visitedNodes()).toEqual(["Calculate"]); + expect(upcomingCardIds()).toEqual(["Question"]) + expect(autoAnswerableOptions("Question")).toEqual(["Group2Response"]); }); test("When formatOutputForAutomations is false, Calculate writes a number and future questions are not auto-answered", () => { @@ -32,13 +29,12 @@ test("When formatOutputForAutomations is false, Calculate writes a number and fu expect(upcomingCardIds()).toEqual(["Calculate", "Question"]); // Step forwards through the Calculate - record("Calculate", { data: { testGroup: 2 }, auto: true }); - upcomingCardIds(); + clickContinue("Calculate", { data: { testGroup: 2 }, auto: true }); - // The Question has NOT been auto-answered + // The Question cannot be auto-answered expect(visitedNodes()).toEqual(["Calculate"]); - expect(upcomingCardIds()).toEqual(["Question"]); + expect(autoAnswerableOptions("Question")).toBeUndefined(); }); const flowWithAutomation: Store.Flow = { diff --git a/editor.planx.uk/src/@planx/components/Question/Public/Question.tsx b/editor.planx.uk/src/@planx/components/Question/Public/Question.tsx index 74842fd2ce..6054544eab 100644 --- a/editor.planx.uk/src/@planx/components/Question/Public/Question.tsx +++ b/editor.planx.uk/src/@planx/components/Question/Public/Question.tsx @@ -35,8 +35,6 @@ const QuestionComponent: React.FC = (props) => { state.autoAnswerableOptions, ]); - console.log("HERE flow", props); - // Questions without edges act like "sticky notes" in the graph for editors only & can be immediately auto-answered let edges: Edges | undefined; if (props.id) edges = flow[props.id]?.edges diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.blanks.test.ts b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.blanks.test.ts index 8d0095ec2c..b3546d3ad1 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.blanks.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.blanks.test.ts @@ -1,7 +1,8 @@ import { Store, useStore } from "../store"; +import { clickContinue } from "./utils"; const { getState, setState } = useStore; -const { resetPreview } = getState(); +const { resetPreview, upcomingCardIds, autoAnswerableOptions, computePassport } = getState(); describe("Auto-answering blanks", () => { beforeEach(() => { @@ -9,7 +10,499 @@ describe("Auto-answering blanks", () => { setState({ flow }); }); - test.todo("TODO"); -}) + test("Checklists with the same options auto-answer the blank", () => { + // Manually select the test path + expect(upcomingCardIds()).toEqual(["TestPathSelection"]); + clickContinue("TestPathSelection", { answers: ["TestPath1"], auto: false }); -const flow: Store.Flow = {}; + // Manually proceed through the first blank + expect(upcomingCardIds()).toEqual(["Path1Checklist1", "Path1Checklist2"]); + clickContinue("Path1Checklist1", { answers: ["Path1Checklist1Blank"], auto: false }); + + // The second blank is auto-answered because we do not have a passport value but we've seen all options before + expect(computePassport()?.data).not.toHaveProperty("option"); + expect(autoAnswerableOptions("Path1Checklist2")).toEqual(["Path1Checklist2Blank"]); + }); + + test("Checklists with different options do not auto-answer the blank", () => { + // Manually select the test path + expect(upcomingCardIds()).toEqual(["TestPathSelection"]); + clickContinue("TestPathSelection", { answers: ["TestPath2"], auto: false }); + + // Manually proceed through the first blank + expect(upcomingCardIds()).toEqual(["Path2Checklist1", "Path2Checklist2"]); + clickContinue("Path2Checklist1", { answers: ["Path2Checklist1Blank"], auto: false }); + + // The second blank is put to the user because we do not have a passport value and we have NOT seen all options before + expect(computePassport()?.data).not.toHaveProperty("option"); + expect(autoAnswerableOptions("Path2Checklist2")).toBeUndefined; + }); + + test("Questions with the same options auto-answer the blank", () => { + // Manually select the test path + expect(upcomingCardIds()).toEqual(["TestPathSelection"]); + clickContinue("TestPathSelection", { answers: ["TestPath3"], auto: false }); + + // Manually proceed through the first blank + expect(upcomingCardIds()).toEqual(["Path3Question1", "Path3Question2"]); + clickContinue("Path3Question1", { answers: ["Path3Question1Blank"], auto: false }); + + // The second blank is auto-answered because we do not have a passport value but we've seen all options before + expect(computePassport()?.data).not.toHaveProperty("option"); + expect(autoAnswerableOptions("Path3Question1")).toEqual(["Path3Question1Blank"]); + }); + + test("Questions with different options do not auto-answer the blank", () => { + // Manually select the test path + expect(upcomingCardIds()).toEqual(["TestPathSelection"]); + clickContinue("TestPathSelection", { answers: ["TestPath4"], auto: false }); + + // Manually proceed through the first blank + expect(upcomingCardIds()).toEqual(["Path4Question1", "Path4Question2", "Path4Question3"]); + clickContinue("Path4Question1", { answers: ["Path4Question1Blank"], auto: false }); + + // The second blank is put to the user because we do not have a passport value and we have NOT seen all options before + expect(computePassport()?.data).not.toHaveProperty("option"); + expect(autoAnswerableOptions("Path4Question2")).toBeUndefined; + + // Manually proceed through the second blank + clickContinue("Path4Question2", { answers: ["Path4Question2Blank"], auto: false }); + + // The third blank is auto-answered because we do not have a passport value but we've seen all options before now + expect(computePassport()?.data).not.toHaveProperty("option"); + expect(autoAnswerableOptions("Path4Question3")).toEqual(["Path4Question3Blank"]); + }); + + test("Checklist then a Question with the same options auto-answer the blank", () => { + // Manually select the test path + expect(upcomingCardIds()).toEqual(["TestPathSelection"]); + clickContinue("TestPathSelection", { answers: ["TestPath5"], auto: false }); + + // Manually proceed through the first blank + expect(upcomingCardIds()).toEqual(["Path5Checklist", "Path5Question"]); + clickContinue("Path5Checklist", { answers: ["Path5ChecklistBlank"], auto: false }); + + // The second blank is auto-answered because we do not have a passport value but we've seen all options before + expect(computePassport()?.data).not.toHaveProperty("option"); + expect(autoAnswerableOptions("Path5Question")).toEqual(["Path5QuestionBlank"]); + }); +}); + +// editor.planx.dev/testing/automate-blanks-test +const flow: Store.Flow = { + "_root": { + "edges": [ + "TestPathSelection" + ] + }, + "Path5Checklist1OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path5Checklist": { + "data": { + "fn": "option", + "text": "Options 1", + "allRequired": false + }, + "type": 105, + "edges": [ + "Path5ChecklistOptionA", + "Path5ChecklistOptionB", + "Path5ChecklistBlank" + ] + }, + "Path4Question2Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path3Question2": { + "data": { + "fn": "option", + "text": "Options 2" + }, + "type": 100, + "edges": [ + "Path3Question2OptionA", + "Path3Question2Blank" + ] + }, + "Path1Checklist1": { + "data": { + "fn": "option", + "text": "Options 1", + "allRequired": false + }, + "type": 105, + "edges": [ + "Path5Checklist1OptionA", + "Path5Checklist1OptionB", + "Path5Checklist1Blank" + ] + }, + "Path5ChecklistOptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "TestPathSelection": { + "data": { + "text": "Which flow?" + }, + "type": 100, + "edges": [ + "TestPath1", + "TestPath2", + "TestPath3", + "TestPath4", + "TestPath5" + ] + }, + "Path3Question1": { + "data": { + "fn": "option", + "text": "Options 1" + }, + "type": 100, + "edges": [ + "Path3Question1OptionA", + "Path3Question1Blank" + ] + }, + "Path5Question": { + "data": { + "fn": "option", + "text": "Options 2" + }, + "type": 100, + "edges": [ + "Path5QuestionOptionA", + "Path5QuestionOptionB", + "Path5QuestionBlank" + ] + }, + "Path3Question1OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path3Question2OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path1Checklist2OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path2Checklist1": { + "data": { + "fn": "option", + "text": "Options 1", + "allRequired": false + }, + "type": 105, + "edges": [ + "Path2Checklist1OptionA", + "Path2Checklist1OptionB", + "Path2Checklist1Blank" + ] + }, + "Path5QuestionBlank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path2Checklist2Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path4Question1": { + "data": { + "fn": "option", + "text": "Options 1" + }, + "type": 100, + "edges": [ + "Path4Question1OptionA", + "Path4Question1Blank" + ] + }, + "TestPath2": { + "data": { + "text": "2", + "description": "Checklists with different options" + }, + "type": 200, + "edges": [ + "Path2Checklist1", + "Path2Checklist2" + ] + }, + "Path4Question2OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path4Question1Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "TestPath5": { + "data": { + "text": "5", + "description": "Checklist then question with same options" + }, + "type": 200, + "edges": [ + "Path5Checklist", + "Path5Question" + ] + }, + "Path4Question1OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path2Checklist2OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path5ChecklistOptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path1Checklist2OptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path5QuestionOptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "TestPath3": { + "data": { + "text": "3", + "description": "Questions with same options" + }, + "type": 200, + "edges": [ + "Path3Question1", + "Path3Question2" + ] + }, + "Path3Question1Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path2Checklist2OptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path4Question2OptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path3Question2Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path2Checklist1Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path5Checklist1Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path4Question3": { + "data": { + "fn": "option", + "text": "Options 3" + }, + "type": 100, + "edges": [ + "Path4Question3OptionB", + "Path4Question3OptionA", + "Path4Question3Blank" + ] + }, + "Path4Question3Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path1Checklist2": { + "data": { + "fn": "option", + "text": "Options 2", + "allRequired": false + }, + "type": 105, + "edges": [ + "Path1Checklist2OptionA", + "Path1Checklist2OptionB", + "Path1Checklist2Blank" + ] + }, + "TestPath1": { + "data": { + "text": "1", + "description": "Checklists with same options" + }, + "type": 200, + "edges": [ + "Path1Checklist1", + "Path1Checklist2" + ] + }, + "TestPath4": { + "data": { + "text": "4", + "description": "Questions with different options" + }, + "type": 200, + "edges": [ + "Path4Question1", + "Path4Question2", + "Path4Question3" + ] + }, + "Path5ChecklistBlank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path4Question2": { + "data": { + "fn": "option", + "text": "Options 2" + }, + "type": 100, + "edges": [ + "Path4Question2OptionA", + "Path4Question2OptionB", + "Path4Question2Blank" + ] + }, + "Path1Checklist2Blank": { + "data": { + "text": "None" + }, + "type": 200 + }, + "Path4Question3OptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path2Checklist1OptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path2Checklist2": { + "data": { + "fn": "option", + "text": "Options 2", + "allRequired": false + }, + "type": 105, + "edges": [ + "Path2Checklist2OptionA", + "Path2Checklist2OptionB", + "Path2Checklist2OptionC", + "Path2Checklist2Blank" + ] + }, + "Path5Checklist1OptionB": { + "data": { + "val": "b", + "text": "B" + }, + "type": 200 + }, + "Path4Question3OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path5QuestionOptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path2Checklist1OptionA": { + "data": { + "val": "a", + "text": "A" + }, + "type": 200 + }, + "Path2Checklist2OptionC": { + "data": { + "val": "c", + "text": "C" + }, + "type": 200 + } +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.parentChild.test.ts b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.parentChild.test.ts index 1631e5dda7..bf16ab4d29 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.parentChild.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/automations.parentChild.test.ts @@ -66,6 +66,7 @@ describe("Parent-child automations and granularity", () => { }); }); +// editor.planx.dev/testing/behaviour-check const flow: Store.Flow = { "_root": { "edges": [ diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts index 4b9a07146c..fdfc3111b1 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts @@ -447,7 +447,7 @@ export const previewStore: StateCreator< const { breadcrumbs, flow, computePassport } = get(); const { type, data, edges } = flow[id]; - // Only Queston & Checklist nodes that have an fn & edges are eligible for auto-answering + // Only Question & Checklist nodes that have an fn & edges are eligible for auto-answering if (!type || ![TYPES.Question, TYPES.Checklist].includes(type) || !data?.fn || !edges) return; // Only proceed if the user has seen at least one node with this fn before @@ -471,9 +471,10 @@ export const previewStore: StateCreator< const blankOption = options.find((option) => !option.data?.val); let optionsThatCanBeAutoAnswered: Array = []; - // Get existing passport value(s) for this node's fn - let passportValues = computePassport()?.data?.[data.fn]?.sort(); - if (!Array.isArray(passportValues)) passportValues = [passportValues].filter(Boolean); + // Get existing passport value(s) for this node's fn & proceed if eligible format (eg not numbers via Calculate nodes) + let passportValues = computePassport().data?.[data.fn]; + if (typeof passportValues === "number") return; + if (!Array.isArray(passportValues)) passportValues = [passportValues].filter(Boolean).sort(); // If we have an existing passport value for this fn, // then proceed through the matching option(s) or the blank option independent if other vals have been seen before