Merge branch 'release/2026.08.13' into feature/CASH-2799

This commit is contained in:
Chloe Herd 2026-07-30 13:13:28 -04:00
commit 1d6c22f54d
3 changed files with 118 additions and 16 deletions

View file

@ -324,18 +324,65 @@ describe("Question Chain component", () => {
}); });
}); });
test("should trigger scroll to .current-question if returnedAnswer is a nextQuestion (not a final answer)", async () => { test("should defer scroll until after the next question fade enters", async () => {
//Arrange //Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({
questionDataProp: [
{
questionSequence: 1,
questionText: "Question 1?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 3,
},
{
answerResult: "DB10840",
answerText: "No",
nextQuestionSequence: null,
},
],
},
{
questionSequence: 3,
questionText: "Question 3?",
answers: [
{
answerResult: "DB09410",
answerText: "Yes",
nextQuestionSequence: null,
},
],
},
],
});
await nextTick();
const testReturnedAnswer = "1|nextQuestion|3|No"; const testReturnedAnswer = "1|nextQuestion|3|No";
const scrollSpy = jest.spyOn(wrapper.vm, "scrollCurrentQuestionIntoView");
//Act //Act
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer); wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
const nextQuestion = wrapper.vm.questions.find((q) => q.questionSequence === 3);
await nextTick(); //Assert — advancing via nextQuestion no longer scrolls immediately
expect(scrollSpy).not.toHaveBeenCalled();
expect(wrapper.vm.currentQuestionNum).toBe(3);
expect(wrapper.vm.isCurrentQuestion(nextQuestion)).toBe(true);
//Assert wrapper.vm.onQuestionEnter(nextQuestion);
expect(Element.prototype.scrollIntoView).toHaveBeenCalled();
expect(scrollSpy).toHaveBeenCalledTimes(1);
});
test("should not scroll onQuestionEnter for a non-current question", () => {
const { wrapper } = setupMocks({});
const scrollSpy = jest.spyOn(wrapper.vm, "scrollCurrentQuestionIntoView");
const answeredQuestion = wrapper.vm.questions[0];
wrapper.vm.onQuestionEnter(answeredQuestion);
expect(scrollSpy).not.toHaveBeenCalled();
}); });
test("should return answer object if returnedAnswer is a final matching answer", async () => { test("should return answer object if returnedAnswer is a final matching answer", async () => {
@ -528,6 +575,27 @@ describe("Question Chain component", () => {
expect(wrapper.vm.currentQuestionNum).toBe(2); expect(wrapper.vm.currentQuestionNum).toBe(2);
expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(true); expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(true);
}); });
test("should show only Q1 and Q2 after Cherokee Q1 Yes then Q2 No", async () => {
const { wrapper } = setupMocks({
questionDataProp: getCherokeeWindshieldQuestions(),
});
await nextTick();
wrapper.vm.handleAnswer(wrapper.vm.questions[0], "1|nextQuestion|2|Yes");
wrapper.vm.handleAnswer(wrapper.vm.questions[1], "2|answer|DW02264|No");
const visibleQuestions = wrapper.vm.questions.filter((question) =>
wrapper.vm.isQuestionVisible(question)
);
expect(wrapper.vm.currentQuestionNum).toBe(0);
expect(visibleQuestions).toHaveLength(2);
expect(visibleQuestions.map((question) => question.questionSequence)).toEqual([1, 2]);
expect(wrapper.vm.isCurrentQuestion(wrapper.vm.questions[0])).toBe(false);
expect(wrapper.vm.isCurrentQuestion(wrapper.vm.questions[1])).toBe(false);
expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(false);
});
}); });
}); });

View file

@ -1,10 +1,16 @@
<template> <template>
<div v-for="(q, i) in questions" :key="i"> <div v-for="q in questions" :key="q.questionSequence">
<transition appear name="fade" mode="out-in"> <transition
appear
:css="isCurrentQuestion(q)"
:name="isCurrentQuestion(q) ? 'fade' : undefined"
mode="out-in"
@after-enter="onQuestionEnter(q)">
<buttonQuestion <buttonQuestion
v-if="isQuestionVisible(q)" v-if="isQuestionVisible(q)"
:key="q.questionSequence"
class="radioQuestion" class="radioQuestion"
:class="isCurrentQuestion(q) && 'current-question'" :class="{ 'current-question': isCurrentQuestion(q) }"
:questionText="q.questionText" :questionText="q.questionText"
:answers="q.answers" :answers="q.answers"
:groupName="`question-${index}-${q.questionSequence}`" :groupName="`question-${index}-${q.questionSequence}`"
@ -61,7 +67,6 @@ export default {
if (this.questions.length > 0) { if (this.questions.length > 0) {
this.initializeCurrentQuestionNum(); this.initializeCurrentQuestionNum();
this.scrollCurrentQuestionIntoView();
} }
}, },
methods: { methods: {
@ -105,6 +110,11 @@ export default {
this.currentQuestionNum = this.getQuestionSequence(firstUnanswered); this.currentQuestionNum = this.getQuestionSequence(firstUnanswered);
} }
}, },
onQuestionEnter(question) {
if (this.isCurrentQuestion(question)) {
this.scrollCurrentQuestionIntoView();
}
},
scrollCurrentQuestionIntoView() { scrollCurrentQuestionIntoView() {
this.$nextTick(() => { this.$nextTick(() => {
document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" }); document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
@ -201,7 +211,6 @@ export default {
if (questionType === "nextQuestion") { if (questionType === "nextQuestion") {
// update to next question index // update to next question index
this.currentQuestionNum = Number(questionAnswer); this.currentQuestionNum = Number(questionAnswer);
this.scrollCurrentQuestionIntoView();
return { return {
incomplete: true, incomplete: true,
answeredQuestions, answeredQuestions,

View file

@ -5,12 +5,10 @@
<div class="container page-container-grouped-styles"> <div class="container page-container-grouped-styles">
<div class="row"> <div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<h5 class="fw-normal mb-0 dark-header" :class="headerColor"> <funnelSubHeader
<span> cmsWidgetName="FunnelSubHeaderWidget"
{{ serviceLocationText }} :overrideHeaderSubText="estimatedTimeText"
</span> alignLeft />
</h5>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" alignLeft />
<datePicker <datePicker
class="mt-5" class="mt-5"
v-model="selectedDate" v-model="selectedDate"
@ -104,6 +102,7 @@ import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/sched
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
import { import {
flushPagePrereqsLogs, flushPagePrereqsLogs,
hasServiceZipInfo, hasServiceZipInfo,
@ -309,6 +308,17 @@ export default {
includeMobile: Boolean(mobileProviderNumber), includeMobile: Boolean(mobileProviderNumber),
pageNameToLog: to.name, pageNameToLog: to.name,
}); });
if (timeSlotsResultMap.inshopTimeSlots) {
vm.estimatedServiceMinutesMinimum =
timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMinimum;
vm.estimatedServiceMinutesMaximum =
timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMaximum;
} else if (timeSlotsResultMap.mobileTimeSlots) {
vm.estimatedServiceMinutesMinimum =
timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMinimum;
vm.estimatedServiceMinutesMaximum =
timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMaximum;
}
assignInshopTimeSlotsFromV2Response( assignInshopTimeSlotsFromV2Response(
vm.inshopProvidersAndTimeSlots, vm.inshopProvidersAndTimeSlots,
timeSlotsResultMap.inshopTimeSlots timeSlotsResultMap.inshopTimeSlots
@ -336,6 +346,19 @@ export default {
viewMoreShopsText() { viewMoreShopsText() {
return this.getCmsContent("ViewMoreShopsWidget", "Text"); return this.getCmsContent("ViewMoreShopsWidget", "Text");
}, },
estimatedTimeText() {
if (!this.estimatedServiceMinutesMinimum || !this.estimatedServiceMinutesMaximum) {
return " ";
}
const durationText = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
return this.getCmsContent("FunnelSubHeaderWidget", "HeaderSubText").replace(
"{custom:DURATION}",
durationText
);
},
serviceZipCode() { serviceZipCode() {
return store.getters.order.serviceLocation.zipCode; return store.getters.order.serviceLocation.zipCode;
}, },
@ -396,6 +419,8 @@ export default {
datesLoaded: false, datesLoaded: false,
datePickerStartDate: toDateString(0), datePickerStartDate: toDateString(0),
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1), datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
estimatedServiceMinutesMinimum: null,
estimatedServiceMinutesMaximum: null,
inshopProvidersAndTimeSlots: [], inshopProvidersAndTimeSlots: [],
allShopProviders: [], allShopProviders: [],
mobileProviderAndTimeSlot: null, mobileProviderAndTimeSlot: null,