Merge remote-tracking branch 'origin/develop' into rlsmerge/develop-to-2025.09.25

This commit is contained in:
Scott Kiener 2025-09-11 15:24:24 -04:00
commit e77c328f1e
26 changed files with 734 additions and 68 deletions

View file

@ -53,6 +53,7 @@ body select {
background-color: #fff; background-color: #fff;
font-family: Urbanist, Arial, Helvetica, sans-serif; font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 400; font-weight: 400;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
} }
body input:focus, body input:focus-visible, body input:focus, body input:focus-visible,
body select:focus, body select:focus,

View file

@ -63,6 +63,7 @@ body {
background-color: #fff; background-color: #fff;
font-family: Urbanist, Arial, Helvetica, sans-serif; font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 400; font-weight: 400;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
&:focus, &:focus,
&:focus-visible { &:focus-visible {
box-shadow: 0 0 0 2.5px #1574a1; box-shadow: 0 0 0 2.5px #1574a1;

View file

@ -16,6 +16,8 @@ const damageLocationsSelected = {
PASSENGERSIDE: "PassengerSide", PASSENGERSIDE: "PassengerSide",
STATIONARY: "Stationary", STATIONARY: "Stationary",
SLIDER: "Slider", SLIDER: "Slider",
DOOR: "Door",
GLASS: "Glass",
}; };
export { damageLocationsSelected }; export { damageLocationsSelected };

View file

@ -268,6 +268,9 @@ export default {
button { button {
margin: 0; margin: 0;
border-radius: 50rem; border-radius: 50rem;
&.btn-secondary {
font-weight: 600;
}
} }
} }
} }

View file

@ -214,6 +214,47 @@ export default {
&.is-insurance { &.is-insurance {
padding-bottom: 3rem; padding-bottom: 3rem;
@include media-breakpoint-down(md) {
padding-bottom: 0.75rem;
.pricing-info {
top: 0.75rem;
right: 0.75rem;
bottom: auto;
left: auto;
width: auto;
margin: 0;
text-align: end;
max-width: 60%;
}
@media (max-width: 320px) {
min-height: 80px;
max-height: 120px;
.pricing-info {
top: 2.1rem;
right: auto;
left: 2.3rem;
bottom: auto;
position: absolute;
width: calc(100% - 2.5rem);
text-align: left;
max-width: none;
}
}
&.has-subheader {
@media (max-width: 320px) {
min-height: 100px;
max-height: 160px;
.pricing-info {
top: 3.9rem;
}
}
}
}
} }
&.has-subheader { &.has-subheader {

View file

@ -109,6 +109,15 @@ export default {
} }
this.globalAlertMessages.push(alertToPush); this.globalAlertMessages.push(alertToPush);
}, },
removeGlobalAlert(alertId) {
const alertIndex = this.globalAlertMessages.findIndex((alert) => alert.id === alertId);
if (alertIndex !== -1) {
this.globalAlertMessages.splice(alertIndex, 1);
}
},
getGlobalAlerts() {
return this.globalAlertMessages;
},
webchatClicked(event) { webchatClicked(event) {
event.preventDefault(); event.preventDefault();
this.launchWebchat(); this.launchWebchat();

View file

@ -82,3 +82,56 @@ export async function isGlassAvailableForCarId(carId, pageNameToLog) {
return true; return true;
} }
export function getSideDoorGlassString(glassLocation, glassName) {
let glassNameString;
switch (glassName) {
case glassLocations.BACK:
glassNameString = glassLocations.REAR + " " + glassLocations.DOOR; // "Back Door"
break;
case glassLocations.FRONT:
glassNameString = glassName + " " + glassLocations.DOOR; // "Front Door"
break;
case glassLocations.VENT:
case glassLocations.QUARTER:
glassNameString = glassName;
break;
}
return glassLocation + " " + glassName + " " + glassLocations.GLASS;
}
export function getDamageInfoWordingText(damageInfo) {
const glassTextArray = [];
damageInfo.glassToReplace.forEach((item) => {
let string = "";
if (item.glassLocation === glassLocations.WINDSHIELD) {
string = item.glassLocation;
}
if (item.glassLocation === glassLocations.REAR) {
string = glassLocations.BACK + " " + glassLocations.GLASS;
}
if (item.glassLocation === glassLocations.DRIVER) {
string = getSideDoorGlassString(item.glassLocation, item.glassName);
// ONLY INCLUDE "DOOR" IF IT'S REAR or FRONT (vent or quarter should NOT)
}
if (item.glassLocation === glassLocations.PASSENGER) {
string = getSideDoorGlassString(item.glassLocation, item.glassName);
}
glassTextArray.push(string);
});
if (glassTextArray.length === 1) {
return glassTextArray[0];
}
let damageInfoWordingText = glassTextArray.join(", "); // string
let lastCommaIndex = damageInfoWordingText.lastIndexOf(", ");
if (lastCommaIndex > -1) {
return (
damageInfoWordingText.slice(0, lastCommaIndex) +
" and " +
damageInfoWordingText.slice(lastCommaIndex + 2, damageInfoWordingText.length)
);
}
return "";
}

View file

@ -286,5 +286,6 @@ export default {
.select-option { .select-option {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
font-weight: 600; font-weight: 600;
color: $black;
} }
</style> </style>

View file

@ -240,4 +240,7 @@ export default {
color: $gray-600; color: $gray-600;
font-family: UrbanistRegular; font-family: UrbanistRegular;
} }
:deep(.caption) {
font-size: 0.875rem;
}
</style> </style>

View file

@ -223,7 +223,8 @@ export default {
"payment-method" "payment-method"
); );
const reviewDropdownPromise = reviewDropdown.methods.loadInitialData(); // const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
// (removed temporarily for Heritage parity effort)
const promiseResultMap = [ const promiseResultMap = [
{ {
@ -238,10 +239,11 @@ export default {
resultKey: "rainDefense", resultKey: "rainDefense",
promise: rainDefensePromise, promise: rainDefensePromise,
}, },
{ // {
resultKey: "reviewDropdownData", // resultKey: "reviewDropdownData",
promise: reviewDropdownPromise, // promise: reviewDropdownPromise,
}, // },
// (removed temporarily for Heritage parity effort)
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
@ -346,7 +348,8 @@ export default {
); );
vm.updateFooterButtonText(vm.customCtaCopy); vm.updateFooterButtonText(vm.customCtaCopy);
vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData); // vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
// (removed temporarily for Heritage parity effort)
if (revalidatePromoResponse) { if (revalidatePromoResponse) {
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse( const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
@ -847,6 +850,18 @@ export default {
(newPromo) => !oldPromoCodes.includes(newPromo.promoCode) (newPromo) => !oldPromoCodes.includes(newPromo.promoCode)
); );
// Remove any existing promo error alerts for this promo code
const existingAlerts = this.$refs.funnelHeader.getGlobalAlerts();
if (existingAlerts.length > 0) {
const errorPromoAlertToRemove = existingAlerts.find(
(alert) =>
alert.messageHeadline === "Promo code error" &&
alert.messageCopy.includes(newlyActivatedPromoCodes[0].promoCode)
);
errorPromoAlertToRemove &&
this.$refs.funnelHeader.removeGlobalAlert(errorPromoAlertToRemove.id);
}
const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode); const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade); this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
} else if ( } else if (
@ -892,6 +907,9 @@ export default {
text-transform: lowercase; text-transform: lowercase;
} }
} }
:deep(.review-table p) {
margin: 0.25rem 0 0 0;
}
} }
.checkbox-group { .checkbox-group {
align-items: start; align-items: start;

View file

@ -15,30 +15,27 @@
<div v-html="apptWordingText" v-show="!isExpanded"></div> <div v-html="apptWordingText" v-show="!isExpanded"></div>
<div class="review-table px-4"> <div class="review-table px-4">
<serviceLocationReview <appointmentReview
class="service-location" class="service-location"
cmsWidgetName="ServiceLocationTitleWidget" cmsWidgetName="AppointmentWidget"
:serviceLocation="serviceLocationInfo" /> :apptWordingText="apptWordingText" />
<hr class="my-0" /> <hr class="my-0" />
<scheduleReview cmsWidgetName="ScheduleWidget" :appointmentType="appointmentType" /> <parityDamageReview
<hr class="my-0" />
<vehicleReview cmsWidgetName="VehicleReviewWidget" :vehicle="vehicleInfo" />
<hr class="my-0" />
<damageReview
cmsWidgetName="DamageReviewWidget" cmsWidgetName="DamageReviewWidget"
damageLocationsWidgetName="DamageLocationsWidget" damageLocationsWidgetName="DamageLocationsWidget"
:damage="damageInfo" /> :damage="damageInfo"
:vehicle="vehicleInfo" />
<hr class="my-0" /> <hr class="my-0" />
<customerReview cmsWidgetName="CustomerReviewWidget" :customer="customerInfo" /> <customerReview cmsWidgetName="CustomerReviewWidget" :customer="customerInfo" />
<hr class="my-0" />
<optInReview cmsWidgetName="OptInReviewWidget" :customer="customerInfo" />
<!-- the following was the order prior to Heritage parity <!-- the following was the order prior to Heritage parity
<vehicleReview cmsWidgetName="VehicleReviewWidget" :vehicle="vehicleInfo" /> <vehicleReview cmsWidgetName="VehicleReviewWidget" :vehicle="vehicleInfo" />
@ -82,8 +79,11 @@
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
import customerReview from "@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review"; import customerReview from "@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review";
import optInReview from "@/layouts/payment-method/review-dropdown/review-sections/opt-in-review/opt-in-review";
import damageReview from "@/layouts/payment-method/review-dropdown/review-sections/damage-review/damage-review"; import damageReview from "@/layouts/payment-method/review-dropdown/review-sections/damage-review/damage-review";
import parityDamageReview from "@/layouts/payment-method/review-dropdown/review-sections/parity-damage-review/parity-damage-review";
import scheduleReview from "@/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review"; import scheduleReview from "@/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review";
import appointmentReview from "@/layouts/payment-method/review-dropdown/review-sections/appointment-review/appointment-review";
import serviceLocationReview from "@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review"; import serviceLocationReview from "@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review";
import servicePackageReview from "@/layouts/payment-method/review-dropdown/review-sections/service-package-review/service-package-review"; import servicePackageReview from "@/layouts/payment-method/review-dropdown/review-sections/service-package-review/service-package-review";
import vehicleReview from "@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review"; import vehicleReview from "@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review";
@ -95,6 +95,7 @@ export default {
props: { props: {
apptWordingText: String, apptWordingText: String,
serviceLocationFullAddress: String, serviceLocationFullAddress: String,
damageInfoHeader: String,
}, },
data() { data() {
return { return {
@ -102,21 +103,22 @@ export default {
}; };
}, },
methods: { methods: {
async loadInitialData() { // the following was used prior to Heritage parity
const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData(); // async loadInitialData() {
// const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
const promiseResultMap = [ // const promiseResultMap = [
{ // {
resultKey: "servicePackageData", // resultKey: "servicePackageData",
promise: servicePackageInitialDataPromise, // promise: servicePackageInitialDataPromise,
}, // },
]; // ];
return settleAllPromises(promiseResultMap); // return settleAllPromises(promiseResultMap);
}, // },
initializeComponent(apiResponses) { // initializeComponent(apiResponses) {
this.$refs.servicePackageReview.initializeComponent(apiResponses.servicePackageData); // this.$refs.servicePackageReview.initializeComponent(apiResponses.servicePackageData);
}, // },
toggleIsExpanded() { toggleIsExpanded() {
this.isExpanded = !this.isExpanded; this.isExpanded = !this.isExpanded;
}, },
@ -143,10 +145,9 @@ export default {
}, },
components: { components: {
customerReview, customerReview,
damageReview, optInReview,
scheduleReview, appointmentReview,
vehicleReview, parityDamageReview,
serviceLocationReview,
}, },
}; };
</script> </script>
@ -163,6 +164,10 @@ export default {
overflow: hidden; overflow: hidden;
visibility: hidden; visibility: hidden;
& > :first-child {
margin-top: -0.75rem;
}
:deep(.service-location) { :deep(.service-location) {
.review-block-content { .review-block-content {
text-transform: capitalize; text-transform: capitalize;

View file

@ -0,0 +1,103 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import appointmentReview from "@/layouts/payment-method/review-dropdown/review-sections/appointment-review/appointment-review";
const testConstants = {
cms: {
header: {
text: "Appointment Details",
},
},
appointment: {
apptWordingText:
"September 10, 2025 arriving between 9:00 AM — 11:00 AM at 123 Main St, anytown, OH 43215",
},
displayContent: {
apptWordingText:
"September 10, 2025 arriving between 9:00 AM — 11:00 AM at 123 Main St, anytown, OH 43215",
},
};
let cmsContent;
describe("Appointment Review Block", () => {
beforeEach(() => {
cmsContent = {
AppointmentWidget: {
HeaderText: testConstants.cms.header.text,
},
};
});
test("Should display header text from cms", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.headerText).toEqual(testConstants.cms.header.text);
});
test("Should render correct display content", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([testConstants.displayContent.apptWordingText]);
});
test("Should emit edit-clicked when editClicked method is called", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.editClicked();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted("edit-clicked")).toBeTruthy();
});
});
function generateDefaultProps() {
return {
cmsWidgetName: "AppointmentWidget",
apptWordingText: testConstants.appointment.apptWordingText,
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(appointmentReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,38 @@
<template>
<reviewBlock
:customHeaderText="headerText"
:content="displayContent"
@edit-clicked="editClicked" />
</template>
<script>
import reviewBlock from "@/layouts/payment-method/review-dropdown/review-block/review-block";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
export default {
name: "appointment-review",
props: {
cmsWidgetName: String,
apptWordingText: String,
},
data() {
return {};
},
methods: {
editClicked() {
this.$emit("edit-clicked");
},
},
computed: {
displayContent() {
return [this.apptWordingText];
},
headerText() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
},
components: {
reviewBlock,
},
};
</script>

View file

@ -13,17 +13,17 @@ const testConstants = {
}, },
}, },
customer: { customer: {
firstName: "First", // firstName: "First",
lastName: "Last", // lastName: "Last",
phoneNumber: "111-111-1111", phoneNumber: "111-111-1111",
emailAddress: "builddigitaltest@safelite.com", emailAddress: "builddigitaltest@safelite.com",
isSmsOptIn: false, // isSmsOptIn: false,
}, },
displayContent: { displayContent: {
fullName: "First Last", // fullName: "First Last",
phoneNumber: "111-111-1111", phoneNumber: "111-111-1111",
emailAddress: "builddigitaltest@safelite.com", emailAddress: "builddigitaltest@safelite.com",
smsOptIn: "Sms", // smsOptIn: "Sms",
}, },
}; };
@ -54,20 +54,20 @@ describe("Customer Review Block", () => {
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text); expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
}); });
test("Should display sms text from cms", async () => { // test("Should display sms text from cms", async () => {
// Arrange // // Arrange
let props = generateDefaultProps(); // let props = generateDefaultProps();
const { wrapper } = setupMocks({ // const { wrapper } = setupMocks({
propsData: props, // propsData: props,
}); // });
// Act // // Act
await wrapper.vm.$nextTick(); // await wrapper.vm.$nextTick();
// Assert // // Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text); // expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
}); // });
test("Should render correct display content", async () => { test("Should render correct display content", async () => {
// Arrange // Arrange
@ -84,7 +84,7 @@ describe("Customer Review Block", () => {
expect(wrapper.vm.displayContent).toEqual([ expect(wrapper.vm.displayContent).toEqual([
testConstants.displayContent.emailAddress, testConstants.displayContent.emailAddress,
testConstants.displayContent.phoneNumber, testConstants.displayContent.phoneNumber,
testConstants.displayContent.smsOptIn, // testConstants.displayContent.smsOptIn,
]); ]);
}); });
}); });
@ -93,11 +93,11 @@ function generateDefaultProps() {
return { return {
cmsWidgetName: "CustomerWidget", cmsWidgetName: "CustomerWidget",
customer: { customer: {
firstName: testConstants.customer.firstName, // firstName: testConstants.customer.firstName,
lastName: testConstants.customer.lastName, // lastName: testConstants.customer.lastName,
phoneNumber: testConstants.customer.phoneNumber, phoneNumber: testConstants.customer.phoneNumber,
emailAddress: testConstants.customer.emailAddress, emailAddress: testConstants.customer.emailAddress,
isSmsOptIn: testConstants.customer.isSmsOptIn, // isSmsOptIn: testConstants.customer.isSmsOptIn,
}, },
}; };
} }

View file

@ -21,7 +21,7 @@ export default {
}, },
computed: { computed: {
displayContent() { displayContent() {
return [this.email, this.phoneNumber, this.smsOptIn]; return [this.email, this.phoneNumber];
// return [this.fullName, this.email, this.phoneNumber, this.smsOptIn]; // prior to Heritage parity // return [this.fullName, this.email, this.phoneNumber, this.smsOptIn]; // prior to Heritage parity
}, },
header() { header() {

View file

@ -0,0 +1,141 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import optInReview from "@/layouts/payment-method/review-dropdown/review-sections/opt-in-review/opt-in-review";
const testConstants = {
cms: {
header: {
text: "Text Message Updates",
},
bodyText: {
text: "We'll send appointment updates to {custom:SMSNUMBER}",
},
processedBodyText: {
text: "We'll send appointment updates to 111-111-1111",
},
},
customer: {
phoneNumber: "111-111-1111",
},
displayContent: {
processedBodyText: "We'll send appointment updates to 111-111-1111",
},
};
let cmsContent;
describe("Opt-In Review Block", () => {
beforeEach(() => {
cmsContent = {
OptInWidget: {
HeaderText: testConstants.cms.header.text,
BodyText: testConstants.cms.bodyText.text,
},
};
});
test("Should display header text from cms", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
});
test("Should render correct display content with phone number replacement", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([testConstants.displayContent.processedBodyText]);
});
test("Should return customer phone number", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.phoneNumber).toEqual(testConstants.customer.phoneNumber);
});
test("Should emit edit-clicked when editClicked method is called", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.editClicked();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted("edit-clicked")).toBeTruthy();
});
test("Should handle missing customer gracefully", async () => {
// Arrange
let props = generateDefaultProps();
props.customer = null;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.phoneNumber).toBeUndefined();
});
});
function generateDefaultProps() {
return {
cmsWidgetName: "OptInWidget",
customer: {
phoneNumber: testConstants.customer.phoneNumber,
},
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(optInReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,42 @@
<template>
<reviewBlock :customHeaderText="header" :content="displayContent" @edit-clicked="editClicked" />
</template>
<script>
import reviewBlock from "@/layouts/payment-method/review-dropdown/review-block/review-block";
export default {
name: "opt-in-review",
props: {
cmsWidgetName: String,
customer: Object,
},
data() {
return {};
},
methods: {
editClicked() {
this.$emit("edit-clicked");
},
},
computed: {
displayContent() {
return [
this.getCmsContent(this.cmsWidgetName, "BodyText")?.replaceAll(
"{custom:SMSNUMBER}",
this.phoneNumber
),
];
},
header() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
phoneNumber() {
return this.customer?.phoneNumber;
},
},
components: {
reviewBlock,
},
};
</script>

View file

@ -0,0 +1,149 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { getDamageInfoWordingText } from "@/helpers/damage-helper";
import parityDamageReview from "@/layouts/payment-method/review-dropdown/review-sections/parity-damage-review/parity-damage-review";
// Mock the damage helper
jest.mock("@/helpers/damage-helper", () => ({
getDamageInfoWordingText: jest.fn(),
}));
const testConstants = {
cms: {
damageInfoHeader: {
text: "We'll replace your {custom:GLASSPARTS}",
},
processedHeader: {
text: "We'll replace your windshield and passenger side window",
},
},
vehicle: {
year: "2020",
make: "Honda",
model: "Civic",
},
damage: {
glassType: "windshield",
location: "passenger",
},
damageInfoWordingText: "windshield and passenger side window",
displayContent: {
vehicleInfo: "2020 Honda Civic",
},
};
let cmsContent;
describe("Parity Damage Review Block", () => {
beforeEach(() => {
cmsContent = {
DamageInfoHeaderWidget: {
Text: testConstants.cms.damageInfoHeader.text,
},
};
// Mock the damage helper function
getDamageInfoWordingText.mockReturnValue(testConstants.damageInfoWordingText);
});
afterEach(() => {
jest.clearAllMocks();
});
test("Should display processed damage info header text with custom replacement", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.damageInfoHeader).toEqual(testConstants.cms.processedHeader.text);
});
test("Should render correct display content with vehicle information", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([testConstants.displayContent.vehicleInfo]);
});
test("Should call getDamageInfoWordingText with damage prop", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
const result = wrapper.vm.damageInfoWordingText;
// Assert
expect(getDamageInfoWordingText).toHaveBeenCalledWith(testConstants.damage);
expect(result).toEqual(testConstants.damageInfoWordingText);
});
test("Should emit edit-clicked when editClicked method is called", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.editClicked();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted("edit-clicked")).toBeTruthy();
});
});
function generateDefaultProps() {
return {
cmsWidgetName: "DamageReviewWidget",
vehicle: {
year: testConstants.vehicle.year,
make: testConstants.vehicle.make,
model: testConstants.vehicle.model,
},
damage: {
glassType: testConstants.damage.glassType,
location: testConstants.damage.location,
},
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(parityDamageReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,46 @@
<template>
<reviewBlock
:headerCmsWidgetName="cmsWidgetName"
:content="displayContent"
:customHeaderText="damageInfoHeader"
@edit-clicked="editClicked" />
</template>
<script>
import reviewBlock from "@/layouts/payment-method/review-dropdown/review-block/review-block";
import { getDamageInfoWordingText } from "@/helpers/damage-helper";
export default {
name: "vehicle-review",
props: {
cmsWidgetName: String,
vehicle: Object,
damage: Object,
},
data() {
return {};
},
methods: {
editClicked() {
this.$emit("edit-clicked");
},
},
computed: {
displayContent() {
return [`${this.vehicle.year} ${this.vehicle.make} ${this.vehicle.model}`];
},
damageInfoWordingText() {
return getDamageInfoWordingText(this.damage);
},
damageInfoHeader() {
return this.getCmsContent("DamageInfoHeaderWidget", "Text")?.replaceAll(
"{custom:GLASSPARTS}",
this.damageInfoWordingText
);
},
},
components: {
reviewBlock,
},
};
</script>

View file

@ -20,8 +20,8 @@
<span v-else>{{ getInlineAltText(token) }}</span> <span v-else>{{ getInlineAltText(token) }}</span>
</span> </span>
<span v-else v-html="token"></span> <span v-else v-html="token"></span>
<span class="nbsp">&nbsp;</span>
</span> </span>
&nbsp;
<a <a
id="afterpay-learnmore" id="afterpay-learnmore"
href="#" href="#"
@ -139,7 +139,7 @@ export default {
line-height: 2rem; line-height: 2rem;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
border-bottom: 1px solid; border-bottom: 1px solid;
padding-bottom: 1rem; padding-bottom: 0.5rem;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
@ -170,4 +170,8 @@ export default {
font-size: 0.875rem; font-size: 0.875rem;
} }
} }
#afterpay-learnmore {
font-weight: 600;
font-size: 0.875rem;
}
</style> </style>

View file

@ -57,7 +57,7 @@
alertClass="alert-info" /> alertClass="alert-info" />
<promoModalQuestion <promoModalQuestion
class="mt-6 d-flex" class="mt-4 d-flex"
v-model="lineItems" v-model="lineItems"
:addableVaps="addableVaps" :addableVaps="addableVaps"
pageName="quote" pageName="quote"
@ -904,9 +904,12 @@ export default {
:deep(.nav-bar) { :deep(.nav-bar) {
position: initial; position: initial;
button {
width: 100%; @media only screen and (min-width: 340px) {
justify-content: center; .col button {
width: 100%;
justify-content: center;
}
} }
a.navigation-link { a.navigation-link {
position: absolute; position: absolute;

View file

@ -1790,7 +1790,7 @@ export default {
text-underline-offset: 4px; text-underline-offset: 4px;
} }
.text-block.duration-text-block b { :deep(.text-block.duration-text-block b) {
font-family: UrbanistSemibold; font-family: UrbanistSemibold;
} }
:deep(.alert-warning.widget-name-AlertRecalNoMobileWidget .alert-heading), :deep(.alert-warning.widget-name-AlertRecalNoMobileWidget .alert-heading),

View file

@ -15,7 +15,7 @@
<alert <alert
v-if="shouldDisplayDropOffAlert" v-if="shouldDisplayDropOffAlert"
ref="alertDropoffInformation" ref="alertDropoffInformation"
class="mb-4 drop-off-alert" class="mt-4 mb-4 drop-off-alert"
:cmsWidgetName="dropOffTimeSlotAlertCMSWidgetName" :cmsWidgetName="dropOffTimeSlotAlertCMSWidgetName"
alertClass="alert-info" alertClass="alert-info"
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />

View file

@ -291,7 +291,7 @@ export default {
this.$store.getters.order.vehicle.carId, this.$store.getters.order.vehicle.carId,
"service-location" "service-location"
); );
if (!closestShops || closestShops.providers?.length === 0) { if (!closestShops || closestShops.inShopProviders?.length === 0) {
this.isLoading = false; this.isLoading = false;
this.displayNoServiceAlert = true; this.displayNoServiceAlert = true;
return null; return null;

View file

@ -496,7 +496,10 @@ export default {
{ zip: this.serviceZipCode, carId: this.carId }, { zip: this.serviceZipCode, carId: this.carId },
"vehicle" "vehicle"
); );
if (!closestShops?.data?.providers || closestShops.data.providers.length === 0) { if (
!closestShops?.data?.inShopProviders ||
closestShops.data.inShopProviders.length === 0
) {
this.displayNoServiceAlert = true; this.displayNoServiceAlert = true;
if (store.getters.externalParameterState?.isExternalParameter) { if (store.getters.externalParameterState?.isExternalParameter) {
return baseMixin.methods.ResetExternalParamsAndHideModal(); return baseMixin.methods.ResetExternalParamsAndHideModal();

View file

@ -79,7 +79,7 @@ export default {
color: $black; color: $black;
font-weight: 500; font-weight: 500;
background: $blue-100; background: $blue-100;
box-shadow: 0 0 0 1px $blue; border: 1px solid $blue;
} }
&:checked:focus + .list-button-content { &:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;