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/constants/experiments.js b/src/constants/experiments.js
index 8df02597c..81444f1e1 100644
--- a/src/constants/experiments.js
+++ b/src/constants/experiments.js
@@ -66,6 +66,9 @@ const experimentSettings = {
// YMM
BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles",
+
+ // Promo Banner
+ SHOW_PROMO_BANNER: "ShowQuotePageRegionalPromoBanner",
};
const experimentTriggers = {
diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue
index ae6ca6229..a94a4e079 100644
--- a/src/digital-components/date-picker/date-picker.vue
+++ b/src/digital-components/date-picker/date-picker.vue
@@ -114,11 +114,9 @@
-
+
+
@@ -47,8 +55,7 @@
+ cmsWidgetName="AfterpayBannerWidget" />
+ class="quote-disclaimer text-left text-md-center" />
+
@@ -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;
+}
diff --git a/src/layouts/scheduling/scheduling-zip-search/scheduling-zip-search.spec.js b/src/layouts/scheduling/scheduling-zip-search/scheduling-zip-search.spec.js
new file mode 100644
index 000000000..f3129613b
--- /dev/null
+++ b/src/layouts/scheduling/scheduling-zip-search/scheduling-zip-search.spec.js
@@ -0,0 +1,145 @@
+import { shallowMount } from "@vue/test-utils";
+import schedulingZipSearch from "./scheduling-zip-search";
+import store from "@/store";
+import { errorMessages } from "@/constants/error-messages";
+import { getMountOptions } from "@/helpers/unit-test-helper.js";
+import {
+ getBillToAccountNumber,
+ getZipCodeData,
+} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
+
+jest.mock("@/store", () => ({
+ dispatch: jest.fn().mockResolvedValue(null),
+}));
+
+jest.mock(
+ "@/layouts/service-location/helpers/service-location-helper/service-location-helper",
+ () => ({
+ getZipCodeData: jest.fn().mockResolvedValue({
+ state: "OH",
+ zipCodeCtu: "03357",
+ }),
+ getBillToAccountNumber: jest.fn().mockResolvedValue("87291"),
+ })
+);
+
+const MOCK_CMS_CONTENT = {
+ ServiceZipQuestionWidget: {
+ QuestionText: "Service ZIP code:",
+ },
+};
+
+function mountComponent(props = {}, cmsContent = {}) {
+ const cmsContentByWidget = {
+ ...MOCK_CMS_CONTENT,
+ ...cmsContent,
+ ServiceZipQuestionWidget: {
+ ...MOCK_CMS_CONTENT.ServiceZipQuestionWidget,
+ ...cmsContent.ServiceZipQuestionWidget,
+ },
+ };
+
+ const cmsMixin = {
+ methods: {
+ getCmsContent: jest.fn((widgetName, fieldName) => {
+ return cmsContentByWidget[widgetName]?.[fieldName] ?? "";
+ }),
+ },
+ };
+
+ const mountOptions = getMountOptions({
+ route: { name: "scheduling" },
+ mixins: [cmsMixin],
+ });
+
+ return shallowMount(schedulingZipSearch, {
+ props: {
+ modelValue: "",
+ pageNameToLog: "scheduling",
+ ...props,
+ },
+ global: mountOptions.global,
+ });
+}
+
+describe("scheduling-zip-search.vue", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("prefills the zip input and CMS label from modelValue / ServiceZipQuestionWidget", () => {
+ const wrapper = mountComponent({ modelValue: "43235" });
+
+ expect(wrapper.find("#sz-service-zip").element.value).toBe("43235");
+ expect(wrapper.vm.zipLabelText).toBe("Service ZIP code:");
+ expect(wrapper.find("label").html()).toContain("Service ZIP code:");
+ wrapper.unmount();
+ });
+
+ test("shows required error when searching with a blank zip", async () => {
+ const wrapper = mountComponent({ modelValue: "" });
+
+ await wrapper.vm.onZipSearch({ preventDefault: jest.fn() });
+
+ expect(wrapper.vm.errorMessage).toBe(errorMessages.SERVICE_ZIP_REQUIRED);
+ expect(wrapper.find(".scheduling-zip-search__error").classes()).toContain("active");
+ expect(getZipCodeData).not.toHaveBeenCalled();
+ expect(wrapper.emitted("zip-searched")).toBeUndefined();
+ wrapper.unmount();
+ });
+
+ test("shows format error when zip is invalid", async () => {
+ const wrapper = mountComponent({ modelValue: "123" });
+
+ await wrapper.vm.onZipSearch({ preventDefault: jest.fn() });
+
+ expect(wrapper.vm.errorMessage).toBe(errorMessages.SERVICE_ZIP_FORMAT);
+ expect(getZipCodeData).not.toHaveBeenCalled();
+ expect(wrapper.emitted("zip-searched")).toBeUndefined();
+ wrapper.unmount();
+ });
+
+ test("does not search when disabled", async () => {
+ const wrapper = mountComponent({ modelValue: "44101", disabled: true });
+
+ await wrapper.vm.onZipSearch({ preventDefault: jest.fn() });
+
+ expect(getZipCodeData).not.toHaveBeenCalled();
+ expect(wrapper.emitted("zip-searched")).toBeUndefined();
+ wrapper.unmount();
+ });
+
+ test("saves zip info and emits billToAccountNumber when a valid zip is searched", async () => {
+ const wrapper = mountComponent({ modelValue: "44101" });
+
+ await wrapper.vm.onZipSearch({ preventDefault: jest.fn() });
+
+ expect(getZipCodeData).toHaveBeenCalledWith("44101", "scheduling");
+ expect(getBillToAccountNumber).toHaveBeenCalledWith("03357", "scheduling");
+ expect(store.dispatch).toHaveBeenCalledWith("saveServiceZipCodeInfo", {
+ zipCode: "44101",
+ state: "OH",
+ zipCodeCtu: "03357",
+ });
+ expect(wrapper.emitted("zip-searched")).toEqual([
+ [{ zipCode: "44101", billToAccountNumber: "87291" }],
+ ]);
+ wrapper.unmount();
+ });
+
+ test("focusZipInput scrolls and focuses the zip input", () => {
+ const wrapper = mountComponent({ modelValue: "43235" });
+ const zipInput = wrapper.find("#sz-service-zip").element;
+ zipInput.scrollIntoView = jest.fn();
+ zipInput.focus = jest.fn();
+
+ wrapper.vm.focusZipInput();
+
+ expect(zipInput.scrollIntoView).toHaveBeenCalledWith({
+ behavior: "smooth",
+ block: "center",
+ });
+ expect(zipInput.focus).toHaveBeenCalledWith({ preventScroll: true });
+ wrapper.unmount();
+ });
+});
diff --git a/src/layouts/scheduling/scheduling-zip-search/scheduling-zip-search.vue b/src/layouts/scheduling/scheduling-zip-search/scheduling-zip-search.vue
new file mode 100644
index 000000000..01fa86259
--- /dev/null
+++ b/src/layouts/scheduling/scheduling-zip-search/scheduling-zip-search.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
diff --git a/src/layouts/scheduling/scheduling.spec.js b/src/layouts/scheduling/scheduling.spec.js
index c76a0985f..33b02e753 100644
--- a/src/layouts/scheduling/scheduling.spec.js
+++ b/src/layouts/scheduling/scheduling.spec.js
@@ -2,19 +2,34 @@ import { shallowMount } from "@vue/test-utils";
import scheduling from "./scheduling";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper";
+import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import store from "@/store";
jest.mock("@/store", () => ({
dispatch: jest.fn().mockResolvedValue(null),
getters: {
order: {
- serviceLocation: { zipCode: "43235", appointmentType: null },
- payment: { isInsurance: false, insuranceCoverage: { isVerified: false } },
+ serviceLocation: {
+ zipCode: "43235",
+ zipCodeCtu: "1234",
+ appointmentType: null,
+ address: null,
+ address2: null,
+ city: null,
+ state: "OH",
+ isVehicleProtected: null,
+ },
+ payment: {
+ isInsurance: false,
+ insuranceCoverage: { isVerified: false },
+ billToAccountNumber: "12345",
+ },
referralNumber: "",
damage: { isRepair: false },
lineItems: { glassParts: [] },
policy: { isItac: false, isNoComp: false },
},
+ lineItems: { supportingItems: [] },
applicationUser: {
experiments: [],
},
@@ -44,12 +59,36 @@ jest.mock("@/helpers/page-prerequisites-helper.js", () => ({
hasInsuranceInfo: jest.fn(() => true),
}));
-function setupMocks() {
+function setupMocks({ isAppleBrowser = false } = {}) {
+ const dispatchStoreAction = jest.fn().mockResolvedValue(null);
const baseMixin = {
methods: {
getCmsContent: jest.fn(() => ""),
setCmsContent: jest.fn(),
getTotalLineItemPrice: jest.fn(() => 0),
+ isAppleBrowser: jest.fn(() => isAppleBrowser),
+ dispatchStoreAction,
+ },
+ computed: {
+ storeActions() {
+ return {
+ SAVE_SERVICE_LOCATION: "saveServiceLocation",
+ SAVE_SCHEDULE: "saveSchedule",
+ SAVE_WAITLIST_REQUESTED: "saveWaitListRequested",
+ SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
+ "saveSupportingItemsSuppressingStateResetting",
+ };
+ },
+ navigationScenarios() {
+ return {
+ CLICKED_FORWARD: "clickedForward",
+ CLICKED_FORWARD_WITH_MOBILE_SERVICE: "clickedForwardWithMobileService",
+ CLICKED_BACK: "clickedBack",
+ };
+ },
+ pageName() {
+ return "scheduling";
+ },
},
};
const mountOptions = getMountOptions({
@@ -62,7 +101,7 @@ function setupMocks() {
});
mountOptions.global.mixins = [baseMixin];
const wrapper = shallowMount(scheduling, mountOptions);
- return { wrapper };
+ return { wrapper, dispatchStoreAction };
}
describe("scheduling.vue", () => {
@@ -124,7 +163,7 @@ describe("scheduling.vue", () => {
});
const { wrapper } = setupMocks();
- wrapper.vm.inShopProvidersAndTimeslots = [
+ wrapper.vm.inshopProvidersAndTimeSlots = [
{ provider: { providerNumber: "05018" }, timeSlots: { days: [] } },
{ provider: { providerNumber: "05019" }, timeSlots: { days: [] } },
];
@@ -161,7 +200,7 @@ describe("scheduling.vue", () => {
});
const { wrapper } = setupMocks();
- wrapper.vm.inShopProvidersAndTimeslots = [
+ wrapper.vm.inshopProvidersAndTimeSlots = [
{ provider: { providerNumber: "05018" }, timeSlots: { days: [] } },
{ provider: { providerNumber: "05019" }, timeSlots: { days: [] } },
];
@@ -169,14 +208,244 @@ describe("scheduling.vue", () => {
await wrapper.vm.handleRequestMoreDates();
- expect(wrapper.vm.inShopProvidersAndTimeslots[0].timeSlots.days).toHaveLength(1);
+ expect(wrapper.vm.inshopProvidersAndTimeSlots[0].timeSlots.days).toHaveLength(1);
expect(
- wrapper.vm.inShopProvidersAndTimeslots[0].timeSlots.days[0].timeSlots[0].id
+ wrapper.vm.inshopProvidersAndTimeSlots[0].timeSlots.days[0].timeSlots[0].id
).toBe("slot-a");
expect(
- wrapper.vm.inShopProvidersAndTimeslots[1].timeSlots.days[0].timeSlots[0].id
+ wrapper.vm.inshopProvidersAndTimeSlots[1].timeSlots.days[0].timeSlots[0].id
).toBe("slot-b");
wrapper.unmount();
});
});
+
+ describe("service zip", () => {
+ test("prefills zipSearchCode and billToAccountNumber from store and renders zip search", () => {
+ const { wrapper } = setupMocks();
+
+ expect(wrapper.vm.zipSearchCode).toBe("43235");
+ expect(wrapper.vm.billToAccountNumber).toBe("12345");
+ expect(wrapper.find("scheduling-zip-search-stub").exists()).toBe(true);
+ wrapper.unmount();
+ });
+
+ test("reloads providers and timeslots when zip-searched is emitted", async () => {
+ store.dispatch.mockClear();
+ store.dispatch.mockImplementation((action) => {
+ if (action === "getProviders") {
+ return Promise.resolve({
+ data: {
+ shopProviders: [{ providerNumber: "05018" }],
+ mobileProviderNumber: "12345",
+ },
+ });
+ }
+ return Promise.resolve(null);
+ });
+ settleAllPromises.mockResolvedValueOnce({
+ inshopTimeSlots: {
+ providerTimeSlots: [
+ { providerNumber: "05018", days: [{ date: "2026-07-22", timeSlots: [] }] },
+ ],
+ },
+ mobileTimeSlots: { days: [{ date: "2026-07-22", timeSlots: [] }] },
+ });
+
+ const { wrapper } = setupMocks();
+ await wrapper.setData({ isLoadingDates: false });
+ wrapper.vm.datePickerEndDate = "2026-08-15";
+ wrapper.vm.selectedScheduling = { appointmentType: "Mobile" };
+ const initialDatePickerKey = wrapper.vm.datePickerKey;
+
+ await wrapper.vm.onZipSearched({ zipCode: "44101", billToAccountNumber: "87291" });
+
+ expect(store.dispatch).toHaveBeenCalledWith("getProviders", {
+ payload: { serviceZipCode: "44101" },
+ pageNameToLog: "scheduling",
+ });
+ expect(wrapper.vm.billToAccountNumber).toBe("87291");
+ expect(settleAllPromises).toHaveBeenCalled();
+ expect(wrapper.vm.datePickerKey).toBe(initialDatePickerKey + 1);
+ expect(wrapper.vm.selectedDate).toBeNull();
+ expect(wrapper.vm.selectedScheduling).toBeNull();
+ expect(wrapper.vm.isWaitlistRequested).toBe(false);
+ expect(wrapper.vm.inshopProvidersAndTimeSlots).toHaveLength(1);
+ expect(wrapper.vm.mobileProviderAndTimeSlot.providerNumber).toBe("12345");
+ wrapper.unmount();
+ });
+
+ test("anchors to zip search when mobile zip is clicked", () => {
+ const { wrapper } = setupMocks();
+ wrapper.vm.$refs.schedulingZipSearch.focusZipInput = jest.fn();
+
+ wrapper.vm.onMobileZipCodeClicked();
+
+ expect(wrapper.vm.$refs.schedulingZipSearch.focusZipInput).toHaveBeenCalled();
+ wrapper.unmount();
+ });
+ });
+
+ describe("forwardButtonAction", () => {
+ test("saves service location, schedule, waitlist and navigates for inshop selection", async () => {
+ const { wrapper, dispatchStoreAction } = setupMocks();
+ wrapper.vm.selectedDate = "2026-07-22";
+ wrapper.vm.isWaitlistRequested = true;
+ wrapper.vm.selectedScheduling = {
+ appointmentType: AppointmentTypeStrings.IN_SHOP,
+ providerNumber: "05018",
+ routeCodeId: "route-inshop",
+ };
+ wrapper.vm.inshopProvidersAndTimeSlots = [
+ {
+ provider: {
+ providerNumber: "05018",
+ address: {
+ streetAddress: "123 Main",
+ city: "Columbus",
+ state: "OH",
+ zipCode: "43235",
+ zipCodeCtu: "9999",
+ },
+ },
+ timeSlots: {
+ estimatedServiceMinutesMinimum: 60,
+ estimatedServiceMinutesMaximum: 120,
+ days: [
+ {
+ date: "2026-07-22",
+ timeSlots: [
+ {
+ id: "route-inshop",
+ startTime: "09:00",
+ endTime: "10:00",
+ },
+ ],
+ },
+ ],
+ },
+ },
+ ];
+
+ await wrapper.vm.forwardButtonAction();
+
+ expect(dispatchStoreAction).toHaveBeenCalledWith(
+ "saveServiceLocation",
+ expect.objectContaining({
+ appointmentType: AppointmentTypeStrings.IN_SHOP,
+ zipCodeCtu: "9999",
+ provider: expect.objectContaining({ providerNumber: "05018" }),
+ }),
+ false
+ );
+ expect(dispatchStoreAction).toHaveBeenCalledWith(
+ "saveSchedule",
+ expect.objectContaining({
+ date: "2026-07-22",
+ routeCode: "route-inshop",
+ startTime: "09:00",
+ endTime: "10:00",
+ jobMinMinutes: "60",
+ jobMaxMinutes: "120",
+ }),
+ false
+ );
+ expect(dispatchStoreAction).toHaveBeenCalledWith("saveWaitListRequested", true, false);
+ expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
+ "clickedForward",
+ "scheduling"
+ );
+ wrapper.unmount();
+ });
+
+ test("navigates with mobile scenario and clears provider address for mobile selection", async () => {
+ const { wrapper, dispatchStoreAction } = setupMocks();
+ wrapper.vm.selectedDate = "2026-07-22";
+ wrapper.vm.selectedScheduling = {
+ appointmentType: AppointmentTypeStrings.MOBILE,
+ providerNumber: "MOBILE1",
+ routeCodeId: "route-mobile",
+ isPremiumAppointment: false,
+ };
+ wrapper.vm.mobileProviderAndTimeSlot = {
+ providerNumber: "MOBILE1",
+ timeSlots: {
+ estimatedServiceMinutesMinimum: 90,
+ estimatedServiceMinutesMaximum: 150,
+ days: [
+ {
+ date: "2026-07-22",
+ timeSlots: [
+ {
+ id: "route-mobile",
+ startTime: "08:00",
+ endTime: "12:00",
+ },
+ ],
+ },
+ ],
+ },
+ };
+
+ await wrapper.vm.forwardButtonAction();
+
+ expect(dispatchStoreAction).toHaveBeenCalledWith(
+ "saveServiceLocation",
+ expect.objectContaining({
+ appointmentType: AppointmentTypeStrings.MOBILE,
+ provider: {
+ providerNumber: "MOBILE1",
+ address: {
+ streetAddress: null,
+ city: null,
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ },
+ },
+ }),
+ false
+ );
+ expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
+ "clickedForwardWithMobileService",
+ "scheduling"
+ );
+ wrapper.unmount();
+ });
+ });
+
+ describe("in-shop Maps link", () => {
+ const address = {
+ city: "Lewis Center",
+ state: "OH",
+ streetAddress: "1343 Cameron Ave",
+ zipCode: "43035",
+ };
+ const query = encodeURIComponent(
+ "safelite,Safelite Autoglass, 1343 Cameron Ave, Lewis Center, OH 43035"
+ );
+
+ test("onInshopAddressClicked opens Maps in a new tab for Google and Apple", () => {
+ const openSpy = jest.spyOn(window, "open").mockImplementation(() => null);
+
+ const { wrapper } = setupMocks();
+ wrapper.vm.onInshopAddressClicked({ address });
+ expect(openSpy).toHaveBeenCalledWith(
+ `https://www.google.com/maps/search/?api=1&query=${query}`,
+ "_blank",
+ "noopener,noreferrer"
+ );
+ wrapper.unmount();
+
+ openSpy.mockClear();
+ const { wrapper: appleWrapper } = setupMocks({ isAppleBrowser: true });
+ appleWrapper.vm.onInshopAddressClicked({ address });
+ expect(openSpy).toHaveBeenCalledWith(
+ `https://maps.apple.com/?q=${query}`,
+ "_blank",
+ "noopener,noreferrer"
+ );
+ appleWrapper.unmount();
+ openSpy.mockRestore();
+ });
+ });
});
diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue
index 2b2a8e803..a3be13b82 100644
--- a/src/layouts/scheduling/scheduling.vue
+++ b/src/layouts/scheduling/scheduling.vue
@@ -1,17 +1,22 @@
-
+
-
+
{
+ 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", () => {
diff --git a/src/store/index.js b/src/store/index.js
index a34307ba9..d6410b196 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -133,6 +133,8 @@ const getDefaultState = () => {
dateOfLoss: null,
damageCause: null,
installOemGlass: null,
+ damageCity: null,
+ damageState: null,
},
lineItems: {
glassParts: null,
@@ -618,6 +620,8 @@ export const mutations = {
if (damageDetails) {
state.order.damage.dateOfLoss = damageDetails.dateOfLoss;
state.order.damage.damageCause = damageDetails.damageCause;
+ state.order.damage.damageState = damageDetails.damageState;
+ state.order.damage.damageCity = damageDetails.damageCity;
}
},
@@ -891,7 +895,8 @@ export const mutations = {
sessionInformation.order.damage?.dateOfLoss
);
state.order.damage.damageCause = sessionInformation.order.damage?.damageCause;
-
+ state.order.damage.damageState = sessionInformation.order.damage?.damageState;
+ state.order.damage.damageCity = sessionInformation.order.damage?.damageCity;
state.order.damage.partQuestionAnswers =
sessionInformation.order.damage.partQuestionAnswers;
state.order.damage.moldingQuestionAnswers =
@@ -2012,35 +2017,30 @@ export const actions = {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
+ const payment = context.getters.payment;
- const carId = vehicle.carId;
- const glassArray = damage.glassToReplace;
- const zipCode = order.serviceLocation.zipCode;
- const vin = vehicle.vin;
- const serviceType = damage.isRepair ? "Repair" : "Install";
- const parentAccountNumber = context.getters.payment.parentAccountNumber;
- const referralSeqNumber = order.referralSequenceNumber;
-
- // create a new array to avoid mutating state
- const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
+ const glassArrayForPayload = convertGlassPieceNamingForApi(damage.glassToReplace);
const partsOrQuestionsEndpoint = vehicle.vinRequired
? endpoints.GetPartsOrQuestionsV2
: endpoints.GetPartsOrQuestions;
+ const requestPayload = {
+ carId: vehicle.carId,
+ glassPieces: glassArrayForPayload,
+ zip: order.serviceLocation.zipCode,
+ vin: vehicle.vin,
+ serviceType: damage.isRepair ? "Repair" : "Install",
+ referralSeqNumber: order.referralSequenceNumber,
+ parentAccountNumber: payment.parentAccountNumber,
+ billToAccountNumber: payment.billToAccountNumber,
+ };
+
const response = await globalMethods
.callHttpClient({
method: partsOrQuestionsEndpoint.method,
endpoint: partsOrQuestionsEndpoint.url,
- payload: {
- carId: carId,
- glassPieces: glassArrayForPayload,
- zip: zipCode,
- vin: vin,
- serviceType: serviceType,
- referralSeqNumber: referralSeqNumber,
- parentAccountNumber: parentAccountNumber,
- },
+ payload: requestPayload,
logApiCall: true,
pageNameToLog: pageNameToLog,
})
@@ -2076,32 +2076,25 @@ export const actions = {
const order = context.state.order;
const payment = context.getters.payment;
- const carId = vehicle.carId;
- const glassArray = damage.glassToReplace;
- const resultsArray = damage.partQuestionAnswers;
- const zipCode = order.serviceLocation.zipCode;
- const vin = vehicle.vin;
- const serviceType = order.serviceLocation?.appointmentType;
- const referralSeqNumber = order.referralSequenceNumber;
- const parentAccountNumber = payment.parentAccountNumber;
+ const glassArrayForPayload = convertGlassPieceNamingForApi(damage.glassToReplace);
+ const resultsArrayForPayload = convertResultsForApi(damage.partQuestionAnswers);
- // create a new array to avoid mutating state
- const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
- const resultsArrayForPayload = convertResultsForApi(resultsArray);
+ const requestPayload = {
+ carId: vehicle.carId,
+ glassPieces: glassArrayForPayload,
+ answerResults: resultsArrayForPayload,
+ zip: order.serviceLocation.zipCode,
+ vin: vehicle.vin,
+ serviceType: order.serviceLocation?.appointmentType,
+ referralSeqNumber: order.referralSequenceNumber,
+ parentAccountNumber: payment.parentAccountNumber,
+ billToAccountNumber: payment.billToAccountNumber,
+ };
const response = await globalMethods.callHttpClient({
method: endpoints.GetParts.method,
endpoint: endpoints.GetParts.url,
- payload: {
- carId: carId,
- glassPieces: glassArrayForPayload,
- answerResults: resultsArrayForPayload,
- zip: zipCode,
- vin: vin,
- serviceType: serviceType,
- referralSeqNumber: referralSeqNumber,
- parentAccountNumber: parentAccountNumber,
- },
+ payload: requestPayload,
logApiCall: true,
pageNameToLog: pageNameToLog,
});
@@ -2864,6 +2857,8 @@ export const actions = {
installOemGlass: order.damage.installOemGlass,
dateOfLoss: order.damage.dateOfLoss,
damageCause: order.damage.damageCause,
+ damageState: order.damage.damageState,
+ damageCity: order.damage.damageCity,
},
lineItems: {
glassParts: lineItems.glassParts,
diff --git a/src/ux-components/button-main/button-main.spec.js b/src/ux-components/button-main/button-main.spec.js
index 76e9afe28..9324726a0 100644
--- a/src/ux-components/button-main/button-main.spec.js
+++ b/src/ux-components/button-main/button-main.spec.js
@@ -36,48 +36,6 @@ describe("buttonMain.vue", () => {
expect(button.attributes()["aria-disabled"]).toEqual("true");
});
- it("Should return loader color", async () => {
- // Arrange
- const wrapper = shallowMount(
- buttonMain,
- setupMocks({
- propsData: {
- loaderColor: "blue",
- loaderEnabled: true,
- },
- })
- );
-
- // Act
- wrapper.vm.clicked();
- await nextTick();
-
- // Assert
- const loader = wrapper.find("loader-stub");
- expect(loader.attributes("class")).toContain("blue");
- });
-
- it("Should return loader position", async () => {
- // Arrange
- const wrapper = shallowMount(
- buttonMain,
- setupMocks({
- propsData: {
- loaderPosition: "right",
- loaderEnabled: true,
- },
- })
- );
-
- // Act
- wrapper.vm.clicked();
- await nextTick();
-
- // Assert
- const loader = wrapper.find("loader-stub");
- expect(loader.attributes("class")).toContain("right");
- });
-
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
// Arrange
const wrapper = shallowMount(
diff --git a/src/ux-components/button-main/button-main.vue b/src/ux-components/button-main/button-main.vue
index 87eb28841..d91ebf7cd 100644
--- a/src/ux-components/button-main/button-main.vue
+++ b/src/ux-components/button-main/button-main.vue
@@ -12,7 +12,8 @@
+ :loaderColor="loaderColor"
+ :loaderPosition="loaderPosition" />
@@ -64,6 +65,7 @@ export default {