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 @@
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+