Merge branch 'release/2026.07.30' into rlsmerge/2026.07.30-to-dev

This commit is contained in:
Carl Nation 2026-07-24 17:13:49 -04:00
commit 72ed0387df
24 changed files with 903 additions and 290 deletions

View file

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

View file

@ -66,6 +66,9 @@ const experimentSettings = {
// YMM
BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles",
// Promo Banner
SHOW_PROMO_BANNER: "ShowQuotePageRegionalPromoBanner",
};
const experimentTriggers = {

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,103 @@ 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 +419,205 @@ 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);
});
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 [
{
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 +644,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,11 @@
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import { useValidateForm } from "vee-validate";
import {
isTerminalQuestionChainAnswer,
mapQuestionAnswersForChain,
normalizeAnswerSelectedValue,
} from "@/helpers/question-chain-helper";
export default {
name: "questionChain",
@ -34,54 +39,86 @@ 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 lastAnsweredQuestion = [...this.questions]
.reverse()
.find((question) => question.answerSelected);
if (isTerminalQuestionChainAnswer(lastAnsweredQuestion?.answerSelected)) {
this.currentQuestionNum = 0;
return;
}
if (lastAnsweredQuestion?.answerSelected?.includes("|nextQuestion|")) {
const nextQuestionSequence = Number(
lastAnsweredQuestion.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 +137,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 +164,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 +197,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,43 @@ 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

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

View file

@ -43,6 +43,7 @@
isRequired
validationRules="phone-number-required" />
<saveProgressPopupSmsConsentQuestion
ref="smsConsentQuestion"
v-model="smsConsent"
:consentCopy="smsConsentCopy"
:isDisabled="isProgressSaved"
@ -240,6 +241,7 @@ export default {
!hasSaveProgressSmsConsentSelection(this.smsConsent, this.showConsentManagement)
) {
this.showConsentErrors = true;
await this.scrollConsentIntoView();
return;
}
@ -252,6 +254,14 @@ export default {
resetPhoneSendButtonStyle() {
this.$refs.phoneSendButton?.resetButtonStyle();
},
async scrollConsentIntoView() {
await this.$nextTick();
const consentEl =
this.$refs.smsConsentQuestion?.$el ??
this.$el?.querySelector(".save-progress-popup-sms-consent");
consentEl?.scrollIntoView({ behavior: "smooth", block: "nearest" });
},
async saveProgress() {
await saveProgressPopupContactToStore(this.dispatchStoreAction, {
contactMethod: this.contactMethod,
@ -372,6 +382,11 @@ export default {
}
}
.modal.modal-component .modal-dialog .modal-content {
max-height: calc(100dvh - 1rem);
display: flex;
flex-direction: column;
overflow: hidden;
p.modal-body {
padding: 0;
}
@ -379,6 +394,9 @@ export default {
display: flex;
flex-direction: column;
padding: 0 1.5rem;
overflow-y: auto;
flex: 1 1 auto;
min-height: 0;
.textbox-question {
padding: 0;
@ -499,6 +517,8 @@ export default {
text-align: left;
order: 3;
margin: 0;
max-height: calc(100dvh - 30rem);
overflow-y: auto;
a {
text-decoration: none;

View file

@ -0,0 +1,59 @@
/**
* 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 isTerminalQuestionChainAnswer(answerSelected) {
if (!answerSelected) {
return false;
}
const [, questionType, answerResult] = answerSelected.split("|");
return questionType?.toLowerCase() === "answer" && !!answerResult;
}
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,90 @@
import {
buildQuestionChainAnswerValue,
isTerminalQuestionChainAnswer,
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("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" }];
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

@ -24,7 +24,7 @@
<cart
ref="cart"
:damage="damageInfo"
:availableVaps="[]"
:availableVaps="availableVaps"
:allowItemRemoval="false"
v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
@ -610,6 +610,9 @@ export default {
lineItems() {
return deepClone(this.$store.getters.lineItems);
},
availableVaps() {
return this.$store.getters.order.lineItems?.vaps ?? [];
},
},
mounted() {

View file

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

View file

@ -87,7 +87,9 @@
class="mb-4"
cmsWidgetName="PhoneExtensionWidget"
v-model="phoneExtension"
inputId="phoneExtension" />
inputId="phoneExtension"
maxLength="5"
validationRules="phone-extension-format" />
</div>
</div>
@ -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));
@ -493,9 +496,12 @@ 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]
);
},
},

View file

@ -1,5 +1,9 @@
<template>
<div class="afterpay-modal-banner" role="alert">
<component
:is="'script'"
src="https://js.squarecdn.com/square-marketplace.js"
async></component>
<div>
<span
class="callout"
@ -17,52 +21,19 @@
<span v-else v-html="token"></span>
</span>
<span class="nbsp">&ensp;</span>
<textLink
linkType="text"
:text="bannerCta"
href="javascript:void(0)"
@click-event="openModal" />
<a
href="#"
class="text-link"
data-afterpay-modal="en_US"
@click.prevent="onLearnMoreClick">
{{ bannerCta }}
</a>
</div>
</div>
<modal
:ref="modalName"
class="afterpay-modal"
:headerText="modalHeaderText"
:isFooterButtonSuppressed="true">
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot>
<span class="afterpay-sections">
<span
v-for="section in afterpaySectionsContent"
:key="section"
class="afterpay-section">
<span class="section-header">
<img v-if="section.AnswerImageUrl" :src="section.AnswerImageUrl" />
<h3 v-if="section.Text" v-html="section.Text"></h3>
</span>
<span v-html="section.content"></span>
</span>
</span>
</template>
<template v-slot:modal-footer-slot>
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
</template>
</modal>
</template>
<script>
import {
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
splitCMSCopyOnBR,
} from "@/helpers/cms-content-helper";
import textLink from "@/ux-components/text-link/text-link";
import modal from "@/digital-components/modal/modal";
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
@ -82,25 +53,18 @@ export default {
name: "afterpay-banner",
props: {
cmsWidgetName: String,
modalWidgetName: String,
},
data() {
return {};
},
methods: {
isInlineImageToken,
isAfterpayPriceToken,
getInlineAltText,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
openModal() {
this.modal?.openModal();
},
closeModal() {
this.modal?.closeModal();
onLearnMoreClick() {
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.CLICKED,
this.bannerCta,
true
);
},
},
computed: {
@ -119,31 +83,6 @@ export default {
bannerCta() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalDisclaimerText() {
return this.getCmsContent(this.modalWidgetName, "BodyText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
afterpaySectionsContent() {
const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
if (!sections) return [];
sections.forEach((section) => {
const sectionContent = this.getCmsContent(section.Name, "BodyText");
section["content"] = sectionContent;
});
return sections;
},
},
components: {
textLink,
modal,
},
};
</script>
@ -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;
}
transform: translateY(-0.1rem);
}
}
}

View file

@ -0,0 +1,59 @@
<template>
<div class="promo-banner" role="note">
<img class="promo-banner__icon" :src="promoTagIcon" alt="" width="31" height="36" />
<p class="promo-banner__copy mb-0" v-html="promoBannerBodyText"></p>
</div>
</template>
<script>
export default {
name: "promo-banner",
props: {
cmsWidgetName: String,
},
computed: {
promoTagIcon() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
promoBannerBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
},
};
</script>
<style lang="scss" scoped>
.promo-banner {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
width: 100%;
padding: 1rem 1.25rem;
background-color: $gray-100;
border-radius: 1rem;
text-align: left;
@include media-breakpoint-up(md) {
justify-content: center;
text-align: center;
}
&__icon {
flex-shrink: 0;
width: 31px;
height: 36px;
}
&__copy {
color: $gray-650;
font-size: $font-size-14;
line-height: 1.5;
:deep(strong) {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
color: $black;
}
}
}
</style>

View file

@ -5,6 +5,14 @@
<div
class="container quote position-relative"
:class="[showSaveProgressPopup ? 'initial-popup-open' : '']">
<div class="row justify-content-center">
<div class="col-12">
<promoBanner
v-if="showPromoBanner"
cmsWidgetName="PromoBannerWidget"
class="mb-4" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-12">
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
@ -47,8 +55,7 @@
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
<afterpayModalBanner
v-if="showAfterpayBanner"
cmsWidgetName="AfterpayBannerWidget"
modalWidgetName="AfterpayModalWidget" />
cmsWidgetName="AfterpayBannerWidget" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
<recalDisclaimer
@ -102,7 +109,12 @@
<textBlock
cmsWidgetName="quoteDisclaimer"
typeStyle="caption"
class="mb-5 quote-disclaimer text-left text-md-center" />
class="quote-disclaimer text-left text-md-center" />
<textBlock
v-if="showPromoBanner"
cmsWidgetName="PromoBannerDisclaimerWidget"
typeStyle="caption"
class="quote-disclaimer text-left text-md-center" />
</div>
</div>
@ -134,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";
@ -684,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
@ -1041,6 +1060,7 @@ export default {
contentGroupModal,
loadingModal,
afterpayModalBanner,
promoBanner,
promoModalQuestion,
recalDisclaimer,
saveProgressModalQuestion,
@ -1166,4 +1186,7 @@ export default {
:deep(.promo-modal-question a) {
@include responsive-font-size-md(0.875rem, 1rem);
}
.quote-disclaimer:last-child {
margin-bottom: 1.5rem;
}
</style>

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