Merge pull request #3280 from Safelite/feature/CASH-3047

CASH-3047: Add ServicePackage description
This commit is contained in:
Chris 2026-07-23 10:55:57 -04:00 committed by GitHub
commit 7399115150
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 290 additions and 112 deletions

View file

@ -58,6 +58,8 @@ const experimentSettings = {
TIER_ONE: "TierOne",
TIER_TWO: "TierTwo",
TIER_THREE: "TierThree",
TIER_TWO_DESCRIPTION: "TierTwo_Description",
TIER_THREE_DESCRIPTION: "TierThree_Description",
// Consent Management
SHOW_CONSENT_MANAGEMENT: "ShowConsentManagement",

View file

@ -4,6 +4,7 @@
class="package-label"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
this.additionalButtonData?.packageDescription ? 'has-description' : '',
this.additionalButtonData.isInsuranceSelected ? 'is-insurance' : '',
this.hasPackageDiscount() ? 'has-package-discount' : '',
]"
@ -13,6 +14,10 @@
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<p
class="package-description m-0"
v-if="this.additionalButtonData?.packageDescription"
v-html="this.additionalButtonData.packageDescription"></p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
@ -202,7 +207,7 @@ export default {
min-height: 52px;
max-height: 126px;
@include media-breakpoint-up(md) {
max-height: 500px;
max-height: 1000px;
padding-bottom: 4.75rem;
}
@include media-breakpoint-down(md) {
@ -242,7 +247,8 @@ export default {
}
}
&.has-subheader {
&.has-subheader,
&.has-description {
@media (max-width: 320px) {
min-height: 100px;
max-height: 160px;
@ -255,16 +261,21 @@ export default {
}
}
&.has-subheader {
&.has-subheader,
&.has-description {
& > .package-specs {
display: inline-flex;
}
}
//Package container height with package discount AND "most popular" label
&.has-package-discount.has-subheader {
&.has-package-discount.has-subheader,
&.has-package-discount.has-description {
min-height: 170px;
}
&.has-package-discount.has-subheader.has-description {
min-height: 190px;
}
//Package container height with package discount only
&.has-package-discount {
min-height: 136px;
@ -307,30 +318,28 @@ export default {
&:checked {
+ .package-label {
max-height: 1000px;
border: 1px solid $blue;
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
max-height: none;
.hide-when-closed {
display: block;
max-height: none;
overflow: visible;
opacity: 1;
transition: opacity 0.2s ease 0.15s;
}
}
}
+ .package-label {
border: 1px solid $blue;
max-height: 500px;
.special-save-box {
background-color: $green-100;
}
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
@ -356,8 +365,9 @@ export default {
flex-direction: column;
width: 100%;
transition: all 0.5s ease;
@include media-breakpoint-up(md) {
max-height: 500px;
max-height: none;
}
.row {
@ -378,6 +388,13 @@ export default {
font-size: 0.75rem;
margin-left: auto;
}
&.package-description {
color: $gray-550;
font-size: 0.875rem;
font-weight: 400;
font-style: italic;
white-space: normal;
}
}
span {
&.service-package-discount {
@ -441,10 +458,17 @@ export default {
}
.hide-when-closed {
display: none;
display: flex;
flex-direction: column;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity 0.15s ease;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
max-height: none;
opacity: 1;
overflow: visible;
}
}
}

View file

@ -4,6 +4,7 @@
class="package-label pricing-by-day-pkg-lbl"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
this.additionalButtonData?.packageDescription ? 'has-description' : '',
!this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
]"
for="testradio">
@ -13,6 +14,10 @@
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<p
class="package-description m-0"
v-if="this.additionalButtonData?.packageDescription"
v-html="this.additionalButtonData.packageDescription"></p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
@ -41,6 +46,10 @@
<span v-html="getDisplayPrice"></span>
</span>
</p>
<p
class="package-description m-0"
v-if="this.additionalButtonData?.packageDescription"
v-html="this.additionalButtonData.packageDescription"></p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
@ -259,13 +268,18 @@ export default {
min-height: 52px;
max-height: 126px;
@include media-breakpoint-up(md) {
max-height: 500px;
max-height: 1000px;
padding-bottom: 3rem;
}
transition: max-height 0.25s ease-in;
&.has-subheader {
&.has-subheader,
&.has-description {
min-height: 72px;
& > .package-specs {
display: inline-flex;
}
}
&:before {
@ -288,12 +302,6 @@ export default {
height: 10px;
min-width: 10px;
}
&.has-subheader {
& > .package-specs {
display: inline-flex;
}
}
}
&:hover {
@ -309,30 +317,28 @@ export default {
&:checked {
+ .package-label {
max-height: 1000px;
border: 1px solid $blue;
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
max-height: none;
.hide-when-closed {
display: block;
max-height: none;
overflow: visible;
opacity: 1;
transition: opacity 0.2s ease 0.15s;
}
}
}
+ .package-label {
border: 1px solid $blue;
max-height: 500px;
.special-save-box {
background-color: $green-200;
}
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
@ -358,8 +364,9 @@ export default {
flex-direction: column;
width: 100%;
transition: all 0.5s ease;
@include media-breakpoint-up(md) {
max-height: 500px;
max-height: none;
}
span.pricing-by-day {
@ -398,6 +405,13 @@ export default {
text-transform: uppercase;
font-size: 0.75rem;
}
&.package-description {
color: $gray-550;
font-size: 0.875rem;
font-weight: 400;
font-style: italic;
white-space: normal;
}
}
span {
&.service-package-discount-pricing-by-day {
@ -458,10 +472,17 @@ export default {
}
.hide-when-closed {
display: none;
display: flex;
flex-direction: column;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity 0.15s ease;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
max-height: none;
opacity: 1;
overflow: visible;
}
}
}

View file

@ -16,6 +16,11 @@ const tierExperimentSettingKeys = {
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
};
const tierDescriptionExperimentSettingKeys = {
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO_DESCRIPTION,
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE_DESCRIPTION,
};
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
return !!partTypeMatches?.length;
@ -284,16 +289,24 @@ export function isServicePackageNameTestActive(experimentSettingsMap) {
}
export function getExperimentPackageDisplayName(tierName, experimentSettingsMap) {
return getExperimentPackageSetting(tierName, tierExperimentSettingKeys, experimentSettingsMap);
}
export function getExperimentPackageDescription(tierName, experimentSettingsMap) {
return getExperimentPackageSetting(
tierName,
tierDescriptionExperimentSettingKeys,
experimentSettingsMap
);
}
function getExperimentPackageSetting(tierName, settingKeys, experimentSettingsMap) {
if (!isServicePackageNameTestActive(experimentSettingsMap)) {
return null;
}
const settingKey = tierExperimentSettingKeys[tierName];
if (!settingKey) {
return null;
}
return experimentHelper.getSettingValue(settingKey, experimentSettingsMap);
const settingKey = settingKeys[tierName];
return settingKey ? experimentHelper.getSettingValue(settingKey, experimentSettingsMap) : null;
}
function maxTier(tierA, tierB) {

View file

@ -969,34 +969,29 @@ describe("service-package-helper.js", () => {
describe("getExperimentPackageDisplayName", () => {
const { experimentSettings } = require("@/constants/experiments");
const activeExperimentSettingsMap = {
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
[experimentSettings.TIER_ONE]: "Essential",
[experimentSettings.TIER_TWO]: "Plus",
[experimentSettings.TIER_THREE]: "Full Service",
[experimentSettings.TIER_TWO_DESCRIPTION]: "Most popular choice",
[experimentSettings.TIER_THREE_DESCRIPTION]: "Best value",
};
it("Returns null when the experiment is not active", () => {
const result = servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_ONE,
{}
);
expect(result).toBeNull();
expect(
servicePackageHelper.getExperimentPackageDisplayName(packageNames.TIER_ONE, {})
).toBeNull();
expect(
servicePackageHelper.getExperimentPackageDescription(packageNames.TIER_TWO, {})
).toBeNull();
});
it("Returns null when the test flag is not true", () => {
const result = servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_ONE,
{
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "false",
[experimentSettings.TIER_ONE]: "Essential",
}
);
expect(result).toBeNull();
});
it("Returns the experiment display name for each tier when active", () => {
const experimentSettingsMap = {
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "false",
[experimentSettings.TIER_ONE]: "Essential",
[experimentSettings.TIER_TWO]: "Plus",
[experimentSettings.TIER_THREE]: "Full Service",
[experimentSettings.TIER_TWO_DESCRIPTION]: "Most popular choice",
};
expect(
@ -1004,28 +999,70 @@ describe("service-package-helper.js", () => {
packageNames.TIER_ONE,
experimentSettingsMap
)
).toBeNull();
expect(
servicePackageHelper.getExperimentPackageDescription(
packageNames.TIER_TWO,
experimentSettingsMap
)
).toBeNull();
});
it("Returns the experiment display name for each tier when active", () => {
expect(
servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_ONE,
activeExperimentSettingsMap
)
).toBe("Essential");
expect(
servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_TWO,
experimentSettingsMap
activeExperimentSettingsMap
)
).toBe("Plus");
expect(
servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_THREE,
experimentSettingsMap
activeExperimentSettingsMap
)
).toBe("Full Service");
});
it("Returns null for an unknown tier", () => {
const result = servicePackageHelper.getExperimentPackageDisplayName("UnknownTier", {
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
[experimentSettings.TIER_ONE]: "Essential",
});
it("Returns the experiment description for tier two and three when active", () => {
expect(
servicePackageHelper.getExperimentPackageDescription(
packageNames.TIER_TWO,
activeExperimentSettingsMap
)
).toBe("Most popular choice");
expect(
servicePackageHelper.getExperimentPackageDescription(
packageNames.TIER_THREE,
activeExperimentSettingsMap
)
).toBe("Best value");
});
expect(result).toBeNull();
it("Returns null for unknown tiers and tiers without configured settings", () => {
expect(
servicePackageHelper.getExperimentPackageDisplayName("UnknownTier", {
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
[experimentSettings.TIER_ONE]: "Essential",
})
).toBeNull();
expect(
servicePackageHelper.getExperimentPackageDescription(
packageNames.TIER_ONE,
activeExperimentSettingsMap
)
).toBeNull();
expect(
servicePackageHelper.getExperimentPackageDescription(
"UnknownTier",
activeExperimentSettingsMap
)
).toBeNull();
});
});
});

View file

@ -856,6 +856,52 @@ describe("service-package-question.vue, matching business rules for package disp
getExperimentPackageLabelSpy.mockRestore();
});
it("should use experiment package descriptions when ServicePackageNameTest is active", () => {
const getExperimentPackageDescriptionSpy = jest
.spyOn(servicePackageQuestion.methods, "getExperimentPackageDescription")
.mockImplementation((tierName) => {
const experimentPackageDescriptions = {
[packageNames.TIER_TWO]: "Most popular choice",
[packageNames.TIER_THREE]: "Best value",
};
return experimentPackageDescriptions[tierName] ?? null;
});
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
Object.assign(processedCmsContent, mockProcessedCmsContent[packageNameKey]);
mockProps.availableLineItems.push(driverFrontWiperLineItem);
mockProps.availableLineItems.push(passengerFrontWiperLineItem);
const wrapper = setupMocks({
mountOptionsMockData: {
store: {
getters: {
order: {
damage: {
isRepair: true,
glassToReplace: [],
},
},
hasAnyNonWindshieldGlassParts: false,
},
},
},
});
expect(
wrapper.vm.servicePackageAnswers[0].additionalButtonData.packageDescription
).toBeNull();
expect(wrapper.vm.servicePackageAnswers[1].additionalButtonData.packageDescription).toBe(
"Most popular choice"
);
expect(wrapper.vm.servicePackageAnswers[2].additionalButtonData.packageDescription).toBe(
"Best value"
);
expect(wrapper.vm.servicePackageAnswers[1].buttonLabelSubCopy).toBe("MOST POPULAR");
expect(wrapper.vm.servicePackageAnswers[2].buttonLabelSubCopy).toBe("");
getExperimentPackageDescriptionSpy.mockRestore();
});
it("should use CMS package names when ServicePackageNameTest is not active", () => {
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
Object.assign(processedCmsContent, mockProcessedCmsContent[packageNameKey]);

View file

@ -159,6 +159,7 @@ export default {
? false
: "Special: Save $" + this.getDiscountPrice(answer.Name),
isInsuranceSelected: this.isInsuranceSelected,
packageDescription: this.getExperimentPackageDescription(answer.Name),
},
}));
@ -265,6 +266,19 @@ export default {
);
},
getExperimentPackageLabel(tierName) {
return this.getExperimentPackageSetting(tierName, {
[packageNames.TIER_ONE]: experimentSettings.TIER_ONE,
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO,
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
});
},
getExperimentPackageDescription(tierName) {
return this.getExperimentPackageSetting(tierName, {
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO_DESCRIPTION,
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE_DESCRIPTION,
});
},
getExperimentPackageSetting(tierName, tierSettingKeys) {
if (
!experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST,
@ -274,13 +288,8 @@ export default {
return null;
}
const tierSettingKey = {
[packageNames.TIER_ONE]: experimentSettings.TIER_ONE,
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO,
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
}[tierName];
return tierSettingKey ? experimentMixin.methods.getSettingValue(tierSettingKey) : null;
const settingKey = tierSettingKeys[tierName];
return settingKey ? experimentMixin.methods.getSettingValue(settingKey) : null;
},
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "HeaderText");

View file

@ -2,7 +2,10 @@
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div
class="package-label"
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
this.additionalButtonData?.packageDescription ? 'has-description' : '',
]"
for="testradio">
<div class="package-specs">
<div v-if="this.additionalButtonData.hasDiscount" class="row">
@ -10,6 +13,10 @@
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<p
class="package-description m-0"
v-if="this.additionalButtonData?.packageDescription"
v-html="this.additionalButtonData.packageDescription"></p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
@ -34,6 +41,10 @@
<span v-html="this.buttonAuxillaryCopy"></span>
</span>
</p>
<p
class="package-description m-0"
v-if="this.additionalButtonData?.packageDescription"
v-html="this.additionalButtonData.packageDescription"></p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
@ -185,12 +196,13 @@ export default {
min-height: 52px;
max-height: 126px;
@include media-breakpoint-up(md) {
max-height: 500px;
max-height: 1000px;
padding-bottom: 3rem;
}
transition: max-height 0.25s ease-in;
&.has-subheader {
&.has-subheader,
&.has-description {
min-height: 72px;
}
@ -231,34 +243,33 @@ export default {
&:checked {
+ .package-label {
max-height: 1000px;
border: 1px solid $blue;
background-color: $blue-100;
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
max-height: none;
.hide-when-closed {
display: block;
max-height: none;
overflow: visible;
opacity: 1;
transition: opacity 0.2s ease 0.15s;
}
p:first-child {
font-weight: 600;
}
}
}
+ .package-label {
border: 1px solid $blue;
max-height: 500px;
background-color: $blue-100;
.special-save-box {
background-color: $green-200;
}
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
@ -284,8 +295,9 @@ export default {
flex-direction: column;
width: 100%;
transition: all 0.5s ease;
@include media-breakpoint-up(md) {
max-height: 500px;
max-height: none;
}
.row {
@ -332,6 +344,13 @@ export default {
text-transform: uppercase;
font-size: 0.75rem;
}
&.package-description {
color: $gray-550;
font-size: 0.875rem;
font-weight: 400;
font-style: italic;
white-space: normal;
}
}
span {
&.service-package-discount {
@ -393,10 +412,17 @@ export default {
}
.hide-when-closed {
display: none;
display: flex;
flex-direction: column;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity 0.15s ease;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
max-height: none;
opacity: 1;
overflow: visible;
}
}
}