Initial adds for analytics and logging.

This commit is contained in:
Jeremy Zimmerman 2022-10-31 12:24:09 -04:00
parent 05844d2500
commit ce5b402cbd
14 changed files with 2151 additions and 0 deletions

View file

@ -0,0 +1,36 @@
const analyticsPageEvents = {
ENTRY: "ENTRY",
EVENT: "EVENT",
};
// GA Constants
const GaEvents = {
GENERIC_EVENT: "event",
PAGE_VIEW_EVENT: "logPageview",
};
const GaCategories = {
API_RESPONSE: "Api_Response",
EVOX: "Evox",
};
const GaActions = {
RESULT: "Result",
CLICKED: "Clicked",
VIF: "vif",
SUBMITTED: "Submitted",
};
const GaLabels = {
SUCCESS: "Success",
ERROR: "Error",
LICENSE_PLATE_LOOKUP: "License_Plate_Look_Up",
VIN_LOOKUP: "Vin_Look_Up",
ADDRESS_LOOKUP: "Address_Look_up",
};
const ValueToLogTypes = {
LAST_5: "last_5",
};
export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes };

View file

@ -0,0 +1,12 @@
import { applicationConfig } from "@/constants/application-config.js";
const cookieNames = {
FUNNEL_SESSION_INFO: `FunnelSessionInfo-${applicationConfig.CURRENT_ENVIRONMENT}`,
// Existing Safelite.com cookies
DXDEV: "dxdev",
SESSION_ID: "sid",
SESSION_KEY: "skey",
};
export { cookieNames };

View file

@ -0,0 +1,14 @@
const experimentUniverses = {
CONCEPT_FUNNEL: "ConceptFunnel",
};
const experimentSettings = {
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
};
const experimentTriggers = {
SITE_ENTRY: "SiteEntry",
PAGE_ENTRY: "PageEntry",
};
export { experimentUniverses, experimentSettings, experimentTriggers };

View file

@ -0,0 +1,209 @@
import { cookieNames } from "@/constants/cookie-names";
import store from "@/store";
import { applicationConfig } from "@/constants/application-config";
/*
Will update the cookie if present, or create a new one if not.
*/
export function updateOrCreateFunnelCookie() {
const wasClaimRegistrationDelayed = getFunnelCookie()?.HasDelayedClaimRegistration;
const shouldSuppressConceptFunnel = getFunnelCookie()?.SuppressConceptFunnel;
// Set up cookie with all the props.
setFunnelCookieProperties({
LastTouched: new Date().toUTCString(),
SavedSessionTimeoutDate: store.getters.applicationUser.savedSessionTimeout,
DidHeritageFunnelUpdateLast: false,
ShouldResetState: false,
ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId,
ReferralParentAccountNumber: store.getters.order.accountNumber,
HasDelayedClaimRegistration: wasClaimRegistrationDelayed,
SuppressConceptFunnel: shouldSuppressConceptFunnel,
});
}
/*
Gets the current instance of the funnel cookie.
Returns null if cookie isn't valid JSON.
*/
export function getFunnelCookie() {
const cookieJson = document.cookie
?.split("; ")
?.find((row) => row.startsWith(`${cookieNames.FUNNEL_SESSION_INFO}=`))
?.split("=")[1];
try {
return JSON.parse(cookieJson);
} catch (error) {
return null;
}
}
/*
Removes cookie from browser.
*/
export function deleteFunnelCookie() {
createOrUpdateCookie(cookieNames.FUNNEL_SESSION_INFO, undefined, { maxAge: 0 });
}
/*
Gets cookie domain value. Localhost will be empty "".
*/
export function getCookieDomainValue() {
return isLocalhost() ? "" : `domain=${getDomainWithoutSubdomain()};`;
}
/*
Gets value of dxdev cookie, and then extracts "did" value from it.
Returns empty string if cookie not found or "did" string not present.
*/
export function getDeviceIdValue() {
// Sometimes these cookie contains more than the device ID.
const cookieValue = getCookieValueByName(cookieNames.DXDEV);
const cookieValuesSplit = cookieValue.split("=");
// If this is the only value, just use that.
if (cookieValuesSplit.length === 2 && cookieValuesSplit[0] === "did") {
return cookieValuesSplit[1];
}
const cookieValueMatch = cookieValue.match("^did=[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}");
if (cookieValueMatch) {
return cookieValueMatch[0].split("=")[1];
}
return "00000000-0000-0000-0000-000000000000";
}
/*
Gets value of skey cookie, returns 0 if not found.
*/
export function getSessionKeyValue() {
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY);
if (cookieValue) {
return cookieValue;
}
return 0;
}
/*
Gets value of skey cookie, returns 0 if not found.
*/
export function getSessionIdValue() {
const cookieValue = getCookieValueByName(cookieNames.SESSION_ID);
if (cookieValue) {
return cookieValue;
}
return "00000000-0000-0000-0000-000000000000";
}
/*
Updates session ID cookie with new expiration date
*/
export function updateSessionIdCookie() {
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), { maxAge: 60 * 30 });
}
export function setCookieProperties(
properties,
{ useDefaultFunnelCookieAttributes = true, maxAge, isSecure }
) {
if (typeof properties == "object") {
Object.keys(properties).forEach((key) => {
createOrUpdateCookie(key, properties[key], {
useDefaultFunnelCookieAttributes,
maxAge,
isSecure,
});
});
}
}
/*
===========================
= PRIVATE FUNCTIONS =
===========================
*/
/*
Used to set properties on the funnel cookie.
Takes an object with properties to set. Will overwrite existing properties.
*/
function setFunnelCookieProperties(properties) {
if (typeof properties == "object") {
let cookie = getFunnelCookie();
if (cookie !== null) {
Object.keys(properties).forEach((key) => {
cookie[key] = properties[key];
});
}
const cookieValueJson = JSON.stringify(cookie ?? {});
createOrUpdateCookie(cookieNames.FUNNEL_SESSION_INFO, cookieValueJson, {});
}
}
/*
Used to create a cookie.
`useDefaultFunnelCookieAttributes` will set the path and domain to our defaults
*/
function createOrUpdateCookie(
key,
value = "",
{ useDefaultFunnelCookieAttributes = true, maxAge, isSecure = true }
) {
let cookieToAdd = `${key}=${value}; `;
if (useDefaultFunnelCookieAttributes) {
cookieToAdd += `path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()} `;
}
if (isSecure && !isLocalhost()) {
cookieToAdd += `secure; `;
}
if (!isNaN(maxAge)) {
cookieToAdd += `max-age=${maxAge};`;
}
document.cookie = cookieToAdd;
}
/*
Gets current domain without the subdomain for cookie.
*/
function getDomainWithoutSubdomain() {
let url = location.hostname;
if (isLocalhost()) {
return "localhost";
}
const urlParts = url.split(".");
return `.${urlParts
.slice(0)
.slice(-(urlParts.length === 4 ? 3 : 2))
.join(".")}`;
}
/*
Gets cookie value by name, returns empty string if not found.
*/
function getCookieValueByName(name) {
const value = "; " + document.cookie;
const parts = value.split("; " + name + "=");
if (parts.length === 2) {
return parts.pop().split(";").shift();
}
return "";
}
function isLocalhost() {
return location.hostname.includes("localhost");
}

View file

@ -0,0 +1,145 @@
import {
getFunnelCookie,
getDeviceIdValue,
getSessionKeyValue,
getSessionIdValue,
} from "@/helpers/heritage-integration/cookie-helper.js";
import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper";
describe("cookies", () => {
afterEach(() => {
removeAllTestCookies();
});
describe("getFunnelCookie 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,
SuppressConceptFunnel: true,
};
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
// Act
var result = getFunnelCookie();
// 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({ funnelCookieValue: JSON.stringify(testCookieValue) });
// Act
var result = getFunnelCookie();
// 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({ funnelCookieValue: testCookieValue });
// Act
var result = getFunnelCookie();
// Assert
expect(result).toEqual(null);
});
test("returns null when funnel cookie doesn't exist", () => {
// Arrange
setupCookies({ includeHeritageCookie: false });
// Act
var result = getFunnelCookie();
// Assert
expect(result).toEqual(null);
});
test("gets correct cookie value", () => {
// Arrange
const testCookieValue = { test: "testValue" };
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
// Act
const actualCookieValue = getFunnelCookie();
// Assert
expect(actualCookieValue).toEqual(testCookieValue);
});
test("getCookieValue: Gets null cookie value", () => {
// Arrange
setupCookies({ includeHeritageCookie: false });
// Act
const actualCookieValue = getFunnelCookie();
// Assert
expect(actualCookieValue).toBeNull();
});
});
describe("getDeviceIdValue", () => {
test("getDeviceIdValue, should return GUID", () => {
// Arrange
setupCookies({});
// Act
const result = getDeviceIdValue();
//Assert
expect(result).toBe("21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe");
});
test("getSessionKeyValue, should return session key int", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionKeyValue();
//Assert
expect(result).toBe("12345");
});
});
describe("getSessionIdValue", () => {
test("getSessionIdValue, should return GUID", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionIdValue();
//Assert
expect(result).toBe("cba0c3d1-3c1b-4305-bb56-31aa50f58e27");
});
});
});

View file

@ -0,0 +1,150 @@
import { queryStrings } from "@/constants/query-strings";
import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import store from "@/store";
import router from "@/router";
/*
If the user has visited the 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") {
// If they have an existing order, return 'heritage' for the page name.
if (existingHeritageOrder) {
return fmgPageValues.HERITAGE;
}
return await getLatestPageForRedirection();
}
// If navigating to a specific page, and that page is not part of the vin pages.
// Return that page, so that it can navigate like normal.
if (toRoute.query[queryStrings.FMG_PAGE] !== undefined && !isVinRelatedPage(toRoute)) {
return overrideYmmsDirectionIfNeeded(toRoute);
}
// If this is not a direct link to a page using fmgPage, not from Safelite.com CTA or this is a vin related page.
// Get the latest page for redirection.
const latestPageRoute = await getLatestPageForRedirection();
return latestPageRoute;
}
/*
Used to navigate to the heritage funnel with the correct query string and url.
*/
export async function navigateToHeritageFunnel(shouldSaveSession = true) {
// Create the order (or save existing order) when navigating to Heritage Funnel.
if (shouldSaveSession) {
await saveSession();
}
router.navigateToExternalUrl(externalUrls.HERITAGE_FUNNEL, {
corid: store.getters.order.referralCorrelationId,
src: "concept-funnel",
conceptsqid: store.getters.applicationUser.savedSessionId,
});
}
/*
Logic for getting the last "valid" page a user visited.
*/
async function getLatestPageForRedirection() {
// 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 getLazyLoadedComponent(fmgPageValues.VEHICLE_MAKE);
const vehicleModelComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_MODEL);
const vehicleStyleComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_STYLE);
const vehicleDamageComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_DAMAGE);
const estimateComponent = await getLazyLoadedComponent(fmgPageValues.ESTIMATE);
const vinLookupComponent = await getLazyLoadedComponent(fmgPageValues.VIN_LOOKUP);
const partQuestionsComponent = await getLazyLoadedComponent(fmgPageValues.PART_QUESTIONS);
const vehiclePartsComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_PARTS);
const moldingQuestionsComponent = await getLazyLoadedComponent(fmgPageValues.MOLDING_QUESTIONS);
const capabilityQuestionsComponent = await getLazyLoadedComponent(
fmgPageValues.CAPABILITY_QUESTIONS
);
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_YEAR;
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MAKE;
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MODEL;
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_STYLE;
} else if (!estimateComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_DAMAGE;
} else {
if (capabilityQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.CAPABILITY_QUESTIONS;
} else if (moldingQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.MOLDING_QUESTIONS;
} else if (vehiclePartsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_PARTS;
} else if (partQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.PART_QUESTIONS;
} else if (
vinLookupComponent.methods.arePagePrerequisitesValid() &&
!store.getters.damage.isRepair
) {
return fmgPageValues.VIN_LOOKUP;
} else {
return fmgPageValues.ESTIMATE;
}
}
}
/*
Overrides functionality to go to the YMMS pages in certain cases.
If this is not one of the cases, it returns the 'to' fmgPage value.
*/
/* istanbul ignore next */
function overrideYmmsDirectionIfNeeded(toRoute) {
const fmgPageValue = toRoute.query[queryStrings.FMG_PAGE];
if (store.getters.payment.insuranceCoverage.isVerified) {
switch (fmgPageValue) {
case fmgPageValues.VEHICLE_YEAR:
case fmgPageValues.VEHICLE_MAKE:
case fmgPageValues.VEHICLE_MODEL:
case fmgPageValues.VEHICLE_STYLE: {
return fmgPageValues.VEHICLE_DAMAGE;
}
default: {
return fmgPageValue;
}
}
} else {
return fmgPageValue;
}
}
/*
Determine if the page is a vin related page.
*/
/* istanbul ignore next */
function isVinRelatedPage(toRoute) {
const fmgPageValue = toRoute.query[queryStrings.FMG_PAGE];
return (
fmgPageValue === fmgPageValues.VIN_LOOKUP ||
fmgPageValue === fmgPageValues.LICENSE_PLATE_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_VEHICLES ||
fmgPageValue === fmgPageValues.ESTIMATE
);
}
async function getLazyLoadedComponent(pageName) {
return (await lazyLoadComponent(pageName)()).default;
}

View file

@ -0,0 +1,445 @@
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 { fmgPageValues } from "@/router/router-constants/fmgPage-values";
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("should return vehicle-year", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_YEAR);
});
test("should return vehicle-make", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_MAKE);
});
test("should return vehicle-model", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_MODEL);
});
test("should return vehicle-style", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_STYLE);
});
test("should return vehicle-damage", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_DAMAGE);
});
test("user has YMMS and no vehicle questions > should return vin-lookup", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: false,
[fmgPageValues.VIN_LOOKUP]: true,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VIN_LOOKUP);
});
test("user has YMMS but no questions or carId > should return estimate", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: false,
[fmgPageValues.VIN_LOOKUP]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.ESTIMATE);
});
test("user has capability questions and molding questions > should return capability questions", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: true,
[fmgPageValues.MOLDING_QUESTIONS]: true,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: false,
[fmgPageValues.VIN_LOOKUP]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.CAPABILITY_QUESTIONS);
});
test("user has molding questions and part questions > should return molding questions", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: true,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: true,
[fmgPageValues.VIN_LOOKUP]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.MOLDING_QUESTIONS);
});
test("user has vehicle parts questions > should return vehicle-parts", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: true,
[fmgPageValues.PART_QUESTIONS]: true,
[fmgPageValues.VIN_LOOKUP]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_PARTS);
});
test("user has part questions > should return part-questions", async () => {
// Arrange
const toRoute = {
query: {},
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: true,
[fmgPageValues.VIN_LOOKUP]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.PART_QUESTIONS);
});
test("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(fmgPageValues.HERITAGE);
});
});
describe("navigateToHeritageFunnel", () => {
test("should save session", async () => {
// Arrange
const mockReferralNumber = "2";
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockAccountNumber = "167132";
const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx";
const mockOrderInfo = getMockOrderInfo(
mockReferralNumber,
mockCorrelationId,
mockReferralDate,
mockAccountNumber,
mockSavedSessionId,
mockCrmCustomerId
);
const mockData = {
actionList: [
{
actionName: storeActions.SAVE_SESSION,
data: mockOrderInfo,
},
],
};
setupMocksForJsFiles(mockData);
const saveSessionFunction = jest.spyOn(orderHelper, "saveSession");
router.navigateToExternalUrl = jest.fn();
// Act
await navigateToHeritageFunnel();
// Assert
expect(saveSessionFunction).toHaveBeenCalled();
// Should save before we navigate to heritage by default
const saveSessionFunctionCallOrder = saveSessionFunction.mock.invocationCallOrder[0];
const routerNavigateFunctionCallOrder =
router.navigateToExternalUrl.mock.invocationCallOrder[0];
expect(saveSessionFunctionCallOrder).toBeLessThan(routerNavigateFunctionCallOrder);
saveSessionFunction.mockRestore();
});
test("should go to heritage funnel", async () => {
// Arrange
const mockReferralNumber = "2";
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockAccountNumber = "167132";
const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx";
const mockOrderInfo = getMockOrderInfo(
mockReferralNumber,
mockCorrelationId,
mockReferralDate,
mockAccountNumber,
mockSavedSessionId,
mockCrmCustomerId
);
const mockData = {
actionList: [
{
actionName: storeActions.SAVE_SESSION,
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,
})
);
});
test("should not save session, but should still navigate", async () => {
// Arrange
const mockReferralNumber = "2";
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockOrderInfo = getMockOrderInfo(
mockReferralNumber,
mockCorrelationId,
mockReferralDate
);
const mockData = {
actionList: [
{
actionName: storeActions.SAVE_SESSION,
data: mockOrderInfo,
},
],
};
setupMocksForJsFiles(mockData);
const saveSessionFunction = jest.spyOn(orderHelper, "saveSession");
router.navigateToExternalUrl = jest.fn();
// Act
await navigateToHeritageFunnel(false);
// Assert
expect(saveSessionFunction).not.toHaveBeenCalled();
expect(router.navigateToExternalUrl).toHaveBeenCalled();
saveSessionFunction.mockRestore();
});
});
/**
* `arePagePrerequisitesValidObject` is an object where the keys are fmgPageValue names and the values are booleans that indicate
* whether arePagePrerequisitesValid is true or false
*/
function mockLazyLoadComponentReturnValues(arePagePrerequisitesValidObject = {}) {
lazyLoadComponent.mockImplementation((pageName) => {
return async () => {
return Promise.resolve({
default: {
methods: {
arePagePrerequisitesValid: jest
.fn()
.mockReturnValueOnce(arePagePrerequisitesValidObject[pageName]),
},
},
});
};
});
}

View file

@ -0,0 +1,113 @@
import { storeActions } from "@/constants/store-actions.js";
import {
getFunnelCookie,
updateOrCreateFunnelCookie,
deleteFunnelCookie,
} from "@/helpers/heritage-integration/cookie-helper.js";
import baseMixin from "@/mixins/base-mixin";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
/*
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 loadSessionIfPresent() {
const funnelCookie = getFunnelCookie();
// Do nothing if there is no cookie, correlation id, or referral number.
if (
funnelCookie == null ||
funnelCookie.ReferralCorrelationId == null ||
!funnelCookie.ReferralNumber
) {
return null;
}
// Reset state if cookie says to.
if (funnelCookie.ShouldResetState) {
baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
deleteFunnelCookie();
return null;
}
// Load referral if there is a cookie, and it doesn't indicate it needs a state reset.
return (
await loadSession(
funnelCookie.ReferralNumber,
funnelCookie.ReferralDate,
funnelCookie.ReferralCorrelationId,
funnelCookie.ReferralParentAccountNumber
)
).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 saveSession() {
var saveSessionPromise;
if (store.getters.applicationUser.saveSessionPromise) {
// queue newest request after current saveSessionPromise resolves
saveSessionPromise = store.getters.applicationUser.saveSessionPromise.then(() => {
// get a new saveSessionPromise
return saveSessionHelper();
});
} else {
// create an initial saveSessionPromise
saveSessionPromise = saveSessionHelper();
}
store.commit(storeMutations.UPDATE_SAVE_SESSION_PROMISE, saveSessionPromise);
// await here to allow for a caller to await and make the function synchronous
await saveSessionPromise;
}
// PRIVATE FUNCTIONS //
/*
Calls API to load session given the referral number, referralDate, and referralCorrelationId
and returns the response.
*/
async function loadSession(referralNumber, referralDate, referralCorrelationId, accountNumber) {
// await the saveSessionPromise in the store to make sure we're loading up to date information
await store.getters.applicationUser.saveSessionPromise;
const response = await baseMixin.methods.dispatchStoreAction(
storeActions.LOAD_SESSION,
{
referralNumber: referralNumber.toString(),
referralDate: referralDate,
referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber?.toString(),
},
false
);
return response;
}
/*
Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing
*/
async function saveSessionHelper() {
const savedSessionInfo = await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_SESSION);
// Update the store with information received from the saveSession response
await baseMixin.methods.dispatchStoreAction(
storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE,
{
referralNumber: savedSessionInfo.data.referralNumber.toString(),
referralCorrelationId: savedSessionInfo.data.referralCorrelationId,
referralDate: savedSessionInfo.data.referralDate,
accountNumber: savedSessionInfo.data.accountNumber.toString(),
savedSessionId: savedSessionInfo.data.savedSessionId,
crmCustomerId: savedSessionInfo.data.crmCustomerId.toString(),
},
false
);
// Update the cookie with the referral information when saved.
updateOrCreateFunnelCookie();
}

View file

@ -0,0 +1,236 @@
import * as cookieHelper from "@/helpers/heritage-integration/cookie-helper";
import { loadSessionIfPresent, saveSession } 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("loadSessionIfPresent", () => {
afterEach(() => {
removeAllTestCookies();
});
test("ShouldResetState == true => funnel cookie is deleted", () => {
// Arrange
cookieHelper.deleteFunnelCookie = jest.spyOn(cookieHelper, "deleteFunnelCookie");
const testShouldResetState = true;
const testCookieValue = {
ShouldResetState: testShouldResetState,
ReferralCorrelationId: "xxx",
ReferralNumber: "12345",
};
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=${JSON.stringify(
testCookieValue
)}; path=/; ${cookieHelper.getCookieDomainValue()}`;
// Act
loadSessionIfPresent();
// Assert
expect(cookieHelper.deleteFunnelCookie).toHaveBeenCalled();
expect(document.cookie).toBe("");
});
test("ShouldResetState == true => reset store", () => {
// Arrange
cookieHelper.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValueOnce({
ShouldResetState: true,
ReferralCorrelationId: "xxx-xxx-xxx",
ReferralNumber: "12345",
});
const mockData = {
actionList: [
{
actionName: storeActions.RESET_STATE,
},
],
};
var mocks = setupMocksForJsFiles(mockData);
// Act
loadSessionIfPresent();
// Assert
expect(cookieHelper.getFunnelCookie).toHaveBeenCalled();
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
storeActions.RESET_STATE
);
});
test("Funnel cookie is null => store is unchanged", () => {
// Arrange
cookieHelper.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValueOnce(null);
const mockData = {
actionList: [
{
actionName: storeActions.RESET_STATE,
},
],
};
var mocks = setupMocksForJsFiles(mockData);
// Act
loadSessionIfPresent();
// Assert
expect(cookieHelper.getFunnelCookie).toHaveBeenCalled();
expect(mocks.baseMixin.methods.dispatchStoreAction).not.toHaveBeenCalledWith(
storeActions.RESET_STATE
);
});
test("Funnel cookie valid, should call loadSession", async () => {
// Arrange
cookieHelper.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValueOnce({
ShouldResetState: false,
ReferralNumber: 123456,
ReferralCorrelationId: "yyy-yyy-yyyy",
ReferralDate: new Date(),
});
const mockData = {
actionList: [
{
actionName: storeActions.LOAD_SESSION,
data: { ReferralNumber: 123456, vehicle: { year: 2010 } },
},
],
};
var mocks = setupMocksForJsFiles(mockData);
// Act
const result = await loadSessionIfPresent();
// Assert
expect(cookieHelper.getFunnelCookie).toHaveBeenCalled();
expect(mocks.baseMixin.methods.dispatchStoreAction).not.toHaveBeenCalledWith(
storeActions.LOAD_SESSION
);
expect(result.ReferralNumber).toBe(123456);
expect(result.vehicle.year).toBe(2010);
});
});
describe("saveSession", () => {
afterEach(() => {
removeAllTestCookies();
});
test("saveSession => should set state order values", async () => {
// Arrange
const mockReferralNumber = "2";
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockAccountNumber = "167132";
const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx";
const mockOrderInfo = getMockOrderInfo(
mockReferralNumber,
mockCorrelationId,
mockReferralDate,
mockAccountNumber,
mockSavedSessionId,
mockCrmCustomerId
);
const mockData = {
actionList: [
{
actionName: storeActions.SAVE_SESSION,
data: mockOrderInfo,
},
{
actionName: storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act
await saveSession();
// Assert
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
storeActions.SAVE_SESSION
);
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE,
{
referralNumber: mockReferralNumber,
referralDate: mockReferralDate,
referralCorrelationId: mockCorrelationId,
accountNumber: mockAccountNumber,
savedSessionId: mockSavedSessionId,
crmCustomerId: mockCrmCustomerId,
},
false
);
});
test("saveSession => should update DidHeritageFunnelUpdateLast cookie value to false", async () => {
// Arrange
const mockReferralNumber = 1566818;
const mockReferralDate = "2022-03-15T10:56:24.597";
const mockReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060";
const mockAccountNumber = "167132";
const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx";
const mockOrderInfo = getMockOrderInfo(
mockReferralNumber,
mockReferralCorrelationId,
mockReferralDate,
mockAccountNumber,
mockSavedSessionId,
mockCrmCustomerId
);
const mockData = {
actionList: [
{
actionName: storeActions.SAVE_SESSION,
data: mockOrderInfo,
},
],
router: router,
};
setupMocksForJsFiles(mockData);
const testCookieValue = {
ReferralNumber: mockReferralNumber,
ReferralDate: mockReferralDate,
ReferralCorrelationId: mockReferralCorrelationId,
ShouldResetState: false,
DidHeritageFunnelUpdateLast: true,
};
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
// Act
await saveSession();
// Assert
expect(cookieHelper.getFunnelCookie().DidHeritageFunnelUpdateLast).toEqual(false);
});
});

View file

@ -0,0 +1,47 @@
import { applicationConfig } from "@/constants/application-config";
import { getFunnelCookie } 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 (getFunnelCookie() !== null) {
const lastTouchedValue = getFunnelCookie().LastTouched;
const timeoutAmount = applicationConfig.ANALYTICS_SESSION_TIMEOUT_MINUTES;
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 (getFunnelCookie() !== null) {
const savedSessionTimeStamp = new Date(getFunnelCookie().SavedSessionTimeoutDate);
const isSavedSessionTimedOut = new Date(new Date().toUTCString()) > savedSessionTimeStamp;
return !isSavedSessionTimedOut;
}
}
/*
Function to calculate 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_DAYS);
return currentDate.toUTCString();
}

View file

@ -0,0 +1,89 @@
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.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.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.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.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.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValue({ SavedSessionTimeoutDate: 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.getFunnelCookie = jest
.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValue({ SavedSessionTimeoutDate: 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_DAYS);
// Act
const result = getDateForSavedSessionTimeout();
// Assert
expect(result).toEqual(currentDate.toUTCString());
});
});

View file

@ -0,0 +1,144 @@
import { storeActions } from "@/constants/store-actions";
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 { Form } from "vee-validate";
import baseMixin from "@/mixins/base-mixin";
import {
getCookieDomainValue,
setCookieProperties,
} from "@/helpers/heritage-integration/cookie-helper";
import {
analyticsPageEvents,
GaCategories,
GaActions,
GaLabels,
GaEvents,
ValueToLogTypes,
} from "@/constants/analytics";
import { queryStrings } from "@/constants/query-strings";
import { routerParams } from "@/router/router-constants/router-params";
// 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.dispatchStoreAction) vs this.dispatchStoreAction
setupBaseMixinDispatchStoreAction(mockData);
mocks.pushEventToGA = jest.fn();
mocks.pushPageViewToGA = jest.fn();
mocks.logEvent = jest.fn();
mocks.pushExperimentsToDataLayer = jest.fn();
mocks.prependActionToMethod = jest.fn();
mocks.dispatchStoreAction = jest.fn();
mocks.dispatchStoreAction.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList?.filter((x) => x.actionName == actionName);
if (actionFilterResult?.length === 1) {
return Promise.resolve({
data: actionFilterResult[0].data,
});
}
});
// Mock const files
mocks.storeActions = storeActions;
mocks.storeMutations = storeMutations;
mocks.navigationScenarios = navigationScenarios;
mocks.vehicleCategories = vehicleCategories;
mocks.fmgPageValues = fmgPageValues;
mocks.analyticsPageEvents = analyticsPageEvents;
mocks.GaCategories = GaCategories;
mocks.GaActions = GaActions;
mocks.GaLabels = GaLabels;
mocks.GaEvents = GaEvents;
mocks.ValueToLogTypes = ValueToLogTypes;
mocks.queryStrings = queryStrings;
mocks.routerParams = routerParams;
// Mock $store and $router when accessing this.$store/$router
mocks.$store = mockData.store;
mocks.$router = mockData.router;
mocks.$route = mockData.route;
mocks.$loadScript = mockData.loadScript;
const global = {
mocks: mocks,
mixins: mockData.mixins,
stubs: { Form },
};
return { global };
}
export function setupMocksForJsFiles(mockData = {}) {
setupBaseMixinDispatchStoreAction(mockData);
return { baseMixin };
}
// Heritage integration common methods
export const cookies = {
[cookieNames.FUNNEL_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: "{}",
dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
sid: "cba0c3d1-3c1b-4305-bb56-31aa50f58e27",
skey: "12345",
};
// Removes test cookies for testing cookie-helper and order-helper
export function removeAllTestCookies() {
Object.keys(cookies).forEach((key) => {
document.cookie = `${key}=;Max-Age=0;`;
document.cookie = `${key}=;Max-Age=0;path=/;${getCookieDomainValue()}`;
});
}
export function getMockOrderInfo(
mockReferralNumber,
mockCorrelationId,
mockReferralDate,
accountNumber = "0",
savedSessionId,
crmCustomerId
) {
return {
referralNumber: mockReferralNumber,
referralCorrelationId: mockCorrelationId,
referralDate: mockReferralDate,
accountNumber: accountNumber,
savedSessionId: savedSessionId,
crmCustomerId: crmCustomerId,
};
}
export function setupCookies({ funnelCookieValue = "", includeHeritageCookie = true }) {
Object.keys(cookies).forEach((key) => {
const cookieValue =
key == cookieNames.FUNNEL_SESSION_INFO ? funnelCookieValue : cookies[key];
if (includeHeritageCookie || key != cookieNames.FUNNEL_SESSION_INFO)
setCookieProperties({ [key]: cookieValue }, { isSecure: false });
});
}
// Private methods
function setupBaseMixinDispatchStoreAction(mockData) {
if (mockData.actionList !== undefined) {
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.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

@ -0,0 +1,212 @@
import { storeActions } from "@/constants/store-actions";
import {
setCookieProperties,
getDeviceIdValue,
getSessionIdValue,
getSessionKeyValue,
} from "@/helpers/heritage-integration/cookie-helper";
import { queryStrings } from "@/constants/query-strings";
import { experimentSettings } from "@/constants/experiments";
import {
analyticsPageEvents,
GaCategories,
GaActions,
GaLabels,
GaEvents,
ValueToLogTypes,
} from "@/constants/analytics";
import { cookieNames } from "@/constants/cookie-names";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
export default {
methods: {
logPageView(pageEvent) {
const currentPageName = getPageNameByQueryString();
var payload = {
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: currentPageName,
sessionId: getSessionIdValue(),
action: "",
event: pageEvent,
shouldUseSessionId: false,
experimentsForUser: store.getters.applicationUser.experiments,
};
baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false);
},
logCustomEvent(category, action, label, value) {
const currentPageName = getPageNameByQueryString();
var payload = {
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: currentPageName,
sessionId: getSessionIdValue(),
category: category,
action: action,
label: label,
value: value,
shouldUseSessionId: false,
experimentsForUser: store.getters.applicationUser.experiments,
};
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false);
},
pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) {
const currentPageName = getPageNameByQueryString();
const labelToLog = getValueToLog(label, valueToLogType);
const eventToBePushed = {
event: GaEvents.GENERIC_EVENT,
category: category,
action: action,
label: labelToLog,
value: undefined,
path: `/fmg/?${queryStrings.FMG_PAGE}=${currentPageName}`,
};
pushToDataLayerIfDefined(eventToBePushed);
if (pushToLogApp) {
this.logCustomEvent(category, action, labelToLog, undefined);
}
},
pushPageViewToGA() {
const currentPageName = getPageNameByQueryString();
const pageViewEvent = {
event: GaEvents.PAGE_VIEW_EVENT,
pagePath: `/fmg/?${queryStrings.FMG_PAGE}=${currentPageName}`,
pageTitle: currentPageName,
};
pushToDataLayerIfDefined(pageViewEvent);
this.logPageView(analyticsPageEvents.ENTRY);
},
pushExperimentsToDataLayer() {
const experiments = store.getters.applicationUser.experiments;
experiments?.forEach((exp) => {
// Set Google Dimension Index based on experiment settings.
let googleDimensionIndex = 99;
if (exp.settings[experimentSettings.GOOGLE_CUSTOM_DIMENSION_INDEX] !== undefined) {
googleDimensionIndex =
exp.settings[experimentSettings.GOOGLE_CUSTOM_DIMENSION_INDEX];
}
// Create object with dimension index and value.
const experimentWithDimension = {
[`experimentId_${googleDimensionIndex}`]: exp.universeId,
[`variationId_${googleDimensionIndex}`]: exp.variationId,
[`experimentName_${googleDimensionIndex}`]: exp.universeName,
[`variationName_${googleDimensionIndex}`]: exp.variationName,
[`customDimension_${googleDimensionIndex}`]: `${exp.universeId}_${exp.variationId}_${exp.universeName}_${exp.variationName}`,
};
// Push to the data layer with the Google Custom Dimension Index.
pushToDataLayerIfDefined(experimentWithDimension);
});
},
prependActionToMethod(object, method, actionToPrepend) {
const baseMethodName = method.name.startsWith("bound ")
? method.name.substring(6)
: method.name;
const baseMethod = object[baseMethodName];
object[baseMethodName] = function () {
actionToPrepend.apply(this, arguments);
return baseMethod.apply(object, arguments);
};
},
async initSession() {
const sid = getSessionIdValue();
const skey = getSessionKeyValue();
var payload = {
userId: getDeviceIdValue(),
sessionId: sid,
userAgent: navigator.userAgent,
referrer: document.referrer,
};
const response = await baseMixin.methods.dispatchStoreAction(
storeActions.INITIALIZE_SESSION,
payload,
false
);
if (response.data) {
if (response.data.sessionKey && skey === 0) {
setCookieProperties(
{ [cookieNames.SESSION_KEY]: response.data.sessionKey },
{
useDefaultFunnelCookieAttributes: false,
}
);
}
if (response.data.sessionId && sid === "00000000-0000-0000-0000-000000000000") {
setCookieProperties(
{ [cookieNames.SESSION_ID]: response.data.sessionId },
{
maxAge: 60 * 30, // 30 minutes
}
);
}
}
},
noSession() {
return (
getSessionKeyValue() === 0 ||
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
);
},
},
computed: {
analyticsPageEvents() {
return analyticsPageEvents;
},
GaCategories() {
return GaCategories;
},
GaActions() {
return GaActions;
},
GaLabels() {
return GaLabels;
},
ValueToLogTypes() {
return ValueToLogTypes;
},
},
};
function pushToDataLayerIfDefined(data) {
if (window.dataLayer !== undefined) {
window.dataLayer.push(data);
}
}
function getPageNameByQueryString() {
const params = new URLSearchParams(location.search);
if (params.has(queryStrings.FMG_PAGE)) {
return params.get(queryStrings.FMG_PAGE);
} else {
return "";
}
}
function getValueToLog(value, valueToLogType) {
if (valueToLogType != null && valueToLogType === ValueToLogTypes.LAST_5) {
return value.slice(-5);
}
return value;
}

View file

@ -0,0 +1,299 @@
import analyticsMixin from "@/mixins/analytics-mixin";
import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import {
analyticsPageEvents,
GaCategories,
GaActions,
GaLabels,
GaEvents,
ValueToLogTypes,
} from "@/constants/analytics";
import store from "@/store";
describe("analyticsMixin.js", () => {
test("logPageView: calls dispatch with type and payload", () => {
const type = "";
const payload = {};
const mockData = {
actionList: [
{
actionName: storeActions.LOG_PAGE_VIEW,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
const testCookieValue = {
sid: "10000000-0000-0000-0000-000000000001",
};
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
analyticsMixin.methods.logPageView(type, payload);
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
});
test("logCustomEvent: calls dispatch with type and payload", () => {
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
analyticsMixin.methods.logCustomEvent("someCat", "someAction", "someLabel", "someVal");
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
});
test("pushEventToGA, should call dataLayer push and logCustomEvent too", () => {
// Arrange
window.dataLayer = [];
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
var mockDataLayer = [];
mockDataLayer.push({
event: "event",
category: "category",
action: "action",
label: "label",
value: undefined,
path: "/fmg/?fmgPage=",
});
// Act
analyticsMixin.methods.pushEventToGA("category", "action", "label", true);
// Assert
expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
});
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", () => {
// Arrange
window.dataLayer = [];
var expectedDataLayer = [];
expectedDataLayer.push({
event: "event",
category: "category",
action: "action",
label: "33333",
value: undefined,
path: "/fmg/?fmgPage=",
});
// Act
analyticsMixin.methods.pushEventToGA(
"category",
"action",
"1111122222333333",
false,
ValueToLogTypes.LAST_5
);
// Assert
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
});
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 logs only the last 3 characters for a 3 character string", () => {
// Arrange
window.dataLayer = [];
var expectedDataLayer = [];
expectedDataLayer.push({
event: "event",
category: "category",
action: "action",
label: "111",
value: undefined,
path: "/fmg/?fmgPage=",
});
// Act
analyticsMixin.methods.pushEventToGA(
"category",
"action",
"111",
false,
ValueToLogTypes.LAST_5
);
// Assert
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
});
test("Experiments, should push to dataLayer with default Google Custom Dimension Index", () => {
// Arrange
window.dataLayer = [];
const mockExperimentData = [
{
settings: {},
variationName: "test",
universeName: "testUniverse",
},
];
// Mock store
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
store.getters = {
applicationUser: {
experiments: [
{
settings: {},
variationName: "test",
universeName: "testUniverse",
},
],
},
};
// Act
analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData);
// Assert
expect(window.dataLayer).toEqual([
{
experimentId_99: undefined,
variationId_99: undefined,
experimentName_99: "testUniverse",
variationName_99: "test",
customDimension_99: "undefined_undefined_testUniverse_test",
},
]);
});
test("Experiments, should push to dataLayer with custom Google Custom Dimension Index", () => {
// Arrange
window.dataLayer = [];
const mockExperimentData = [
{
settings: { "Google Custom Dimension Index": "5" },
variationName: "test",
universeName: "testUniverse",
},
];
// Mock store
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
store.getters = {
applicationUser: {
experiments: [
{
settings: { "Google Custom Dimension Index": "5" },
variationName: "test",
universeName: "testUniverse",
},
],
},
};
// Act
analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData);
// Assert
expect(window.dataLayer).toEqual([
{
experimentId_5: undefined,
variationId_5: undefined,
experimentName_5: "testUniverse",
variationName_5: "test",
customDimension_5: "undefined_undefined_testUniverse_test",
},
]);
});
test("Obj is not null after action prepended", () => {
//Arrange
const obj = { baseMethodName: "testMethodName", data: "testData" };
const method = { name: "testMethodName", data: "testData" };
const action = "testAction";
//Act
analyticsMixin.methods.prependActionToMethod(obj, method, action);
//Assert
expect(obj != null);
});
test("Obj method name does not include bound", () => {
//Arrange
const obj = { baseMethodName: "testMethodName", data: "testData" };
const method = { name: "testMethodName", data: "testData" };
const action = "testAction";
//Act
analyticsMixin.methods.prependActionToMethod(obj, method, action);
//Assert
expect(method.name.startsWith("bound ")).toBe(false);
});
test("Prepended action does not include bound", () => {
//Arrange
const obj = { baseMethodName: "testMethodName", data: "testData" };
const method = { name: "testMethodName", data: "testData" };
const action = "testAction";
//Act
analyticsMixin.methods.prependActionToMethod(obj, method, action);
//Assert
expect(action.startsWith("bound ")).toBe(false);
});
test("analyticsPageEvents returns constants analyticsPageEvents", () => {
//Act
const analyticsPE = analyticsMixin.computed.analyticsPageEvents();
//Assert
expect(analyticsPE).toEqual(analyticsPageEvents);
});
test("GaActions returns constants GaActions", () => {
//Act
const gaActions = analyticsMixin.computed.GaActions();
//Assert
expect(gaActions).toEqual(GaActions);
});
test("GaCategories returns constants GaCategories", () => {
//Act
const gaCategories = analyticsMixin.computed.GaCategories();
//Assert
expect(gaCategories).toEqual(GaCategories);
});
test("GaLabels returns constants GaLabels", () => {
//Act
const gaLabels = analyticsMixin.computed.GaLabels();
//Assert
expect(gaLabels).toEqual(GaLabels);
});
});