Merge branch 'develop' into feature/CSR-2182-ajc

This commit is contained in:
Adam Caouette 2024-09-04 08:30:18 -04:00
commit 737ef41ad1
47 changed files with 11119 additions and 36820 deletions

View file

@ -1,5 +1,6 @@
{
"tabWidth": 4,
"bracketSameLine": true,
"printWidth": 100
"printWidth": 100,
"trailingComma" : "es5"
}

View file

@ -18,7 +18,7 @@ pr:
resources:
containers:
- container: node
image: packagerepository.sagaws.net:8082/safelite/node-build:15
image: packagerepository.sagaws.net:8082/safelite/node-build:18
- container: awscli
image: packagerepository.sagaws.net:8082/safelite/awscli2-build:3.9
- container: prettier-node
@ -28,7 +28,7 @@ resources:
type: github
name: Safelite/AzureDevOps
endpoint: Safelite
ref: refs/tags/t5.5.40
ref: refs/tags/t5.6.41
variables:
- group: Digital-Infrastructure
@ -37,15 +37,15 @@ variables:
stages:
# PR's
- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
- stage: TestFormat
displayName: Test Code Format
jobs:
- job: checkFormatting
displayName: Check formatting
container: prettier-node
steps:
- bash: prettier --check "$(Build.SourcesDirectory)/src/**/*.(js|vue)"
displayName: Run Prettier check
# - stage: TestFormat
# displayName: Test Code Format
# jobs:
# - job: checkFormatting
# displayName: Check formatting
# container: prettier-node
# steps:
# - bash: prettier --check "$(Build.SourcesDirectory)/src/**/*.(js|vue)"
# displayName: Run Prettier check
- stage: TestPr
displayName: Run Unit Tests For PullRequest

View file

@ -3,7 +3,7 @@ module.exports = {
coverageReporters: ["html", "text", "jest-junit", "cobertura"],
testResultsProcessor: "jest-junit",
preset: "@vue/cli-plugin-unit-jest",
transform: { "^.+\\.vue$": "vue-jest" },
transform: { "^.+\\.vue$": "@vue/vue3-jest" },
moduleFileExtensions: ["js", "vue"],
modulePathIgnorePatterns: ["vin-lookup"],
collectCoverageFrom: [

47337
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,42 +11,39 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@popperjs/core": "^2.10.2",
"axios": "^0.23.0",
"bootstrap": "^5.1.1",
"canvas-confetti": "^1.4.0",
"core-js": "^3.6.5",
"http-status-codes": "^2.1.4",
"iframe-resizer": "^4.3.7",
"jest-junit": "^13.0.0",
"bootstrap": "^5.3.3",
"core-js": "^3.38.1",
"http-status-codes": "^2.3.0",
"iframe-resizer": "^4.4.5",
"jest-junit": "^13.2.0",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.2",
"maska": "^1.5.0",
"vee-validate": "^4.5.7",
"vue": "^3.0.0",
"vue-plugin-load-script": "^2.1.0",
"vue-router": "^4.0.11",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0",
"yup": "^0.32.11"
"jquery-ui": "^1.14.0",
"maska": "^1.5.2",
"vee-validate": "^4.13.2",
"vue": "^3.4.38",
"vue-plugin-load-script": "^2.1.1",
"vue-router": "^4.4.3",
"vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^2.0.0-0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.0.0",
"prettier": "^2.2.1",
"sass": "^1.26.5",
"@babel/eslint-parser": "^7.25.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-unit-jest": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/compiler-sfc": "^3.4.38",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/test-utils": "^2.4.6",
"@vue/vue3-jest": "^27.0.0",
"eslint": "8.57",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.27.0",
"prettier": "^3.3.3",
"sass": "^1.77.8",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"vue-jest": "^5.0.0-0"
"typescript": "^4.9.5"
},
"eslintConfig": {
"root": true,
@ -58,7 +55,7 @@
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
"parser": "@babel/eslint-parser"
},
"rules": {
"no-unused-vars": "off"

View file

@ -2,5 +2,12 @@ module.exports = {
env: {
jest: true,
},
rules: {
"vue/multi-word-component-names": "off",
"vue/valid-next-tick": "off",
"vue/no-v-text-v-html-on-component": "off",
"vue/no-reserved-component-names": "off",
"vue/require-toggle-inside-transition": "off",
},
//...
};

View file

@ -31,6 +31,7 @@ const applicationConfig = {
"https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent",
FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging",
AFFILIATE_COOKIE_CONTAINING_NAME: "_Track",
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
};
export { applicationConfig };

View file

@ -10,6 +10,9 @@ const cookieNames = {
DXDEV: "dxdev",
SESSION_ID: "sid",
SESSION_KEY: "skey",
//Commission Junction Cookie
CJE: "cje",
};
const cookieExpirations = {
@ -17,6 +20,8 @@ const cookieExpirations = {
DXDEV: convertToSeconds({ years: 1 }),
FUNNEL_USER_ID: convertToSeconds({ weeks: 1 }),
FUNNEL_SESSION_KEY: convertToSeconds({ minutes: 30 }),
CJE: convertToSeconds({ days: 395 }),
NON_CJ: convertToSeconds({ days: 100000 }),
};
export { cookieNames, cookieExpirations };

View file

@ -36,6 +36,13 @@ const queryStrings = {
VIN_SELECTION: "vinselection",
SERVICE_PACKAGE: "servicepackage",
NUMBER_OF_CHIPS: "numberofchips",
UTM_SOURCE: "_source",
UTM_MEDIUM: "_medium",
UTM_CAMPAIGN: "_campaign",
CJ_EVENT: "cjevent",
CJUNCTION: "cjunction",
ORGANIC: "organic",
ORGANIC_SOCIAL: "organic_social",
};
export { queryStrings };

View file

@ -61,7 +61,7 @@ describe("baseInputButton.vue", () => {
describe("mouse clicks", () => {
describe("checkbox", () => {
describe("clicked once", () => {
test("correct event and value are emitted", async () => {
test("correct event and value are emitted and input is checked", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
@ -71,31 +71,12 @@ describe("baseInputButton.vue", () => {
},
},
});
// Act
await wrapper.trigger("click");
await wrapper.find("input").setChecked();
// Assert
expect(wrapper.emitted()["update:modelValue"][0][0]).toEqual(["X"]);
});
test("input is checked", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
value: "X",
},
},
});
const input = wrapper.find("input");
// Act
await wrapper.trigger("click");
// Assert
expect(input.element.checked).toBe(true);
expect(wrapper.find("input").element.checked).toBe(true);
});
});
@ -135,25 +116,7 @@ describe("baseInputButton.vue", () => {
});
const input = wrapper.find("input");
// Act
await wrapper.trigger("click");
// Assert
expect(input.element.checked).toBe(false);
});
test("input is unchecked", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
value: "X",
modelValue: ["X"],
},
},
});
const input = wrapper.find("input");
expect(input.element.checked).toBe(true);
// Act
await wrapper.trigger("click");

View file

@ -1147,7 +1147,12 @@ function setupMocks(mountOptionsMockData = {}) {
const baseMountOptions = getMountOptions(
Object.assign(defaultMountOptions, mountOptionsMockData)
);
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
// Add in group name to avoid vee-validate failures
if (!allMountOptions.propsData.groupName) {
allMountOptions.propsData.groupName = "group-name";
}
return allMountOptions;
}

View file

@ -177,8 +177,8 @@ export default {
const baseClasses = this.isOverflowScrollable
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
: this.buttonTypeString == "listCard"
? "w-100"
: "";
? "w-100"
: "";
const withSmallQuestionClass = this.isSmallQuestionText
? baseClasses + " small-question-text"
: baseClasses;

View file

@ -732,7 +732,9 @@ export default {
margin: 0 auto 2rem auto;
max-width: 414px;
position: relative;
transition: height ease 2s, opacity ease 2s;
transition:
height ease 2s,
opacity ease 2s;
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
@ -805,7 +807,9 @@ export default {
outline: none;
height: 1.5rem;
opacity: 1;
transition: height ease 250ms, opacity ease 250ms;
transition:
height ease 250ms,
opacity ease 250ms;
input[type="radio"] {
position: absolute; //override bootstrap
@ -818,7 +822,9 @@ export default {
&:focus + label,
&:checked:focus + label {
box-shadow: 0 0 0 3px #fff, 0 0 0 5.5px #1574a1;
box-shadow:
0 0 0 3px #fff,
0 0 0 5.5px #1574a1;
background-color: $blue;
color: $white;
&.current-day {

View file

@ -76,13 +76,17 @@ export default {
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
}
&:focus {
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
color: $white;
background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%);
}
@ -100,7 +104,9 @@ export default {
&.has-loader {
color: $white;
background: $blue-700;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
pointer-events: none;
}
&.delay {
@ -125,7 +131,9 @@ export default {
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px $white,
0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}

View file

@ -1,7 +1,9 @@
import { cookieNames, cookieExpirations } from "@/constants/cookie-names";
import store from "@/store";
import { applicationConfig } from "@/constants/application-config";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { queryStrings } from "@/constants/query-strings";
//import { cookieParameters } from "@/constants/cookie-parameters";
/*
Will update the cookie if present, or create a new one if not.
*/
@ -221,6 +223,79 @@ export function getAffiliateCookies() {
return affiliateCookies;
}
export function setupAdvertiserTracking() {
var utmSource = getQuerystringParameter(queryStrings.UTM_SOURCE);
var utmMedium = getQuerystringParameter(queryStrings.UTM_MEDIUM);
var utmCampaign = getQuerystringParameter(queryStrings.UTM_CAMPAIGN);
const cjEvent = getQuerystringParameter(queryStrings.CJ_EVENT);
// helper check for if an object is defined (but maybe falsey)
const isDefined = (x) => x !== null && x !== undefined;
if (isDefined(cjEvent) != "") {
const cjeCookieName = cookieNames.CJE;
if (!isCookieSet(cjeCookieName)) {
createCJEventCookie(cjEvent, cjeCookieName);
}
}
//Firstly, Can't continue without utm-source or utm-medium.
//Secondly, Ignore the advertiser for CJ-affiliate-click and Email-click.
if (
!isDefined(utmSource) ||
isDefined(utmSource) == "" ||
!isDefined(utmMedium) ||
isDefined(utmMedium) == "" ||
utmSource?.toLowerCase() == queryStrings.CJUNCTION ||
utmMedium?.toLowerCase() == queryStrings.EMAIL ||
utmMedium?.toLowerCase() == queryStrings.ORGANIC ||
utmMedium?.toLowerCase() == queryStrings.ORGANIC_SOCIAL
) {
return;
}
//UTC date time format yyyy-MM-dd/HH:mm:ss
const currentDateTime = new Date().toISOString().replace(/T/, "/").replace(/\..+/, "");
//Format cookie, by removing spaces and special characters.
utmSource = FormatCookieElementName(utmSource);
utmMedium = FormatCookieElementName(utmMedium);
utmCampaign = FormatCookieElementName(utmCampaign);
var cookieNameBuilder = utmSource + "_" + utmMedium;
//Since utmCampaign is optional, ignore the rest.
if (isDefined(utmCampaign) && utmCampaign != "") {
cookieNameBuilder = cookieNameBuilder + "_" + utmCampaign;
}
const cookieName =
(cookieNameBuilder.length > applicationConfig.COOKIE_NAME_LENGTH_MAX_LIMIT
? cookieNameBuilder.substring(0, applicationConfig.COOKIE_NAME_LENGTH_MAX_LIMIT)
: cookieNameBuilder) + applicationConfig.AFFILIATE_COOKIE_CONTAINING_NAME;
if (!isCookieSet(cookieName)) {
const values = [
`utm_source= ${utmSource}`,
`utm_medium= ${utmMedium}`,
`utm_campaign= ${utmCampaign}`,
`timestamp= ${currentDateTime}`,
`tagEvent= ${cjEvent}`,
];
let valueString = values.join("&");
createOrUpdateCookie(cookieName, valueString, {
useDefaultFunnelCookieAttributes: true,
maxAge: cookieExpirations.NON_CJ,
});
} else {
const cookieValue = getCookieValueByName(cookieName);
if (cookieValue) {
let cookieValueObj = convertStringToObject(cookieValue);
cookieValueObj.timestamp = currentDateTime; // Update the property
// Set the updated cookie
const cookieValueString = objectToString(cookieValueObj);
createOrUpdateCookie(cookieName, cookieValueString, {
useDefaultFunnelCookieAttributes: true,
maxAge: cookieExpirations.NON_CJ,
});
}
}
}
/*
===========================
= PRIVATE FUNCTIONS =
@ -253,7 +328,7 @@ function setFunnelCookieProperties(properties) {
function createOrUpdateCookie(
key,
value = "",
{ useDefaultFunnelCookieAttributes = true, maxAge, isSecure = true }
{ useDefaultFunnelCookieAttributes = true, maxAge, isSecure = true, crossSiteAccess = false }
) {
let cookieToAdd = `${key}=${value}; `;
@ -266,6 +341,9 @@ function createOrUpdateCookie(
if (!isNaN(maxAge)) {
cookieToAdd += `max-age=${maxAge};`;
}
if (crossSiteAccess && isSecure) {
cookieToAdd += `sameSite=None;`;
}
document.cookie = cookieToAdd;
}
@ -315,3 +393,42 @@ function getCookiesContainingName(name) {
}
return matchingCookies;
}
function createCJEventCookie(cjEvent, cookieName) {
createOrUpdateCookie(cookieName, cjEvent, {
useDefaultFunnelCookieAttributes: true,
maxAge: cookieExpirations.CJE,
isSecure: true,
crossSiteAccess: true,
});
}
function FormatCookieElementName(cookie) {
// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
const problemSymbols = '\\s\\(\\)<>@,;:\\\\/\\[\\]\\?=\\{}"';
const otherSymbols = "!#$%^&+\\-*|~`";
const symbolsToIgnore = new RegExp("[" + problemSymbols + otherSymbols + "]+", "g");
return (cookie || "").replace(symbolsToIgnore, ".");
}
function convertStringToObject(str) {
let obj = {};
let pairs = str.split("&");
pairs.forEach((pair) => {
let [key, value] = pair.split("=");
obj[key] = value;
});
return obj;
}
function objectToString(obj) {
let str = "";
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
str += key + "=" + obj[key] + "&";
}
}
// Remove the trailing '&'
return str.slice(0, -1);
}

View file

@ -9,6 +9,7 @@ import { settleAllPromises } from "@/helpers/layout-helper";
import experimentMixin from "@/mixins/experiment-mixin";
import { experimentSettings } from "@/constants/experiments";
import { includesWindshieldReplacement } from "@/helpers/damage-helper";
import { applicationConfig } from "@/constants/application-config.js";
import store from "@/store";
import router from "@/router";
@ -33,7 +34,7 @@ export async function getDirectNavigation(toRoute) {
// vehicle -> vehicle-damage
// quote -> vin-lookup/estimate
// insurance-company -> vin-lookup/estimate
if (store.getters.requiresVerifiedRedirecting) {
if (store.getters.payment?.insuranceCoverage?.isVerified) {
if (fmgPageValue === fmgPageValues.VEHICLE) {
return fmgPageValues.VEHICLE_DAMAGE;
}
@ -106,6 +107,8 @@ export async function getImplicitNavigation(toRoute) {
*/
export async function navigateToHeritageFunnel({ shouldSaveSession, pageNameToLog }) {
console.log(new Date() + " navigateToHeritageFunnel: " + JSON.stringify(applicationConfig));
console.log(new Date() + " navigateToHeritageFunnel: " + applicationConfig.HERITAGE_FUNNEL);
// Create the order (or save existing order) when navigating to Heritage Funnel.
if (shouldSaveSession) {
await saveSession({ pageNameToLog: pageNameToLog, shouldAwaitSaveSessionQueue: true });
@ -133,7 +136,7 @@ export async function navigateToHeritageFunnel({ shouldSaveSession, pageNameToLo
heritageParms["forceEndToEndInsurance"] = true;
}
router.navigateToExternalUrl(externalUrls.HERITAGE_FUNNEL, heritageParms);
router.navigateToExternalUrl(applicationConfig.HERITAGE_FUNNEL, heritageParms);
}
export async function skipVinLookup() {

View file

@ -174,6 +174,7 @@ describe("capabilityQuestions.vue", () => {
test("Should trigger handleCompletedQuestionChainAnswers if watched data changes", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.handleCompletedQuestionChainAnswers = jest.fn();
const spy = jest.spyOn(wrapper.vm, "handleCompletedQuestionChainAnswers");
// Act

View file

@ -215,9 +215,10 @@ describe("estimate.vue", () => {
describe("estimate.vue", () => {
test("should call forwardButtonAction if isExternalParameter is true and form is valid", async () => {
store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, true);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_VIN_SELECTION, "decline");
store.getters = {
isExternalParameter: true,
externalParameterEstimate: { vinSelection: "decline" },
};
// Set up the component
const { wrapper } = setupMocks({});

View file

@ -178,6 +178,7 @@ describe("moldingQuestions.vue", () => {
test("Should trigger handleCompletedQuestionChainAnswers if watched data changes", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.handleCompletedQuestionChainAnswers = jest.fn();
const spy = jest.spyOn(wrapper.vm, "handleCompletedQuestionChainAnswers");
// Act
@ -268,6 +269,7 @@ describe("moldingQuestions.vue", () => {
},
};
});
const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction");
// Act

View file

@ -164,6 +164,7 @@ describe("partQuestions.vue...", () => {
order: {},
};
const { wrapper } = setupMocks({});
wrapper.vm.handleCompletedQuestionChainAnswers = jest.fn();
const spy = jest.spyOn(wrapper.vm, "handleCompletedQuestionChainAnswers");
// Act

View file

@ -225,12 +225,12 @@ export default {
// instead of what comes from the backend. This is to prevent issues with part interchange.
const availableFrontWipers = orderHasFrontWipers
? frontWipersOnOrder
: resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.FRONT_WIPER) ??
[];
: (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.FRONT_WIPER) ??
[]);
const availableRearWipers = orderHasRearWipers
? rearWipersOnOrder
: resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.REAR_WIPER) ??
[];
: (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.REAR_WIPER) ??
[]);
const lineItemsToTax = [
resultMap.rainDefense,
@ -526,14 +526,18 @@ export default {
await submitWorkOrder({ pageNameToLog: "payment-method", submitAfterSave: true });
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: false }
);
} else {
if (this.paymentMethod == paymentMethods.INSURANCE) {
this.dispatchStoreAction(this.storeActions.SAVE_PAYMENT_TYPE, true, false);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_INSURANCE,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: false }
);
} else {
this.setupPia();
@ -555,14 +559,16 @@ export default {
} catch (error) {
console.log("error: response from pia submit work order:" + error.message);
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
this.$route.params[routerParams.DISPLAY_PIA_ALERT] = true;
this.$route.query[queryStrings.DISPLAY_PIA_ALERT] = true;
return;
}
}
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_PAY_NOW,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: false }
);
},
hasSubmittedOrder() {
@ -683,7 +689,7 @@ export default {
shouldDisplayPiaAlert() {
return (
this.$route.query[queryStrings.DISPLAY_PIA_ALERT] ||
this.$route.params[routerParams.DISPLAY_PIA_ALERT]
eval(window.history.state.displayPiaAlert)
);
},
// Necessary to make the watcher of lineItems work

View file

@ -747,7 +747,7 @@ export default {
shouldDisplayPiaAlert(payMethod) {
return (
this.$route.query[queryStrings.DISPLAY_PIA_ALERT] === payMethod ||
this.$route.params[routerParams.DISPLAY_PIA_ALERT] === payMethod
eval(window.history.state.displayPiaAlert)
);
},
},

View file

@ -125,6 +125,7 @@ import { partTypeStrings } from "@/constants/part-type-strings";
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
import { nextTick } from "vue";
import { packageNames } from "@/constants/package-names";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
export default {
@ -271,6 +272,20 @@ export default {
validateAlerts[0].shouldAutoFade
);
}
const payment = store.getters.payment;
const policy = store.getters.policy;
if (payment?.isInsurance) {
const vehicleChangedDuringPolicyLookupInHeritage =
payment.insuranceCoverage?.coverageStatus && policy?.policyNumber !== "";
// This is a safeguard to prevent insurance from seeing quote page again.
// Usually this is caught in vehicle-questions-mixin with a referral length 6 check
if (vehicleChangedDuringPolicyLookupInHeritage) {
navigateToHeritageFunnel({ shouldSaveSession: true, pageNameToLog: "quote" });
}
}
if (store.getters.isExternalParameter) {
if (store.getters.externalParameterQuote.isInsurance == true) {
vm.isInsuranceSelected = true;

View file

@ -384,6 +384,7 @@ describe("service-package-question.vue", () => {
},
],
});
wrapper.vm.selectDefaultPackage = jest.fn();
const selectDefaultPackageMock = jest.spyOn(wrapper.vm, "selectDefaultPackage");
await nextTick();

View file

@ -233,7 +233,10 @@ export default {
},
getDiscountedPackagePriceString(packageName, servicePackageDiscount) {
const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, { discountedPrice: true, servicePackageDiscount })
this.getPackagePrice(packageName, {
discountedPrice: true,
servicePackageDiscount,
})
).toFixed(2);
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
},

View file

@ -101,6 +101,7 @@ describe("service-package-radio.vue", () => {
});
const mockProps = {
groupName: "group-name",
buttonLabel: "buttonLabel test copy",
buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy",
buttonLabelSubCopy: "buttonLabelSubCopy test copy",

View file

@ -173,7 +173,8 @@ export default {
width: 100%;
padding: 1rem;
border: 1px solid $gray-300;
box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15),
box-shadow:
0px 4px 8px -4px rgba(0, 0, 0, 0.15),
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
@ -220,7 +221,9 @@ export default {
+ .package-label {
&:before {
border: 1px solid #8e9292;
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
box-shadow:
0px 0px 0px 4px #9fcee6,
0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}

View file

@ -309,7 +309,7 @@ describe("service-zip.vue", () => {
// Act
const wrapper = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
jest.spyOn(wrapper.vm, "navigateForwardWithSingleCarMatch").mockImplementation();
await wrapper.vm.forwardButtonAction();
@ -329,7 +329,7 @@ describe("service-zip.vue", () => {
// Act
const wrapper = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
jest.spyOn(wrapper.vm, "navigateForwardWithSingleCarMatch").mockImplementation();
await wrapper.vm.forwardButtonAction();
@ -397,6 +397,7 @@ describe("service-zip.vue", () => {
wrapper.vm.displayInvalidZipAlert = true;
wrapper.vm.displayNonServiceableZipAlert = true;
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
jest.spyOn(wrapper.vm, "navigateForwardWithSingleCarMatch").mockImplementation();
await wrapper.vm.forwardButtonAction();

View file

@ -87,7 +87,6 @@ import {
skipVinLookupNotRepair,
} from "@/helpers/heritage-integration/navigation-helper";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
import baseMixin from "@/mixins/base-mixin.js";
import { queryStrings } from "@/constants/query-strings";
@ -208,17 +207,7 @@ export default {
const payment = this.$store.getters.payment;
const policy = this.$store.getters.policy;
const vehicleChangedDuringPolicyLookupInHeritage =
payment.isInsurance &&
payment.insuranceCoverage.coverageStatus &&
policy.policyNumber !== "";
if (vehicleChangedDuringPolicyLookupInHeritage) {
navigateToHeritageFunnel({
shouldSaveSession: true,
pageNameToLog: "estimate",
});
} else if (this.$store.getters.order.referralNumber?.length === 6) {
if (this.$store.getters.order.referralNumber?.length === 6) {
await this.navigateForwardWithSingleCarMatch();
} else if (this.isRepair) {
const supportingItemsPromise = await this.dispatchStoreActionWithLogging(

View file

@ -75,8 +75,13 @@ export default {
: [];
return filteredAnswers.map((ans) => {
const newName = ans.Name.includes("-") ? ans.Name.split("-")[1] : ans.Name;
ans.Name = newName;
return ans;
return {
Name: newName,
AnswerImageUrl: ans.AnswerImageUrl,
ImageId: ans.ImageId,
SubText: ans.SubText,
Text: ans.Text,
};
});
},
},

View file

@ -83,8 +83,13 @@ export default {
return filteredAnswers.map((ans) => {
const newName = ans.Name.includes("-") ? ans.Name.split("-")[1] : ans.Name;
ans.Name = newName;
return ans;
return {
Name: newName,
AnswerImageUrl: ans.AnswerImageUrl,
ImageId: ans.ImageId,
SubText: ans.SubText,
Text: ans.Text,
};
});
},
shouldDisplayReplaceOptionsQuestion() {

View file

@ -142,8 +142,13 @@ export default {
return filteredAnswers.map((ans) => {
const newName = ans.Name.includes("-") ? ans.Name.split("-")[1] : ans.Name;
ans.Name = newName;
return ans;
return {
Name: newName,
AnswerImageUrl: ans.AnswerImageUrl,
ImageId: ans.ImageId,
SubText: ans.SubText,
Text: ans.Text,
};
});
},
isDriverSideReplaceOptionsQuestionAvailable() {

View file

@ -310,12 +310,11 @@ describe("vehicle-damage.vue", () => {
describe("alert", () => {
test("when displayVehicleChangeAlert router params is true, the alert: 'vehicleChangeAlert' should be rendered", () => {
// Arrange & Act
window.history.pushState({ displayVehicleChangeAlert: "true" }, "", "");
const { wrapper } = setupMocks({
mountOptionsMockData: {
route: {
params: {
[routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true,
},
state: { displayVehicleChangeAlert: true },
},
},
});
@ -325,21 +324,24 @@ describe("vehicle-damage.vue", () => {
test("when displayVehicleChangeAlert router params is false, the alert: 'vehicleChangeAlert' should not be rendered", () => {
// Arrange & Act
window.history.pushState({ displayVehicleChangeAlert: "false" }, "", "");
const { wrapper } = setupMocks({
mountOptionsMockData: {
route: {
params: {
state: {
[routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false,
},
},
},
});
// Assert
expect(wrapper.findComponent({ ref: "vehicleChangeAlert" }).exists()).toBe(false);
});
test("when displayVehicleChangeAlert router params is undefined, the alert: 'vehicleChangeAlert' should not be rendered", () => {
// Arrange & Act
window.history.pushState({ displayVehicleChangeAlert: "false" }, "", "");
const { wrapper } = setupMocks({
mountOptionsMockData: {
route: {
@ -850,7 +852,7 @@ function setupMocks({ pageHeaderWidgetHeaderText, mountOptionsMockData, funnelCo
navigateWithSaving: jest.fn(),
},
route: {
params: {
state: {
[routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false,
},
},

View file

@ -103,6 +103,7 @@ import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routerParams } from "@/router/router-constants/router-params";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
@ -405,20 +406,26 @@ export default {
if (skipVin) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false }
);
} else {
if (store.getters.damage.isRepair) {
this.$router.navigateWithSaving(
this.navigationScenarios
.CLICKED_FORWARD_WITH_REPAIR_AND_VERIFIED_INSURANCE,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false }
);
} else {
this.$router.navigateWithSaving(
this.navigationScenarios
.CLICKED_FORWARD_WITH_REPLACE_AND_VERIFIED_INSURANCE,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false }
);
}
}
@ -427,17 +434,23 @@ export default {
else if (store.getters.vehicle.vin) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_VIN,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false }
);
} else if (skipVin) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false }
);
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false }
);
}
},
@ -566,7 +579,7 @@ export default {
);
},
shouldDisplayVehicleChangeAlert() {
return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT];
return eval(window.history.state.displayVehicleChangeAlert);
},
shouldHideBackButton() {
return this.$store.getters.requiresVerifiedRedirecting;

View file

@ -66,6 +66,11 @@ defineRule(
defineRule(
"check-for-repair-and-replace",
(selectedWindshieldDamageType, selectedDamageLocations) => {
// vee-validate adds an extra arrray on initial selection
// remove here
selectedDamageLocations[0] = selectedDamageLocations[0].filter((location) => {
return !Array.isArray(location);
});
return (
selectedWindshieldDamageType.toString() != damageLocationsSelected.REPAIR ||
(!selectedDamageLocations.includes(damageLocationsSelected.WINDSHIELD) &&

View file

@ -73,7 +73,7 @@ const basePartResponse = {
};
describe("vehicle-parts.vue", () => {
test("Set cms content called on load", async (done) => {
test("Set cms content called on load", (done) => {
//Arrange
store.getters.pageData.mockReturnValue(basePartResponse);
store.getters.lineItems = { glassParts: null };

View file

@ -398,11 +398,7 @@ export default {
},
methods: {
isDisabled(options) {
if (
!options.length ||
store.getters.requiresVerifiedRedirecting ||
getFunnelCookie()?.HasDelayedClaimRegistration
) {
if (!options.length || store.getters.payment?.insuranceCoverage?.isVerified) {
return true;
} else {
return false;

View file

@ -222,12 +222,7 @@ export default {
// Provider Ctu
if (isDefined(order.serviceLocation.zipCodeCtu)) {
const parsedCtu = parseInt(order.serviceLocation.zipCodeCtu);
if (!isNaN(parsedCtu)) {
payload.providerCtu = parseInt(order.serviceLocation.zipCodeCtu);
} else {
payload.providerCtu = "";
}
payload.providerCtu = order.serviceLocation.zipCodeCtu;
} else {
payload.providerCtu = "";
}

View file

@ -27,8 +27,8 @@ describe("input-button-wrapper-mixin", () => {
expect(inputButtonTwo.vm.isMultiSelect).toBe(true);
expect(wrapper.vm.value).toEqual([]);
await inputButtonOne.find("input").trigger("click");
await inputButtonTwo.find("input").trigger("click");
await inputButtonOne.find("input").setChecked();
await inputButtonTwo.find("input").setChecked();
// Assert
expect(wrapper.vm.value).toEqual(["value1", "value2"]);
@ -52,9 +52,9 @@ describe("input-button-wrapper-mixin", () => {
expect(inputButtonTwo.vm.isMultiSelect).toBe(true);
expect(wrapper.vm.value).toEqual([]);
await inputButtonOne.find("input").trigger("click");
await inputButtonTwo.find("input").trigger("click");
await inputButtonOne.find("input").trigger("click");
await inputButtonOne.find("input").setChecked();
await inputButtonTwo.find("input").setChecked();
await inputButtonOne.find("input").setChecked(false);
// Assert
expect(wrapper.vm.value).toEqual(["value2"]);
@ -80,8 +80,8 @@ describe("input-button-wrapper-mixin", () => {
expect(inputButtonTwo.vm.isMultiSelect).toBe(false);
expect(wrapper.vm.value).toEqual("");
await inputButtonOne.find("input").trigger("click");
await inputButtonTwo.find("input").trigger("click");
await inputButtonOne.find("input").setChecked();
await inputButtonTwo.find("input").setChecked();
// Assert
expect(wrapper.vm.value).toEqual("value2");
@ -105,9 +105,9 @@ describe("input-button-wrapper-mixin", () => {
expect(inputButtonTwo.vm.isMultiSelect).toBe(false);
expect(wrapper.vm.value).toEqual("");
await inputButtonOne.find("input").trigger("click");
await inputButtonTwo.find("input").trigger("click");
await inputButtonOne.find("input").trigger("click");
await inputButtonOne.find("input").setChecked();
await inputButtonTwo.find("input").setChecked();
await inputButtonOne.find("input").setChecked();
// Assert
expect(wrapper.vm.value).toEqual("value1");
@ -295,23 +295,20 @@ function setupBaseInputButtonWrapper({ mockData = {} }) {
parentComponentTemplate += `<baseInputButtonWrapper v-model="value" groupName="myGroupName" :isMultiSelect="${mockData.isMultiSelect}" value="value1" />`;
parentComponentTemplate += `<baseInputButtonWrapper v-model="value" groupName="myGroupName" :isMultiSelect="${mockData.isMultiSelect}" value="value2" />`;
parentComponentTemplate += `</div>`;
const wrapper = mount(
{
data() {
return {
value: mockData.initialValue ?? (mockData.isMultiSelect ? [] : ""),
$route: {
query: {
fmgPage: "myPage",
},
const wrapper = mount({
data() {
return {
value: mockData.initialValue ?? (mockData.isMultiSelect ? [] : ""),
$route: {
query: {
fmgPage: "myPage",
},
};
},
template: parentComponentTemplate,
components: { baseInputButtonWrapper },
},
};
},
{}
);
template: parentComponentTemplate,
components: { baseInputButtonWrapper },
});
return { wrapper };
}

View file

@ -123,9 +123,8 @@ export default {
answerString;
// mark this question as suppressed if needed (question-chain uses this)
if (answeredQuestion.suppressThisQuestion) {
glass.questions[
answeredQuestion.questionNum - 1
].suppressThisQuestion = true;
glass.questions[answeredQuestion.questionNum - 1].suppressThisQuestion =
true;
}
});

View file

@ -19,6 +19,7 @@ import {
updateSessionIdCookie,
deleteFunnelCookie,
getAffiliateCookies,
setupAdvertiserTracking,
} from "@/helpers/heritage-integration/cookie-helper";
import { loadSessionIfPresent, saveSession } from "@/helpers/heritage-integration/order-helper";
import {
@ -103,7 +104,7 @@ const routes = [
if (affiliateCookies != null && affiliateCookies.length > 0) {
store.commit(storeMutations.UPDATE_AFFILIATE_COOKIES, affiliateCookies);
}
setupAdvertiserTracking();
const loadSessionResponse = await loadSessionIfPresent(
to.query.isInsurance != null
? to.query.isInsurance == "true"
@ -129,7 +130,7 @@ const routes = [
// Reroute around pages that should not be available.
// Exception 1: Integrated Users should not see vehicle or quote pages.
if (store.getters.requiresVerifiedRedirecting) {
if (store.getters.payment?.insuranceCoverage?.isVerified) {
if (to.query.fmgPage === fmgPageValues.VEHICLE) {
to.query.fmgPage = fmgPageValues.VEHICLE_DAMAGE;
} else if (
@ -286,7 +287,7 @@ router.afterEach(async (to, from) => {
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
// If saving on navigation is requested, check for saved SessionId or EmailAddress to determine if saving is appropriate
if (eval(to.params.isSavingNavigation)) {
if (eval(window.history.state.isSavingNavigation)) {
if (
store.getters.applicationUser.savedSessionId ||
store.getters.order.customer?.emailAddress
@ -394,7 +395,7 @@ async function navigate(
destinationFmgPageValue,
Object.keys(optionalPageData).length > 0
? optionalPageData
: existingPageDataForPage ?? {}
: (existingPageDataForPage ?? {})
);
optionalParams.isSavingNavigation = isSavingNavigation;
@ -430,7 +431,7 @@ async function navigate(
router.push({
name: "root",
query: Object.assign(optionalQuery, queryStringsObject),
params: optionalParams,
state: optionalParams,
});
} else if (matchingScenarioMap.destinationUrl !== undefined) {
navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery);
@ -524,7 +525,7 @@ async function DisplayPageError() {
// we use the pageError querystring as a counter to how many times a user has experienced an error and been routed here.
// once they receive more than 1 pageerrors, we'll reset their state to hopefully correct any issues they may be having.
var navPage = fmgPageValues.VEHICLE;
if (store.getters.requiresVerifiedRedirecting) {
if (store.getters.payment?.insuranceCoverage?.isVerified) {
navPage = fmgPageValues.VEHICLE_DAMAGE;
}
@ -590,7 +591,7 @@ async function runExperiments(nextPage) {
}
function getRedirectedStartPage() {
if (store.getters.requiresVerifiedRedirecting) {
if (store.getters.payment?.insuranceCoverage?.isVerified) {
return fmgPageValues.VEHICLE_DAMAGE;
} else {
return funnelStartPageName;

View file

@ -214,7 +214,9 @@ html {
box-shadow: none;
}
&.btn.btn-primary:focus-visible {
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $gray-700;
box-shadow:
0 0 0 3px $white,
0 0 0 5.5px $gray-700;
}
}
}

View file

@ -75,13 +75,17 @@ export default {
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
}
&:focus {
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
color: $white;
background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%);
}
@ -99,7 +103,9 @@ export default {
&.has-loader {
color: $white;
background: $blue-700;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
pointer-events: auto;
}
&.delay {
@ -124,7 +130,9 @@ export default {
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px $white,
0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}

View file

@ -27,6 +27,7 @@ describe("checkbox.vue", () => {
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: {
groupName: "checkbox-button-test",
buttonLabel: "label text",
value: "test value",
},
@ -42,7 +43,15 @@ describe("checkbox.vue", () => {
it("Should have correct screenreader-only text", async () => {
// Act
let { wrapper } = setupMocks({});
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: {
groupName: "checkbox-button-test",
buttonLabel: "label text",
value: "test value",
},
},
});
// Arrange
await wrapper.setProps({

View file

@ -141,7 +141,9 @@ export default {
}
&:checked:focus + .list-button-horizontal-content {
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
box-shadow:
0 0 0 3px,
0 0 0 5.5px $blue-700;
}
&:checked + .list-button-horizontal-content p:first-child {

View file

@ -295,6 +295,12 @@ describe("list-button.vue", () => {
});
function setupMocks({ mockData }) {
// Add in group name to avoid vee-validate failures
if (!mockData.propsData) {
mockData.propsData = { groupName: "group-name" };
} else if (!mockData.propsData.groupName) {
mockData.propsData.groupName = "group-name";
}
const wrapper = mount(listButton, {
...mockData,
mixins: [inputButtonWrapperMixin],

View file

@ -57,6 +57,12 @@ describe("radio.vue", () => {
});
function setupMocks({ mountOptionsMockData = {} }) {
// Add in group name to avoid vee-validate failures
if (!mountOptionsMockData.propsData) {
mountOptionsMockData.propsData = { groupName: "group-name" };
} else if (!mountOptionsMockData.propsData.groupName) {
mountOptionsMockData.propsData.groupName = "group-name";
}
const wrapper = mount(radio, {
...mountOptionsMockData,
mixins: [inputButtonWrapperMixin],