Merge branch 'release/2025.01.30' into rlsmerge/2025.01.30-to-develop

This commit is contained in:
CarlNation 2025-01-27 13:14:49 -05:00
commit 0e7ad4bbdd
6 changed files with 385 additions and 58 deletions

View file

@ -27,7 +27,6 @@ module.exports = {
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
"!src/layouts/return-user/*.vue", // Temp test exclusion while in development
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

View file

@ -45,6 +45,9 @@
<script>
import textLink from "@/ux-components/text-link/text-link";
import buttonMain from "@/ux-components/button-main/button-main";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import analyticsMixin from "@/mixins/analytics-mixin";
import router from "@/router/index.js";
export default {
name: "navbar",
@ -94,10 +97,28 @@ export default {
document.onkeydown = function (e) {
return false;
};
this.$emit("ForwardClicked");
// check session expired and initSession to recreate cookies
if (analyticsMixin.methods.sessionExpired()) {
this.routeReturnUser();
} else {
this.$emit("ForwardClicked");
}
},
linkClick() {
this.$emit("BackClicked");
// check session expired and initSession to recreate cookies
if (analyticsMixin.methods.sessionExpired()) {
this.routeReturnUser();
} else {
this.$emit("BackClicked");
}
},
routeReturnUser() {
analyticsMixin.methods.initSession();
router.push({
path: "/",
query: { fmgPage: fmgPageValues.RETURN_USER },
});
},
},
};

View file

@ -0,0 +1,312 @@
// Components
import returnUser from "@/layouts/return-user/return-user.vue";
// Supporting Files
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { dispatchStoreAction } from "@/mixins/base-mixin.js";
import store from "@/store";
import router from "@/router";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import { Form } from "vee-validate";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { settleAllPromises } from "@/helpers/layout-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
import { experimentSettings } from "../../constants/experiments";
import { storeActions } from "@/constants/store-actions";
import buttonMain from "@/ux-components/button-main/button-main";
// Constants
// Setup global mocks
let mockStoreActionData = {};
let mockStoreData = {};
let mockExperimentSettings = {
experiments: [
{
universeName: "ConceptFunnel",
settings: {
SuppressVinCapture: false,
},
},
],
};
function resetMockStoreData() {
mockStoreData = {
vehicle: {
year: "2000",
make: "TestMake",
model: "TestModel",
style: "TestStyle",
carId: "TestID",
vin: null,
registration: {
licensePlate: null,
},
},
serviceLocation: {
address: null,
address2: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
appointmentType: null,
isVehicleProtected: null,
provider: {
providerNumber: null,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
},
},
techNotes: null,
},
customer: {
firstName: null,
lastName: null,
emailAddress: null,
phoneNumber: null,
isSmsOptIn: null,
},
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [{ glassLocation: "Windshield", glassName: "windshield" }],
partQuestionAnswers: null,
moldingQuestionAnswers: null,
capabilityQuestionAnswers: null,
dateOfLoss: null,
damageCause: null,
},
lineItems: {
glassParts: [
{
canSafeliteRecalibrate: true,
childParts: [
{
kitPrice: 0,
laborAmount: 23.55,
partNumber: "GGG FW4896",
salesTax: 1.77,
sellingPrice: 0,
},
],
color: "Green Tint",
description:
"solar, soundproofing, lane keep assist, lane departure warning system, w/adaptive cruise control",
id: "db22fd44-10dd-456f-979b-ff88cf68cca6",
kitPrice: 0,
laborAmount: 60,
partNumber: "FW04896GTYN",
partType: "WINDSHIELD",
recalibrationType: "STATIC",
requiresCapabilityQuestions: false,
requiresRecalibration: true,
salesTax: 63.86,
sellingPrice: 791.46,
},
],
supportingItems: null,
vaps: null,
serverData: null,
promos: null,
},
payment: {
isInsurance: null,
insuranceCoverage: {
isVerified: null,
coverageStatus: null,
coverageType: null,
coverageVerificationType: null,
},
parentAccountNumber: 0,
billToAccountNumber: null,
isPia: null,
piaType: null,
inactivePromos: null,
paypalToken: null,
nextGenSettledAmount: 0,
ccToken: {
subscriptionId: null,
expMonth: null,
expYear: null,
cardType: null,
billToPostalCode: null,
billToFirstName: null,
billToLastName: null,
referenceNumber: null,
authCode: null,
transactionId: null,
transReferenceNumber: null,
lastFour: null,
},
},
policy: {
currentDeductible: 0,
policyNumber: null,
isItac: false,
additionalAuthFlag: null,
isNoComp: false,
insuranceCompanyName: null,
},
schedule: {
date: null,
startTime: null,
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
externalParameterServiceZip: {
zipCode: null,
emailAddress: null,
},
externalParameterState: { isExternalParameter: false },
};
}
function applyMockStoreDataToGetters() {
store.getters = {
vehicle: mockStoreData.vehicle,
};
store.state.order = mockStoreData;
store.state.applicationUser.experiments = mockExperimentSettings;
}
async function mockDispatchStoreAction(actionName) {
return mockStoreActionData[actionName];
}
jest.mock("@/mixins/base-mixin.js", () => ({
methods: {
dispatchStoreAction: jest.fn(),
dispatchStoreActionWithLogging: jest.fn().mockImplementation(mockDispatchStoreAction),
},
}));
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: () => Promise.resolve("content"),
}));
jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
saveSession: jest.fn(),
}));
jest.mock("@/helpers/heritage-integration/cookie-helper", () => ({
deleteFunnelCookie: jest.fn(),
getFunnelCookie: jest.fn(),
}));
router.navigateWithoutSaving = jest.fn();
router.navigateWithSaving = jest.fn();
// Tests
describe("return-user.vue", () => {
beforeEach(() => {
resetMockStoreData();
jest.clearAllMocks();
});
describe("Test prerequisites are valid and child components are rendered", () => {
test("expect pagePrerequisites are valid to be called", () => {
// Arrange
const wrapper = setupMocks({});
applyMockStoreDataToGetters();
// Act
const pagePrerequisitesSpy = jest.spyOn(wrapper.vm, "arePagePrerequisitesValid");
wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(pagePrerequisitesSpy).toBeCalled();
expect(getFunnelCookie).toHaveBeenCalled();
});
test("renders child components", () => {
const wrapper = setupMocks({});
expect(wrapper.findComponent(funnelHeader).exists()).toBe(true);
expect(wrapper.findComponent(funnelSubHeader).exists()).toBe(true);
expect(wrapper.findComponent(navbar).exists()).toBe(true);
});
});
describe("Navigation", () => {
test("Check forwardButtonAction is working", async () => {
// Arrange
const wrapper = setupMocks({});
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toBeCalledWith(
navigationScenarios.CLICKED_FORWARD,
wrapper.vm.$route
);
});
test("expect functions in startOver to be called", async () => {
//Arrange
const wrapper = setupMocks({});
// Act
const dispatchStoreActionSpy = jest.spyOn(wrapper.vm, "dispatchStoreAction");
await wrapper.vm.$nextTick();
await wrapper.vm.startOver();
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toBeCalledWith(
navigationScenarios.CLICKED_FORWARD,
wrapper.vm.$route
);
expect(dispatchStoreActionSpy).toHaveBeenCalledWith(storeActions.RESET_STATE);
expect(deleteFunnelCookie).toHaveBeenCalled();
});
});
});
function setupMocks({ customMountOptions }) {
const route = { query: { fmgPage: "return-user" }, params: {} };
baseMixin.methods.ResetExternalParamsAndHideModal = jest.fn();
const mountOptions = getMountOptions({
...customMountOptions,
route: route,
});
mountOptions.global.mocks["$store"] = store;
mountOptions.global.mocks["$router"] = router;
baseMixin.methods.isFormValid = jest.fn().mockReturnValue(true);
mountOptions["attachTo"] = document.body;
const wrapper = shallowMount(returnUser, mountOptions, {
stubs: {
Form,
funnelHeader,
funnelSubHeader,
navbar,
loadingModal: true,
buttonMain,
},
});
return wrapper;
}

View file

@ -241,57 +241,46 @@ export default {
this.displayMismatchStateAndZipAlert = false;
},
async setMobileLocation() {
if (
this.internalModel.addressQuestions.zipCode !==
this.modelValue.addressQuestions.zipCode
) {
this.resetAlerts();
// Validate the Zip Code
const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode,
this.resetAlerts();
// Validate the Zip Code
const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode,
"service-location"
);
if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true;
this.resetModalButtonStyle();
} else if (zipCodeData.state != this.internalModel.addressQuestions.state) {
this.displayMismatchStateAndZipAlert = true;
this.resetModalButtonStyle();
} else {
// retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart(
serviceZipCode,
"service-location"
);
if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true;
this.resetModalButtonStyle();
} else if (zipCodeData.state != this.internalModel.addressQuestions.state) {
this.displayMismatchStateAndZipAlert = true;
this.resetModalButtonStyle();
} else {
// retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart(
serviceZipCode,
"service-location"
);
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
const billToAccountNumber = await getBillToAccountNumber(
this.internalModel.zipCodeCtu
);
const billToAccountNumber = await getBillToAccountNumber(
this.internalModel.zipCodeCtu
);
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
this.$emit("updated-bill-to-account-number", billToAccountNumber);
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
this.$emit("updated-bill-to-account-number", billToAccountNumber);
// update the page level model
this.$emit("update:modelValue", this.internalModel);
//Page advance to Schedule page
this.$emit("mobileLocationSelected");
}
} else {
// Update the page level model
// update the page level model
this.$emit("update:modelValue", this.internalModel);
//Page advance to Schedule page

View file

@ -712,11 +712,8 @@ export default {
return !areAllSessionCookiesSet();
},
async validateSession() {
// The noSession function checks the cookies related to analytics logging(sid). it is not the funnel info cookie.
// The sid cookie for analytics will expire every 30 minutes and get recreated in initSession. If this happens
// and we also have the funnel cookie present, that indicates they have an existing session that is now expired
// so route them to the return user page.
// sessionExpired is true when one of the analytics cookies(sid, dxdev) has expired but we still have the funnelSessionInfo cookie
sessionExpired() {
const fromHeritage = getQuerystringParameter(queryStrings.FROM_HERITAGE) === "true";
const funnelCookieLastTouched = getFunnelCookie()?.LastTouched;
if (
@ -725,11 +722,15 @@ export default {
funnelCookieLastTouched !== null &&
funnelCookieLastTouched !== undefined
) {
await this.initSession();
router.push({
path: "/",
query: { fmgPage: fmgPageValues.RETURN_USER },
});
return true;
} else {
return false;
}
},
async validateSession() {
// do not init session if it is expired. let the click event on nav-bar handle session expired logic
if (this.sessionExpired()) {
return;
}

View file

@ -57,6 +57,11 @@ const routes = [
log(` --from.redirectedFrom:>${JSON.stringify(from.redirectedFrom)}<`, "");
await analyticsMixin.methods.validateSession();
// after session is validated, remove the fromHeritage querystring if it exists so session expiration works
if (to.query) {
delete to.query[queryStrings.FROM_HERITAGE];
}
if (getFunnelCookie()?.SuppressConceptFunnel) {
log(" --go to heritage suppressConceptFunnel: ");