Merge branch 'develop' into feature/CSR-92

This commit is contained in:
Max 2022-04-27 11:51:30 -04:00
commit c7c926ccad
34 changed files with 853 additions and 203 deletions

View file

@ -79,4 +79,44 @@ stages:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)
# QA Build/Deploy
- stage: Qa
variables:
- group: FixMyGlassQa
jobs:
- deployment: qaBuildDeployment
displayName: Build and Deploy FMG - QA
environment: digitalCloud-qa
container: node
workspace:
clean: all
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: templates/digital/step-build-vue.yml@AzureDevOps
parameters:
buildOutputDir: dist
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters:
artifactName: vueDist
awsProfile: $(qaDeploymentProfile)
outputPath: /fmg/
deployBuckets:
safelite-qa-fmg-us-east-1:
clearFolder: true
deployFolder: ''
region: us-east-1
appDeployVariables:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
cfDistributionId: $(cfDistributionId)

View file

@ -1,6 +1,10 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script>
window.dataLayer = [{}];
</script>
<script><%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY %></script>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
@ -11,9 +15,17 @@
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- Google Tag Manager -->
<noscript>
<iframe src="<%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC %>"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>

View file

@ -1,6 +1,9 @@
import { shallowMount } from "@vue/test-utils";
import buttonQuestion from "@/common-components/button-question/button-question";
import { nextTick } from "vue";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store",()=>{return{};},{virtual:true});
describe("buttonQuestion.vue", () => {
it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => {
@ -45,6 +48,65 @@ describe("buttonQuestion.vue", () => {
});
});
describe("buttonQuestion.vue", () => {
it("Fieldset classes should contain ui-radio if button type is radio", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
buttonType: "radio",
}
});
// Assert
const Div = wrapper.find('fieldset div');
expect(Div.classes()).toContain("ui-radio");
});
});
// testing a computed property
describe("buttonQuestion.vue", () => {
it("getColLength should return '12' if prop isWide is set to true", () => {
// Act
const localThis = { isWide: true }
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("12");
});
});
describe("buttonQuestion.vue", () => {
it("getColLength should return '' if prop isWide is set to false", () => {
// Act
const localThis = {
isWide: false,
answers: ['a', 'b']
}
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
});
});
describe("buttonQuestion.vue", () => {
it("Should return answer.Text if prop useTextForValue is true", async () => {
// Act
const localThis = { useTextForValue: true };
const answer = { 'Name': 'testName', 'Text': 'testText' };
// Assert
expect(buttonQuestion.methods.getValues.call(localThis, answer)).toBe('testText');
});
});
describe("buttonQuestion.vue", () => {
it("Should return answer.Name if prop useTextForValue is false and answer.Name exists", async () => {
// Act
const localThis = { useTextForValue: false };
const answer = { 'Name': 'testName', 'Text': 'testText' };
// Assert
expect(buttonQuestion.methods.getValues.call(localThis, answer)).toBe('testName');
});
});
describe("buttonQuestion.vue", () => {
it("Should trigger event modelValue change to new value on when radio button selected", async () => {
// Act
@ -76,3 +138,54 @@ describe("buttonQuestion.vue", () => {
});
});
describe("buttonQuestion.vue", () => {
it("Should add a value to this.selectedValues if prop isMultiSelect is true, checkValue is true and this.selectedValues already exists", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
isMultiSelect: true,
modelValue: [ 'a', 'b' ]
}
});
const val = { checkValue: true, value: "2021", }
wrapper.vm.handleCheckedChanged(val);
// Assert
expect(wrapper.vm.selectedValues).toEqual(["a", "b", "2021"]);
});
});
describe("buttonQuestion.vue", () => {
it("Should remove a value to this.selectedValues if prop isMultiSelect is true, checkValue is false and this.selectedValues already exists", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
isMultiSelect: true,
modelValue: [ 'a', 'b' ]
}
});
const val = { checkValue: false, value: "a", }
wrapper.vm.handleCheckedChanged(val);
// Assert
expect(wrapper.vm.selectedValues).toEqual(["b"]);
});
});
describe("buttonQuestion.vue", () => {
it("Should do nothing to this.selectedValues if this.selectedValues is not an array", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
isMultiSelect: true,
modelValue: 'a',
}
});
const val = { checkValue: true, value: "c", }
wrapper.vm.handleCheckedChanged(val);
// Assert
expect(wrapper.vm.selectedValues).toEqual("a");
});
});

View file

@ -35,6 +35,7 @@
:selectedValues="selectedValues"
data-test="button"
:validationRules="validationRules"
:class="[suppressError ? 'alertError' : '']"
/>
</div>
</fieldset>

View file

@ -66,6 +66,10 @@ const endpoints = {
LogExperimentExposureIfAssigned:{
url: "/analytics/api/v1/analytics/log-experiment-exposure",
method: "POST",
},
LogActivity:{
url: "/analytics/api/v1/analytics/activity",
method: "POST",
}
};

View file

@ -18,6 +18,7 @@ const storeActions = {
SET_REFERRAL_INFORMATION: "setReferralInformation",
VALIDATE_ZIP: "validateZip",
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
LOG_ACTIVITY: "logActivity",
// DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",

View file

@ -18,7 +18,7 @@ export function updateOrCreateFunnelCookie() {
ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId,
ReferralParentAccountNumber: store.getters.order.parentAccountNumber,
ReferralParentAccountNumber: store.getters.order.accountNumber,
});
}
@ -72,7 +72,7 @@ export function getDeviceIdValue(){
return cookieValueMatch[0].split('=')[1];
}
return '';
return '00000000-0000-0000-0000-000000000000';
}
/*
@ -88,6 +88,19 @@ export function getSessionKeyValue(){
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';
}
/*
===========================
= PRIVATE FUNCTIONS =

View file

@ -1,4 +1,4 @@
import {getFunnelCookie, getDeviceIdValue, getSessionKeyValue} from "@/helpers/heritage-integration/cookie-helper.js";
import {getFunnelCookie, getDeviceIdValue, getSessionKeyValue, getSessionIdValue} from "@/helpers/heritage-integration/cookie-helper.js";
import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper";
describe("cookies", () => {
@ -126,5 +126,19 @@ describe("cookies", () => {
});
});
describe("getSessionIdValue", () => {
test("getSessionIdValue, should return GUID", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionIdValue();
//Assert
expect(result).toBe('cba0c3d1-3c1b-4305-bb56-31aa50f58e27');
});
});
})

View file

@ -48,7 +48,7 @@ export async function navigateToHeritageFunnel() {
externalUrls.HERITAGE_FUNNEL,
{
corid: store.getters.order.referralCorrelationId,
src: "concept-funnel"
src: "concept-funnel",
}
);
}

View file

@ -41,6 +41,7 @@ export async function saveOrder() {
referralNumber: savedOrderInfo.data.referralNumber,
referralCorrelationId: savedOrderInfo.data.referralCorrelationId,
referralDate: savedOrderInfo.data.referralDate,
accountNumber: savedOrderInfo.data.accountNumber
}, false);
// Update the cookie with the referral information when saved.

View file

@ -62,6 +62,7 @@ export const cookies = {
"anotherCookie": "{}",
"someOtherCookie": "{}",
"dxdev": "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
"sid": "cba0c3d1-3c1b-4305-bb56-31aa50f58e27",
"skey": "12345"
};

View file

@ -281,7 +281,7 @@ export default {
navigateForward(partsData){
// Temporary easter egg to navigate to heritage funnel.
const vehicleYearsToShowHeritageFunnel = [ 2001, 2002, 2010 ];
const vehicleYearsToShowHeritageFunnel = [ 2001, 2002, 2010, 2016 ];
if (vehicleYearsToShowHeritageFunnel.includes(store.getters.vehicle.year)) {
navigateToHeritageFunnel();
return;

View file

@ -5,7 +5,7 @@
selectingInitiatesLoad
:questionText="questionText"
:answers="makes"
groupName="Choose Vehicle Make"
groupName="ChooseVehicleMake"
textPosition="text-start"
v-model="selectedValueAsArray"
isRequired=true

View file

@ -5,7 +5,7 @@
selectingInitiatesLoad
:questionText="questionText"
:answers="models"
groupName="Choose Vehicle Model"
groupName="ChooseVehicleModel"
textPosition="text-start"
v-model="selectedValueAsArray"
isRequired=true

View file

@ -5,7 +5,7 @@
selectingInitiatesLoad
:questionText="questionText"
:answers="styles"
groupName="Choose Vehicle Style"
groupName="ChooseVehicleStyle"
textPosition="text-start"
v-model="selectedValueAsArray"
isRequired=true

View file

@ -5,7 +5,7 @@
selectingInitiatesLoad
:questionText="questionText"
:answers="years"
groupName="Choose Vehicle Year"
groupName="ChooseVehicleYear"
textPosition="text-start"
v-model="selectedValueAsArray"
isRequired=true

View file

@ -5,6 +5,7 @@ import App from "./App.vue";
import router from "./router";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import analyticsMixin from "@/mixins/analytics-mixin.js";
import "../node_modules/bootstrap/dist/js/bootstrap.js";
// Vue App Setup
@ -15,5 +16,6 @@ vueApp.use(store);
vueApp.use(LoadScript);
vueApp.use(Maska);
vueApp.mixin(baseMixin);
vueApp.mixin(analyticsMixin);
vueApp.mount("#app");

View file

@ -0,0 +1,66 @@
import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin";
import { settleAllPromises } from "@/helpers/layout-helper";
import { getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import { queryStrings } from "@/constants/query-strings";
export default {
methods: {
async logPageEvent(destinationFmgPageValue, pageEvent){
const logActivityPromise = baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOG_ACTIVITY,
{
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: destinationFmgPageValue,
sessionId: getSessionIdValue(),
shouldUseSessionId: true,
pageEvent: {
action: '',
event: pageEvent,
}
}, false);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "logActivity",
promise: logActivityPromise,
},
];
let resultMap = await settleAllPromises(promiseResultMap);
},
pushEventToGA(category, action, label, value, pageName) {
const eventToBePushed = {
'event': 'ga_event',
'category': category,
'action': action,
'label': label,
'value': value,
'path': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`
}
pushToDataLayerIfDefined(eventToBePushed);
},
pushPageViewToGA(pageName) {
const pageViewEvent = {
'event': 'logPageview',
'pagePath': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`,
'pageTitle': pageName
};
pushToDataLayerIfDefined(pageViewEvent);
}
},
computed: {
storeActions() {
return storeActions;
},
},
};
function pushToDataLayerIfDefined(data) {
if (window.dataLayer !== undefined) {
window.dataLayer.push(data);
}
}

View file

@ -0,0 +1,21 @@
import analyticsMixin from "@/mixins/analytics-mixin";
import { setupMocksForJsFiles } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
describe("analyticsMixin.js", () => {
test("logPageEvent: calls dispatch with type and payload", () => {
const type = "";
const payload = {};
const mockData = {
actionList: [{
actionName: storeActions.LOG_ACTIVITY
}],
}
var mocks = setupMocksForJsFiles(mockData);
analyticsMixin.methods.logPageEvent(type, payload);
expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).toBeCalled();
});
});

View file

@ -11,10 +11,10 @@ export default {
};
},
methods: {
setCmsContent(cmsContent){
setCmsContent(cmsContent) {
this.$root.cmsContentByWidget = cmsContent;
},
getCmsContent(widgetName, fieldName){
getCmsContent(widgetName, fieldName) {
return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : '';
},
dispatchNonBlockingStoreAction(type, payload, encodePayload = true) {
@ -25,10 +25,10 @@ export default {
return store.dispatch(type, payload);
},
savePageDataToStore(page, data){
savePageDataToStore(page, data) {
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data });
},
onSubmit() {}, // DO NOT REMOVE; needed to prevent default form submit behavior
onSubmit() { }, // DO NOT REMOVE; needed to prevent default form submit behavior
onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it
// get error names array

View file

@ -4,6 +4,7 @@ import { storeActions } from "@/constants/store-actions";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events";
import { queryStrings } from "@/constants/query-strings";
// Heritage integration
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
@ -14,10 +15,12 @@ import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpe
import baseMixin from "@/mixins/base-mixin";
import eventBus from "@/helpers/event-bus/event-bus";
import store from "@/store";
import analyticsMixin from "@/mixins/analytics-mixin";
// Components
import ComponentTest from "@/layouts/component-test/component-test.vue";
import FormTest from "@/layouts/form-test/form-test.vue";
import { analyticsPageEvents } from "./router-constants/analytics-page-events";
const routes = [
{
@ -36,7 +39,6 @@ const routes = [
async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string.
try {
// If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
await GoToFunnelStartOn404(next);
@ -120,6 +122,11 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.afterEach((to, from) => {
analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]);
analyticsMixin.methods.logPageEvent(to.query[queryStrings.FMG_PAGE], analyticsPageEvents.ENTRY);
});
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => {
navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData);
}
@ -162,7 +169,7 @@ async function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery
if (getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) {
await saveOrder();
}
router.push({
name: "root",
query: Object.assign(optionalQuery, {

View file

@ -0,0 +1,5 @@
const analyticsPageEvents = {
ENTRY: "ENTRY",
};
export { analyticsPageEvents };

View file

@ -54,7 +54,7 @@ const getDefaultState = () => {
referralNumber: null,
referralDate: null,
referralCorrelationId: null,
parentAccountNumber: 0,
accountNumber: 0,
},
applicationUser: {
eventBus: [],
@ -124,7 +124,7 @@ export const mutations = {
state.order.referralDate = referralDate;
},
updateParentAcctNumber(state, parentAcctNumber) {
state.order.parentAccountNumber = parentAcctNumber;
state.order.accountNumber = parentAcctNumber;
},
updateIsInsurance(state, isInsurance) {
state.order.payment.isInsurance = isInsurance;
@ -234,7 +234,7 @@ export const mutations = {
state.order.damage.numberOfChips = orderInformation.numberOfChips;
state.order.lineItems.glassParts = orderInformation.parts;
state.order.parentAccountNumber = orderInformation.parentAccountNumber;
state.order.accountNumber = orderInformation.accountNumber;
state.order.serviceLocation.zipCode = orderInformation.zipCode;
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
@ -421,6 +421,42 @@ export const actions = {
});
},
logActivity(context, { userId, sessionKey, pageName, sessionId, pageEvent, customEvent, shouldUseSessionId }) {
var customEventData = {};
customEvent?.forEach(function(event)
{
var category = event.category;
var action = event.action;
var label = event.label;
var value = event.value;
customEventData[category] = {
category: category,
action: action,
label: label,
value: value
};
})
return globalMethods.callHttpClient({
method: endpoints.LogActivity.method,
endpoint: endpoints.LogActivity.url,
payload: {
userId: userId,
sessionKey: sessionKey,
sessionId: sessionId,
pageName: pageName,
applicationName: 'SafeliteDotCom',
shouldUseSessionId: shouldUseSessionId,
pageEvent: {
action: pageEvent.action,
event: pageEvent.event,
},
customEvent: customEventData
}
});
},
// Parts API Actions
getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) {
return globalMethods.callHttpClient({
@ -450,12 +486,14 @@ export const actions = {
make: vehicle.make,
model: vehicle.model,
style: vehicle.style,
vin: vehicle.vin
},
numberOfChips: damage.numberOfChips,
zipCode: 43215, // TODO CSR-416, should not be hardcoded (state.order.serviceLocation.zipCode)
glassToReplace: damage.glassToReplace,
referralNumber: context.state.order.referralNumber,
referralDate: context.state.order.referralDate
referralDate: context.state.order.referralDate,
accountNumber: context.state.order.accountNumber
},
});
},

View file

@ -221,7 +221,7 @@ describe("Mutations", () => {
isRepair: false,
numberOfChips: 0,
parts: [],
parentAccountNumber: "123456789",
accountNumber: "123456789",
insuranceInfo: {}
});
@ -617,6 +617,32 @@ describe("Actions", () => {
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx");
});
it("logActivity action, should return nothing", async () => {
// Arrange
const context = state;
var pageEvent = {
action: "",
event: "ENTRY",
}
var customEvent = [{
category: "tstCat",
action: "click",
label: "damage",
value: "psych"
}];
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ });
});
// Assert
const response = await actions.logActivity(context, { userId: "userId", sessionKey: "sessionKey", pageName: "pageName", sessionId: "sessionId", pageEvent: pageEvent, customEvent: customEvent, shouldUseSessionId: true });
expect(response).toEqual({});
});
});
describe("Getters", () => {

View file

@ -1,85 +1,126 @@
.has-error {
&.list-button,
&.list-card {
border: 1px solid $red;
color: $red;
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 0 2.5px $red;
}
input[type=checkbox]:checked + label {
box-shadow: 0 0 0 1px $red !important;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px !important;
}
}
&.list-button-horizontal {
color: $red;
label {
html {
.has-error {
&.list-button,
&.list-card {
border: 1px solid $red;
color: $red;
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 0 2.5px $red;
}
input[type=checkbox]:checked + label {
box-shadow: 0 0 0 1px $red;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px;
}
}
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 1px $red !important;
}
}
&.ui-radio,
&.ui-checkbox {
input[type=checkbox],
input[type=radio],
input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
&.list-button-horizontal {
color: $red;
}
input,
select {
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
label {
border: 1px solid $red;
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
}
}
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 1px $red;
}
}
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
&.ui-radio,
&.ui-checkbox {
input[type=checkbox],
input[type=radio],
input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
color: $red;
}
input,
select {
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
}
}
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
}
}
}
//Restore to default style if alert box is present
.alertError {
.has-error {
&.list-button,
&.list-card {
border: 1px solid $gray-500;
input:not(:focus) {
+ label {
box-shadow: 0 0 0 1px $gray-500;
border-radius: .5rem;
}
}
input:checked:focus {
+ label {
box-shadow: 0 0 0 2.5px $blue;
border-radius: .5rem;
}
}
input:checked:not(:focus) {
+ label {
box-shadow: 0 0 0 1px $blue;
border-radius: .5rem;
}
}
input:focus {
border: 1px solid $gray-500;
+ label {
box-shadow: 0 0 0 2.5px transparent;
}
}
&:hover {
box-shadow: 0 0 0 4px $blue-300;
+ label {
box-shadow: 0 0 0 2.5px transparent;
border: 1px solid $blue;
}
}
}
}
}
.form-test-error {
color: $red;
font-size: .875rem;
font-weight: 500;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
background: $gray-200;
cursor: pointer;
pointer-events: all;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200;
box-shadow: none;
}
}
}
.form-test-error {
color: $red;
font-size: .875rem;
font-weight: 500;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
background: $gray-200;
cursor: pointer;
pointer-events: all;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200 !important;
box-shadow: none !important;
}
}

View file

@ -100,7 +100,7 @@ describe("list-button-horizontal.vue", () => {
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
wrapper.vm.triggerButton();
await nextTick();
@ -123,7 +123,7 @@ describe("list-button-horizontal.vue", () => {
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
wrapper.vm.triggerButton();
await nextTick();
@ -146,7 +146,7 @@ describe("list-button-horizontal.vue", () => {
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
wrapper.vm.triggerButton();
await nextTick();
@ -195,4 +195,54 @@ describe("list-button-horizontal.vue", () => {
// Assert
expect(wrapper.componentVM.checkValue).toEqual("Car-Front");
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
selectingInitiatesLoad: false,
},
});
// Assert
wrapper.vm.handleInputChange();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isMultiSelect: true,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
selectingInitiatesLoad: false,
isMultiSelect: false,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
});

View file

@ -2,8 +2,11 @@
<div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
@keyup.space="triggerButton()"
@keyup.up="handleKeyupArrow()"
@keyup.down="handleKeyupArrow()"
@keyup.left="handleKeyupArrow()"
@keyup.right="handleKeyupArrow()"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -12,13 +15,14 @@
:value="value"
:aria-required="isRequired"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
@change="handleInputChange()"
/>
<label
tabindex="-1"
:for="buttonID"
:aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4"
@mouseup="triggerButton()"
>
<span
class="m-0"
@ -31,13 +35,16 @@
class="m-0 small"
:class="textPosition"
>
{{buttonLabelSubCopy}}
{{ buttonLabelSubCopy }}
</span>
<span v-if="screenReaderOnlyText" class="sr-only">
{{screenReaderOnlyText}}
<span
v-if="screenReaderOnlyText"
class="sr-only"
>
{{ screenReaderOnlyText }}
</span>
<loader
v-if="isLoaderDisplayed && !isMultiSelect"
v-if="isLoaderDisplayed && selectingInitiatesLoad"
:class="[loaderColor, loaderPosition]"
/>
</label>
@ -77,27 +84,47 @@ export default {
checkValue: Boolean,
};
},
created(){
if(Array.isArray(this.selectedValues)){
this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0];
created() {
if (Array.isArray(this.selectedValues)) {
this.checkValue = this.isMultiSelect
? this.selectedValues.includes(this.value)
: this.selectedValues[0];
}
},
methods: {
displayLoader() {
this.isLoaderDisplayed = true;
},
handleClick(value) {
if(this.selectingInitiatesLoad) {
this.displayLoader();
handleInputChange() {
if(!this.selectingInitiatesLoad) {
this.handleCheckChange();
}
},
handleKeyupArrow() {
if (this.isMultiSelect) {
return; // Prevent arrow keys from doing anything if element is a checkbox
}
if(!this.selectingInitiatesLoad) {
this.handleCheckChange();
}
this.handleChange(value);
this.handleChange(this.value);
},
handleCheckChange(newValue, oldValue){
const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) {
this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });
triggerButton() {
if(this.selectingInitiatesLoad) {
this.displayLoader();
this.handleCheckChange();
}
this.handleChange(this.value);
},
handleCheckChange() {
const emitEvent = {
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(),
};
this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
}
},
components: {
@ -105,6 +132,7 @@ export default {
},
setup(props) {
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const fieldOptions = {
type: inputType,
checkedValue: props.value,
@ -118,13 +146,11 @@ export default {
}
const {
checked,
handleChange,
errors,
} = useField(props.groupName, props.validationRules, fieldOptions);
return {
checked,
handleChange,
errors,
fieldOptions, // only need to expose this for unit test purposes
@ -137,10 +163,11 @@ export default {
.list-button-horizontal {
input[type="radio"],
input[type="checkbox"] {
position: absolute;
height: 0;
opacity: 0;
width: 0;
height: 0;
position: absolute;
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue;
z-index: 2;

View file

@ -96,11 +96,8 @@ describe("list-button.vue", () => {
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
wrapper.vm.triggerButton();
await nextTick();
@ -119,10 +116,8 @@ describe("list-button.vue", () => {
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
wrapper.vm.triggerButton();
await nextTick();
const loader = wrapper.find("loader-stub");
@ -140,11 +135,8 @@ describe("list-button.vue", () => {
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
wrapper.vm.triggerButton();
await nextTick();
@ -197,4 +189,53 @@ describe("list-button.vue", () => {
expect(wrapper.componentVM.checkValue).toEqual("Car-Front");
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
selectingInitiatesLoad: false,
},
});
// Assert
wrapper.vm.handleInputChange();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isMultiSelect: true,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
selectingInitiatesLoad: false,
isMultiSelect: false,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
});

View file

@ -2,8 +2,11 @@
<div
class="list-group list-button d-flex flex-column w-100 mb-2"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
@keyup.space="triggerButton()"
@keyup.up="handleKeyupArrow()"
@keyup.down="handleKeyupArrow()"
@keyup.left="handleKeyupArrow()"
@keyup.right="handleKeyupArrow()"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -12,13 +15,14 @@
:value="value"
:aria-required="isRequired"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
@change="handleInputChange()"
>
<label
tabindex="-1"
:for="buttonID"
:aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4"
@mouseup="triggerButton()"
>
<span
class="m-0"
@ -40,7 +44,7 @@
{{ screenReaderOnlyText }}
</span>
<loader
v-if="isLoaderDisplayed && !isMultiSelect"
v-if="isLoaderDisplayed && selectingInitiatesLoad"
:class="[this.loaderColor, this.loaderPosition]"
/>
</label>
@ -80,33 +84,47 @@ export default {
checkValue: Boolean,
};
},
created(){
if(Array.isArray(this.selectedValues)){
this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0];
created() {
if (Array.isArray(this.selectedValues)) {
this.checkValue = this.isMultiSelect
? this.selectedValues.includes(this.value)
: this.selectedValues[0];
}
},
methods: {
displayLoader() {
this.isLoaderDisplayed = true;
},
handleClick(value) {
handleInputChange() {
if(!this.selectingInitiatesLoad) {
this.handleCheckChange();
}
},
handleKeyupArrow() {
if (this.isMultiSelect) {
return; // Prevent arrow keys from doing anything if element is a checkbox
}
if(!this.selectingInitiatesLoad) {
this.handleCheckChange();
}
this.handleChange(this.value);
},
triggerButton() {
if(this.selectingInitiatesLoad) {
this.displayLoader();
this.handleCheckChange();
}
this.handleChange(value);
this.handleChange(this.value);
},
handleCheckChange(value, oldValue){
const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) {
const emitEvent = {
checkValue: this.checkValue,
value: this.value.toString(),
buttonId: this.buttonID.toString(),
};
this.$emit('isCheckedChanged', emitEvent);
this.$emit("update:modelValue", emitEvent);
}
handleCheckChange() {
const emitEvent = {
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(),
};
this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
},
},
components: {
@ -128,13 +146,11 @@ export default {
}
const {
checked,
handleChange,
errors,
} = useField(props.groupName, props.validationRules, fieldOptions);
return {
checked,
handleChange,
errors,
fieldOptions, // only need to expose this for unit test purposes
@ -154,10 +170,10 @@ export default {
opacity: 0;
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue inset;
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + label {
box-shadow: 0 0 0 2.5px $blue inset;
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + label {
color: $black;
@ -165,6 +181,9 @@ export default {
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + label {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + label p,
&:checked + label span {
font-weight: 500;

View file

@ -1,5 +1,6 @@
import { shallowMount } from "@vue/test-utils";
import listCard from "./list-card";
import { nextTick } from "vue";
describe("list-card.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
@ -18,7 +19,6 @@ describe("list-card.vue", () => {
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
});
@ -38,7 +38,6 @@ describe("list-card.vue", () => {
// Assert
const paragraph = wrapper.find("p");
expect(paragraph.text()).toEqual("Windshield");
});
@ -59,7 +58,6 @@ describe("list-card.vue", () => {
// Assert
const paragraph = wrapper.find("p:nth-of-type(2)");
expect(paragraph.text()).toEqual("Test");
});
@ -80,7 +78,6 @@ describe("list-card.vue", () => {
// Assert
const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox");
});
@ -101,7 +98,6 @@ describe("list-card.vue", () => {
// Assert
const input = wrapper.find("input");
expect(input.attributes().name).toEqual("radio 1");
});
@ -122,7 +118,6 @@ describe("list-card.vue", () => {
// Assert
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
@ -247,5 +242,88 @@ describe("list-card.vue", () => {
expect(wrapper.vm.fieldOptions.initialValue).toEqual([ 'Windshield' ]);
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
selectingInitiatesLoad: false,
},
});
// Assert
wrapper.vm.handleInputChange();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isMultiSelect: true,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
selectingInitiatesLoad: false,
isMultiSelect: false,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
it("Should run handleChange if triggerButton is triggered", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
selectingInitiatesLoad: false,
},
});
// Assert
wrapper.vm.triggerButton();
await nextTick();
expect(wrapper.vm.handleChange).toBeCalled;
expect(wrapper.vm.handleCheckChange).not.toBeCalled;
expect(wrapper.vm.displayLoader).not.toBeCalled;
});
it("Should run handleCheckChange and displayLoader if triggerButton is triggered and seletingInitiatesLoad is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
selectingInitiatesLoad: true,
},
});
// Assert
wrapper.vm.triggerButton();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
expect(wrapper.vm.displayLoader).toBeCalled;
});
});

View file

@ -6,8 +6,11 @@
isWide ? 'horizontal' : '',
(errors.length > 0 || hasError) ? 'has-error' : '',
]"
@mouseup="handleChange(value)"
@keyup.space="handleChange(value)"
@keyup.space="triggerButton()"
@keyup.up="handleKeyupArrow()"
@keyup.down="handleKeyupArrow()"
@keyup.left="handleKeyupArrow()"
@keyup.right="handleKeyupArrow()"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -16,13 +19,15 @@
:value="value"
:aria-required="isRequired"
v-model="checkValue"
@change="handleCheckChange(value)"
@change="handleInputChange()"
/>
<label
tabindex="-1"
:for="buttonID"
:aria-labelledby="buttonID"
class="d-flex w-100 align-items-center px-2 h-100"
:class="getLabelClasses"
tabindex="-1"
@mouseup="triggerButton()"
>
<img
:id="buttonImageId"
@ -93,15 +98,6 @@ export default {
: this.selectedValues[0];
}
},
watch: {
// Changing this will impact pre-selection data loads on vehicle-parts.
// If changed, please regression test that vehicle-parts data still loads correctly with previous selections.
modelValue(newVal) {
if (newVal !== undefined) {
this.checkValue = newVal.value;
}
},
},
computed: {
getLabelClasses() {
if (this.isWide) {
@ -116,16 +112,44 @@ export default {
},
},
methods: {
handleCheckChange(newValue, oldValue) {
const isInitialization = typeof oldValue === "function";
if (!isInitialization) {
const emitEvent = {
checkValue: this.checkValue,
value: this.value.toString(),
buttonId: this.buttonID.toString(),
};
this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
handleInputChange() {
if(!this.selectingInitiatesLoad) {
this.handleCheckChange();
}
},
handleKeyupArrow() {
if (this.isMultiSelect) {
return; // Prevent arrow keys from doing anything if element is a checkbox
}
if(!this.selectingInitiatesLoad) {
this.handleCheckChange();
}
this.handleChange(this.value);
},
triggerButton() {
if(this.selectingInitiatesLoad) {
this.displayLoader();
this.handleCheckChange();
}
this.handleChange(this.value);
},
handleCheckChange() {
const emitEvent = {
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(),
};
this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
},
},
watch: {
// Changing this will impact pre-selection data loads on vehicle-parts.
// If changed, please regression test that vehicle-parts data still loads correctly with previous selections.
modelValue(newVal) {
if (newVal !== undefined) {
this.checkValue = newVal.value;
}
},
},
@ -213,18 +237,17 @@ export default {
box-shadow: 0 0 0 1px $blue;
border-radius: 0.5rem;
}
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue;
border-radius: 0.5rem;
}
&:focus + label {
box-shadow: 0 0 0 2.5px $blue;
border-radius: 0.5rem;
}
&:checked {
&:focus + label {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked:focus + label {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + label {
p {
color: $black;

View file

@ -56,19 +56,15 @@ export default {
handleClick(value) {
this.handleChange(value);
},
handleCheckChange(newValue, oldValue) {
const isInitialization = typeof oldValue === "function";
if (!isInitialization) {
handleCheckChange() {
const emitEvent = {
checkValue: this.checkValue,
value: this.value.toString(),
buttonID: this.buttonID && this.buttonID.toString(),
};
const emitEvent = {
checkValue: this.checkValue,
value: this.value.toString(),
buttonID: this.buttonID.toString(),
};
this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
}
this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
},
},
setup(props) {

View file

@ -3,7 +3,13 @@ process.env.VUE_APP_CONSUMER_API_GATEWAY =
process.env.VUE_APP_HERITAGE_FUNNEL =
"http://localhost:38000/default.aspx";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY =
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo"
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo";
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-M6XCRH');";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC = "https://www.googletagmanager.com/ns.html?id=GTM-M6XCRH&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x";
module.exports = {
outputDir: "dist/fmg",

View file

@ -2,6 +2,10 @@ process.env.VUE_APP_CONSUMER_API_GATEWAY = "__VUE_APP_CONSUMER_API_GATEWAY__";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "__VUE_APP_GOOGLE_PLACES_API_KEY__";
process.env.VUE_APP_HERITAGE_FUNNEL = "__VUE_APP_HERITAGE_FUNNEL__";
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC = "__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__"
module.exports = {
outputDir: "dist/fmg",
publicPath: "/fmg",