CASH-3034- part-questions/Capability questions/Moulding questions UI fixes and question-chain helper

This commit is contained in:
Minojhini Valaiyapathi 2026-07-16 10:25:06 -04:00
parent 4e3a6d3067
commit e37a3178a3
14 changed files with 606 additions and 92 deletions

View file

@ -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,8 +403,126 @@ 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;

View file

@ -2,9 +2,9 @@
<div v-for="(q, i) in questions" :key="i">
<transition appear name="fade" mode="out-in">
<buttonQuestion
v-if="q.answerSelected || q.questionSequence === currentQuestionNum"
v-if="isQuestionVisible(q)"
class="radioQuestion"
:class="q.questionSequence === currentQuestionNum && 'current-question'"
:class="isCurrentQuestion(q) && 'current-question'"
:questionText="q.questionText"
:answers="q.answers"
:groupName="`question-${index}-${q.questionSequence}`"
@ -19,6 +19,10 @@
<script>
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

View file

@ -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...", () => {

View file

@ -10,7 +10,9 @@
</div>
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7 question-chain-wrapper">
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
<div
v-for="(questionsDatum, i) in questionsData"
:key="questionsDatum.key ?? questionsDatum.answerKey ?? i">
<questionChain
ref="questionChain"
v-model="selectedAnswers[questionsDatum.answerKey]"
@ -18,10 +20,12 @@
:index="i"
v-if="showThisQuestionChain(questionsDatum, i)"
:answerKey="questionsDatum.answerKey"
:hasSavedAnswer="!!questionsDatum.answerData?.answerResult"
:hasDownstreamSavedAnswers="hasDownstreamSavedAnswers(i)"
:validationRules="validationRules" />
<!-- Save your progress slot -->
<slot></slot>
</div>
<!-- Save your progress slot (once per page, not per glass piece) -->
<slot></slot>
<navbar
ref="navbar"
@ -60,12 +64,22 @@ export default {
},
},
methods: {
hasDownstreamSavedAnswers(glassIndex) {
return this.questionsData
?.slice(glassIndex + 1)
.some((glass) => 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");

View file

@ -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;
}

View file

@ -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",
});
});
});
});

View file

@ -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({});

View file

@ -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: {

View file

@ -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({});

View file

@ -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: {

View file

@ -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({});

View file

@ -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: {

View file

@ -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,

View file

@ -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", () => {