From e37a3178a3aedc8bd829fba824607a514ad16b40 Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Thu, 16 Jul 2026 10:25:06 -0400 Subject: [PATCH 01/13] CASH-3034- part-questions/Capability questions/Moulding questions UI fixes and question-chain helper --- .../question-chain/question-chain.spec.js | 213 +++++++++++++++++- .../question-chain/question-chain.vue | 140 ++++++++---- .../questions-page-layout.spec.js | 35 +++ .../questions-page-layout.vue | 24 +- src/helpers/question-chain-helper.js | 49 ++++ src/helpers/question-chain-helper.spec.js | 74 ++++++ .../capability-questions.spec.js | 4 +- .../capability-questions.vue | 7 +- .../molding-questions.spec.js | 4 +- .../molding-questions/molding-questions.vue | 7 +- .../part-questions/part-questions.spec.js | 4 +- src/layouts/part-questions/part-questions.vue | 9 +- src/mixins/vehicle-questions-mixin.js | 46 +++- src/mixins/vehicle-questions-mixin.spec.js | 82 +++++++ 14 files changed, 606 insertions(+), 92 deletions(-) create mode 100644 src/helpers/question-chain-helper.js create mode 100644 src/helpers/question-chain-helper.spec.js diff --git a/src/digital-components/question-chain/question-chain.spec.js b/src/digital-components/question-chain/question-chain.spec.js index 7435feefd..1bdc43d4a 100644 --- a/src/digital-components/question-chain/question-chain.spec.js +++ b/src/digital-components/question-chain/question-chain.spec.js @@ -109,11 +109,11 @@ describe("Question Chain component", () => { expect(wrapper.emitted()).not.toHaveProperty("update:modelValue"); }); - test("should NOT emit update:modelValue if returnedAnswer contains 'nextQuestion'", () => { + test("should not emit update:modelValue for nextQuestion during normal forward progress", () => { //Arrange const { wrapper } = setupMocks({}); const testQuestion = {}; - const testReturnedAnswer = "1|nextQuestion|DB10840|No"; + const testReturnedAnswer = "1|nextQuestion|3|No"; //Act wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer); @@ -121,6 +121,24 @@ describe("Question Chain component", () => { //Assert expect(wrapper.emitted()).not.toHaveProperty("update:modelValue"); }); + + test("should emit update:modelValue for nextQuestion when revising a saved answer", async () => { + //Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({ hasSavedAnswer: true }); + const testQuestion = {}; + const testReturnedAnswer = "1|nextQuestion|3|No"; + + //Act + wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer); + + //Assert + expect(wrapper.emitted()).toHaveProperty("update:modelValue"); + expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({ + incomplete: true, + index: 0, + }); + }); }); describe("method getQuestionChainAnswerIfComplete...", () => { @@ -213,25 +231,87 @@ describe("Question Chain component", () => { wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer); //Assert - expect(wrapper.vm.questions[1].answerSelected).toBeUndefined; + expect(wrapper.vm.questions[1].answerSelected).toBeUndefined(); }); - test("should return false if returnedAnswer is a nextQuestion (not a final answer)", async () => { + test("should follow the No path and show the next question when changing Q1 from Yes to No", async () => { + const { wrapper } = setupMocks({ + questionDataProp: [ + { + questionSequence: 1, + questionText: "Is this the Overland edition?", + answers: [ + { + answerResult: null, + answerText: "Yes", + nextQuestionSequence: 2, + }, + { + answerResult: null, + answerText: "No", + nextQuestionSequence: 5, + }, + ], + }, + { + questionSequence: 2, + questionText: "Rain sensing wipers?", + answers: [ + { answerResult: "PART-A", answerText: "Yes", nextQuestionSequence: 3 }, + { answerResult: "PART-B", answerText: "No", nextQuestionSequence: 3 }, + ], + answerSelected: "2|answer|PART-A|Yes", + }, + { + questionSequence: 3, + questionText: "Laminated door glass?", + answers: [ + { answerResult: "PART-C", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "PART-D", answerText: "No", nextQuestionSequence: null }, + ], + answerSelected: "3|answer|PART-C|Yes", + }, + { + questionSequence: 5, + questionText: "Factory installed antenna?", + answers: [ + { answerResult: "PART-E", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "PART-F", answerText: "No", nextQuestionSequence: null }, + ], + }, + ], + }); + await wrapper.setProps({ index: 0 }); + + const result = wrapper.vm.getQuestionChainAnswerIfComplete("1|nextQuestion|5|No"); + + expect(result.incomplete).toBe(true); + expect(wrapper.vm.questions[1].answerSelected).toBeUndefined(); + expect(wrapper.vm.questions[2].answerSelected).toBeUndefined(); + expect(wrapper.vm.currentQuestionNum).toBe(5); + expect(result.answeredQuestions).toHaveLength(1); + expect(result.answeredQuestions[0].selectedAnswerText).toBe("No"); + }); + + test("should return incomplete answer if returnedAnswer is a nextQuestion (not a final answer)", async () => { //Arrange const { wrapper } = setupMocks({}); - const testReturnedAnswer = "1|nextQuestion|DB10840|No"; + const testReturnedAnswer = "1|nextQuestion|3|No"; //Act const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer); //Assert - expect(result).toBeFalsy(); + expect(result).toMatchObject({ + incomplete: true, + index: 0, + }); }); test("should trigger scroll to .current-question if returnedAnswer is a nextQuestion (not a final answer)", async () => { //Arrange const { wrapper } = setupMocks({}); - const testReturnedAnswer = "1|nextQuestion|DB10840|No"; + const testReturnedAnswer = "1|nextQuestion|3|No"; //Act wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer); @@ -323,9 +403,127 @@ describe("Question Chain component", () => { expect(wrapper.vm.questions[0].answers[0].problemQuestionId).toBe(9531); expect(wrapper.vm.questions[0].answers[1].problemQuestionId).toBe(9529); }); + + test("should normalize uppercase answerSelected to match button value on created", async () => { + const { wrapper } = setupMocks({ + questionDataProp: [ + { + questionSequence: 1, + questionText: "Is your vehicle equipped with rain sensing wipers?", + answerSelected: "1|ANSWER|DW01705|YES", + answers: [ + { + answerResult: "DW01705", + answerText: "Yes", + nextQuestionSequence: null, + }, + { + answerResult: "DW01591", + answerText: "No", + nextQuestionSequence: null, + }, + ], + }, + ], + }); + + await nextTick(); + + expect(wrapper.vm.questions[0].answerSelected).toBe("1|answer|DW01705|Yes"); + }); + test("should show question 3 after answering No on Cherokee Overland question", async () => { + const { wrapper } = setupMocks({ + questionDataProp: getCherokeeWindshieldQuestions(), + }); + await nextTick(); + + const q1 = wrapper.vm.questions[0]; + wrapper.vm.handleAnswer(q1, "1|nextQuestion|3|No"); + + expect(wrapper.vm.currentQuestionNum).toBe(3); + expect( + wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question)) + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ questionSequence: 1 }), + expect.objectContaining({ questionSequence: 3 }), + ]) + ); + expect( + wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question)) + ).not.toEqual(expect.arrayContaining([expect.objectContaining({ questionSequence: 2 })])); + }); + + test("should resume at question 3 after remount when Q1 No was already saved", async () => { + const questionDataProp = getCherokeeWindshieldQuestions(); + questionDataProp[0].answerSelected = "1|nextQuestion|3|No"; + + const { wrapper } = setupMocks({ questionDataProp }); + await nextTick(); + + expect(wrapper.vm.currentQuestionNum).toBe(3); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true); + }); }); }); +function getCherokeeWindshieldQuestions() { + return [ + { + questionSequence: 1, + questionText: + "Is your Cherokee the Overland edition which can be identified by having a wood and leather wrapped steering wheel?", + answers: [ + { answerResult: "", answerText: "Yes", nextQuestionSequence: 2 }, + { answerResult: "", answerText: "No", nextQuestionSequence: 3 }, + ], + }, + { + questionSequence: 2, + questionText: + "Is your vehicle equipped with rain sensing wipers that adjust their speed automatically when it rains?", + answers: [ + { answerResult: "DW02270", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "DW02264", answerText: "No", nextQuestionSequence: null }, + ], + }, + { + questionSequence: 3, + questionText: + "Is your vehicle equipped with rain sensing wipers that adjust their speed automatically when it rains?", + answers: [ + { answerResult: "DW02268", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "", answerText: "No", nextQuestionSequence: 4 }, + ], + }, + { + questionSequence: 4, + questionText: + "Is your vehicle equipped with automatic climate control which will change the fan speed automatically in order to maintain a set temperature?", + answers: [ + { answerResult: "", answerText: "Yes", nextQuestionSequence: 5 }, + { answerResult: "", answerText: "No", nextQuestionSequence: 6 }, + ], + }, + { + questionSequence: 5, + questionText: "Is your vehicle equipped with heated seats?", + answers: [ + { answerResult: "DW02104", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "DW02103", answerText: "No", nextQuestionSequence: null }, + ], + }, + { + questionSequence: 6, + questionText: "Is your vehicle equipped with heated seats?", + answers: [ + { answerResult: "DW02102", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "DW02101", answerText: "No", nextQuestionSequence: null }, + ], + }, + ]; +} + function setupMocks({ questionDataProp = [ { @@ -351,6 +549,7 @@ function setupMocks({ const mountOptions = getMountOptions({}); mountOptions.propsData = { questionData: questionDataProp, + index: 0, }; mountOptions["attachTo"] = document.body; diff --git a/src/digital-components/question-chain/question-chain.vue b/src/digital-components/question-chain/question-chain.vue index b1960eb51..b37755ee7 100644 --- a/src/digital-components/question-chain/question-chain.vue +++ b/src/digital-components/question-chain/question-chain.vue @@ -2,9 +2,9 @@
import buttonQuestion from "@/digital-components/button-question/button-question"; import { useValidateForm } from "vee-validate"; +import { + mapQuestionAnswersForChain, + normalizeAnswerSelectedValue, +} from "@/helpers/question-chain-helper"; export default { name: "questionChain", @@ -34,54 +38,81 @@ export default { modelValue: Object, index: Number, answerKey: String, + hasSavedAnswer: Boolean, + hasDownstreamSavedAnswers: Boolean, }, async created() { // do a test validation check upon create to prevent out of sync / incorrect valid states await useValidateForm(); // NOTE: needs to have async/await here; tested and won't work without it this.questionData.map((q, i) => { + const answers = mapQuestionAnswersForChain(q); const question = { questionText: q.questionText, questionSequence: q.questionSequence, - answers: q.answers.map((a) => { - return { - buttonLabel: a.answerText, - // Name will either be nextQuestionSequence or answerResult - // Name will be used by list-button as the input value. - // It must be a single string or number, so concatenating together a string with - // 4 pieces of data separated by pipe characters: - // question number|type of answer|answer value|answer text - value: a.nextQuestionSequence - ? q.questionSequence + - "|nextQuestion|" + - a.nextQuestionSequence + - "|" + - a.answerText - : q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText, - nextQuestionSequence: a.nextQuestionSequence, - answerResult: a.answerResult, - problemQuestionId: a.problemQuestionId, - questionSequence: q.questionSequence, - questionType: a.nextQuestionSequence ? "nextQuestion" : "answer", - }; - }), - answerSelected: q.answerSelected || "", + answers, + answerSelected: normalizeAnswerSelectedValue(q.answerSelected, answers), }; if (!q.suppressThisQuestion) { this.questions.push(question); } }); - if (!this.modelValue?.length > 0 && this.questions.length > 0) { - // set this.currentQuestionNum to first valid question - this.currentQuestionNum = this.questions[0].questionSequence; - // scroll the next question into view - this.$nextTick(() => { - document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" }); - }); + if (this.questions.length > 0) { + this.initializeCurrentQuestionNum(); + this.scrollCurrentQuestionIntoView(); } }, methods: { + getQuestionSequence(question) { + return Number(question?.questionSequence); + }, + isCurrentQuestion(question) { + return this.getQuestionSequence(question) === Number(this.currentQuestionNum); + }, + isQuestionVisible(question) { + return !!question.answerSelected || this.isCurrentQuestion(question); + }, + initializeCurrentQuestionNum() { + const pendingNextQuestion = [...this.questions] + .reverse() + .find((question) => question.answerSelected?.includes("|nextQuestion|")); + + if (pendingNextQuestion) { + const nextQuestionSequence = Number( + pendingNextQuestion.answerSelected.split("|")[2] + ); + + if ( + this.questions.some( + (question) => this.getQuestionSequence(question) === nextQuestionSequence + ) + ) { + this.currentQuestionNum = nextQuestionSequence; + return; + } + } + + const firstUnanswered = this.questions.find((question) => !question.answerSelected); + + if (firstUnanswered) { + this.currentQuestionNum = this.getQuestionSequence(firstUnanswered); + } + }, + scrollCurrentQuestionIntoView() { + this.$nextTick(() => { + document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" }); + }); + }, + syncAnswerSelectedToQuestionData(questionSequence, answerSelected) { + const sourceQuestion = this.questionData?.find( + (q) => this.getQuestionSequence(q) === Number(questionSequence) + ); + + if (sourceQuestion) { + sourceQuestion.answerSelected = answerSelected; + } + }, getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) { if (!question?.answers || !selectedAnswer) { return null; @@ -100,10 +131,20 @@ export default { */ question.answerSelected = returnedAnswer; - const isQuestionChainComplete = this.getQuestionChainAnswerIfComplete(returnedAnswer); + this.syncAnswerSelectedToQuestionData(question.questionSequence, returnedAnswer); + const questionChainAnswer = this.getQuestionChainAnswerIfComplete(returnedAnswer); - if (isQuestionChainComplete) { - this.$emit("update:modelValue", isQuestionChainComplete); + if (!questionChainAnswer) { + return; + } + + const shouldNotifyParent = + !questionChainAnswer.incomplete || + this.hasSavedAnswer || + this.hasDownstreamSavedAnswers; + + if (shouldNotifyParent) { + this.$emit("update:modelValue", questionChainAnswer); } }, getQuestionChainAnswerIfComplete(returnedAnswer) { @@ -117,20 +158,24 @@ export default { // "5|answer|DW02104|Yes" const returnedAnswerArray = returnedAnswer.split("|"); - const questionNum = parseInt(returnedAnswerArray[0]); + const questionNum = Number(returnedAnswerArray[0]); const questionType = returnedAnswerArray[1]; const questionAnswer = returnedAnswerArray[2]; const answeredQuestions = []; this.questions.forEach((q) => { + const questionSequence = this.getQuestionSequence(q); + // find this question and mark it as "answered" by populating answerSelected - if (q.questionSequence === questionNum) { + if (questionSequence === questionNum) { q.answerSelected = returnedAnswer; + this.syncAnswerSelectedToQuestionData(questionSequence, returnedAnswer); } // remove all answers AFTER this question... // (needed in case user is changing previously answered questions) - if (q.questionSequence > questionNum) { + if (questionSequence > questionNum) { delete q.answerSelected; + this.syncAnswerSelectedToQuestionData(questionSequence, ""); } if (q.answerSelected) { answeredQuestions.push({ @@ -146,22 +191,21 @@ export default { } }); - // return false if there's a nextQuestion... or return an object with final answers (truthy) + // return incomplete answer if there's a nextQuestion... or return final answers (truthy) if (questionType === "nextQuestion") { // update to next question index - this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question - // scroll the next question into view - this.$nextTick(() => { - document - .querySelector(".current-question") - .scrollIntoView({ behavior: "smooth" }); - }); - return false; + this.currentQuestionNum = Number(questionAnswer); + this.scrollCurrentQuestionIntoView(); + return { + incomplete: true, + answeredQuestions, + index: this.index, + }; } else { // reset current question index (removes .current-question class) this.currentQuestionNum = 0; // reset count const answeredQuestion = this.questions.find( - (q) => q.questionSequence === questionNum + (q) => this.getQuestionSequence(q) === questionNum ); // return an object with the part answer, all the answered questions, and the part index diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js index 96e5434cd..ba96232db 100644 --- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js +++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js @@ -150,6 +150,41 @@ describe("questionsPageLayout.vue", () => { wrapper.unmount(); }); + + test("Should return false when answerData was cleared even if answerSelected remains", async () => { + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 1 }); + + const testGlassPiece = { + questions: [ + { + questionSequence: 1, + questionText: "Is your Grand Cherokee the Laredo model?", + answers: [], + answerSelected: "1|answer|DD11132|Yes", + }, + ], + answerData: {}, + }; + + expect(wrapper.vm.showThisQuestionChain(testGlassPiece, 0)).toBe(false); + + wrapper.unmount(); + }); + + test("Should return false for reset chains ahead of the current index", async () => { + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 1 }); + + const testGlassPiece = { + questions: [{ questionSequence: 1, questionText: "Rain sensing wipers?", answers: [] }], + answerData: null, + }; + + expect(wrapper.vm.showThisQuestionChain(testGlassPiece, 2)).toBe(false); + + wrapper.unmount(); + }); }); describe("method handleForwardButtonAction...", () => { diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue index c9f9989e2..3152f5b14 100644 --- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue +++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue @@ -10,7 +10,9 @@
-
+
- -
+ + glass.answerData?.answerResult); + }, showThisQuestionChain(glass, i) { - // return false if no questions or if suppressed if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) { return false; } - return this.index === i || glass.answerData?.answerResult?.length > 0; + + if (this.index === i) { + return true; + } + + // Only show completed chains that still have a saved answer + return !!glass.answerData?.answerResult; }, handleForwardButtonAction() { this.$emit("forwardButtonAction"); diff --git a/src/helpers/question-chain-helper.js b/src/helpers/question-chain-helper.js new file mode 100644 index 000000000..b5ba8c168 --- /dev/null +++ b/src/helpers/question-chain-helper.js @@ -0,0 +1,49 @@ +/** + * Builds the pipe-delimited value used by question-chain button options. + * Format: questionNumber|type|answer value|answer text + */ +export function buildQuestionChainAnswerValue({ + questionSequence, + nextQuestionSequence, + answerResult, + answerText, +}) { + if (nextQuestionSequence) { + return `${questionSequence}|nextQuestion|${nextQuestionSequence}|${answerText}`; + } + + return `${questionSequence}|answer|${answerResult}|${answerText}`; +} + +export function mapQuestionAnswersForChain(question) { + return question.answers.map((answer) => ({ + buttonLabel: answer.answerText, + value: buildQuestionChainAnswerValue({ + questionSequence: question.questionSequence, + nextQuestionSequence: answer.nextQuestionSequence, + answerResult: answer.answerResult, + answerText: answer.answerText, + }), + nextQuestionSequence: answer.nextQuestionSequence, + answerResult: answer.answerResult, + problemQuestionId: answer.problemQuestionId, + questionSequence: question.questionSequence, + questionType: answer.nextQuestionSequence ? "nextQuestion" : "answer", + })); +} + +export function normalizeAnswerSelectedValue(answerSelected, answers) { + if (!answerSelected) { + return ""; + } + + if (answers.some((answer) => answer.value === answerSelected)) { + return answerSelected; + } + + const matchedAnswer = answers.find( + (answer) => answer.value.toUpperCase() === String(answerSelected).toUpperCase() + ); + + return matchedAnswer?.value ?? answerSelected; +} diff --git a/src/helpers/question-chain-helper.spec.js b/src/helpers/question-chain-helper.spec.js new file mode 100644 index 000000000..fa8db3714 --- /dev/null +++ b/src/helpers/question-chain-helper.spec.js @@ -0,0 +1,74 @@ +import { + buildQuestionChainAnswerValue, + mapQuestionAnswersForChain, + normalizeAnswerSelectedValue, +} from "./question-chain-helper"; + +describe("question-chain-helper", () => { + describe("buildQuestionChainAnswerValue", () => { + test("returns nextQuestion format when nextQuestionSequence is set", () => { + expect( + buildQuestionChainAnswerValue({ + questionSequence: 1, + nextQuestionSequence: 3, + answerResult: "ignored", + answerText: "Yes", + }) + ).toBe("1|nextQuestion|3|Yes"); + }); + + test("returns answer format for final answers", () => { + expect( + buildQuestionChainAnswerValue({ + questionSequence: 1, + nextQuestionSequence: null, + answerResult: "DW01705", + answerText: "Yes", + }) + ).toBe("1|answer|DW01705|Yes"); + }); + }); + + describe("normalizeAnswerSelectedValue", () => { + const answers = [{ value: "1|answer|DW01705|Yes" }, { value: "1|answer|DW01591|No" }]; + + test("returns empty string when answerSelected is empty", () => { + expect(normalizeAnswerSelectedValue("", answers)).toBe(""); + }); + + test("returns exact match when casing already matches", () => { + expect(normalizeAnswerSelectedValue("1|answer|DW01705|Yes", answers)).toBe( + "1|answer|DW01705|Yes" + ); + }); + + test("normalizes uppercase legacy values to button value casing", () => { + expect(normalizeAnswerSelectedValue("1|ANSWER|DW01705|YES", answers)).toBe( + "1|answer|DW01705|Yes" + ); + }); + }); + + describe("mapQuestionAnswersForChain", () => { + test("maps API answers to question-chain button answers", () => { + const result = mapQuestionAnswersForChain({ + questionSequence: 1, + answers: [ + { + answerResult: "DW01705", + answerText: "Yes", + nextQuestionSequence: null, + problemQuestionId: 123, + }, + ], + }); + + expect(result[0]).toMatchObject({ + buttonLabel: "Yes", + value: "1|answer|DW01705|Yes", + problemQuestionId: 123, + questionType: "answer", + }); + }); + }); +}); diff --git a/src/layouts/capability-questions/capability-questions.spec.js b/src/layouts/capability-questions/capability-questions.spec.js index fb4515f50..271a2e34c 100644 --- a/src/layouts/capability-questions/capability-questions.spec.js +++ b/src/layouts/capability-questions/capability-questions.spec.js @@ -213,7 +213,7 @@ describe("capabilityQuestions.vue", () => { }); describe("forwardButtonAction", () => { - test("Should clear out answerData", () => { + test("Should clear out answerData", async () => { // Arrange const { wrapper } = setupMocks({}); @@ -245,7 +245,7 @@ describe("capabilityQuestions.vue", () => { }); // Act - wrapper.vm.forwardButtonAction(); + await wrapper.vm.forwardButtonAction(); //Assert expect(wrapper.vm.questionsData[0].answerData).toEqual({}); diff --git a/src/layouts/capability-questions/capability-questions.vue b/src/layouts/capability-questions/capability-questions.vue index c4f5df92c..6b9f5aaa1 100644 --- a/src/layouts/capability-questions/capability-questions.vue +++ b/src/layouts/capability-questions/capability-questions.vue @@ -180,11 +180,6 @@ export default { }; }); - // clear out answerData for future page loads; must occur prior to store save - this.questionsData.forEach((glass) => { - glass.answerData = {}; - }); - // save to vuex store as order.damage.capabilityQuestionAnswers (array) // used in GET_PART_FROM_CAPABILITY_QUESTION_ANSWER call following this one await this.dispatchStoreAction( @@ -211,6 +206,8 @@ export default { } this.navigateForward(partsOrQuestions); + + this.clearQuestionsAnswerData(); }, }, components: { diff --git a/src/layouts/molding-questions/molding-questions.spec.js b/src/layouts/molding-questions/molding-questions.spec.js index 8daa6e780..bb668b62c 100644 --- a/src/layouts/molding-questions/molding-questions.spec.js +++ b/src/layouts/molding-questions/molding-questions.spec.js @@ -217,7 +217,7 @@ describe("moldingQuestions.vue", () => { }); describe("forwardButtonAction", () => { - test("Should clear out answerData", () => { + test("Should clear out answerData", async () => { // Arrange const { wrapper } = setupMocks({}); @@ -240,7 +240,7 @@ describe("moldingQuestions.vue", () => { }); // Act - wrapper.vm.forwardButtonAction(); + await wrapper.vm.forwardButtonAction(); //Assert expect(wrapper.vm.questionsData[0].answerData).toEqual({}); diff --git a/src/layouts/molding-questions/molding-questions.vue b/src/layouts/molding-questions/molding-questions.vue index a7f4900de..1e1cf1cb4 100644 --- a/src/layouts/molding-questions/molding-questions.vue +++ b/src/layouts/molding-questions/molding-questions.vue @@ -181,11 +181,6 @@ export default { }; }); - // clear out answerData for future page loads; must occur prior to store save - this.questionsData.forEach((glass) => { - glass.answerData = {}; - }); - // save to vuex store as order.damage.moldingQuestionArrays (array) await this.dispatchStoreAction( this.storeActions.SAVE_MOLDING_QUESTION_ANSWERS, @@ -209,6 +204,8 @@ export default { } this.navigateForward(partsOrQuestions); + + this.clearQuestionsAnswerData(); }, }, components: { diff --git a/src/layouts/part-questions/part-questions.spec.js b/src/layouts/part-questions/part-questions.spec.js index 4e7cb53dd..527f5dd74 100644 --- a/src/layouts/part-questions/part-questions.spec.js +++ b/src/layouts/part-questions/part-questions.spec.js @@ -258,9 +258,7 @@ describe("partQuestions.vue...", () => { }); // Act - wrapper.vm.forwardButtonAction(); - - await nextTick(); + await wrapper.vm.forwardButtonAction(); //Assert expect(wrapper.vm.questionsData[0].answerData).toEqual({}); diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 1c342543f..f6c3c7667 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -182,13 +182,6 @@ export default { }; }); - // clear out answerData for future page loads; must occur prior to store save - this.questionsData.forEach((glass) => { - if (glass.answerData) { - glass.answerData = {}; - } - }); - this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false); // save to vuex store as order.damage.partQuestionAnswers (array) @@ -209,6 +202,8 @@ export default { const glassPartsForStore = partsLookup.data.glassPieceParts; this.navigateForward(glassPartsForStore); + + this.clearQuestionsAnswerData(); }, }, components: { diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js index 1636f0c47..265d54fa2 100644 --- a/src/mixins/vehicle-questions-mixin.js +++ b/src/mixins/vehicle-questions-mixin.js @@ -7,9 +7,17 @@ import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; import { getIsWindshieldOnly } from "@/helpers/damage-helper"; import { partTypeStrings } from "@/constants/part-type-strings"; +import { buildQuestionChainAnswerValue } from "@/helpers/question-chain-helper"; export default { methods: { + clearQuestionsAnswerData() { + this.questionsData?.forEach((glass) => { + if (glass.answerData) { + glass.answerData = {}; + } + }); + }, hasPartQuestions(partsOrQuestions) { return partsOrQuestions?.some((pq) => pq.partQuestions?.length > 0); }, @@ -114,11 +122,12 @@ export default { ); }); // set the answerString to use for answerSelected - if (chosenAns.nextQuestionSequence) { - answerString = `${answeredQuestion.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`; - } else { - answerString = `${answeredQuestion.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`; - } + answerString = buildQuestionChainAnswerValue({ + questionSequence: answeredQuestion.questionNum, + nextQuestionSequence: chosenAns.nextQuestionSequence, + answerResult: chosenAns.answerResult, + answerText: chosenAns.answerText, + }); // mark this question as answered (question-chain will read this) glass.questions[answeredQuestion.questionNum - 1].answerSelected = @@ -177,6 +186,8 @@ export default { */ const self = vm ?? this; + const isIncomplete = !!answer.incomplete; + const hadSavedAnswer = !!self.questionsData[answer.index]?.answerData?.answerResult; // clear out any preloaded answers self.selectedAnswers = {}; @@ -193,7 +204,6 @@ export default { */ const answeredQuestionText = answeredQuestion.questionText.toUpperCase(); - const answeredQuestionAnswer = answeredQuestion.selectedAnswer.toUpperCase(); const answeredQuestionAnswerText = answeredQuestion.selectedAnswerText.toUpperCase(); const answeredQuestionNum = answeredQuestion.questionNum; @@ -208,8 +218,7 @@ export default { if (answeredQuestionIndex === 0) question.answerSelected = null; if (answeredQuestionNum - 1 === questionIndex) { - // on the right question - question.answerSelected = answeredQuestionAnswer; + question.answerSelected = answeredQuestion.selectedAnswer; } }); } @@ -346,6 +355,27 @@ export default { }); }); + if (isIncomplete) { + // User changed an earlier answer; downstream questions were cleared in the chain + self.questionsData[answer.index].answerData = null; + self.currentGlassIndex = answer.index; + + if (hadSavedAnswer) { + self.questionsData[answer.index].key = + (self.questionsData[answer.index].key ?? + self.questionsData[answer.index].answerKey ?? + answer.index) + Date.now().toString(); + } + + return; + } + + // Force re-render so the updated selection displays when changing a prior answer + self.questionsData[answer.index].key = + (self.questionsData[answer.index].key ?? + self.questionsData[answer.index].answerKey ?? + answer.index) + Date.now().toString(); + // set final answer data for the current answered glass part self.questionsData[answer.index].answerData = { answerResult: answer.answerResult, diff --git a/src/mixins/vehicle-questions-mixin.spec.js b/src/mixins/vehicle-questions-mixin.spec.js index 0fd027258..a557adefa 100644 --- a/src/mixins/vehicle-questions-mixin.spec.js +++ b/src/mixins/vehicle-questions-mixin.spec.js @@ -499,6 +499,88 @@ describe("vehicle-questions-mixin", () => { // Assert expect(wrapper.vm.selectedAnswers).toMatchObject({}); }); + + test("should clear answerData and keep currentGlassIndex when answer is incomplete", () => { + const answer = { + incomplete: true, + answeredQuestions: [ + { + questionText: "Is this the Overland edition?", + selectedAnswer: "1|nextQuestion|5|No", + selectedAnswerText: "No", + questionNum: 1, + }, + ], + index: 0, + }; + const { wrapper } = setupMocks({}); + + wrapper.vm.currentGlassIndex = 1; + wrapper.vm.questionsData = [ + { + glassLocation: "Windshield", + glassName: "Single", + questions: [ + { + questionSequence: 1, + questionText: "Is this the Overland edition?", + answers: [], + }, + ], + answerData: { answerResult: "OLD-PART" }, + answerKey: "Windshield-Single", + }, + { + glassLocation: "Driver", + glassName: "Front", + questions: [], + answerData: { answerResult: "OTHER-PART" }, + }, + ]; + + wrapper.vm.handleCompletedQuestionChainAnswers(answer, "", wrapper.vm); + + expect(wrapper.vm.questionsData[0].answerData).toBeNull(); + expect(wrapper.vm.questionsData[1].answerData).toBeNull(); + expect(wrapper.vm.currentGlassIndex).toBe(0); + }); + + test("should not bump key when incomplete answer had no saved answer", () => { + const answer = { + incomplete: true, + answeredQuestions: [ + { + questionText: "Is this the Overland edition?", + selectedAnswer: "1|nextQuestion|2|Yes", + selectedAnswerText: "Yes", + questionNum: 1, + }, + ], + index: 0, + }; + const { wrapper } = setupMocks({}); + + wrapper.vm.questionsData = [ + { + glassLocation: "Windshield", + glassName: "Single", + questions: [ + { + questionSequence: 1, + questionText: "Is this the Overland edition?", + answers: [], + }, + ], + answerData: null, + answerKey: "Windshield-Single", + key: "stable-key", + }, + ]; + + wrapper.vm.handleCompletedQuestionChainAnswers(answer, "", wrapper.vm); + + expect(wrapper.vm.questionsData[0].key).toBe("stable-key"); + }); }); describe("questions in glass parts that are after the answered glass", () => { From bb02e45d66c02d270c7fec3ea311dafd8aff9f93 Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Thu, 16 Jul 2026 11:00:41 -0400 Subject: [PATCH 02/13] CASH-3034 - prettier check --- .../question-chain/question-chain.spec.js | 28 +++++++++++++++---- .../questions-page-layout.spec.js | 4 ++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/digital-components/question-chain/question-chain.spec.js b/src/digital-components/question-chain/question-chain.spec.js index 1bdc43d4a..1dad2d5de 100644 --- a/src/digital-components/question-chain/question-chain.spec.js +++ b/src/digital-components/question-chain/question-chain.spec.js @@ -266,8 +266,16 @@ describe("Question Chain component", () => { questionSequence: 3, questionText: "Laminated door glass?", answers: [ - { answerResult: "PART-C", answerText: "Yes", nextQuestionSequence: null }, - { answerResult: "PART-D", answerText: "No", nextQuestionSequence: null }, + { + answerResult: "PART-C", + answerText: "Yes", + nextQuestionSequence: null, + }, + { + answerResult: "PART-D", + answerText: "No", + nextQuestionSequence: null, + }, ], answerSelected: "3|answer|PART-C|Yes", }, @@ -275,8 +283,16 @@ describe("Question Chain component", () => { questionSequence: 5, questionText: "Factory installed antenna?", answers: [ - { answerResult: "PART-E", answerText: "Yes", nextQuestionSequence: null }, - { answerResult: "PART-F", answerText: "No", nextQuestionSequence: null }, + { + answerResult: "PART-E", + answerText: "Yes", + nextQuestionSequence: null, + }, + { + answerResult: "PART-F", + answerText: "No", + nextQuestionSequence: null, + }, ], }, ], @@ -451,7 +467,9 @@ describe("Question Chain component", () => { ); expect( wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question)) - ).not.toEqual(expect.arrayContaining([expect.objectContaining({ questionSequence: 2 })])); + ).not.toEqual( + expect.arrayContaining([expect.objectContaining({ questionSequence: 2 })]) + ); }); test("should resume at question 3 after remount when Q1 No was already saved", async () => { diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js index ba96232db..db34bd262 100644 --- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js +++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js @@ -177,7 +177,9 @@ describe("questionsPageLayout.vue", () => { await wrapper.setProps({ index: 1 }); const testGlassPiece = { - questions: [{ questionSequence: 1, questionText: "Rain sensing wipers?", answers: [] }], + questions: [ + { questionSequence: 1, questionText: "Rain sensing wipers?", answers: [] }, + ], answerData: null, }; From 9ce35eb3c18fcda6797c97c0797f4742b254d76e Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 16 Jul 2026 14:12:30 -0400 Subject: [PATCH 03/13] CASH-3031: Update afterpay learn more link --- .../afterpay-modal-banner.vue | 211 ++---------------- src/layouts/quote/quote.vue | 3 +- 2 files changed, 22 insertions(+), 192 deletions(-) diff --git a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue index cc708dc6a..764d5c9c0 100644 --- a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue +++ b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue @@ -1,5 +1,9 @@ @@ -198,117 +137,9 @@ export default { @include media-breakpoint-up(md) { padding-left: 1rem; } - } -} -.afterpay-modal { - &.modal.modal-component { - :deep(.modal-dialog) { - margin: 0 1rem; - top: 0; - - @include media-breakpoint-up(md) { - width: 100%; - max-width: 840px; - top: 3.4rem; - right: 0; - left: 0; - transform: none; - margin: 0 auto; - padding: 0 1rem; - } - .modal-content { - border-radius: 0.5rem; - margin: 1.5rem 0; - - .modal-header.mt-6 { - background-color: $blue-100; - margin-top: 0; - padding: 1.5rem 2rem 1.5rem 1.5rem; - font-family: UrbanistSemibold; - - .modal-title.justify-content-center { - // OVERRIDE - justify-content: flex-start; - } - .btn-close { - top: 1.25rem; - right: 1.5rem; - } - } - .afterpay-section { - display: block; - border: 1px solid $gray-200; - border-radius: 0.25rem; - padding: 1rem; - - .section-header { - display: flex; - align-items: center; - margin-bottom: 1rem; - - img { - width: 40px; - height: 40px; - } - h3 { - font-family: UrbanistBold; - font-size: 1rem; - margin: 0 0 0 0.5rem; - color: $black; - } - } - p { - font-size: 0.75rem; - } - strong { - font-family: UrbanistSemibold; - font-size: 0.875rem; - } - } - - .modal-body { - padding: 1.5rem; - - .afterpay-sections { - @include media-breakpoint-up(md) { - display: flex; - column-gap: 1rem; - align-items: stretch; - } - .afterpay-section { - margin-bottom: 1rem; - - @include media-breakpoint-up(md) { - flex: 0 1 33%; - margin-bottom: 0; - } - - &:last-of-type { - margin-bottom: 0; - } - } - } - } - } - &.modal-dialog-centered { - height: auto; - min-height: auto; - } - } - } - - :deep(.modal-footer) { - position: relative; - background: $gray-100; - - .modal-disclaimer { - font-style: italic; - font-size: 0.75rem; - a { - padding: 0; - line-height: 1.625; - } + img { + transform: translateY(-0.1rem); } } } diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 50512bcbf..920aa8617 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -47,8 +47,7 @@ + cmsWidgetName="AfterpayBannerWidget" /> Date: Fri, 17 Jul 2026 14:10:44 -0400 Subject: [PATCH 04/13] CASH-3049 | Add promo banner to quote page --- src/constants/experiments.js | 3 + .../quote/promo-banner/promo-banner.vue | 59 +++++++++++++++++++ src/layouts/quote/quote.vue | 26 +++++++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/layouts/quote/promo-banner/promo-banner.vue diff --git a/src/constants/experiments.js b/src/constants/experiments.js index 0caec2080..e30984163 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -61,6 +61,9 @@ const experimentSettings = { // YMM BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles", + + // Promo Banner + SHOW_PROMO_BANNER: "ShowQuotePageRegionalPromoBanner", }; const experimentTriggers = { diff --git a/src/layouts/quote/promo-banner/promo-banner.vue b/src/layouts/quote/promo-banner/promo-banner.vue new file mode 100644 index 000000000..f26a42593 --- /dev/null +++ b/src/layouts/quote/promo-banner/promo-banner.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index cad68ef0f..42e3845ab 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -5,6 +5,14 @@
+
+
+ +
+
@@ -102,7 +110,12 @@ + class="quote-disclaimer text-left text-md-center" /> +
@@ -133,6 +146,7 @@ import textBlock from "@/digital-components/text-block/text-block"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner"; +import promoBanner from "@/layouts/quote/promo-banner/promo-banner"; import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue"; import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question"; import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question"; @@ -683,6 +697,12 @@ export default { showAfterpayBanner() { return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration; }, + showPromoBanner() { + return experimentMixin.methods.hasSettingEqualTo( + experimentSettings.SHOW_PROMO_BANNER, + "true" + ); + }, isRecalPriceRemove() { return ( experimentMixin.methods @@ -1034,6 +1054,7 @@ export default { contentGroupModal, loadingModal, afterpayModalBanner, + promoBanner, promoModalQuestion, recalDisclaimer, saveProgressModalQuestion, @@ -1159,4 +1180,7 @@ export default { :deep(.promo-modal-question a) { @include responsive-font-size-md(0.875rem, 1rem); } +.quote-disclaimer:last-child { + margin-bottom: 1.5rem; +} From 96c288a1ca70c22917a2dc6e0b93a792a86b4576 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Mon, 20 Jul 2026 13:09:33 -0400 Subject: [PATCH 05/13] CASH-1911: Add scrollbar and scroll to error message for short viewports --- .../save-progress-popup-question.spec.js | 24 +++++++++++++++++++ .../save-progress-popup-question.vue | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js index 273f39f11..29d102d01 100644 --- a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js +++ b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js @@ -159,13 +159,37 @@ describe("save-progress-popup-question ", () => { wrapper.vm.selectContactMethod("PhoneAnswer"); wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true }); const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle"); + const scrollSpy = jest + .spyOn(wrapper.vm, "scrollConsentIntoView") + .mockResolvedValue(undefined); await wrapper.vm.validatePhoneAndSave(); expect(wrapper.vm.showConsentErrors).toBe(true); + expect(scrollSpy).toHaveBeenCalled(); expect(resetSpy).toHaveBeenCalled(); }); + test("should scroll consent checkboxes into view", async () => { + const { wrapper } = setupMocks({ + props: { + modalWidgetName: "SaveProgressPopupWidget", + }, + }); + const mockScrollIntoView = jest.fn(); + const consentEl = document.createElement("fieldset"); + consentEl.className = "save-progress-popup-sms-consent"; + consentEl.scrollIntoView = mockScrollIntoView; + wrapper.element.appendChild(consentEl); + + await wrapper.vm.scrollConsentIntoView(); + + expect(mockScrollIntoView).toHaveBeenCalledWith({ + behavior: "smooth", + block: "nearest", + }); + }); + test("should reset the send button loader when phone validation fails", async () => { const { wrapper } = setupMocks({ props: { diff --git a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue index 172f81718..fc4e95530 100644 --- a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue +++ b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue @@ -43,6 +43,7 @@ isRequired validationRules="phone-number-required" /> Date: Tue, 21 Jul 2026 15:16:23 -0400 Subject: [PATCH 06/13] CASH-3065 - Pass billToAccountNumber to v2 parts endpoint --- src/store/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/store/index.js b/src/store/index.js index d9ca5c6fe..ea09dfb84 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2015,6 +2015,7 @@ export const actions = { const vin = vehicle.vin; const serviceType = damage.isRepair ? "Repair" : "Install"; const parentAccountNumber = context.getters.payment.parentAccountNumber; + const billToAccountNumber = context.getters.payment.billToAccountNumber; const referralSeqNumber = order.referralSequenceNumber; // create a new array to avoid mutating state @@ -2036,6 +2037,7 @@ export const actions = { serviceType: serviceType, referralSeqNumber: referralSeqNumber, parentAccountNumber: parentAccountNumber, + billToAccountNumber: billToAccountNumber, }, logApiCall: true, pageNameToLog: pageNameToLog, From cc8448752e7da99e9e37b8c621bd9afc8f219c0e Mon Sep 17 00:00:00 2001 From: mvalaiyapathi Date: Wed, 22 Jul 2026 09:11:16 -0400 Subject: [PATCH 07/13] Revert "CASH-3065 - Pass billToAccountNumber to v2 parts endpoint" --- src/store/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/store/index.js b/src/store/index.js index ea09dfb84..d9ca5c6fe 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2015,7 +2015,6 @@ export const actions = { const vin = vehicle.vin; const serviceType = damage.isRepair ? "Repair" : "Install"; const parentAccountNumber = context.getters.payment.parentAccountNumber; - const billToAccountNumber = context.getters.payment.billToAccountNumber; const referralSeqNumber = order.referralSequenceNumber; // create a new array to avoid mutating state @@ -2037,7 +2036,6 @@ export const actions = { serviceType: serviceType, referralSeqNumber: referralSeqNumber, parentAccountNumber: parentAccountNumber, - billToAccountNumber: billToAccountNumber, }, logApiCall: true, pageNameToLog: pageNameToLog, From adb149f22e01f94a095db46f511500b69340b4b0 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Wed, 22 Jul 2026 11:35:07 -0400 Subject: [PATCH 08/13] CASH-3052: Group VAPS in adyen cart --- src/layouts/payment-adyen/payment-adyen.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index fc3ad668d..213d21cb1 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -24,7 +24,7 @@ Date: Fri, 24 Jul 2026 07:45:26 -0400 Subject: [PATCH 09/13] CASH-3075 add validation for phone extenstion CASH-3075 add validation for phone extenstion --- src/constants/error-messages.js | 1 + src/layouts/policy-info/policy-info.vue | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 58f22ebe2..262979ed4 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -32,6 +32,7 @@ const errorMessages = { DATE_REQUIRED: "Please select a date", PHONE_REQUIRED: "Please enter your phone number", PHONE_FORMAT: "Phone number must be 10 digits", + PHONE_EXTENSION_FORMAT: "The specified extension is invalid", SMS_CONSENT_REQUIRED_1: "Please select checkbox to receive text messages", SMS_CONSENT_REQUIRED_2: "Please select at least one consent option", YEAR_REQUIRED: "Please select your vehicle year", diff --git a/src/layouts/policy-info/policy-info.vue b/src/layouts/policy-info/policy-info.vue index 14c7b93ce..f246b9387 100644 --- a/src/layouts/policy-info/policy-info.vue +++ b/src/layouts/policy-info/policy-info.vue @@ -87,7 +87,9 @@ class="mb-4" cmsWidgetName="PhoneExtensionWidget" v-model="phoneExtension" - inputId="phoneExtension" /> + inputId="phoneExtension" + maxLength="5" + validationRules="phone-extension-format" />
@@ -321,6 +323,7 @@ defineRule("policy-number-format", (value) => { defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED)); defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT)); defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED)); +defineRule("phone-extension-format", regex(/^\d{1,5}$/, errorMessages.PHONE_EXTENSION_FORMAT)); defineRule("date-of-loss-required", required(errorMessages.DATE_OF_LOSS_REQUIRED)); defineRule("damage-types-required", required(errorMessages.DAMAGE_TYPES_REQUIRED)); defineRule("state-required", required(errorMessages.STATE_REQUIRED)); From 5b6211301cd58f1a2905a5423622e66662f788dc Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Fri, 24 Jul 2026 13:28:39 -0400 Subject: [PATCH 10/13] CASH-3078 - Fix question-chain resolving when answerResult has a part number --- .../question-chain/question-chain.spec.js | 77 +++++++++++++++++++ .../question-chain/question-chain.vue | 14 +++- src/helpers/question-chain-helper.js | 10 +++ src/helpers/question-chain-helper.spec.js | 16 ++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/digital-components/question-chain/question-chain.spec.js b/src/digital-components/question-chain/question-chain.spec.js index 1dad2d5de..d0e54a895 100644 --- a/src/digital-components/question-chain/question-chain.spec.js +++ b/src/digital-components/question-chain/question-chain.spec.js @@ -482,9 +482,86 @@ describe("Question Chain component", () => { expect(wrapper.vm.currentQuestionNum).toBe(3); expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true); }); + + test("should resolve on Q1 Yes and not show Q2 for Honda HUD question", async () => { + const { wrapper } = setupMocks({ + questionDataProp: getHondaAccordWindshieldQuestions(), + }); + await nextTick(); + + const q1 = wrapper.vm.questions[0]; + wrapper.vm.handleAnswer(q1, "1|answer|FW04796|Yes"); + + expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({ + answerResult: "FW04796", + index: 0, + }); + expect(wrapper.vm.currentQuestionNum).toBe(0); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(false); + }); + + test("should resolve on Q3 No and not continue after remount", async () => { + const questionDataProp = getHondaAccordWindshieldQuestions(); + questionDataProp[0].answerSelected = "1|nextQuestion|2|No"; + questionDataProp[1].answerSelected = "2|nextQuestion|3|No"; + questionDataProp[2].answerSelected = "3|answer|FW04793|No"; + + const { wrapper } = setupMocks({ questionDataProp }); + await nextTick(); + + expect(wrapper.vm.currentQuestionNum).toBe(0); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true); + expect( + wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question)) + ).toHaveLength(3); + }); + + test("should show Q2 after answering No on Honda HUD question", async () => { + const { wrapper } = setupMocks({ + questionDataProp: getHondaAccordWindshieldQuestions(), + }); + await nextTick(); + + const q1 = wrapper.vm.questions[0]; + wrapper.vm.handleAnswer(q1, "1|nextQuestion|2|No"); + + expect(wrapper.vm.currentQuestionNum).toBe(2); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(true); + }); }); }); +function getHondaAccordWindshieldQuestions() { + return [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with a Heads-up Display which projects vehicle information, such as speed, onto the windshield?", + answers: [ + { answerResult: "FW04796", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "", answerText: "No", nextQuestionSequence: 2 }, + ], + }, + { + questionSequence: 2, + questionText: + "Is your vehicle equipped with an auto-dimming rearview mirror which will darken automatically when a vehicles approaches from the rear at night?", + answers: [ + { answerResult: "FW04795", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "", answerText: "No", nextQuestionSequence: 3 }, + ], + }, + { + questionSequence: 3, + questionText: "Is your vehicle equipped with a power moonroof?", + answers: [ + { answerResult: "FW04794", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "FW04793", answerText: "No", nextQuestionSequence: null }, + ], + }, + ]; +} + function getCherokeeWindshieldQuestions() { return [ { diff --git a/src/digital-components/question-chain/question-chain.vue b/src/digital-components/question-chain/question-chain.vue index b37755ee7..467e9619e 100644 --- a/src/digital-components/question-chain/question-chain.vue +++ b/src/digital-components/question-chain/question-chain.vue @@ -20,6 +20,7 @@ import buttonQuestion from "@/digital-components/button-question/button-question"; import { useValidateForm } from "vee-validate"; import { + isTerminalQuestionChainAnswer, mapQuestionAnswersForChain, normalizeAnswerSelectedValue, } from "@/helpers/question-chain-helper"; @@ -74,13 +75,18 @@ export default { return !!question.answerSelected || this.isCurrentQuestion(question); }, initializeCurrentQuestionNum() { - const pendingNextQuestion = [...this.questions] + const lastAnsweredQuestion = [...this.questions] .reverse() - .find((question) => question.answerSelected?.includes("|nextQuestion|")); + .find((question) => question.answerSelected); - if (pendingNextQuestion) { + if (isTerminalQuestionChainAnswer(lastAnsweredQuestion?.answerSelected)) { + this.currentQuestionNum = 0; + return; + } + + if (lastAnsweredQuestion?.answerSelected?.includes("|nextQuestion|")) { const nextQuestionSequence = Number( - pendingNextQuestion.answerSelected.split("|")[2] + lastAnsweredQuestion.answerSelected.split("|")[2] ); if ( diff --git a/src/helpers/question-chain-helper.js b/src/helpers/question-chain-helper.js index b5ba8c168..fdf7f194c 100644 --- a/src/helpers/question-chain-helper.js +++ b/src/helpers/question-chain-helper.js @@ -32,6 +32,16 @@ export function mapQuestionAnswersForChain(question) { })); } +export function isTerminalQuestionChainAnswer(answerSelected) { + if (!answerSelected) { + return false; + } + + const [, questionType, answerResult] = answerSelected.split("|"); + + return questionType?.toLowerCase() === "answer" && !!answerResult; +} + export function normalizeAnswerSelectedValue(answerSelected, answers) { if (!answerSelected) { return ""; diff --git a/src/helpers/question-chain-helper.spec.js b/src/helpers/question-chain-helper.spec.js index fa8db3714..f3a31acc1 100644 --- a/src/helpers/question-chain-helper.spec.js +++ b/src/helpers/question-chain-helper.spec.js @@ -1,5 +1,6 @@ import { buildQuestionChainAnswerValue, + isTerminalQuestionChainAnswer, mapQuestionAnswersForChain, normalizeAnswerSelectedValue, } from "./question-chain-helper"; @@ -29,6 +30,21 @@ describe("question-chain-helper", () => { }); }); + describe("isTerminalQuestionChainAnswer", () => { + test("returns true when answer has a part number and no next question", () => { + expect(isTerminalQuestionChainAnswer("1|answer|FW04796|Yes")).toBe(true); + expect(isTerminalQuestionChainAnswer("3|answer|FW04793|No")).toBe(true); + }); + + test("returns false when answer continues to the next question", () => { + expect(isTerminalQuestionChainAnswer("1|nextQuestion|2|No")).toBe(false); + }); + + test("returns false when answer type is answer but part number is missing", () => { + expect(isTerminalQuestionChainAnswer("1|answer||No")).toBe(false); + }); + }); + describe("normalizeAnswerSelectedValue", () => { const answers = [{ value: "1|answer|DW01705|Yes" }, { value: "1|answer|DW01591|No" }]; From 8be35fea23282f9e424dc301e31abc05d2886305 Mon Sep 17 00:00:00 2001 From: Carl Nation Date: Fri, 24 Jul 2026 16:48:57 -0400 Subject: [PATCH 11/13] CASH-3036 missed otherResponsibleParty CASH-3036 missed otherResponsibleParty --- src/layouts/policy-info/policy-info.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/layouts/policy-info/policy-info.vue b/src/layouts/policy-info/policy-info.vue index f246b9387..b59811c3a 100644 --- a/src/layouts/policy-info/policy-info.vue +++ b/src/layouts/policy-info/policy-info.vue @@ -496,9 +496,11 @@ export default { const additionalDamageFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.ADDITIONAL_DAMAGE]; const rentalFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.RENTAL]; + const otherResponsibleParty = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.OTHER_RESPONSIBLE_PARTY]; return Boolean( this.morePolicyQuestions?.[additionalDamageFlag] || - this.morePolicyQuestions?.[rentalFlag] + this.morePolicyQuestions?.[rentalFlag] || + this.morePolicyQuestions?.[otherResponsibleParty] ); }, }, From f21f21e68ddc645b81d862e8a1206c5c0db143d0 Mon Sep 17 00:00:00 2001 From: Carl Nation Date: Fri, 24 Jul 2026 16:54:50 -0400 Subject: [PATCH 12/13] prettier --- src/layouts/policy-info/policy-info.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/layouts/policy-info/policy-info.vue b/src/layouts/policy-info/policy-info.vue index b59811c3a..3858eb1ad 100644 --- a/src/layouts/policy-info/policy-info.vue +++ b/src/layouts/policy-info/policy-info.vue @@ -496,7 +496,8 @@ export default { const additionalDamageFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.ADDITIONAL_DAMAGE]; const rentalFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.RENTAL]; - const otherResponsibleParty = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.OTHER_RESPONSIBLE_PARTY]; + const otherResponsibleParty = + morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.OTHER_RESPONSIBLE_PARTY]; return Boolean( this.morePolicyQuestions?.[additionalDamageFlag] || this.morePolicyQuestions?.[rentalFlag] || From 0a777012d872acc82307bce72234dc7a76f2553c Mon Sep 17 00:00:00 2001 From: Carl Nation Date: Fri, 24 Jul 2026 17:04:23 -0400 Subject: [PATCH 13/13] test test --- src/layouts/policy-info/policy-info.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/policy-info/policy-info.spec.js b/src/layouts/policy-info/policy-info.spec.js index 6caaa9f34..4907461b7 100644 --- a/src/layouts/policy-info/policy-info.spec.js +++ b/src/layouts/policy-info/policy-info.spec.js @@ -87,12 +87,12 @@ describe("policy-info.vue", () => { expect(wrapper.vm.displayClaimNumber).toBe(true); }); - test("is false when only unrelated flags (e.g. otherResponsibleParty, thirdPartyVehicle, injuries) are checked", async () => { + test("is false when only unrelated flags (e.g. thirdPartyVehicle, injuries) are checked", async () => { const { wrapper } = setupMocks(); wrapper.vm.morePolicyQuestions = { additionalDamage: false, rental: false, - otherResponsibleParty: true, + otherResponsibleParty: false, thirdPartyVehicle: true, injuries: true, };