Merge pull request #3261 from Safelite/rslmerge/2026.07.16-to-develop

Rslmerge/2026.07.16 to develop
This commit is contained in:
mvalaiyapathi 2026-07-10 12:16:17 -04:00 committed by GitHub
commit c288bf4e3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 624 additions and 57 deletions

View file

@ -32,7 +32,7 @@ const errorMessages = {
DATE_REQUIRED: "Please select a date",
PHONE_REQUIRED: "Please enter your phone number",
PHONE_FORMAT: "Phone number must be 10 digits",
SMS_CONSENT_REQUIRED: "Please select at least one consent option",
SMS_CONSENT_REQUIRED: "Please select checkbox to receive text messages",
YEAR_REQUIRED: "Please select your vehicle year",
MAKE_REQUIRED: "Please select your vehicle make",
MODEL_REQUIRED: "Please select your vehicle model",

View file

@ -52,6 +52,12 @@ const experimentSettings = {
// Claim and Coverage for 2.0
ENABLE_NON_CLAIM_AND_COVERAGE_FLOW: "EnableNonC&CFlow",
NON_CLAIM_AND_COVERAGE_TEST_PARENT_ACCOUNT: "NonC&CTestParentAccount",
// Service Package Name
SHOW_SERVICE_PACKAGE_NAME_TEST: "ShowServicePackageNameTest",
TIER_ONE: "TierOne",
TIER_TWO: "TierTwo",
TIER_THREE: "TierThree",
};
const experimentTriggers = {

View file

@ -244,10 +244,8 @@ describe("Question Chain component", () => {
test("should return answer object if returnedAnswer is a final matching answer", async () => {
//Arrange
const { wrapper } = setupMocks({});
const testReturnedAnswer = "1|answer|DB10840|No";
await wrapper.setData({
questions: [
const { wrapper } = setupMocks({
questionDataProp: [
{
questionSequence: 1,
questionText: "Question here?",
@ -256,17 +254,21 @@ describe("Question Chain component", () => {
answerResult: "DB09410",
answerText: "Yes",
nextQuestionSequence: null,
problemQuestionId: 9529,
},
{
answerResult: "DB10840",
answerText: "No",
nextQuestionSequence: null,
problemQuestionId: 9531,
},
],
},
],
});
const testReturnedAnswer = "1|answer|DB10840|No";
await wrapper.setProps({ index: 0 });
await nextTick();
//Act
const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
@ -274,16 +276,52 @@ describe("Question Chain component", () => {
//Assert
expect(result).toMatchObject({
answerResult: "DB10840",
answeredQuestions: [
{ questionNum: 1, questionText: "Question here?", selectedAnswerText: "No" },
{
questionNum: 1,
questionText: "Does only your center sliding piece need to be replaced?",
selectedAnswerText: "No",
},
],
problemQuestionId: 9531,
index: 0,
});
expect(result.answeredQuestions).toEqual([
{
questionText: "Question here?",
selectedAnswer: "1|answer|DB10840|No",
selectedAnswerText: "No",
questionNum: 1,
problemQuestionId: 9531,
},
]);
});
test("should include problemQuestionId from parts-or-questions answer on created", async () => {
//Arrange
const { wrapper } = setupMocks({
questionDataProp: [
{
questionSequence: 1,
questionText:
"Does the rubber seal around your windshield have a chrome strip running through it?",
answers: [
{
answerResult: "WCR 848",
answerText: "Yes",
nextQuestionSequence: null,
problemQuestionId: 9531,
},
{
answerResult: "WCR 848",
answerText: "No",
nextQuestionSequence: null,
problemQuestionId: 9529,
},
],
},
],
});
//Act
await nextTick();
//Assert
expect(wrapper.vm.questions[0].answers[0].problemQuestionId).toBe(9531);
expect(wrapper.vm.questions[0].answers[1].problemQuestionId).toBe(9529);
});
});
});

View file

@ -60,6 +60,7 @@ export default {
: q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText,
nextQuestionSequence: a.nextQuestionSequence,
answerResult: a.answerResult,
problemQuestionId: a.problemQuestionId,
questionSequence: q.questionSequence,
questionType: a.nextQuestionSequence ? "nextQuestion" : "answer",
};
@ -81,6 +82,17 @@ export default {
}
},
methods: {
getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) {
if (!question?.answers || !selectedAnswer) {
return null;
}
const matchedAnswer = question.answers.find(
(answer) => answer.value === selectedAnswer
);
return matchedAnswer?.problemQuestionId ?? null;
},
handleAnswer(question, returnedAnswer) {
/*
returnedAnswer example format:
@ -126,6 +138,10 @@ export default {
selectedAnswer: q.answerSelected,
selectedAnswerText: q.answerSelected.split("|")[3],
questionNum: q.questionSequence,
problemQuestionId: this.getProblemQuestionIdFromSelectedAnswer(
q,
q.answerSelected
),
});
}
});
@ -144,10 +160,17 @@ export default {
} else {
// reset current question index (removes .current-question class)
this.currentQuestionNum = 0; // reset count
const answeredQuestion = this.questions.find(
(q) => q.questionSequence === questionNum
);
// return an object with the part answer, all the answered questions, and the part index
return {
answerResult: questionAnswer,
problemQuestionId: this.getProblemQuestionIdFromSelectedAnswer(
answeredQuestion,
returnedAnswer
),
answeredQuestions: answeredQuestions,
index: this.index,
};

View file

@ -5,8 +5,24 @@ import cart from "@/fmg-components/cart/cart.vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { packageNames } from "@/constants/package-names";
import store from "@/store";
jest.mock("@/mixins/experiment-mixin.js", () => ({
methods: {
getSettingValue(settingName) {
return false;
},
hasSetting(settingName) {
return false;
},
hasSettingEqualTo(settingName, settingValue) {
return false;
},
},
}));
global.$logger = {
logInformation: jest.fn(),
logWarning: jest.fn(),
@ -354,9 +370,73 @@ describe("cart.vue", () => {
expect(found).toBe(true);
});
});
describe("servicePackageTitle", () => {
const servicePackageCmsContent = {
ServicePackageTitle: {
Answers: [
{
Name: packageNames.TIER_ONE,
SubWidgetName: "EconomyServiceTitle",
},
{
Name: packageNames.TIER_TWO,
SubWidgetName: "StandardServiceTitle",
},
{
Name: packageNames.TIER_THREE,
SubWidgetName: "PremiumServiceTitle",
},
],
},
EconomyServiceTitle: {
Text: "Glass service only",
},
};
const defaultServicePackageProps = {
servicePackageOptionsCmsName: "ServicePackageTitle",
damage: {
glassToReplace: [{ glassLocation: "Windshield" }],
isRepair: false,
},
modelValue: {
glassParts: [],
supportingItems: [],
vaps: [],
promos: [],
},
availableVaps: [],
};
test("uses CMS widget name when experiment is not active", () => {
const { wrapper } = setupMocks({
props: defaultServicePackageProps,
cmsContent: servicePackageCmsContent,
});
expect(wrapper.vm.servicePackageTitleWidget).toBe("EconomyServiceTitle");
expect(wrapper.vm.servicePackageTitleText).toBeNull();
});
test("uses experiment display name when ServicePackageNameTest is active", () => {
const getExperimentPackageLabelSpy = jest
.spyOn(cart.methods, "getExperimentPackageLabel")
.mockReturnValue("Essential");
const { wrapper } = setupMocks({
props: defaultServicePackageProps,
cmsContent: servicePackageCmsContent,
});
expect(wrapper.vm.servicePackageTitleText).toBe("Essential");
getExperimentPackageLabelSpy.mockRestore();
});
});
});
function setupMocks({ options, props }) {
function setupMocks({ options, props, cmsContent }) {
const mountOptions = getMountOptions({
...options,
});
@ -365,7 +445,9 @@ function setupMocks({ options, props }) {
methods: {
getTierOnePackagePrice: jest.fn(),
filterOutFees: jest.fn(),
getCmsContent: jest.fn(),
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName];
}),
},
};

View file

@ -34,7 +34,9 @@
<!-- Service Type -->
<div class="service-type">
<textBlock :cmsWidgetName="servicePackageTitleWidget" />
<textBlock
:cmsWidgetName="servicePackageTitleWidget"
:customText="servicePackageTitleText" />
<span>
{{ getLineItemAmount(packagePrice) }}
</span>
@ -193,6 +195,7 @@ import { formatToUSDollar } from "@/helpers/cms-content-helper";
// Constants
import { partTypeStrings } from "@/constants/part-type-strings";
import { packageNames } from "@/constants/package-names";
import { cartItemCategories } from "@/constants/cart-item-categories";
import { cartItemTypes } from "@/constants/cart-item-types";
import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance";
@ -267,6 +270,24 @@ export default {
return subTotal;
},
getExperimentPackageLabel(tierName) {
if (
!experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST,
"true"
)
) {
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;
},
getQuotePageDiscount() {
const quotePageDiscountLineItems = this.quotePageDiscountCartItem;
let subTotal = 0;
@ -547,6 +568,9 @@ export default {
return currentPackage.SubWidgetName;
},
servicePackageTitleText() {
return this.getExperimentPackageLabel(this.packageLevel);
},
packageLevel() {
const tier = getHighestFullySatisfiedTier(
this.glassToReplace,

View file

@ -45,14 +45,14 @@ describe("save-progress-popup-question ", () => {
});
describe("contact method tabs", () => {
test("should default to the phone tab", () => {
test("should default to the email tab", () => {
const { wrapper } = setupMocks({
props: {
modalWidgetName: "SaveProgressPopupWidget",
},
});
expect(wrapper.vm.isPhoneTabSelected).toBe(true);
expect(wrapper.vm.isPhoneTabSelected).toBe(false);
});
test("should use tab labels from phone and email specific CMS widgets", () => {
@ -83,21 +83,24 @@ describe("save-progress-popup-question ", () => {
},
});
expect(wrapper.vm.modalDisclaimerText).toBe("Phone disclaimer");
expect(wrapper.vm.modalDisclaimerText).toBe("Email disclaimer");
wrapper.vm.selectContactMethod("EmailAnswer");
wrapper.vm.selectContactMethod("PhoneAnswer");
await wrapper.vm.$nextTick();
expect(wrapper.vm.modalDisclaimerText).toBe("Email disclaimer");
expect(wrapper.vm.modalDisclaimerText).toBe("Phone disclaimer");
});
test("should use phone question CMS widget on the phone tab", () => {
test("should use phone question CMS widget on the phone tab", async () => {
const { wrapper } = setupMocks({
props: {
modalWidgetName: "SaveProgressPopupWidget",
},
});
wrapper.vm.selectContactMethod("PhoneAnswer");
await wrapper.vm.$nextTick();
expect(wrapper.vm.phoneQuestionWidgetName).toBe("SaveProgressPopupPhoneQuestionWidget");
});
@ -108,15 +111,26 @@ describe("save-progress-popup-question ", () => {
},
});
wrapper.vm.selectContactMethod("EmailAnswer");
await wrapper.vm.$nextTick();
expect(wrapper.vm.contactMethod).toBe("EmailAnswer");
expect(wrapper.vm.isPhoneTabSelected).toBe(false);
expect(wrapper.vm.userInput).toBe("");
expect(wrapper.vm.emailQuestionWidgetName).toBe("SaveProgressPopupEmailQuestionWidget");
});
test("should show phone content when the phone tab is selected", async () => {
const { wrapper } = setupMocks({
props: {
modalWidgetName: "SaveProgressPopupWidget",
},
});
wrapper.vm.selectContactMethod("PhoneAnswer");
await wrapper.vm.$nextTick();
expect(wrapper.vm.contactMethod).toBe("PhoneAnswer");
expect(wrapper.vm.isPhoneTabSelected).toBe(true);
expect(wrapper.vm.userInput).toBe("");
});
test("should reset sms consent when switching tabs", async () => {
const { wrapper } = setupMocks({
props: {
@ -126,7 +140,7 @@ describe("save-progress-popup-question ", () => {
wrapper.vm.smsConsent = { transactional: true, marketing: true };
wrapper.vm.showConsentErrors = true;
wrapper.vm.selectContactMethod("EmailAnswer");
wrapper.vm.selectContactMethod("PhoneAnswer");
await wrapper.vm.$nextTick();
expect(wrapper.vm.smsConsent).toEqual({ transactional: false, marketing: false });
@ -142,6 +156,7 @@ describe("save-progress-popup-question ", () => {
},
});
wrapper.vm.selectContactMethod("PhoneAnswer");
wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true });
const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle");
@ -158,6 +173,7 @@ describe("save-progress-popup-question ", () => {
},
});
wrapper.vm.selectContactMethod("PhoneAnswer");
wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: false });
const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle");
@ -179,6 +195,7 @@ describe("save-progress-popup-question ", () => {
await flushPromises();
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
wrapper.vm.selectContactMethod("PhoneAnswer");
wrapper.vm.userInput = "555-123-4567";
wrapper.vm.smsConsent = { transactional: true, marketing: false };
wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true });
@ -203,6 +220,7 @@ describe("save-progress-popup-question ", () => {
});
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
wrapper.vm.selectContactMethod("PhoneAnswer");
wrapper.vm.userInput = "555-123-4567";
wrapper.vm.smsConsent = { transactional: true, marketing: false };
@ -210,7 +228,7 @@ describe("save-progress-popup-question ", () => {
expect(dispatchStoreAction).toHaveBeenCalledWith(
storeActions.SAVE_PHONE_NUMBER,
"555-123-4567",
"5551234567",
false
);
expect(dispatchStoreAction).not.toHaveBeenCalledWith(
@ -238,7 +256,6 @@ describe("save-progress-popup-question ", () => {
});
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
wrapper.vm.selectContactMethod("EmailAnswer");
wrapper.vm.userInput = "test@example.com";
await wrapper.vm.saveProgress();
@ -267,6 +284,7 @@ describe("save-progress-popup-question ", () => {
});
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
wrapper.vm.selectContactMethod("PhoneAnswer");
wrapper.vm.userInput = "555-123-4567";
wrapper.vm.smsConsent = { transactional: true, marketing: false };
@ -289,7 +307,6 @@ describe("save-progress-popup-question ", () => {
});
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
wrapper.vm.selectContactMethod("EmailAnswer");
wrapper.vm.userInput = "test@example.com";
await wrapper.vm.saveProgress();

View file

@ -118,7 +118,7 @@ export default {
data() {
return {
userInput: "",
contactMethod: saveProgressPopupContactMethods.PHONE,
contactMethod: saveProgressPopupContactMethods.EMAIL,
smsConsent: defaultSaveProgressSmsConsent(),
smsConsentCopy: getSaveProgressSmsConsentFallbackCopy(),
isProgressSaved: false,

View file

@ -7,15 +7,14 @@ import {
import { errorMessages } from "@/constants/error-messages";
describe("save-progress-popup-sms-consent-question", () => {
it("should render two consent checkboxes with default copy", () => {
it("should render the transactional consent checkbox with default copy", () => {
const wrapper = shallowMount(saveProgressPopupSmsConsentQuestion);
const fallbackCopy = getSaveProgressSmsConsentFallbackCopy();
const checkboxes = wrapper.findAllComponents({ name: "checkboxQuestion" });
expect(checkboxes).toHaveLength(2);
expect(checkboxes).toHaveLength(1);
expect(checkboxes.at(0).props("labelText")).toBe(fallbackCopy.transactional);
expect(checkboxes.at(1).props("labelText")).toBe(fallbackCopy.marketing);
});
it("should emit updated consent when a checkbox changes", async () => {
@ -47,8 +46,8 @@ describe("save-progress-popup-sms-consent-question", () => {
const checkboxes = wrapper.findAllComponents({ name: "checkboxQuestion" });
expect(checkboxes).toHaveLength(1);
expect(checkboxes.at(0).props("labelText")).toBe("API transactional copy");
expect(checkboxes.at(1).props("labelText")).toBe("API marketing copy");
});
it("should disable consent checkboxes when isDisabled is true", () => {
@ -61,7 +60,6 @@ describe("save-progress-popup-sms-consent-question", () => {
const checkboxes = wrapper.findAllComponents({ name: "checkboxQuestion" });
expect(checkboxes.at(0).props("isDisabled")).toBe(true);
expect(checkboxes.at(1).props("isDisabled")).toBe(true);
expect(wrapper.find("fieldset").attributes("disabled")).toBe("");
});
@ -76,7 +74,6 @@ describe("save-progress-popup-sms-consent-question", () => {
const checkboxes = wrapper.findAllComponents({ name: "checkboxQuestion" });
expect(checkboxes.at(0).props("hasError")).toBe(false);
expect(checkboxes.at(1).props("hasError")).toBe(false);
expect(wrapper.text()).not.toContain(errorMessages.SMS_CONSENT_REQUIRED);
});
@ -94,7 +91,6 @@ describe("save-progress-popup-sms-consent-question", () => {
const checkboxes = wrapper.findAllComponents({ name: "checkboxQuestion" });
expect(checkboxes.at(0).props("hasError")).toBe(true);
expect(checkboxes.at(1).props("hasError")).toBe(true);
});
it("should clear consent errors when a selection is made while showConsentErrors is true", async () => {
@ -111,6 +107,5 @@ describe("save-progress-popup-sms-consent-question", () => {
const checkboxes = wrapper.findAllComponents({ name: "checkboxQuestion" });
expect(checkboxes.at(0).props("hasError")).toBe(false);
expect(checkboxes.at(1).props("hasError")).toBe(false);
});
});

View file

@ -9,12 +9,13 @@
:labelText="consentCopy.transactional"
:hasError="showConsentError"
:isDisabled="isDisabled" />
<!-- Temporarily hiding marketing consent checkbox. Will be re-added in a future release.
<checkboxQuestion
v-model="marketingConsent"
checkboxName="saveProgressMarketingConsent"
:labelText="consentCopy.marketing"
:hasError="showConsentError"
:isDisabled="isDisabled" />
:isDisabled="isDisabled" /> -->
<span
v-if="showConsentError"
class="save-progress-popup-sms-consent__error d-inline-flex small mt-1"

View file

@ -87,18 +87,36 @@ export default {
}).then(
(response) => {
if (logApiCall) {
let additionalEventData = "";
if (additionalSuccessEventDataHandler) {
additionalEventData = "_" + additionalSuccessEventDataHandler(response);
}
const endpointWithoutParams =
analyticsMixIn.methods.removeParamsFromEndpoint(endpoint);
const gaAction = `${pageNameToLog}_${endpointWithoutParams}`;
if (additionalSuccessEventDataHandler) {
const handlerResult = additionalSuccessEventDataHandler(response);
const additionalEntries = Array.isArray(handlerResult)
? handlerResult
: [handlerResult];
additionalEntries.forEach((entry) => {
if (entry === undefined || entry === null || entry === "") {
return;
}
analyticsMixIn.methods.pushEventToGA(
GaCategories.API_RESPONSE,
`${pageNameToLog}_${endpointWithoutParams}`,
`${GaLabels.SUCCESS}${additionalEventData}`,
gaAction,
`${GaLabels.SUCCESS}_${entry}`,
true
);
});
} else {
analyticsMixIn.methods.pushEventToGA(
GaCategories.API_RESPONSE,
gaAction,
GaLabels.SUCCESS,
true
);
}
}
return resolve(response);

View file

@ -30,6 +30,33 @@ it("Global Methods - Call Http Client - Should Resolve Promise", () => {
});
});
it("Global Methods - Call Http Client - Should log multiple success event entries", async () => {
const endpoint = "https://mock.safelite.com";
const httpArgs = setupMocksForHttpClient({ endpoint: endpoint });
httpArgs.additionalSuccessEventDataHandler = () => [
"Email provided: true",
"Phone provided: false",
];
analyticsMixIn.methods.pushEventToGA = jest.fn();
analyticsMixIn.methods.removeParamsFromEndpoint = jest.fn((url) => url);
await globalMethods.callHttpClient(httpArgs);
expect(analyticsMixIn.methods.pushEventToGA).toHaveBeenCalledTimes(2);
expect(analyticsMixIn.methods.pushEventToGA).toHaveBeenCalledWith(
"Api_Response",
expect.any(String),
"Success_Email provided: true",
true
);
expect(analyticsMixIn.methods.pushEventToGA).toHaveBeenCalledWith(
"Api_Response",
expect.any(String),
"Success_Phone provided: false",
true
);
});
it("Global Methods - Call Http Client - Should Reject Promise", () => {
//Arrange
const endpoint = "https://mock.safelite.com";

View file

@ -10,12 +10,24 @@ export function isPhoneContactMethod(contactMethod) {
return contactMethod === saveProgressPopupContactMethods.PHONE;
}
export function normalizePhoneNumberForStore(phoneNumber) {
if (!phoneNumber) {
return phoneNumber;
}
return String(phoneNumber).replace(/\D/g, "");
}
export async function saveProgressPopupContactToStore(
dispatchStoreAction,
{ contactMethod, userInput, smsConsent, pageName }
) {
if (isPhoneContactMethod(contactMethod)) {
await dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, userInput, false);
await dispatchStoreAction(
storeActions.SAVE_PHONE_NUMBER,
normalizePhoneNumberForStore(userInput),
false
);
await dispatchStoreAction(storeActions.SAVE_IS_SMS_OPT_IN, smsConsent.transactional, false);
const existingPageData = store.getters.pageData(pageName) ?? {};

View file

@ -2,6 +2,7 @@ import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import {
isPhoneContactMethod,
normalizePhoneNumberForStore,
saveProgressPopupContactMethods,
saveProgressPopupContactToStore,
} from "./save-progress-popup-contact-helper";
@ -28,6 +29,17 @@ describe("save-progress-popup-contact-helper", () => {
});
});
describe("normalizePhoneNumberForStore", () => {
it("should strip non-digit characters from a formatted phone number", () => {
expect(normalizePhoneNumberForStore("555-123-4567")).toBe("5551234567");
});
it("should return empty values unchanged", () => {
expect(normalizePhoneNumberForStore("")).toBe("");
expect(normalizePhoneNumberForStore(null)).toBe(null);
});
});
describe("saveProgressPopupContactToStore", () => {
it("should save phone number and persist sms consent on phone tab", async () => {
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
@ -42,7 +54,7 @@ describe("save-progress-popup-contact-helper", () => {
expect(dispatchStoreAction).toHaveBeenCalledWith(
storeActions.SAVE_PHONE_NUMBER,
"555-123-4567",
"5551234567",
false
);
expect(dispatchStoreAction).not.toHaveBeenCalledWith(

View file

@ -1,12 +1,20 @@
import { partTypeStrings } from "@/constants/part-type-strings";
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
import { packageNames, externalParamPackageLabels } from "@/constants/package-names";
import { experimentSettings } from "@/constants/experiments";
import baseMixin from "@/mixins/base-mixin.js";
import { getItemsWithoutRecalParts } from "@/helpers/recal-helper";
import {
getPromosThatMatchLineItemsOnOrder,
removeVapsPromosFromPromoArray,
} from "@/helpers/promotions-helper";
import * as experimentHelper from "@/helpers/experiment-helper";
const tierExperimentSettingKeys = {
[packageNames.TIER_ONE]: experimentSettings.TIER_ONE,
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO,
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
};
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
@ -267,6 +275,27 @@ export function getPackageLabel(packageName) {
}
}
export function isServicePackageNameTestActive(experimentSettingsMap) {
return experimentHelper.hasSettingEqualTo(
experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST,
"true",
experimentSettingsMap
);
}
export function getExperimentPackageDisplayName(tierName, experimentSettingsMap) {
if (!isServicePackageNameTestActive(experimentSettingsMap)) {
return null;
}
const settingKey = tierExperimentSettingKeys[tierName];
if (!settingKey) {
return null;
}
return experimentHelper.getSettingValue(settingKey, experimentSettingsMap);
}
function maxTier(tierA, tierB) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return packageNames.TIER_THREE;

View file

@ -966,6 +966,68 @@ describe("service-package-helper.js", () => {
expect(result).toEqual(packageNames.TIER_ONE);
});
});
describe("getExperimentPackageDisplayName", () => {
const { experimentSettings } = require("@/constants/experiments");
it("Returns null when the experiment is not active", () => {
const result = servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_ONE,
{}
);
expect(result).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.TIER_ONE]: "Essential",
[experimentSettings.TIER_TWO]: "Plus",
[experimentSettings.TIER_THREE]: "Full Service",
};
expect(
servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_ONE,
experimentSettingsMap
)
).toBe("Essential");
expect(
servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_TWO,
experimentSettingsMap
)
).toBe("Plus");
expect(
servicePackageHelper.getExperimentPackageDisplayName(
packageNames.TIER_THREE,
experimentSettingsMap
)
).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",
});
expect(result).toBeNull();
});
});
});
// Constants

View file

@ -320,6 +320,7 @@ describe("partQuestions.vue...", () => {
glassName: "Single",
isSuppressedPart: undefined,
result: "FW04848",
problemQuestionId: null,
},
],
false

View file

@ -176,6 +176,7 @@ export default {
glassLocation: glass.glassLocation,
glassName: glass.glassName,
result: (glass.answerData && glass.answerData.answerResult) || "",
problemQuestionId: glass.answerData?.problemQuestionId ?? null,
answeredQuestions: glass.answerData?.answeredQuestions,
isSuppressedPart: glass.isSuppressedPart,
};

View file

@ -817,6 +817,79 @@ describe("service-package-question.vue, matching business rules for package disp
expectedModifiedAnswers[packageNameKey].tierThree
);
});
it("should use experiment package names when ServicePackageNameTest is active", () => {
const getExperimentPackageLabelSpy = jest
.spyOn(servicePackageQuestion.methods, "getExperimentPackageLabel")
.mockImplementation((tierName) => {
const experimentPackageNames = {
[packageNames.TIER_ONE]: "Essential",
[packageNames.TIER_TWO]: "Plus",
[packageNames.TIER_THREE]: "Full Service",
};
return experimentPackageNames[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].buttonLabel).toBe("Essential");
expect(wrapper.vm.servicePackageAnswers[1].buttonLabel).toBe("Plus");
expect(wrapper.vm.servicePackageAnswers[2].buttonLabel).toBe("Full Service");
getExperimentPackageLabelSpy.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]);
mockProps.availableLineItems.push(driverFrontWiperLineItem);
mockProps.availableLineItems.push(passengerFrontWiperLineItem);
const wrapper = setupMocks({
mountOptionsMockData: {
store: {
getters: {
order: {
damage: {
isRepair: true,
glassToReplace: [],
},
},
hasAnyNonWindshieldGlassParts: false,
},
},
},
});
runPackageAnswerExpectStatements(
wrapper.vm.servicePackageAnswers[0],
expectedModifiedAnswers[packageNameKey].tierOne
);
runPackageAnswerExpectStatements(
wrapper.vm.servicePackageAnswers[1],
expectedModifiedAnswers[packageNameKey].tierTwo
);
runPackageAnswerExpectStatements(
wrapper.vm.servicePackageAnswers[2],
expectedModifiedAnswers[packageNameKey].tierThree
);
});
});
function runPackageAnswerExpectStatements(servicePackageAnswer, expectedOutput) {

View file

@ -141,7 +141,7 @@ export default {
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
value: answer.Name,
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
buttonLabel: this.getPackageButtonLabel(answer.Name, answer.SubWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName),
//The package supports service-package-discount if it has the text value in CMS
@ -259,6 +259,29 @@ export default {
}
return this.servicePackageRadio;
},
getPackageButtonLabel(tierName, subWidgetName) {
return (
this.getExperimentPackageLabel(tierName) ?? this.getHeaderTextFromCms(subWidgetName)
);
},
getExperimentPackageLabel(tierName) {
if (
!experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST,
"true"
)
) {
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;
},
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "HeaderText");
},

View file

@ -141,6 +141,7 @@ export default {
// add answerData to current glass
glass.answerData = {
answerResult: answerResult,
problemQuestionId: answeredGlass.problemQuestionId ?? null,
answeredQuestions: answeredGlass.answeredQuestions,
};
}
@ -320,6 +321,7 @@ export default {
questionText: question.questionText,
selectedAnswerText: matchedAnswer.answerText,
questionNum: question.questionSequence,
problemQuestionId: matchedAnswer.problemQuestionId ?? null,
suppressThisQuestion: question.suppressThisQuestion,
};
// set the answerData (used as indicator that it has been already answered)
@ -327,6 +329,7 @@ export default {
answerResult: matchedAnswer.nextQuestionSequence
? matchedAnswer.nextQuestionSequence
: matchedAnswer.answerResult,
problemQuestionId: matchedAnswer.problemQuestionId ?? null,
answeredQuestions: [answeredQuestionObj],
};
@ -346,6 +349,7 @@ export default {
// set final answer data for the current answered glass part
self.questionsData[answer.index].answerData = {
answerResult: answer.answerResult,
problemQuestionId: answer.problemQuestionId ?? null,
answeredQuestions: answer.answeredQuestions,
};

View file

@ -2941,8 +2941,10 @@ export const actions = {
},
logApiCall: true,
pageNameToLog: pageNameToLog,
additionalSuccessEventDataHandler: (response) =>
additionalSuccessEventDataHandler: (response) => [
"Email provided: " + (order.customer.emailAddress ? "true" : "false"),
"Phone provided: " + (order.customer.phoneNumber ? "true" : "false"),
],
});
},
@ -4318,11 +4320,17 @@ function convertResultsForApi(resultsArray) {
if (!resultsArray) return [];
const converted = [];
resultsArray.forEach((answer) => {
converted.push({
const convertedAnswer = {
location: answer.glassLocation,
name: answer.glassName,
result: answer.result,
});
};
if (answer.problemQuestionId != null) {
convertedAnswer.problemQuestionId = answer.problemQuestionId;
}
converted.push(convertedAnswer);
});
return converted;
}

View file

@ -4470,4 +4470,115 @@ describe("isVinOptionalVehicle", () => {
expect(vinOptionalResult).toEqual(expectedVinSkip);
}
);
it("getParts action, should include problemQuestionId in answerResults payload", async () => {
const context = {
getters: {
vehicle: { carId: "CR00065283", vin: "SAJWA6A73F8K13235" },
damage: {
glassToReplace: [{ glassLocation: "Windshield", glassName: "Single" }],
partQuestionAnswers: [
{
glassLocation: "Windshield",
glassName: "Single",
result: "FW04848",
problemQuestionId: 38560,
answeredQuestions: [
{
questionText:
"Is your vehicle equipped with the Panoramic Sunroof which can be identified by having a glass panel over the rear seats?",
selectedAnswer: "1|nextQuestion|2|Yes",
selectedAnswerText: "Yes",
questionNum: 1,
problemQuestionId: 38557,
},
{
questionText:
"Is your vehicle equipped with a heated windshield that melts snow and ice from underneath the windshield wiper blades?",
selectedAnswer: "2|answer|FW04848|Yes",
selectedAnswerText: "Yes",
questionNum: 2,
problemQuestionId: 38560,
},
],
},
],
},
payment: { parentAccountNumber: "167132" },
},
state: {
order: {
serviceLocation: { zipCode: "43085", appointmentType: null },
referralSequenceNumber: "11330779",
},
},
};
globalMethods.callHttpClient.mockImplementation(({ payload }) => {
return Promise.resolve({ data: { glassPieceParts: [] }, payload });
});
await actions.getParts(context, { pageNameToLog: "part-questions" });
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(
expect.objectContaining({
endpoint: endpoints.GetParts.url,
payload: expect.objectContaining({
answerResults: [
{
location: "Windshield",
name: "Single",
result: "FW04848",
problemQuestionId: 38560,
},
],
}),
})
);
});
it("getParts action, should omit problemQuestionId when not saved on part question answer", async () => {
const context = {
getters: {
vehicle: { carId: "CR00065283", vin: "SAJWA6A73F8K13235" },
damage: {
glassToReplace: [{ glassLocation: "Windshield", glassName: "Single" }],
partQuestionAnswers: [
{
glassLocation: "Windshield",
glassName: "Single",
result: "FW04848",
},
],
},
payment: { parentAccountNumber: "167132" },
},
state: {
order: {
serviceLocation: { zipCode: "43085", appointmentType: null },
referralSequenceNumber: "11330779",
},
},
};
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: { glassPieceParts: [] } });
});
await actions.getParts(context, { pageNameToLog: "part-questions" });
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(
expect.objectContaining({
payload: expect.objectContaining({
answerResults: [
{
location: "Windshield",
name: "Single",
result: "FW04848",
},
],
}),
})
);
});
});