From 1a36c380719fb1a1504d23a78dc1342787b78667 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 12:54:54 -0400 Subject: [PATCH 01/40] CASH-2820 | Load more shops New textLink component for "View more shops" button Add in method which adds in new shops New data member which tracks all providers New data member which tracks if new shops are being loaded --- src/layouts/scheduling/scheduling.vue | 112 +++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue index 2b2a8e803..9674802ca 100644 --- a/src/layouts/scheduling/scheduling.vue +++ b/src/layouts/scheduling/scheduling.vue @@ -1,6 +1,6 @@ @@ -48,7 +49,7 @@ export default { ); if (!this.isDisabled) { if (!this.suppressLoader) this.isLoaderDisplayed = true; - this.$emit("click-event"); + //this.$emit("click-event"); } }, resetButtonStyle() { @@ -64,6 +65,7 @@ export default { From f4e55f359cc16bfa787f251f233c636e53ca2937 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Tue, 21 Jul 2026 14:33:51 -0400 Subject: [PATCH 21/40] Use teleport to avoid rendering inconsistency. --- src/ux-components/button-main/button-main.vue | 4 ++-- src/ux-components/intercept-overlay/intercept-overlay.vue | 1 + src/ux-components/loader/loader.vue | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ux-components/button-main/button-main.vue b/src/ux-components/button-main/button-main.vue index 689df5931..d91ebf7cd 100644 --- a/src/ux-components/button-main/button-main.vue +++ b/src/ux-components/button-main/button-main.vue @@ -49,7 +49,7 @@ export default { ); if (!this.isDisabled) { if (!this.suppressLoader) this.isLoaderDisplayed = true; - //this.$emit("click-event"); + this.$emit("click-event"); } }, resetButtonStyle() { @@ -65,7 +65,7 @@ export default { From 5eec03aa8466c2b951df7906e90ab3091260b210 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 29 Jul 2026 08:56:01 -0400 Subject: [PATCH 27/40] Revert "Merge pull request #3289 from Safelite/feature/CASH-2815-revert" This reverts commit 22f6052848bfb3e7722f9f80ed5b0b010f58ea14, reversing changes made to 6fe17f6810454ff35fd60e89e182dc4854b29a04. --- src/layouts/scheduling/scheduling.spec.js | 3 + src/layouts/scheduling/scheduling.vue | 7 + .../waitlist-question.spec.js | 185 ++++++++++++++++++ .../waitlist-question/waitlist-question.vue | 146 ++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 src/layouts/scheduling/waitlist-question/waitlist-question.spec.js create mode 100644 src/layouts/scheduling/waitlist-question/waitlist-question.vue diff --git a/src/layouts/scheduling/scheduling.spec.js b/src/layouts/scheduling/scheduling.spec.js index d64ffa779..c76a0985f 100644 --- a/src/layouts/scheduling/scheduling.spec.js +++ b/src/layouts/scheduling/scheduling.spec.js @@ -15,6 +15,9 @@ jest.mock("@/store", () => ({ lineItems: { glassParts: [] }, policy: { isItac: false, isNoComp: false }, }, + applicationUser: { + experiments: [], + }, }, })); diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue index 3ace9edf7..2b2a8e803 100644 --- a/src/layouts/scheduling/scheduling.vue +++ b/src/layouts/scheduling/scheduling.vue @@ -48,6 +48,10 @@ @address-clicked="onInshopAddressClicked(provider)" /> + diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js new file mode 100644 index 000000000..83582df3a --- /dev/null +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js @@ -0,0 +1,185 @@ +import { mount } from "@vue/test-utils"; +import waitlistQuestion from "./waitlist-question"; +import store from "@/store"; +import { experimentSettings } from "@/constants/experiments"; + +jest.mock("@/store", () => ({ + getters: { + applicationUser: { + experiments: [], + }, + }, +})); + +const MOCK_CMS_CONTENT = { + WaitListLabelWidget: { Text: "Want to be notified sooner?" }, + WaitListQuestionWidget: { QuestionText: "Add me to the waitlist" }, +}; + +function dateStringOffsetFromToday(offsetDays) { + const d = new Date(); + d.setDate(d.getDate() + offsetDays); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String( + d.getDate() + ).padStart(2, "0")}`; +} + +function enableWaitlistExperiment(thresholdDays = 0) { + store.getters.applicationUser.experiments = [ + { + isActive: true, + settings: { + [experimentSettings.DISPLAY_WAITLIST]: "true", + [experimentSettings.WAITLIST_THRESHOLD_DAYS]: String(thresholdDays), + }, + }, + ]; +} + +function mountComponent(props = {}) { + const cmsMixin = { + methods: { + getCmsContent: jest.fn((widgetName, fieldName) => { + return MOCK_CMS_CONTENT[widgetName]?.[fieldName] ?? ""; + }), + }, + }; + + return mount(waitlistQuestion, { + props: { + modelValue: false, + availableDates: [dateStringOffsetFromToday(10)], + ...props, + }, + global: { + mixins: [cmsMixin], + }, + }); +} + +describe("waitlist-question.vue", () => { + beforeEach(() => { + enableWaitlistExperiment(); + }); + + afterEach(() => { + store.getters.applicationUser.experiments = []; + }); + + it("renders the label and checkbox CMS content", () => { + const wrapper = mountComponent(); + + expect(wrapper.text()).toContain("Want to be notified sooner?"); + expect(wrapper.text()).toContain("Add me to the waitlist"); + }); + + it("reflects the modelValue prop on the checkbox", () => { + const wrapper = mountComponent({ modelValue: true }); + + expect(wrapper.find("input[type='checkbox']").element.checked).toBe(true); + }); + + it("emits update:modelValue with true when the checkbox is checked", async () => { + const wrapper = mountComponent({ modelValue: false }); + + const input = wrapper.find("input[type='checkbox']"); + await input.setValue(true); + + expect(wrapper.emitted("update:modelValue")).toEqual([[true]]); + }); + + it("emits update:modelValue with false when the checkbox is unchecked", async () => { + const wrapper = mountComponent({ modelValue: true }); + + const input = wrapper.find("input[type='checkbox']"); + await input.setValue(false); + + expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); + }); + + it("renders nothing when shouldDisplay is false", () => { + store.getters.applicationUser.experiments = []; + + const wrapper = mountComponent(); + + expect(wrapper.find("input[type='checkbox']").exists()).toBe(false); + }); + + describe("clicking the container", () => { + it("toggles localValue to true when clicking outside the checkbox", async () => { + const wrapper = mountComponent({ modelValue: false }); + + await wrapper.find(".waitlist-label").trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toEqual([[true]]); + }); + + it("toggles localValue to false when clicking outside the checkbox", async () => { + const wrapper = mountComponent({ modelValue: true }); + + await wrapper.find(".waitlist-question").trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); + }); + + it("does not toggle when the click target is the checkbox input itself", () => { + // jsdom doesn't reliably run a checkbox's native activation behavior + // (toggling + firing "change") for script-dispatched clicks, so this + // calls the handler directly with the real input element as the + // event target to verify the guard is skipped in that case. + const wrapper = mountComponent({ modelValue: false }); + const inputElement = wrapper.find("input[type='checkbox']").element; + + wrapper.vm.handleContainerClick({ target: inputElement }); + + expect(wrapper.emitted("update:modelValue")).toBeUndefined(); + }); + + it("does not toggle when the click lands inside the checkbox wrapper but not on the input", async () => { + const wrapper = mountComponent({ modelValue: false }); + + await wrapper.find(".ui-checkbox").trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toBeUndefined(); + }); + }); + + describe("shouldDisplay", () => { + it("is false when the DISPLAY_WAITLIST experiment is off", () => { + store.getters.applicationUser.experiments = [ + { + isActive: true, + settings: { [experimentSettings.WAITLIST_THRESHOLD_DAYS]: "3" }, + }, + ]; + + const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(10)] }); + + expect(wrapper.vm.shouldDisplay).toBe(false); + }); + + it("is false when the earliest available date is within the threshold", () => { + enableWaitlistExperiment(3); + + const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(2)] }); + + expect(wrapper.vm.shouldDisplay).toBe(false); + }); + + it("is true when the experiment is on and the earliest available date exceeds the threshold", () => { + enableWaitlistExperiment(3); + + const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(10)] }); + + expect(wrapper.vm.shouldDisplay).toBe(true); + }); + + it("is false when there are no available dates", () => { + enableWaitlistExperiment(0); + + const wrapper = mountComponent({ availableDates: [] }); + + expect(wrapper.vm.shouldDisplay).toBe(false); + }); + }); +}); diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue new file mode 100644 index 000000000..c6a81c8b0 --- /dev/null +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -0,0 +1,146 @@ + + + + + From 920d1a4b37bdf01e680b4126d02f7ab5cd7a5bac Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Thu, 30 Jul 2026 07:15:23 -0400 Subject: [PATCH 28/40] CASH-3054 - inconsistent fade transition Part Question --- .../question-chain/question-chain.spec.js | 78 +++++++++++++++++-- .../question-chain/question-chain.vue | 19 +++-- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/digital-components/question-chain/question-chain.spec.js b/src/digital-components/question-chain/question-chain.spec.js index d0e54a895..2bddbb2be 100644 --- a/src/digital-components/question-chain/question-chain.spec.js +++ b/src/digital-components/question-chain/question-chain.spec.js @@ -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 - 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 scrollSpy = jest.spyOn(wrapper.vm, "scrollCurrentQuestionIntoView"); //Act 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 - expect(Element.prototype.scrollIntoView).toHaveBeenCalled(); + wrapper.vm.onQuestionEnter(nextQuestion); + + 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 () => { @@ -528,6 +575,27 @@ describe("Question Chain component", () => { expect(wrapper.vm.currentQuestionNum).toBe(2); 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); + }); }); }); diff --git a/src/digital-components/question-chain/question-chain.vue b/src/digital-components/question-chain/question-chain.vue index 467e9619e..248ef2204 100644 --- a/src/digital-components/question-chain/question-chain.vue +++ b/src/digital-components/question-chain/question-chain.vue @@ -1,10 +1,16 @@