Revert "Merge pull request #3289 from Safelite/feature/CASH-2815-revert"

This reverts commit 22f6052848, reversing
changes made to 6fe17f6810.
This commit is contained in:
scottkiener-at-safelite 2026-07-29 08:56:01 -04:00
parent e9e64fb462
commit 5eec03aa84
4 changed files with 341 additions and 0 deletions

View file

@ -15,6 +15,9 @@ jest.mock("@/store", () => ({
lineItems: { glassParts: [] },
policy: { isItac: false, isNoComp: false },
},
applicationUser: {
experiments: [],
},
},
}));

View file

@ -48,6 +48,10 @@
@address-clicked="onInshopAddressClicked(provider)" />
</div>
</Transition>
<waitlistQuestion
class="mt-5"
v-model="isWaitlistRequested"
:availableDates="availableDates" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
@ -68,6 +72,7 @@ import datePicker from "@/layouts/scheduling/date-picker/date-picker";
import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mobile-scheduling-card";
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question";
import store from "@/store";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -315,6 +320,7 @@ export default {
mobileProviderAndTimeSlot: null,
mobilePremiumAppointmentFee: null,
selectedScheduling: null,
isWaitlistRequested: false,
};
},
methods: {
@ -442,6 +448,7 @@ export default {
mobileSchedulingCard,
inshopSchedulingCard,
interceptOverlay,
waitlistQuestion,
},
};
</script>

View file

@ -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);
});
});
});

View file

@ -0,0 +1,146 @@
<template>
<template v-if="shouldDisplay">
<div
class="waitlist-question bg-light rounded"
v-bind="$attrs"
@click="handleContainerClick">
<textBlock
cmsWidgetName="WaitListLabelWidget"
typeStyle="medium"
fontWeight="600"
class="waitlist-label"
marginTopSizeOverride="0" />
<div ref="checkboxWrapper">
<checkboxQuestion
class="mt-3"
cmsWidgetName="WaitListQuestionWidget"
v-model="localValue" />
</div>
</div>
<div v-if="localValue" class="rounded waitlist-success">
<img :src="waitListSuccessImage" class="success-image" alt="" />
<span v-html="waitListSuccessText" class="success-text"></span>
</div>
</template>
</template>
<script>
import textBlock from "@/digital-components/text-block/text-block";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
import experimentMixin from "@/mixins/experiment-mixin";
import { experimentSettings } from "@/constants/experiments";
import {
calcDaysBetweenDates,
convertDateToDateString,
} from "@/layouts/schedule/helpers/schedule-helper.js";
export default {
name: "waitlistQuestion",
mixins: [experimentMixin],
emits: ["update:modelValue"],
props: {
modelValue: {
type: Boolean,
default: false,
},
availableDates: {
type: Array,
default: () => [],
},
},
computed: {
localValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
waitListSuccessImage() {
return this.getCmsContent("WaitListSuccessWidget", "Image");
},
waitListSuccessText() {
return this.getCmsContent("WaitListSuccessWidget", "BodyText");
},
waitlistThresholdDays() {
return this.hasSetting(experimentSettings.WAITLIST_THRESHOLD_DAYS)
? parseInt(this.getSettingValue(experimentSettings.WAITLIST_THRESHOLD_DAYS), 10)
: 0;
},
daysUntilEarliestAvailableDate() {
const earliestDateString = this.availableDates?.[0];
if (!earliestDateString) return null;
const todayString = convertDateToDateString(new Date());
return calcDaysBetweenDates(todayString, earliestDateString);
},
shouldDisplay() {
return (
this.hasSettingEqualTo(experimentSettings.DISPLAY_WAITLIST, "true") &&
this.daysUntilEarliestAvailableDate !== null &&
this.waitlistThresholdDays < this.daysUntilEarliestAvailableDate
);
},
},
methods: {
handleContainerClick(event) {
// The checkbox's own label/input already toggles localValue natively,
// so ignore clicks that originate inside it to avoid double-toggling.
// 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;
},
},
components: {
textBlock,
checkboxQuestion,
},
};
</script>
<style lang="scss" scoped>
.waitlist-question {
box-shadow: 0 1px 5px 0 #00000033;
padding: 16px;
.waitlist-label {
font-family: UrbanistSemibold;
}
:deep(.ui-checkbox) {
padding-left: 0px;
input {
border-radius: 4px;
box-shadow: 0 1px 5px 0 #00000033;
}
}
:deep(.form-check-input) {
margin-left: 0px;
}
}
.waitlist-success {
margin: 1rem 0 0;
background-color: #ecf5e9;
display: flex;
align-items: flex-start;
border: 1px solid #0c7e47;
border-radius: 5px;
font-size: 1.125rem;
padding: 0.75rem 1rem;
.success-text {
margin-left: 0.5rem;
:deep(p) {
margin-bottom: 0;
}
}
.success-image {
margin-top: 4.5px;
width: 1rem;
height: 1rem;
color: #0c7e47;
}
}
</style>