Merge pull request #301 from Safelite/feature/CSR-99

CSR-99 / Some of CSR-98
This commit is contained in:
Frank Rua 2022-03-29 12:19:34 -04:00 committed by GitHub
commit 1993ba40ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1655 additions and 795 deletions

View file

@ -78,4 +78,5 @@ stages:
appDeployVariables:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
cfDistributionId: $(cfDistributionId)

6
src/.eslintrc.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
env: {
jest: true
},
//...
}

View file

@ -93,7 +93,7 @@ function setupMocks({
categoryValue = "CAR"
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.dispatch = jest.fn(() => {});
store.getters = { vehicle: { category: categoryValue, imageUrl: imageUrlValue } };
const mountOptions = getMountOptions({
store: {

View file

@ -1,6 +1,9 @@
const applicationConfig = {
CONSUMER_APIGATEWAY_URL: process.env.VUE_APP_CONSUMER_API_GATEWAY,
GOOGLE_PLACES_API_KEY: process.env.VUE_APP_GOOGLE_PLACES_API_KEY,
ANALYTICS_SESSION_TIMEOUT: 30,
SAVED_SESSION_TIMEOUT: 45
};
export { applicationConfig };

View file

@ -0,0 +1,6 @@
const cookieNames = {
CONCEPT_SESSION_INFO: "ConceptSessionInfo",
};
export { cookieNames };

View file

@ -47,6 +47,14 @@ const endpoints = {
url: "/parts/api/v1/parts/parts-or-questions",
method: "POST",
},
SaveOrder: {
url: "/order/api/v1/order/save",
method: "POST",
},
LoadOrder: {
url: "/order/api/v1/order/load",
method: "POST",
}
};
export { endpoints };

View file

@ -0,0 +1,7 @@
const queryStrings = {
FMG_PAGE: 'fmgPage',
START_TYPE: 'start_type'
};
export { queryStrings };

View file

@ -12,12 +12,17 @@ const storeActions = {
LOOKUP_VEHICLE_BY_YMMS: "lookupVehicleByYmms",
LOOKUP_VEHICLE_BY_VIN: "lookupVehicleByVin",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
SAVE_ORDER: "saveOrder",
LOAD_ORDER: "loadOrder",
SET_REFERRAL_INFORMATION: "setReferralInformation",
SET_LOAD_ORDER_DATA: "setLoadOrderData",
// DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies",
RESET_REGISTRATION_STATE_AND_DEPENDENCIES: "resetRegistrationAndDependencies",
RESET_PARTS_STATE_AND_DEPENDENCIES: "resetPartsAndDependencies",
RESET_STATE: "resetState",
};
export { storeActions };

View file

@ -15,6 +15,12 @@ const storeMutations = {
UPDATE_GLASS_TO_REPLACE: "updateGlassToReplace",
UPDATE_PARTS: "updateParts",
// ORDER MUTATIONS
UPDATE_REFERRAL_NUMBER: "updateReferralNumber",
UPDATE_REFERRAL_DATE: "updateReferralDate",
UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId",
UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber",
// EVENT BUS MUTATIONS
ADD_EVENT_TO_BUS: "addEventToBus",
REMOVE_EVENT_FROM_BUS: "removeEventFromBus",
@ -24,9 +30,11 @@ const storeMutations = {
RESET_DAMAGE_STATE: "resetDamageState",
RESET_REGISTRATION_STATE: "resetRegistrationState",
RESET_PARTS_STATE: "resetPartsState",
RESET_STATE: "resetState",
// OTHER MUTATIONS
UPDATE_PAGE_DATA: "updatePageData",
SET_LOAD_CONCEPT_SESSION_INFO: "setLoadOrderInformation"
};

View file

@ -1,8 +0,0 @@
const widgetNames = {
FUNNEL_SUB_HEADER_WIDGET: "FunnelSubHeaderWidget",
RADIO_QUESTION_WIDGET: "RadioQuestionWidget",
VEHICLE_BANNER_WIDGET: "VehicleBannerWidget",
FUNNEL_HEADER_WIDGET: "FunnelHeaderWidget",
};
export { widgetNames };

View file

@ -6,6 +6,7 @@ export default {
callHttpClient({ method, endpoint, payload }) {
return new Promise((resolve, reject) => {
const apiGatewayUrl = applicationConfig.CONSUMER_APIGATEWAY_URL;
const payloadAndAnalyticsData = Object.assign({}, payload, {
AppName: "FixMyGlass",
});

View file

@ -1,5 +1,4 @@
import { storeActions } from "@/constants/store-actions.js";
import { widgetNames } from "@/constants/widget-names.js";
import store from "@/store";
export function fetchCmsContentForPage(fmgPage) {

View file

@ -0,0 +1,78 @@
import { cookieNames } from "@/constants/cookie-names";
import store from "@/store";
/*
Will update the cookie if present, or create a new one if not.
*/
export function updateOrCreateConceptCookie() {
console.log("Updating cookie...", {
LastTouched: new Date().toUTCString(),
SavedQuoteTimeoutDate: store.getters.applicationUser.savedSessionTimeout,
DidHeritageFunnelUpdateLast: false,
ShouldResetState: false,
ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId,
});
// Create the cookie
document.cookie = `${cookieNames.CONCEPT_SESSION_INFO}={}; path=/`;
// Set up cookie with all the props.
setConceptCookieProperties({
LastTouched: new Date().toUTCString(),
SavedQuoteTimeoutDate: store.getters.applicationUser.savedSessionTimeout,
DidHeritageFunnelUpdateLast: false,
ShouldResetState: false,
ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId,
});
}
/*
Gets the current instance of the concept funnel cookie.
Returns null if cookie isn't valid JSON.
*/
export function getConceptCookie() {
const cookieJson = document.cookie
?.split("; ")
?.find(row => row.startsWith(`${cookieNames.CONCEPT_SESSION_INFO}=`))
?.split("=")[1];
try {
return JSON.parse(cookieJson);
} catch (error) {
return null;
}
}
/*
Removes concept cookie from browser.
*/
export function deleteConceptCookie() {
document.cookie = `${cookieNames.CONCEPT_SESSION_INFO}=; Max-Age=0; path=/; domain=${location.hostname}`;
}
/*
Used to set properties on the concept funnel cookie.
Takes an object with properties to set. Will overwrite existing properties.
*/
function setConceptCookieProperties(properties) {
if (typeof properties == "object") {
let cookie = getConceptCookie();
if (cookie !== null) {
Object.keys(properties).forEach(key => {
cookie[key] = properties[key];
});
const cookieValueJson = JSON.stringify(cookie);
document.cookie = `${cookieNames.CONCEPT_SESSION_INFO}=${cookieValueJson}; path=/`;
}
}
}

View file

@ -0,0 +1,104 @@
import {getConceptCookie} from "@/helpers/heritage-integration/cookie-helper.js";
import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper";
describe("cookies", () => {
afterEach(() => {
removeAllTestCookies();
})
describe("getConceptCookie method", () => {
test("gets correct value when cookie is present", () => {
// Arrange
const testReferralNumber = 1566818;
const testReferralDate = "2022-03-15T10:56:24.597";
const testReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060";
const testShouldResetState = false;
const testDidHeritageFunnelUpdateLast = true;
const testCookieValue = {
ReferralNumber: testReferralNumber,
ReferralDate: testReferralDate,
ReferralCorrelationId: testReferralCorrelationId,
ShouldResetState: testShouldResetState,
DidHeritageFunnelUpdateLast: testDidHeritageFunnelUpdateLast
}
setupCookies({ conceptCookieValue: JSON.stringify(testCookieValue) });
// Act
var result = getConceptCookie();
// Assert
expect(result).toEqual(testCookieValue);
expect(typeof result).toEqual("object");
expect(result.ReferralNumber).toEqual(testReferralNumber);
expect(result.ReferralDate).toEqual(testReferralDate);
expect(result.ReferralCorrelationId).toEqual(testReferralCorrelationId);
expect(result.ShouldResetState).toEqual(testShouldResetState);
expect(result.DidHeritageFunnelUpdateLast).toEqual(testDidHeritageFunnelUpdateLast);
});
test("returns empty object when value is empty object", () => {
// Arrange
const testCookieValue = {};
setupCookies({ conceptCookieValue: JSON.stringify(testCookieValue) });
// Act
var result = getConceptCookie();
// Assert
expect(result).toEqual(testCookieValue);
expect(typeof result).toEqual("object");
expect(Object.keys(result)).toHaveLength(0);
});
test("returns null when value is empty string", () => {
// Arrange
const testCookieValue = "";
setupCookies({ conceptCookieValue: testCookieValue });
// Act
var result = getConceptCookie();
// Assert
expect(result).toEqual(null);
});
test("returns null when concept cookie doesn't exist", () => {
// Arrange
setupCookies({ includeHeritageCookie: false });
// Act
var result = getConceptCookie();
// Assert
expect(result).toEqual(null);
});
test("gets correct cookie value", () => {
// Arrange
const testCookieValue = { test: "testValue" };
setupCookies({ conceptCookieValue: JSON.stringify(testCookieValue) });
// Act
const actualCookieValue = getConceptCookie();
// Assert
expect(actualCookieValue).toEqual(testCookieValue);
});
test("getCookieValue: Gets null cookie value", () => {
// Arrange
setupCookies({ includeHeritageCookie: false });
// Act
const actualCookieValue = getConceptCookie();
// Assert
expect(actualCookieValue).toBeNull();
});
})
})

View file

@ -0,0 +1,72 @@
import { queryStrings } from "@/constants/query-strings";
import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import {saveOrder} from "@/helpers/heritage-integration/order-helper.js";
import store from "@/store";
import router from "@/router";
/*
If the user has visited the concept funnel before this method will determine the bets place to
drop them so they don't start at the beginning again. This method will return 'heritage' if
the user has an existing order and they come back in from the Safelite.com CTA.
*/
export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) {
// If the user is coming in via the Safelite.Com CTA
if (toRoute.query[queryStrings.START_TYPE] === 'fmg') {
console.log("Start type is FMG... trying to figure out where to send them...");
// If they have an existing order, return 'heritage' for the page name.
if (existingHeritageOrder) {
console.log("Existing heritage order found, returning 'heritage' for page redirect...");
return 'heritage';
}
}
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites.
// This also works if a user has a 'fmg' start_type query string but no current order.
// That shouldn't happen, but it's possible.
const vehicleMakeComponent = (await lazyLoadComponent('vehicle-make')()).default;
const vehicleModelComponent = (await lazyLoadComponent('vehicle-model')()).default;
const vehicleStyleComponent = (await lazyLoadComponent('vehicle-style')()).default;
const vehicleDamageComponent = (await lazyLoadComponent('vehicle-damage')()).default;
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-year";
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-make";
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-model";
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-style";
} else if (store.getters.damage.isRepair == null || !store.getters.vehicle.carId) {
return 'vehicle-damage'
} else {
if (store.getters.vehicle.vin) {
return "vin-lookup";
} else {
return "estimate"
}
}
}
/*
Used to navigate to the heritage funnel with the correct query string and url.
*/
export async function navigateToHeritageFunnel() {
// Create the order (or save existing order) when navigating to Heritage Funnel.
await saveOrder();
router.navigateToExternalUrl(
externalUrls.HERITAGE_FUNNEL,
{
corid: store.getters.order.referralCorrelationId,
src: "concept-funnel",
}
);
}

View file

@ -0,0 +1,370 @@
import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import * as orderHelper from "@/helpers/heritage-integration/order-helper";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { storeActions } from "@/constants/store-actions";
import { setupMocksForJsFiles, getMockOrderInfo } from "@/helpers/unit-test-helper.js";
import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { queryStrings } from "@/constants/query-strings";
import store from "@/store";
import router from "@/router";
// Mock Lazy Load
jest.mock("@/router/dynamic-routing/component-loader.js", () => ({
lazyLoadComponent: jest.fn(),
}));
describe("getPageToRouteExistingOrderTo", () => {
test("getPageToRouteExistingOrderTo, should return vehicle-year", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
lazyLoadComponent
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe('vehicle-year');
});
test("getPageToRouteExistingOrderTo, should return vehicle-model", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
lazyLoadComponent
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe('vehicle-model');
});
test("getPageToRouteExistingOrderTo, should return vehicle-damage", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
lazyLoadComponent
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
});
store.getters.damage.isRepair = undefined;
store.getters.vehicle.carId = 'C00000';
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe('vehicle-damage');
});
test("getPageToRouteExistingOrderTo, should return vin-lookup", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
lazyLoadComponent
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
});
store.getters.damage.isRepair = true;
store.getters.vehicle.carId = 'C00000';
store.getters.vehicle.vin = "1FADP3F26DL212886"
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe('vin-lookup');
});
test("getPageToRouteExistingOrderTo, should return estimate", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
lazyLoadComponent
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
});
store.getters.damage.isRepair = true;
store.getters.vehicle.carId = 'C00000';
store.getters.vehicle.vin = null;
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe('estimate');
});
test("getPageToRouteExistingOrderTo, existing order, should return heritage", async () => {
// Arrange
const toRoute = {
query: {
[queryStrings.START_TYPE]: 'fmg'
}
}
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, true);
// Assert
expect(result).toBe("heritage");
})
});
describe("navigateToHeritageFunnel", () => {
test("should save order", async () => {
// Arrange
const mockReferralNumber = 2;
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate);
const mockData = {
actionList: [{
actionName: storeActions.SAVE_ORDER,
data: mockOrderInfo,
}]
}
setupMocksForJsFiles(mockData);
const saveOrderFunction = jest.spyOn(orderHelper, "saveOrder");
router.navigateToExternalUrl = jest.fn();
// Act
await navigateToHeritageFunnel();
// Assert
expect(saveOrderFunction).toHaveBeenCalled();
// Should alway save before we navigate to heritage
const saveOrderFunctionCallOrder = saveOrderFunction.mock.invocationCallOrder[0];
const routerNavigateFunctionCallOrder = router.navigateToExternalUrl.mock.invocationCallOrder[0];
expect(saveOrderFunctionCallOrder).toBeLessThan(routerNavigateFunctionCallOrder);
saveOrderFunction.mockRestore();
});
test("should go to heritage funnel", async () => {
// Arrange
const mockReferralNumber = 2;
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate);
const mockData = {
actionList: [{
actionName: storeActions.SAVE_ORDER,
data: mockOrderInfo,
}]
}
setupMocksForJsFiles(mockData);
store.getters.order.referralCorrelationId = mockCorrelationId
router.navigateToExternalUrl = jest.fn();
// Act
await navigateToHeritageFunnel();
// Assert
expect(router.navigateToExternalUrl).toHaveBeenCalled();
expect(router.navigateToExternalUrl).toHaveBeenCalledWith(externalUrls.HERITAGE_FUNNEL,
expect.objectContaining({
corid: mockCorrelationId
})
);
});
});

View file

@ -0,0 +1,74 @@
import { storeActions } from "@/constants/store-actions.js";
import { getConceptCookie, updateOrCreateConceptCookie, deleteConceptCookie } from "@/helpers/heritage-integration/cookie-helper.js";
import baseMixin from "@/mixins/base-mixin";
/*
Will call API and hydrate state with data from API if present. If there is no order present
method will return null. If the cookie dictates the state should be reset
it will reset the state and go back to the start of the funnel.
*/
export async function loadOrderIfPresent() {
console.log("attempting to load referral....");
const conceptCookie = getConceptCookie();
console.log(conceptCookie)
// Do nothing if there is no cookie or no correlation id.
if (conceptCookie == null || conceptCookie.ReferralCorrelationId == null) {
console.log("No referral found");
return null;
}
// Reset state if cookie says to.
if (conceptCookie.ShouldResetState) {
console.log("Resetting state...");
baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.RESET_STATE);
deleteConceptCookie();
return null;
}
console.log("calling load order from loadOrderIfPresent()...");
// Load referral if there is a cookie, and it doesn't indicate it needs a state reset.
return (await loadOrder(conceptCookie.ReferralNumber, conceptCookie.ReferralDate, conceptCookie.ReferralCorrelationId)).data;
}
/*
Will call API to save existing order, or create new one depending where it's called from.
This will also set Referral information in the store after saving, and then
update the cookie.
*/
export async function saveOrder() {
console.log("saving order...");
const savedOrderInfo = await baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.SAVE_ORDER);
// Save the referral information back from the store.
await baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.SET_REFERRAL_INFORMATION, {
referralNumber: savedOrderInfo.data.referralNumber,
referralCorrelationId: savedOrderInfo.data.referralCorrelationId,
referralDate: savedOrderInfo.data.referralDate,
}, false);
// Update the cookie with the referral information when saved.
updateOrCreateConceptCookie();
}
// PRIVATE FUNCTIONS //
/*
Calls API to load order given the referral number, referralDate, and referralCorrelationId
and returns the response.
*/
async function loadOrder(referralNumber, referralDate, referralCorrelationId) {
console.log("loading order...", referralNumber, referralDate, referralCorrelationId);
const response = await baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOAD_ORDER,
{
referralNumber: referralNumber.toString(),
referralDate: referralDate,
referralCorrelationId: referralCorrelationId
}, false);
return response;
}

View file

@ -0,0 +1,170 @@
import * as cookieHelper from "@/helpers/heritage-integration/cookie-helper";
import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper";
import { cookieNames } from "@/constants/cookie-names";
import { setupMocksForJsFiles, removeAllTestCookies, getMockOrderInfo, setupCookies } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import router from "@/router";
describe("loadOrderIfPresent", () => {
afterEach(() => {
removeAllTestCookies();
});
test("ShouldResetState == true => concept cookie is deleted", () => {
// Arrange
const testShouldResetState = true;
const testCookieValue = {
ShouldResetState: testShouldResetState
}
document.cookie = `${cookieNames.ORDER_INFO}=${JSON.stringify(testCookieValue)}; path=/; domain=${location.hostname}`;
// Act
loadOrderIfPresent();
// Assert
expect(document.cookie).toBe("");
});
test("ShouldResetState == true => reset store", () => {
// Arrange
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie").mockReturnValueOnce({ ShouldResetState: true, ReferralCorrelationId: "xxx-xxx-xxx" });
const mockData = {
actionList: [{
actionName: storeActions.RESET_STATE
}],
}
var mocks = setupMocksForJsFiles(mockData);
// Act
loadOrderIfPresent();
// Assert
expect(cookieHelper.getConceptCookie).toHaveBeenCalled();
expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).toHaveBeenCalledWith(storeActions.RESET_STATE);
});
test("Concept cookie is null => store is unchanged", () => {
// Arrange
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie").mockReturnValueOnce(null);
const mockData = {
actionList: [{
actionName: storeActions.RESET_STATE
}],
}
var mocks = setupMocksForJsFiles(mockData);
// Act
loadOrderIfPresent();
// Assert
expect(cookieHelper.getConceptCookie).toHaveBeenCalled();
expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).not.toHaveBeenCalledWith(storeActions.RESET_STATE);
});
test("Concept cookie valid, should call loadOrder", async () => {
// Arrange
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie")
.mockReturnValueOnce({ ShouldResetState: false, ReferralNumber: 123456, ReferralCorrelationId: "yyy-yyy-yyyy", ReferralDate: new Date()});
const mockData = {
actionList: [{
actionName: storeActions.LOAD_ORDER,
data: { ReferralNumber: 123456, vehicle: { year: 2010 } }
}],
}
var mocks = setupMocksForJsFiles(mockData);
// Act
const result = await loadOrderIfPresent();
// Assert
expect(cookieHelper.getConceptCookie).toHaveBeenCalled();
expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).not.toHaveBeenCalledWith(storeActions.LOAD_ORDER);
expect(result.ReferralNumber).toBe(123456);
expect(result.vehicle.year).toBe(2010);
});
});
describe("saveOrder", () => {
afterEach(() => {
removeAllTestCookies();
});
test("saveOrder => should set state order values", async () => {
// Arrange
const mockReferralNumber = 2;
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate);
const mockData = {
actionList: [
{
actionName: storeActions.SAVE_ORDER,
data: mockOrderInfo,
},
{
actionName: storeActions.SET_REFERRAL_INFORMATION
}
]
}
const mocks = setupMocksForJsFiles(mockData);
// Act
await saveOrder();
// Assert
expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).toHaveBeenCalledWith(storeActions.SAVE_ORDER);
expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).toHaveBeenCalledWith(storeActions.SET_REFERRAL_INFORMATION, {
referralNumber: mockReferralNumber,
referralDate: mockReferralDate,
referralCorrelationId: mockCorrelationId
}, false);
});
test("saveOrder => should update DidHeritageFunnelUpdateLast cookie value to false", async () => {
// Arrange
const testReferralNumber = 1566818;
const testReferralDate = "2022-03-15T10:56:24.597";
const testReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060";
const mockOrderInfo = getMockOrderInfo(testReferralNumber, testReferralCorrelationId, testReferralDate);
const mockData = {
actionList: [{
actionName: storeActions.SAVE_ORDER,
data: mockOrderInfo,
}],
router: router
};
setupMocksForJsFiles(mockData);
const testCookieValue = {
ReferralNumber: testReferralNumber,
ReferralDate: testReferralDate,
ReferralCorrelationId: testReferralCorrelationId,
ShouldResetState: false,
DidHeritageFunnelUpdateLast: true
}
setupCookies({ conceptCookieValue: JSON.stringify(testCookieValue) });
// Act
await saveOrder();
// Assert
expect(cookieHelper.getConceptCookie().DidHeritageFunnelUpdateLast).toEqual(false);
});
});

View file

@ -0,0 +1,52 @@
import { applicationConfig } from "@/constants/application-config";
import { getConceptCookie} from "@/helpers/heritage-integration/cookie-helper.js";
/*
Method to determine if our analytics session has timed out or not.
Amount used for timeout is configurable in application-config.js
*/
export function isAnalyticsSessionStillActive() {
if (getConceptCookie() !== null) {
const lastTouchedValue = getConceptCookie().LastTouched;
const timeoutAmount = applicationConfig.ANALYTICS_SESSION_TIMEOUT;
const isMoreThanHalfHourAgo = ((new Date() - new Date(lastTouchedValue)) / 60000) > timeoutAmount;
if (isMoreThanHalfHourAgo) {
return false;
}
return true;
}
}
/*
Method to determine if the users 'saved' session is still active.
When user state is created, there is a date that is saved into state
this method checks against that date.
Note: That time for saved session timeout is configurable in application-config.js
*/
export function isSavedSessionStillActive() {
if (getConceptCookie() !== null) {
const savedSessionTimeStamp = new Date(getConceptCookie().SavedQuoteTimeoutDate);
const isSavedSessionTimedOut = (new Date(new Date().toUTCString()) > savedSessionTimeStamp);
console.log(savedSessionTimeStamp, isSavedSessionTimedOut);
console.log("Saved Session Timed Out? --->", isSavedSessionTimedOut);
if (isSavedSessionTimedOut) {
return false;
}
return true;
}
}
/*
Function to get the date for the saved session timeout.
*/
export function getDateForSavedSessionTimeout() {
const currentDate = new Date(new Date().toUTCString())
currentDate.setDate(currentDate.getDate() + applicationConfig.SAVED_SESSION_TIMEOUT)
return currentDate.toUTCString();
}

View file

@ -0,0 +1,82 @@
import * as cookieHelper from "@/helpers/heritage-integration/cookie-helper";
import { isAnalyticsSessionStillActive, isSavedSessionStillActive, getDateForSavedSessionTimeout} from "@/helpers/heritage-integration/session-helper";
import { applicationConfig } from "@/constants/application-config";
describe("isAnalyticsSessionStillActive", () => {
test("isAnalyticsSessionStillActive, should return true", () => {
// Arrange
const mockDate = new Date(new Date().toUTCString())
mockDate.setDate(mockDate.getDate() + 1)
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie")
.mockReturnValue({ LastTouched: mockDate });
// Act
const result = isAnalyticsSessionStillActive();
// Assert
expect(result).toBe(true);
});
test("isAnalyticsSessionStillActive, should return false", () => {
// Arrange
const mockDate = new Date(new Date().toUTCString())
mockDate.setDate(mockDate.getDate() - 1)
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie")
.mockReturnValue({ LastTouched: mockDate });
// Act
const result = isAnalyticsSessionStillActive();
// Assert
expect(result).toBe(false);
});
});
describe("isSavedSessionStillActive", () => {
test("isSavedSessionStillActive, should return true", () => {
// Arrange
const mockDate = new Date(new Date().toUTCString())
mockDate.setDate(mockDate.getDate() + 1);
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie")
.mockReturnValue({ SavedQuoteTimeoutDate: mockDate });
// Act
const result = isSavedSessionStillActive();
// Assert
expect(result).toBe(true);
});
test("isSavedSessionStillActive, should return false", () => {
// Arrange
const mockDate = new Date(new Date().toUTCString())
mockDate.setDate(mockDate.getDate() - 1)
cookieHelper.getConceptCookie = jest.spyOn(cookieHelper, "getConceptCookie")
.mockReturnValue({ SavedQuoteTimeoutDate: mockDate });
// Act
const result = isSavedSessionStillActive();
// Assert
expect(result).toBe(false);
});
});
describe("getDateForSavedSessionTimeout", () => {
test("getDateForSavedSessionTimeout, should equal application config setting", () =>{
// Arrange
const currentDate = new Date(new Date().toUTCString())
currentDate.setDate(currentDate.getDate() + applicationConfig.SAVED_SESSION_TIMEOUT)
// Act
const result = getDateForSavedSessionTimeout();
// Assert
expect(result).toEqual(currentDate.toUTCString());
});
})

View file

@ -3,27 +3,17 @@ import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { cookieNames } from "@/constants/cookie-names";
import baseMixin from "@/mixins/base-mixin";
// Common methods
export function getMountOptions(mockData) {
// Define our mocks to attached to the 'global' object for Vue/Jest.
const mocks = {};
//this is mocking if you use the mixin directly(baseMixin.methods.dispatchNonBlockingStoreAction) vs this.dispatchNonBlockingStoreAction
if (mockData.actionList !== undefined) {
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList.filter(
(x) => x.actionName == actionName
);
setupBaseMixinDispatchNonBlockingStoreAction(mockData);
if (actionFilterResult.length > 0 && actionFilterResult.length === 1) {
return Promise.resolve({
data: actionFilterResult[0].data,
});
}
});
}
mocks.dispatchNonBlockingStoreAction = jest.fn();
mocks.dispatchNonBlockingStoreAction.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList.filter(
@ -49,10 +39,65 @@ export function getMountOptions(mockData) {
mocks.$router = mockData.router;
mocks.$route = mockData.route;
const global = {
mocks: mocks,
};
return { global };
}
export function setupMocksForJsFiles(mockData = {}) {
setupBaseMixinDispatchNonBlockingStoreAction(mockData);
return { baseMixin };
}
// Heritage integration common methods
export const cookies = {
[cookieNames.CONCEPT_SESSION_INFO]: `{"ReferralNumber":"1566818","ReferralDate":"2022-03-15T10:56:24.597","ReferralCorrelationId":"404d2b04-f86e-45c3-b373-127b6217b060","ShouldResetState":false,"DidHeritageFunnelUpdateLast":true}`,
"UNIQUE_SESSION_ID": "33756020-b58e-4ec7-b8b8-3f1576719c40",
"anotherCookie": "{}",
"someOtherCookie": "{}"
};
export function removeAllTestCookies() {
Object.keys(cookies).forEach(key => {
document.cookie = `${key}=;Max-Age=0;`;
document.cookie = `${key}=;Max-Age=0;path=/`;
});
}
export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate) {
return {
referralNumber: mockReferralNumber,
referralCorrelationId: mockCorrelationId,
referralDate: mockReferralDate
}
}
export function setupCookies({ conceptCookieValue = "", includeHeritageCookie = true }) {
Object.keys(cookies).forEach(key => {
const cookieValue = key == cookieNames.CONCEPT_SESSION_INFO ? conceptCookieValue : cookies[key];
if (includeHeritageCookie || key != cookieNames.CONCEPT_SESSION_INFO)
document.cookie = `${key}=${cookieValue}; path=/;`;
});
}
// Private methods
function setupBaseMixinDispatchNonBlockingStoreAction(mockData) {
if (mockData.actionList !== undefined) {
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList.filter(
(x) => x.actionName == actionName
);
if (actionFilterResult.length > 0 && actionFilterResult.length === 1) {
return Promise.resolve({
data: actionFilterResult[0].data,
});
}
});
}
}

View file

@ -1,233 +0,0 @@
<template>
<div class="container-fluid container-shadow p-2 rounded-3">
<div class="row my-3">
<div class="col">
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
</div>
</div>
<form>
<div class="row my-4">
<div class="col">
<label for="autocomplete">Street address</label>
<input ref="autocomplete"
id="autocomplete"
placeholder="Search"
class="form-control search-location"
type="text"
autocomplete="off" />
</div>
</div>
</form>
<form>
<div v-show="showAddressFields" class="row my-4">
<div class="col">
<label for="city">City</label>
<input ref="city"
id="city"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
</form>
<form>
<div v-show="showAddressFields" class="row my-4">
<div class="col-6">
<label for="state">State</label>
<input ref="state"
id="state"
class="form-control"
type="text"
autocomplete="off" />
</div>
<div class="col-6">
<label for="zip">Zip</label>
<input ref="zip"
id="zip"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
</form>
<div v-show="displayVerificationWarning" class="row my-4">
<div class="col">
<span style="color: red;">An accurate match may not have have been located. Please verify your address before continuing</span>
</div>
</div>
<div v-show="displayNoMatchWarning" class="row my-4">
<div class="col">
<span style="color: red;">An accurate match could not be located. Please re-enter your street address before continuing</span>
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="firstName">First name</label>
<input ref="firstName"
id="firstName"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="lastName">Last name</label>
<input ref="lastName"
id="lastName"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="email">Email address</label>
<input ref="email"
id="email"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
</div>
</template>
<script>
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
export default {
name: "App",
components: {
vehicleBanner,
},
data() {
return {
showAddressFields: false,
displayVerificationWarning: false,
displayNoMatchWarning: false
};
},
mounted() {
this.$refs.vehicleBanner.initializeComponent({
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
});
let addressField1 = document.getElementById("autocomplete");
let self = this;
//let apiKey = "AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo"; // localhost:8080
let apiKey = "AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w"; // Dev and above
this.$loadScript(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`)
.then(() => {
// Script is loaded, initialize the autocomplete textbox
self.autocomplete = new window.google.maps.places.Autocomplete(
self.$refs.autocomplete,
{
componentRestrictions: { country: ["us"] },
fields: ["address_components"],
types: ["address"],
}
);
self.autocomplete.addListener('place_changed', fillInAddress);
addressField1.onblur = function() {
var hover = document.querySelector(".pac-container .pac-item:hover");
// if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place
if (hover === null) {
var item = document.querySelector(".pac-container .pac-item");
if (item != null) {
var firstResult = item.textContent;
var geocoder = new window.google.maps.Geocoder();
geocoder.geocode({
address: firstResult
}, function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]);
self.displayVerificationWarning = true;
self.displayNoMatchWarning = false;
}
});
}
else {
let city = document.querySelector("#city");
let state = document.querySelector("#state");
let zip = document.querySelector("#zip");
city.value = "";
state.value = "";
zip.value = "";
self.displayVerificationWarning = false;
self.displayNoMatchWarning = true;
}
}
};
function fillInAddress(place) {
if (!place) {
place = self.autocomplete.getPlace();
}
if (place && place.address_components) {
let address1 = "";
let city = document.querySelector("#city");
let state = document.querySelector("#state");
let zip = document.querySelector("#zip");
self.showAddressFields = true;
for (const component of place.address_components) {
const componentType = component.types[0];
switch (componentType) {
case "street_number": {
address1 = `${component.long_name} ${address1}`;
break;
}
case "route": {
address1 += component.short_name;
break;
}
case "locality": {
city.value = component.long_name;
break;
}
case "administrative_area_level_1": {
state.value = component.short_name;
break;
}
case "postal_code": {
zip.value = component.long_name;
break;
}
}
addressField1.value = address1;
self.displayVerificationWarning = false;
self.displayNoMatchWarning = false;
}
}
else {
self.displayVerificationWarning = true;
self.displayNoMatchWarning = false;
}
}
})
.catch(() => {
// Failed to fetch script
console.log("Unable to load Google Places API script");
});
},
};
</script>

View file

@ -1,138 +0,0 @@
<template>
<div class="container-fluid">
<h4 class="m-0 p-2 bg-light rounded">List Buttons</h4>
<buttonQuestion :answers="years" text-position="text-start" questionText="List Button as Radio" groupName="years" />
<buttonQuestion :answers="checkboxAnswers" isMultiSelect text-position="text-start" questionText="List Button as checkbox" groupName="check-box-answers" />
<buttonQuestion :answers="checkboxAnswers" isMultiSelect text-position="text-end" questionText="List Button as checkbox text end" groupName="check-box-answers-text-right" />
<buttonQuestion :answers="years" questionText="List Button as Radio text center (default)" groupName="years-text-center" />
<buttonQuestion :answers="years" selectingInitiatesLoad questionText="List Button as Radio with spinner" groupName="years-text-spinner" />
<span>(Selecting button with spinner will cause page overlay, page refresh needed to select buttons from other groups)</span>
<h4 class="m-0 p-2 bg-light rounded mt-5">List ButtonHorizontals</h4>
<buttonQuestion buttonType="listButtonHorizontal" :answers="radioAnswers" questionText="List Button Horizontal as radio" groupName="lbh-radio" />
<buttonQuestion buttonType="listButtonHorizontal" isMultiSelect :answers="horizontalCheckboxAnswers" questionText="List Button Horizontal as checkbox" groupName="lbh-checkbox" />
<h4 class="m-0 p-2 bg-light rounded mt-5">List Cards</h4>
<buttonQuestion buttonType="listCard" :answers="listCardRadio" questionText="List Card as radio" groupName="lc-radio" />
<buttonQuestion isMultiSelect buttonType="listCard" :answers="listCardCheckBox" questionText="List Card as checkbox" groupName="lc-checkbox" />
<buttonQuestion buttonType="listCard" :answers="listCardGroup" questionText="List Cards as radios" groupName="lc-mult" />
<buttonQuestion buttonType="listCard" isMultiSelect :answers="listCardGroup" questionText="List Cards as checkboxes" groupName="lcc-mult" />
<buttonQuestion isWide buttonType="listCard" :answers="listCardGroup" questionText="List Cards as radios full width (text left image right)" groupName="lc-multWide" />
<buttonQuestion isWide isMultiSelect buttonType="listCard" :answers="listCardGroup" questionText="List Cards as checkboxes full width (text left image right)" groupName="lcc-multWide" />
<h4 class="m-0 p-2 bg-light rounded mt-5">Radio</h4>
<buttonQuestion buttonType="radio" :answers="radioAnswers" questionText="Used with radio" groupName="radio" />
</div>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
export default {
name: "App",
components: {
buttonQuestion
},
data() {
return {
years: [2023, 2022, 2021, 2020],
checkboxAnswers: [
"Checkbox Answer 1",
"Checkbox Answer 2",
"Checkbox Answer 3",
],
radioAnswers: ["Radio Answer 1", "Radio Answer 2", "Radio Answer 3"],
horizontalCheckboxAnswers: ["HCB Answer 1","HCB Answer 2","HCB Answer 3"],
horizontalRadioAnswers: ["HR Answer 1", "HR Answer 2", "HR Answer 3"],
listCardCheckBox: [
{
Name: "List-Card-CB",
Text: "List Card CB",
SubText: "checkbox",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
listCardRadio: [
{
Name: "List-Card-R",
Text: "List Card R",
SubText: "Radio",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
listCardCheckBoxHorizontalSubText: [
{
Name: "List-Card-CBHst",
Text: "List Card CBHst",
SubText: "With Subtext",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
listCardCheckBoxHorizontal: [
{
Name: "List-Card-CBH",
Text: "List Card CBH",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
listRadioHorizontalSubText: [
{
Name: "List-Card-RHst",
Text: "List Card RHst",
SubText: "With Subtext",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
listRadioHorizontal: [
{
Name: "List-Card-RH",
Text: "List Card RH",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
listCardGroup: [
{
Name: "Side-Window-1",
Text: "Side Window",
SubText: "With Subtext",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
{
Name: "Side-Window-2",
Text: "Side Window",
SubText: "With Subtext that is more than one line",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
{
Name: "Side-Window-3",
Text: "Side Window",
ImageId: "53343ce4-5b6a-46aa-a80a-f948c1723955",
AnswerImageUrl: "https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3",
},
],
};
},
}
</script>

View file

@ -1,74 +0,0 @@
<template>
<div class="container-fluid nested-radio">
<div class="row mb-1">
<div class="col">
<listCard
groupName="listcard1"
isWide="true"
buttonImage="https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3"
buttonLabel="Green Tint, Blue Shade"
buttonID="radio1"
/>
</div>
</div>
<div class="row mb-1">
<div class="col">
<buttonQuestion
buttonType="radio"
groupName="Demo Group"
buttonID="button1"
isRequired
value="test button"
screenReaderOnlyText="rain sensor, solar, 3rd visor band"
:answers="demoArray"
questionText="ok now select your features"
/>
</div>
</div>
<div class="row mb-2">
<div class="col">
<listCard
groupName="listcard1"
isWide="true"
buttonImage="https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3"
buttonLabel="Green Tint"
buttonID="radio2"
/>
</div>
</div>
</div>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import listCard from "@/ux-components/list-card/list-card";
export default {
name: "nestedRadio",
components: {
buttonQuestion,
listCard,
},
data() {
return {
demoArray: [
"rain sensor, solar, 3rd visor band",
"rain sensor, heated glass, solar, 3rd visor band",
],
};
},
};
</script>
<style lang="scss">
.nested-radio {
.ui-radio {
flex-direction: column;
margin: 0.25rem 0;
}
p {
font-size: 0.875rem;
}
}
</style>

View file

@ -3,7 +3,7 @@ import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-que
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
jest.mock("@/store",()=>{return{};},{virtual:true});
describe("replace-options-question.vue", () => {
test("Selected damage option is emitted upon selection.", async () => {

View file

@ -3,7 +3,7 @@ import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-doo
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
jest.mock("@/store",()=>{return{};},{virtual:true});
describe("replace-options-question.vue", () => {
test("Selected side door option is emitted upon selection.", async () => {
@ -95,7 +95,7 @@ describe("replace-options-question.vue", () => {
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.dispatch = jest.fn(() => {});
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
const mountOptions = getMountOptions({
store: {

View file

@ -69,13 +69,15 @@ import alert from "@/ux-components/alert/alert";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
@ -148,7 +150,10 @@ export default {
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
if(store.getters.vehicle.carId){
return true;
}
return false;
},
resetDependentState() {
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
@ -280,6 +285,12 @@ export default {
},
navigateForward(partsData){
// CSR-98 TEMP
if (store.getters.vehicle.year == 2010) {
navigateToHeritageFunnel();
return;
}
//found problem questions
if (partsData.data.partsOrQuestions.some(pq => pq.partQuestions != null && pq.partQuestions.length > 0)){
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_DAMAGE_WITH_PART_QUESTIONS, this.$route, {}, {}, partsData.data);

View file

@ -84,7 +84,10 @@ export default {
);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.year !== null;
if (store.getters.vehicle.year){
return true;
}
return false;
},
resetDependentState() {
// Set

View file

@ -85,7 +85,10 @@ export default {
);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.make !== null;
if(store.getters.vehicle.make){
return true;
}
return false;
},
resetDependentState() {
// Set

View file

@ -193,7 +193,7 @@ describe("vehicle-parts.vue", () => {
test("PageData / isRepair populated in Vuex. arePagePrerequisitesValid should be true ", async () => {
//Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.pageData.mockReturnValue(basePartResponse);
store.getters.lineItems = { glassParts: {} }
const { wrapper } = setupMocks({

View file

@ -1,26 +1,42 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage="false" />
<funnelSubHeader ref="funnelSubHeader" />
<div class="container-fluid prevent-squish my-5">
<div class="row">
<div class="col">
<alert class="rounded border-0 shadow-sm" alertClass="alert-warning" :alertHeadline="alertWidgetData.headline" :alertCopy="alertWidgetData.copy" :isDismissible="false" />
</div>
<div class="row">
<div class="col">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
:alertHeadline="alertWidgetData.headline"
:alertCopy="alertWidgetData.copy"
:isDismissible="false"
/>
</div>
</div>
</div>
<div v-for="(item, i) in PartsForQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<div class="container-fluid">
<hr v-if="i > 0" />
</div>
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<div class="container-fluid">
<hr v-if="i > 0" />
</div>
<glassPartQuestion :ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`" v-model="glassParts[item.glassLocation + '-' + item.glassName]" :glassLocation="item.glassLocation" :glassName="item.glassName" :colorAnswers="item.colorAnswers" />
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
/>
</div>
<funnelFooter ref="funnelFooter" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" />
</div>
<funnelFooter
ref="funnelFooter"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</template>
<script>
@ -32,199 +48,197 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import alert from "@/ux-components/alert/alert";
// Supporting Files
import {
fetchCmsContentForPage
} from "@/helpers/cms-content-helper";
import {
settleAllPromises
} from "@/helpers/layout-helper";
import {
fmgPageValues
} from "@/router/router-constants/fmgPage-values";
import {
storeMutations
} from "@/constants/store-mutations";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
export default {
name: "vehicle-parts",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [{
resultKey: "cmsContent",
promise: cmsContentPromise,
}, ];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.$refs.funnelHeader.initializeComponent(
resultMap.cmsContent.FunnelHeaderWidget
);
vm.$refs.vehicleBanner.initializeComponent(
resultMap.cmsContent.VehicleBannerWidget
);
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
name: "vehicle-parts",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.$refs.funnelHeader.initializeComponent(
resultMap.cmsContent.FunnelHeaderWidget
);
vm.$refs.vehicleBanner.initializeComponent(
resultMap.cmsContent.VehicleBannerWidget
);
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
// Glass Part Question dynamic component
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
.forEach((c) =>
vm.$refs[c][0].initializeComponent({
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
})
);
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
.forEach((c) =>
vm.$refs[c][0].initializeComponent({
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
})
);
// Set alertData for the page alert. These use props so we don't call
// initializeComponent here.
vm.alertWidgetData = {
copy: resultMap.cmsContent.AlertWidget.BodyText,
headline: resultMap.cmsContent.AlertWidget.HeadlineText,
};
});
},
data() {
// Set alertData for the page alert. These use props so we don't call
// initializeComponent here.
vm.alertWidgetData = {
copy: resultMap.cmsContent.AlertWidget.BodyText,
headline: resultMap.cmsContent.AlertWidget.HeadlineText,
};
});
},
data() {
return {
glassParts: {},
alertWidgetData: Object,
};
},
components: {
glassPartQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
alert,
},
computed: {
PartsForQuestions() {
const partsData = this.PartsFromApi;
// Map API result data, to vehicle-parts data structure
const mappedData = partsData.partsOrQuestions.map((g) => {
return {
glassParts: {},
alertWidgetData: Object,
glassName: g.glassName,
glassLocation: g.glassLocation,
colorAnswers: g.parts.reduce((arr, p) => {
arr.push({
ColorAnswerText: p.color,
FeatureAnswers: [
{
FeatureAnswerText:
p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
},
],
});
return arr;
}, []),
};
});
return mappedData;
},
components: {
glassPartQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
alert,
PartsFromApi() {
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
},
computed: {
PartsForQuestions() {
const partsData = this.PartsFromApi;
// Map API result data, to vehicle-parts data structure
const mappedData = partsData.partsOrQuestions.map((g) => {
return {
glassName: g.glassName,
glassLocation: g.glassLocation,
colorAnswers: g.parts.reduce((arr, p) => {
arr.push({
ColorAnswerText: p.color,
FeatureAnswers: [{
FeatureAnswerText: p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
}, ],
});
return arr;
}, []),
};
});
return mappedData;
},
PartsFromApi() {
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
},
RefPrefix() {
return "partQuestion";
},
RefPrefix() {
return "partQuestion";
},
methods: {
arePagePrerequisitesValid() {
// Check if isRepair is populated and if the pageData we need is here (Parts data)
if (
store.getters.damage.isRepair !== null &&
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
.length !== 0
) {
return true;
}
},
methods: {
arePagePrerequisitesValid() {
// Check if isRepair is populated and if the pageData we need is here (Parts data)
console.log(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
if (
(store.getters.damage.isRepair != null) &&
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS))
.length !== 0
) {
return true;
}
return false;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
const selectedGlassPartNumbers = [];
const matchedParts = [];
return false;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
const selectedGlassPartNumbers = [];
const matchedParts = [];
// Compile all selected parts from the page.
for (let [key, value] of Object.entries(this.glassParts)) {
for (let [glassKey, glassValue] of Object.entries(value)) {
selectedGlassPartNumbers.push(glassValue[0]);
}
}
// Match them to the parts from the API.
for (let [key, value] of Object.entries(
this.PartsFromApi.partsOrQuestions
)) {
for (let [partKey, partValue] of Object.entries(value.parts)) {
const currentPart =
this.PartsFromApi.partsOrQuestions[key].parts[partKey];
const isMatched = selectedGlassPartNumbers.some(
(p) => p === currentPart.partNumber
);
if (isMatched) {
matchedParts.push(currentPart);
}
}
}
// If no parts could be matched, throw an error.
if (matchedParts.length === 0) {
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store.
store.commit(storeMutations.UPDATE_PARTS, matchedParts);
// Navigate to the next page.
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_PARTS,
this.$route
);
},
resetDependentState() {
// Nothing additional to reset here: The page save is already fully resetting all the line-items on the order
},
LoadInitialPartsData() {
const partsData = this.PartsFromApi;
const alreadyPopulatedPartsData =
this.$store.getters.lineItems.glassParts === null ? {} :
this.$store.getters.lineItems.glassParts;
partsData.partsOrQuestions.map((g) => {
// If the part is already populated, use the value from the store and populate the v-model.
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
const partNumber = alreadyPopulatedPartsData[key].partNumber;
g.parts.forEach((p) => {
if (p.partNumber === partNumber) {
this.glassParts[g.glassLocation + "-" + g.glassName] = {
[g.glassLocation]: [partNumber],
};
}
});
});
});
// Compile all selected parts from the page.
for (let [key, value] of Object.entries(this.glassParts)) {
for (let [glassKey, glassValue] of Object.entries(value)) {
selectedGlassPartNumbers.push(glassValue[0]);
}
}
// Match them to the parts from the API.
for (let [key, value] of Object.entries(
this.PartsFromApi.partsOrQuestions
)) {
for (let [partKey, partValue] of Object.entries(value.parts)) {
const currentPart =
this.PartsFromApi.partsOrQuestions[key].parts[partKey];
const isMatched = selectedGlassPartNumbers.some(
(p) => p === currentPart.partNumber
);
if (isMatched) {
matchedParts.push(currentPart);
}
}
}
// If no parts could be matched, throw an error.
if (matchedParts.length === 0) {
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store.
store.commit(storeMutations.UPDATE_PARTS, matchedParts);
// Navigate to the next page.
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_PARTS,
this.$route
);
},
mounted(){
this.LoadInitialPartsData();
}
resetDependentState() {
// Nothing additional to reset here: The page save is already fully resetting all the line-items on the order
},
LoadInitialPartsData() {
const partsData = this.PartsFromApi;
const alreadyPopulatedPartsData =
this.$store.getters.lineItems.glassParts === null
? {}
: this.$store.getters.lineItems.glassParts;
partsData.partsOrQuestions.map((g) => {
// If the part is already populated, use the value from the store and populate the v-model.
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
const partNumber = alreadyPopulatedPartsData[key].partNumber;
g.parts.forEach((p) => {
if (p.partNumber === partNumber) {
this.glassParts[g.glassLocation + "-" + g.glassName] = {
[g.glassLocation]: [partNumber],
};
}
});
});
});
},
},
mounted() {
this.LoadInitialPartsData();
},
};
</script>

View file

@ -96,7 +96,10 @@ export default {
);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.model !== null;
if(store.getters.vehicle.model){
return true;
}
return false;
},
resetDependentState() {
// Invokes

View file

@ -2,7 +2,6 @@ import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { widgetNames } from "@/constants/widget-names.js";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
export default {
@ -44,9 +43,6 @@ export default {
navigationScenarios() {
return navigationScenarios;
},
widgetNames() {
return widgetNames;
},
vehicleCategories() {
return vehicleCategories;
},

View file

@ -1,6 +1,5 @@
import baseMixin from "@/mixins/base-mixin";
import { storeActions } from "@/constants/store-actions.js";
import { widgetNames } from "@/constants/widget-names.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
@ -67,18 +66,6 @@ describe("baseMixin.js", () => {
// Assert
expect(navigationScenariosForTest).toEqual(navigationScenarios);
});
test("computed: widgetNames should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
// Act
let widgetNamesForTest = mixIn.computed.widgetNames();
// Assert
expect(widgetNamesForTest).toEqual(widgetNames);
});
test("computed: vehicleCategories should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
@ -131,7 +118,6 @@ function getMixInInstance({ isDispatchSuccess = true }) {
const baseMixIn = baseMixin;
baseMixIn.methods.$route = route;
baseMixIn.methods.storeActions = storeActions;
baseMixIn.methods.widgetNames = widgetNames;
baseMixIn.methods.vehicleCategories = vehicleCategories;
store.dispatch = storeDispatch;

View file

@ -1,19 +1,23 @@
// Supporting files
import { createWebHistory, createRouter } from "vue-router";
import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events";
// Heritage integration
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
import { updateOrCreateConceptCookie,getConceptCookie } from "@/helpers/heritage-integration/cookie-helper";
import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper";
import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel} from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin";
import eventBus from "@/helpers/event-bus/event-bus";
import store from "@/store";
// Components
import ComponentTest from "@/layouts/component-test/component-test.vue";
import AddressPOC from "@/layouts/address-poc/address-poc.vue";
import FormTest from "@/layouts/form-test/form-test.vue";
import NestedRadio from "@/layouts/nested-radio-poc/nested-radio.vue";
import buttonQuestionExamples from "@/layouts/button-question-examples/button-question-examples.vue"
const routes = [
{
@ -26,26 +30,6 @@ const routes = [
name: "FormTest",
component: FormTest,
},
{
path: "/address-poc", // This is a temporary route for testing.
name: "AddressPOC",
component: AddressPOC,
},
{
path: "/form-test", // This is a temporary route for testing.
name: "FormTest",
component: FormTest,
},
{
path: "/nested-radio", // This is a temporary route for testing.
name: "NestedRadio",
component: NestedRadio,
},
{
path: "/button-question-examples", // This is a temporary route for testing.
name: "buttonQuestionExamples",
component: buttonQuestionExamples,
},
{
path: "/",
name: "root",
@ -55,9 +39,35 @@ const routes = [
await GoToFunnelStartOn404(next);
} else {
try {
// If the saved session has timed out, clear the session, execute 404 logic.
if (!isSavedSessionStillActive()) {
await GoToFunnelStartOn404(next);
}
// Process concept funnel cookie.
updateOrCreateConceptCookie();
// On entering the concept funnel "fresh", read cookie information, decide what to do next.
if (from.redirectedFrom === undefined) {
const loadOrderResponse = await loadOrderIfPresent();
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse);
// If getPageToRouteExistingOrderTo determines that the return user needs to
// go back to heritage funnel, send them there and stop our current navigation.
if (pageToRedirectTo === 'heritage') {
await navigateToHeritageFunnel();
return next(false);
}
// Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo;
console.log("Page to redirect to: ", pageToRedirectTo);
}
// If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) {
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === to.query.fmgPage)[0].components;
@ -123,10 +133,14 @@ router.navigateAfterSave = (scenario, currentRoute, optionalQuery = {}, optional
navigate(scenario, currentRoute, true, optionalQuery, optionalParams, optionalPageData);
}
router.navigateToExternalUrl = (url, optionalQuery = {}) => {
navigateToUrl(url, optionalQuery);
}
// PRIVATE FUNCTIONS
// Navigate to the next route, depending on the scenario.
function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) {
async function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) {
if (!scenario) {
console.error("No scenario provided. Please review the routing table.");
return;
@ -134,8 +148,9 @@ function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {},
// Match our maps up and navigate if we have a destination.
const matchingScenarioMap = getNavigationMap(scenario, currentRoute);
const destinationFmgPageValue = matchingScenarioMap.destinationFmgPageValue;
if (matchingScenarioMap.destinationFmgPageValue !== undefined) {
if (destinationFmgPageValue !== undefined) {
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one.
// If we need to do invalidation
@ -146,17 +161,22 @@ function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {},
}
// Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided.
baseMixin.methods.savePageDataToStore(matchingScenarioMap.destinationFmgPageValue, optionalPageData);
baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData);
// if cookie and referralNumber/Date exists
if (getConceptCookie()?.ReferralNumber && getConceptCookie()?.ReferralDate) {
await saveOrder();
}
router.push({
name: "root",
query: Object.assign(optionalQuery, {
fmgPage: matchingScenarioMap.destinationFmgPageValue,
fmgPage: destinationFmgPageValue,
}),
params: optionalParams,
params: optionalParams
});
} else if (matchingScenarioMap.destinationUrl !== undefined) {
navigateToUrl(matchingScenarioMap.destinationUrl);
navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery);
}
}
@ -177,9 +197,15 @@ function getNavigationMap(scenario, currentRoute) {
//---------------------------------------------------------- Private Functions ----------------------------------------------------------
// Navigate to an external url.
function navigateToUrl(url) {
function navigateToUrl(url, optionalQuery = {}) {
// possibly show some loading screen in the future here.
window.location.assign(url);
var externalUrl = new URL(url);
for (const queryKey in optionalQuery) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
}
window.location.assign(externalUrl);
}
// Get route information by page name.

View file

@ -0,0 +1,5 @@
const externalUrls = {
HERITAGE_FUNNEL: process.env.VUE_APP_HERITAGE_FUNNEL,
};
export { externalUrls };

View file

@ -4,6 +4,7 @@ const navigationScenarios = {
SELECTED_MAKE: "SELECTED_MAKE",
SELECTED_STYLE: "SELECTED_STYLE",
CLICKED_BACK: "CLICKED_BACK",
CLICKED_FORWARD: "CLICKED_FORWARD",
SELECTED_PARTS: "SELECTED_PARTS",
SELECTED_DAMAGE_WITH_SINGLE_PART: "SELECTED_DAMAGE_WITH_SINGLE_PART",
SELECTED_DAMAGE_WITH_MULTIPLE_PARTS: "SELECTED_DAMAGE_WITH_MULTIPLE_PARTS",

View file

@ -1,3 +0,0 @@
const routerParameterKeys = { };
export { routerParameterKeys };

View file

@ -1,5 +1,6 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { applicationConfig } from "@/constants/application-config";
const routingTable = [
{
@ -59,7 +60,7 @@ const routingTable = [
},
{
scenario: navigationScenarios.SELECTED_DAMAGE_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.REVEAL,
destinationFmgPageValue: fmgPageValues.REVEAL
},
{
scenario: navigationScenarios.SELECTED_DAMAGE_WITH_MULTIPLE_PARTS,
@ -80,6 +81,10 @@ const routingTable = [
},
{
scenario: navigationScenarios.SELECTED_PARTS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
],

View file

@ -1,55 +1,64 @@
import { createStore } from "vuex";
import { endpoints } from "@/constants/endpoints.js";
import { storeMutations } from "@/constants/store-mutations";
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
import createPersistedState from "vuex-persistedstate";
import globalMethods from "@/global-methods";
// Export State
export const state = {
order: {
vehicle: {
year: null,
make: null,
model: null,
style: null,
carId: null,
category: null,
imageUrl: null,
imageVifNumber: null,
imageColor: null,
registration: {
licensePlate: null,
address: null,
city: null,
state: null,
zipCode: null,
firstName: null,
lastName: null,
const getDefaultState = () => {
return {
order: {
vehicle: {
year: null,
make: null,
model: null,
style: null,
carId: null,
category: null,
vin: null,
imageUrl: null,
imageVifNumber: null,
imageColor: null,
registration: {
licensePlate: null,
address: null,
city: null,
state: null,
zipCode: null,
firstName: null,
lastName: null,
},
},
serviceLocation: {
zip: null,
},
customer: {
emailAddress: null,
},
damage: {
isRepair: null,
numberOfChips: null,
glassToReplace: null,
},
lineItems: {
glassParts: null,
otherParts: null
},
referralNumber: null,
referralDate: null,
referralCorrelationId: null,
parentAccountNumber: null,
},
serviceLocation: {
zip: null,
applicationUser: {
eventBus: [],
pageData: {},
savedSessionTimeout: getDateForSavedSessionTimeout()
},
customer: {
emailAddress: null,
},
damage: {
isRepair: null,
numberOfChips: null,
glassToReplace: null,
},
lineItems:{
glassParts: {},
otherParts: {}
}
},
applicationUser: {
eventBus: [],
pageData: {}
},
}
}
};
export const state = getDefaultState();
// Export Mutations
export const mutations = {
@ -81,49 +90,35 @@ export const mutations = {
updateVehicleImageColor(state, imageColor) {
state.order.vehicle.imageColor = imageColor;
},
updateIsRepair(state, isRepair){
updateIsRepair(state, isRepair) {
state.order.damage.isRepair = isRepair;
},
updateNumberOfChips(state, numberOfChips){
updateNumberOfChips(state, numberOfChips) {
state.order.damage.numberOfChips = numberOfChips;
},
updateGlassToReplace(state, glassToReplace){
updateGlassToReplace(state, glassToReplace) {
state.order.damage.glassToReplace = glassToReplace;
},
updateParts(state, partsData){
updateParts(state, partsData) {
state.order.lineItems.glassParts = partsData;
},
updatePageData(state, pageData){
updatePageData(state, pageData) {
state.applicationUser.pageData[pageData.page] = pageData.data;
},
updateRegistrationAddress(state, registrationAddress){
state.order.vehicle.registration.address = registrationAddress;
updateReferralCorrelationId(state, referralCorrelationId) {
state.order.referralCorrelationId = referralCorrelationId;
},
updateRegistrationCity(state, registrationCity){
state.order.vehicle.registration.city = registrationCity;
updateReferralNumber(state, referralNumber) {
state.order.referralNumber = referralNumber;
},
updateRegistrationState(state, registrationState){
state.order.vehicle.registration.state = registrationState;
updateReferralDate(state, referralDate) {
state.order.referralDate = referralDate;
},
updateRegistrationZipCode(state, registrationZipCode){
state.order.vehicle.registration.zipCode = registrationZipCode;
},
updateRegistrationFirstName(state, registrationFirstName){
state.order.vehicle.registration.firstName = registrationFirstName;
},
updateRegistrationLastName(state, registrationLastName){
state.order.vehicle.registration.lastName = registrationLastName;
},
updateRegistrationLicensePlate(state, registrationLicensePlate){
state.order.vehicle.registration.licensePlate = registrationLicensePlate;
},
updateServiceLocationZip(state, serviceLocationZip){
state.order.vehicle.serviceLocation.zip = serviceLocationZip;
},
updateCustomerEmailAddress(state, customerEmailAddress){
state.order.vehicle.customer.emailAddress = customerEmailAddress;
updateParentAcctNumber(state, parentAcctNumber) {
state.order.parentAccountNumber = parentAcctNumber;
},
// EVENT BUS MUTATIONS
addEventToBus(state, event) {
state.applicationUser.eventBus.push(event);
@ -168,6 +163,32 @@ export const mutations = {
resetPartsState(state) {
state.order.lineItems.glassParts = null;
state.order.lineItems.otherParts = null;
},
resetState(state) {
Object.assign(state, getDefaultState());
},
// Misc Mutations
setLoadOrderInformation(state, orderInformation) {
state.order.referralNumber = orderInformation.referralNumber;
state.order.referralDate = orderInformation.referralDate;
state.order.referralCorrelationId = orderInformation.referralCorrelationId;
state.order.vehicle = {
year: orderInformation.vehicle?.year,
make: orderInformation.vehicle?.make,
model: orderInformation.vehicle?.model,
style: orderInformation.vehicle?.style,
carId: orderInformation.vehicle?.carId,
category: orderInformation.vehicle?.category,
imageUrl: orderInformation.vehicle?.imageUrl,
imageVifNumber: orderInformation.vehicle?.imageVifNumber,
imageColor: orderInformation.vehicle?.imageVifColor
};
state.order.damage.glassToReplace = orderInformation.glassToReplace;
state.order.damage.isRepair = orderInformation.isRepair;
state.order.damage.numberOfChips = orderInformation.numberOfChips;
state.order.lineItems.glassParts = orderInformation.parts;
state.order.parentAccountNumber = orderInformation.parentAccountNumber;
}
}
@ -187,7 +208,9 @@ export const getters = {
lineItems: (state) => state.order.lineItems,
pageData: (state) => (page) => {
return state.applicationUser.pageData[page];
}
},
applicationUser: (state) => state.applicationUser,
order: (state) => state.order,
}
// Export Actions
@ -278,6 +301,9 @@ export const actions = {
resetPartsAndDependencies(context) {
context.commit(storeMutations.RESET_PARTS_STATE);
},
resetState(context) {
context.commit(storeMutations.RESET_STATE);
},
// Content API Actions
getRouteInfo(context, { pageName }) {
@ -310,6 +336,13 @@ export const actions = {
});
},
// Misc Actions
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
},
// Parts API Actions
getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) {
return globalMethods.callHttpClient({
@ -322,6 +355,45 @@ export const actions = {
vin: vin
},
});
},
// Order API Actions
saveOrder(context) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
return globalMethods.callHttpClient({
method: endpoints.SaveOrder.method,
endpoint: endpoints.SaveOrder.url,
payload: {
vehicle: {
carId: vehicle.carId,
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
style: vehicle.style,
},
numberOfChips: damage.numberOfChips,
glassToReplace: damage.glassToReplace,
referralNumber: context.state.order.referralNumber,
referralDate: context.state.order.referralDate
},
});
},
loadOrder(context, { referralNumber, referralDate, referralCorrelationId }) {
return globalMethods.callHttpClient({
method: endpoints.LoadOrder.method,
endpoint: endpoints.LoadOrder.url,
payload: {
referralNumber: referralNumber,
referralDate: referralDate,
referralCorrelationId: referralCorrelationId
},
}).then((response) => {
context.commit(storeMutations.SET_LOAD_CONCEPT_SESSION_INFO, response.data);
return response;
});
}
}
@ -337,3 +409,5 @@ export default createStore({
getters,
actions,
});
// Private Functions

View file

@ -200,6 +200,38 @@ describe("Mutations", () => {
expect(storeState.applicationUser.pageData['vehicle-year']).toEqual({});
});
it("setLoadOrderInformation, should set order information in state", () => {
// Arrange
const storeState = state;
// Act
mutations.setLoadOrderInformation(storeState, {
referralNumber: 123,
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
vehicle: {
year: "2019",
make: "Acura",
model: "ILX",
style: "4 DOOR SEDAN",
carId: "C0000001",
category: "CAR"
},
glassToReplace: ["Windshield"],
isRepair: false,
numberOfChips: 0,
parts: [],
parentAccountNumber: "123456789",
});
// Assert
expect(storeState.order.referralNumber).toEqual(123);
expect(storeState.order.referralCorrelationId).toEqual("xxx-xxx-xxx");
expect(storeState.order.vehicle.year).toEqual("2019");
expect(storeState.order.vehicle.make).toEqual("Acura");
expect(storeState.order.vehicle.model).toEqual("ILX");
});
});
describe("Actions", () => {
@ -466,6 +498,66 @@ describe("Actions", () => {
expect(response.data).toEqual({ imageUrl: "https://test.com" });
});
it("saveOrder action, returns order information", async () => {
// Arrange
const context = state;
context.getters = {
vehicle: {},
damage: {},
};
context.state = {
order: {}
};
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: { referralNumber: 123 } });
});
// Act
const response = await actions.saveOrder(context);
// Assert
expect(response.data).toEqual({ referralNumber: 123 });
});
it("loadOrder action, returns order information, calls mutation", async () => {
// Arrange
const context = state;
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: { referralNumber: 123 } });
});
const commit = jest.fn();
context.commit = commit;
// Act
const response = await actions.loadOrder(context, {referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx"});
// Assert
expect(response.data).toEqual({ referralNumber: 123 });
expect(commit).toBeCalledWith(storeMutations.SET_LOAD_CONCEPT_SESSION_INFO, {"referralNumber": 123});
});
it("setReferralInformation, should call commit three times", () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
actions.setReferralInformation(context, {referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx"});
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_NUMBER, "123");
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_DATE, new Date().toUTCString());
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx");
});
});
describe("Getters", () => {

View file

@ -1,5 +1,7 @@
process.env.VUE_APP_CONSUMER_API_GATEWAY =
"https://consumerapidev.safelite.com";
process.env.VUE_APP_HERITAGE_FUNNEL =
"http://localhost:38000/default.aspx";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY =
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo"
@ -22,4 +24,7 @@ module.exports = {
},
},
},
configureWebpack: {
devtool: 'source-map'
},
};

View file

@ -1,5 +1,6 @@
process.env.VUE_APP_CONSUMER_API_GATEWAY = "__VUE_APP_CONSUMER_API_GATEWAY__";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "__VUE_APP_GOOGLE_PLACES_API_KEY__";
process.env.VUE_APP_HERITAGE_FUNNEL = "__VUE_APP_HERITAGE_FUNNEL__";
module.exports = {
outputDir: "dist/fmg",