From cd2be9c384fc55d2fdf13b60c1a91e65522745d3 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 08:21:39 -0400 Subject: [PATCH 1/4] CASH-2815 | Waitlist working Pre personal tech review --- src/layouts/scheduling/scheduling.spec.js | 3 + src/layouts/scheduling/scheduling.vue | 7 + .../waitlist-question.spec.js | 146 ++++++++++++++++++ .../waitlist-question/waitlist-question.vue | 136 ++++++++++++++++ 4 files changed, 292 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..e3bcc3e07 --- /dev/null +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js @@ -0,0 +1,146 @@ +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("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..aba80f23b --- /dev/null +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -0,0 +1,136 @@ + + + + + From 2e15c6120a2c677497a2cccbbf675e81172a53c7 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 08:32:39 -0400 Subject: [PATCH 2/4] CASH-2815 | Pass clicks on the component through to the checkbox --- .../waitlist-question.spec.js | 39 +++++++++++++++++++ .../waitlist-question/waitlist-question.vue | 13 ++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js index e3bcc3e07..83582df3a 100644 --- a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js @@ -105,6 +105,45 @@ describe("waitlist-question.vue", () => { 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 = [ diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue index aba80f23b..61c54dd86 100644 --- a/src/layouts/scheduling/waitlist-question/waitlist-question.vue +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -1,6 +1,9 @@ @@ -66,7 +65,7 @@ export default { }, waitlistThresholdDays() { return this.hasSetting(experimentSettings.WAITLIST_THRESHOLD_DAYS) - ? parseInt(this.getSettingValue(experimentSettings.WAITLIST_THRESHOLD_DAYS)) + ? parseInt(this.getSettingValue(experimentSettings.WAITLIST_THRESHOLD_DAYS), 10) : 0; }, daysUntilEarliestAvailableDate() { @@ -88,7 +87,10 @@ export default { handleContainerClick(event) { // The checkbox's own label/input already toggles localValue natively, // so ignore clicks that originate inside it to avoid double-toggling. - if (event.target.closest(".ui-checkbox")) return; + // Checking against our own wrapper element (rather than an internal + // class name owned by checkboxQuestion) keeps this decoupled from + // that component's markup. + if (this.$refs.checkboxWrapper?.contains(event.target)) return; this.localValue = !this.localValue; }, }, From ae9a9c860418fb046f2cc24208fd5fdf96fe42bd Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 08:49:44 -0400 Subject: [PATCH 4/4] CASH-2815 | Clean up CSS that isn't used --- src/layouts/scheduling/waitlist-question/waitlist-question.vue | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue index 9ae96eaa7..c6a81c8b0 100644 --- a/src/layouts/scheduling/waitlist-question/waitlist-question.vue +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -104,10 +104,7 @@ export default {