Merge branch 'develop' into feature/CSR-1851

This commit is contained in:
Leah Schumann 2023-12-07 06:42:29 -05:00
commit 2ea2176865
20 changed files with 786 additions and 153 deletions

View file

@ -73,6 +73,8 @@ body select {
background-position: 95% 50%; background-position: 95% 50%;
background-size: 15px 9px; background-size: 15px 9px;
appearance: none; appearance: none;
font-size: 16px;
color: #000;
} }
body .creditCardSpecific div { body .creditCardSpecific div {
padding: 8px 0; padding: 8px 0;

View file

@ -85,6 +85,8 @@ body {
background-position: 95% 50%; background-position: 95% 50%;
background-size: 15px 9px; background-size: 15px 9px;
appearance: none; appearance: none;
font-size: 16px;
color: #000;
} }
.creditCardSpecific { .creditCardSpecific {

View file

@ -6,6 +6,7 @@ const applicationConfig = {
COOKIE_PATH: "/", COOKIE_PATH: "/",
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod" CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
APPLICATION_NAME: "FixMyGlass", APPLICATION_NAME: "FixMyGlass",
ANALYTICS_APPLICATION_NAME: "FixMyGlassNextGen",
APPLICATION_ABBREVIATION: "fmg", APPLICATION_ABBREVIATION: "fmg",
PAGE_QUERYSTRING: "fmgPage", PAGE_QUERYSTRING: "fmgPage",
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass", SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass",

View file

@ -3,10 +3,32 @@ import { applicationConfig } from "@/constants/application-config.js";
const cookieNames = { const cookieNames = {
FUNNEL_SESSION_INFO: `FunnelSessionInfo-${applicationConfig.CURRENT_ENVIRONMENT}`, FUNNEL_SESSION_INFO: `FunnelSessionInfo-${applicationConfig.CURRENT_ENVIRONMENT}`,
FUNNEL_SESSION_KEY: `FunnelSessionKey-${applicationConfig.CURRENT_ENVIRONMENT}`,
FUNNEL_USER_ID: `FunnelUserId-${applicationConfig.CURRENT_ENVIRONMENT}`,
// Existing Safelite.com cookies // Existing Safelite.com cookies
DXDEV: "dxdev", DXDEV: "dxdev",
SESSION_ID: "sid", SESSION_ID: "sid",
SESSION_KEY: "skey", SESSION_KEY: "skey",
}; };
export { cookieNames }; const cookieExpirations = {
SESSION_ID: convertToSeconds({ minutes: 30 }),
DXDEV: convertToSeconds({ years: 1 }),
FUNNEL_USER_ID: convertToSeconds({ weeks: 1 }),
FUNNEL_SESSION_KEY: convertToSeconds({ minutes: 30 }),
};
export { cookieNames, cookieExpirations };
function convertToSeconds({ years, months, weeks, days, hours, minutes, seconds }) {
let total = seconds ?? 0;
total += (minutes ?? 0) * 60;
total += (hours ?? 0) * 60 * 60;
total += (days ?? 0) * 24 * 60 * 60;
total += (weeks ?? 0) * 7 * 24 * 60 * 60;
total += (months ?? 0) * 30 * 24 * 60 * 60;
total += (years ?? 0) * 365 * 24 * 60 * 60;
return total;
}

View file

@ -1,29 +1,31 @@
<template> <template>
<div class="textarea-question"> <div class="textarea-question">
<div class="label-wrapper mb-1" :aria-label="questionText"> <div class="label-wrapper d-flex flex-column mb-1" :aria-label="questionText">
<!-- Wrap label and span because v-html prevents v-if from displaying if v-if <span> is inside <label>--> <!-- Wrap label and span because v-html prevents v-if from displaying if v-if <span> is inside <label>-->
<label for="textarea-question" class="fw-bold" v-html="questionText"></label> <div class="d-flex mb-1">
<span v-if="!isRequired" class="fw-normal ms-1">(Optional)</span> <label :for="textAreaLabelCopy" class="fw-bold" v-html="questionText"></label>
<span v-if="!isRequired" class="fw-normal ms-1">(Optional)</span>
</div>
<textarea
:id="textAreaLabelCopy"
ref="textarea"
v-model="value"
v-maska="mask"
@keyup="updateCount"
class="p-4"
:maxlength="maxLength"
role="textbox"
aria-multiline="true"
:aria-required="isRequired">
</textarea>
<p
tabindex="-1"
class="caption mt-2 mb-0 w-100"
id="charactersRemaining"
:class="[urgentCountdown ? 'urgent-countdown' : '']">
{{ remainingCount }}/{{ maxLength }} characters remaining
</p>
</div> </div>
<textarea
id="textareaQuestion"
ref="textarea"
v-model="value"
v-maska="mask"
@keyup="updateCount"
class="p-4"
:maxlength="maxLength"
role="textbox"
aria-multiline="true"
:aria-required="isRequired">
</textarea>
<p
tabindex="-1"
class="caption mt-2 mb-0"
id="charactersRemaining"
:class="[urgentCountdown ? 'urgent-countdown' : '']">
{{ remainingCount }}/{{ maxLength }} characters remaining
</p>
</div> </div>
</template> </template>
@ -38,6 +40,10 @@ export default {
default: 150, default: 150,
}, },
modelValue: String, modelValue: String,
textAreaLabelCopy: {
type: String,
default: "text area",
},
}, },
// TODO: At some point in the future we should probably add the tie in to validation here in case the field must be populated for some other use cases // TODO: At some point in the future we should probably add the tie in to validation here in case the field must be populated for some other use cases
setup() {}, setup() {},
@ -74,7 +80,7 @@ export default {
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.textarea-question { .textarea-question {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -83,7 +89,6 @@ export default {
} }
.label-wrapper { .label-wrapper {
display: flex; display: flex;
align-items: center;
label { label {
span { span {
color: $gray-500; color: $gray-500;

View file

@ -16,8 +16,7 @@
<div class="price-table"> <div class="price-table">
<div class="service-type"> <div class="service-type">
<textBlock :cmsWidgetName="servicePackageTitleWidget" /> <textBlock :cmsWidgetName="servicePackageTitleWidget" />
<span tabindex="0"> <span>
<span class="sr-only"> {{ screenReaderPackagePriceText }} </span>
{{ getFormattedAmount("", packagePrice) }} {{ getFormattedAmount("", packagePrice) }}
</span> </span>
</div> </div>
@ -64,40 +63,25 @@
<span v-else-if="cartItem == recycleFeeCartItem"> <span v-else-if="cartItem == recycleFeeCartItem">
{{ recycleFeeCartItem.name }} {{ recycleFeeCartItem.name }}
</span> </span>
<span tabindex="0" <span>{{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}</span>
><span class="sr-only">{{ screenReaderRecycleFeeText }}</span
>{{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}</span
>
</div> </div>
<!-- Sub total, sales tax, total columns --> <!-- Sub total, sales tax, total columns -->
<div class="sub-total"> <div class="sub-total">
<span>{{ subtotalText }}</span <span>{{ subtotalText }}</span
><span tabindex="0" ><span>{{ getFormattedAmount("", subTotal) }}</span>
><span class="sr-only">{{ screenReaderSubTotalText }}</span
>{{ getFormattedAmount("", subTotal) }}</span
>
</div> </div>
<div class="sales-tax"> <div class="sales-tax">
<span>{{ salesTaxText }}</span <span>{{ salesTaxText }}</span
><span tabindex="0" ><span>{{ getFormattedAmount("", salesTax) }}</span>
><span class="sr-only">{{ screenReaderSalesTaxText }}</span
>{{ getFormattedAmount("", salesTax) }}</span
>
</div> </div>
<div v-if="showAsPaid" class="amount-paid"> <div v-if="showAsPaid" class="amount-paid">
<span>{{ amountPaidText }}</span <span>{{ amountPaidText }}</span
><span tabindex="0" ><span>{{ getFormattedAmount("", amountPaid) }}</span>
><span class="sr-only">{{ screenReaderAmountPaidText }}</span
>{{ getFormattedAmount("", amountPaid) }}</span
>
</div> </div>
<div class="amount-due"> <div class="amount-due">
<span>{{ amountDueText }}</span <span>{{ amountDueText }}</span
><span tabindex="0" ><span>{{ getFormattedAmount("", amountDue) }}</span>
><span class="sr-only">{{ screenReaderTotalAmountDueText }}</span
>{{ getFormattedAmount("", amountDue) }}</span
>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
import { cookieNames } from "@/constants/cookie-names"; import { cookieNames, cookieExpirations } from "@/constants/cookie-names";
import store from "@/store"; import store from "@/store";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
@ -78,11 +78,43 @@ export function getDeviceIdValue() {
return "00000000-0000-0000-0000-000000000000"; return "00000000-0000-0000-0000-000000000000";
} }
export function regenerateDeviceId() {
if (!isCookieSet(cookieNames.DXDEV)) {
setCookieProperties(
{
[cookieNames.DXDEV]: `did=${crypto.randomUUID()}`,
},
{ maxAge: cookieExpirations.DXDEV }
);
}
}
export function getUserIdValue() {
const cookieValue = getCookieValueByName(cookieNames.FUNNEL_USER_ID);
if (cookieValue) {
return cookieValue;
}
return "00000000-0000-0000-0000-000000000000";
}
export function regenerateUserId() {
if (!isCookieSet(cookieNames.FUNNEL_USER_ID)) {
setCookieProperties(
{
[cookieNames.FUNNEL_USER_ID]: crypto.randomUUID(),
},
{ maxAge: cookieExpirations.FUNNEL_USER_ID }
);
}
}
/* /*
Gets value of skey cookie, returns 0 if not found. Gets value of skey cookie, returns 0 if not found.
*/ */
export function getSessionKeyValue() { export function getSessionKeyValue() {
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY); const cookieValue = getCookieValueByName(cookieNames.FUNNEL_SESSION_KEY);
if (cookieValue) { if (cookieValue) {
return cookieValue; return cookieValue;
@ -91,6 +123,17 @@ export function getSessionKeyValue() {
return 0; return 0;
} }
export function setSessionKeyIfUnset(value) {
if (!isCookieSet(cookieNames.FUNNEL_SESSION_KEY)) {
setCookieProperties(
{
[cookieNames.FUNNEL_SESSION_KEY]: value,
},
{ maxAge: cookieExpirations.FUNNEL_SESSION_KEY }
);
}
}
/* /*
Gets value of skey cookie, returns 0 if not found. Gets value of skey cookie, returns 0 if not found.
*/ */
@ -104,11 +147,24 @@ export function getSessionIdValue() {
return "00000000-0000-0000-0000-000000000000"; return "00000000-0000-0000-0000-000000000000";
} }
export function setSessionIdIfUnset(value) {
if (!isCookieSet(cookieNames.SESSION_ID)) {
setCookieProperties(
{
[cookieNames.SESSION_ID]: value,
},
{ maxAge: cookieExpirations.SESSION_ID }
);
}
}
/* /*
Updates session ID cookie with new expiration date Updates session ID cookie with new expiration date
*/ */
export function updateSessionIdCookie() { export function updateSessionIdCookie() {
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), { maxAge: 60 * 30 }); createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), {
maxAge: cookieExpirations.SESSION_ID,
});
} }
export function setCookieProperties( export function setCookieProperties(
@ -126,6 +182,34 @@ export function setCookieProperties(
} }
} }
export function isCookieSet(name) {
const val = getCookieValueByName(name);
return !!val;
}
export function areAllSessionCookiesSet() {
return (
isCookieSet(cookieNames.SESSION_ID) &&
isCookieSet(cookieNames.DXDEV) &&
isCookieSet(cookieNames.FUNNEL_SESSION_KEY) &&
isCookieSet(cookieNames.FUNNEL_USER_ID)
);
}
export function refreshSessionExpiration() {
refreshCookieExpiration(cookieNames.SESSION_ID, cookieExpirations.SESSION_ID);
refreshCookieExpiration(cookieNames.DXDEV, cookieExpirations.DXDEV);
refreshCookieExpiration(cookieNames.FUNNEL_USER_ID, cookieExpirations.FUNNEL_USER_ID);
refreshCookieExpiration(cookieNames.FUNNEL_SESSION_KEY, cookieExpirations.FUNNEL_SESSION_KEY);
}
export function refreshCookieExpiration(name, expirationTime) {
if (isCookieSet(name)) {
createOrUpdateCookie(name, getCookieValueByName(name), { maxAge: expirationTime });
}
}
/* /*
=========================== ===========================
= PRIVATE FUNCTIONS = = PRIVATE FUNCTIONS =

View file

@ -3,10 +3,25 @@ import {
getDeviceIdValue, getDeviceIdValue,
getSessionKeyValue, getSessionKeyValue,
getSessionIdValue, getSessionIdValue,
setCookieProperties,
regenerateDeviceId,
getUserIdValue,
regenerateUserId,
setSessionKeyIfUnset,
setSessionIdIfUnset,
isCookieSet,
refreshCookieExpiration,
} from "@/helpers/heritage-integration/cookie-helper.js"; } from "@/helpers/heritage-integration/cookie-helper.js";
import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper"; import { cookieNames } from "@/constants/cookie-names";
import { removeAllTestCookies, setupCookies, setupCrypto } from "@/helpers/unit-test-helper";
const randomUUID = "68d89736-c277-46f1-8fee-c3dacdb23c08";
describe("cookies", () => { describe("cookies", () => {
beforeEach(() => {
setupCrypto(randomUUID);
});
afterEach(() => { afterEach(() => {
removeAllTestCookies(); removeAllTestCookies();
}); });
@ -106,40 +121,284 @@ describe("cookies", () => {
}); });
}); });
describe("getDeviceIdValue", () => { describe("Device Id", () => {
test("getDeviceIdValue, should return GUID", () => { describe("getDeviceIdValue", () => {
// Arrange test("getDeviceIdValue, should return GUID", () => {
setupCookies({}); // Arrange
setupCookies({});
// Act // Act
const result = getDeviceIdValue(); const result = getDeviceIdValue();
//Assert //Assert
expect(result).toBe("21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe"); expect(result).toBe("21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe");
});
test("Should return 0s if unset", () => {
//Arrange
//Act
const result = getDeviceIdValue();
//Assert
expect(result).toBe("00000000-0000-0000-0000-000000000000");
});
test("Should return id even if cookie contains other data", () => {
//Arrange
setCookieProperties(
{
[cookieNames.DXDEV]:
"did=f4a1a9e8-b3f3-4936-8c30-2f06a98644af&tz=-300&tzd=1",
},
{}
);
//Act
const result = getDeviceIdValue();
//Assert
expect(result).toBe("f4a1a9e8-b3f3-4936-8c30-2f06a98644af");
});
}); });
test("getSessionKeyValue, should return session key int", () => { describe("regenerateDeviceId", () => {
// Arrange test("Generates a new id if unset", () => {
setupCookies({}); // Arrange
// Act
regenerateDeviceId();
const result = getDeviceIdValue();
// Act // Assert
const result = getSessionKeyValue(); expect(result).not.toBe("00000000-0000-0000-0000-000000000000");
expect(global.crypto.randomUUID).toBeCalled();
});
//Assert test("Does not create a new id if already set", () => {
expect(result).toBe("12345"); // Arrange
setupCookies({});
// Act
regenerateDeviceId();
const result = getDeviceIdValue();
// Assert
expect(result).toBe("21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe");
expect(global.crypto.randomUUID).not.toBeCalled();
});
}); });
}); });
describe("getSessionIdValue", () => { describe("User Id", () => {
test("getSessionIdValue, should return GUID", () => { describe("getUserIdValue", () => {
test("Should return GUID", () => {
// Arrange
setupCookies({});
// Act
const result = getUserIdValue();
//Assert
expect(result).toBe("11aec5e8-92ba-4dc9-a8b6-179a916d8d7a");
});
test("Should return 0s if unset", () => {
//Arrange
//Act
const result = getUserIdValue();
//Assert
expect(result).toBe("00000000-0000-0000-0000-000000000000");
});
});
describe("regenerateUserId", () => {
test("Generates a new id if unset", () => {
// Arrange
// Act
regenerateUserId();
const result = getUserIdValue();
// Assert
expect(result).not.toBe("00000000-0000-0000-0000-000000000000");
expect(global.crypto.randomUUID).toBeCalled();
});
test("Does not create a new id if already set", () => {
// Arrange
setupCookies({});
// Act
regenerateUserId();
const result = getUserIdValue();
// Assert
expect(result).toBe("11aec5e8-92ba-4dc9-a8b6-179a916d8d7a");
expect(global.crypto.randomUUID).not.toBeCalled();
});
});
});
describe("Session Key", () => {
describe("getSessionKeyValue", () => {
test("Should return session key int", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionKeyValue();
//Assert
expect(result).toBe("12345");
});
test("Should return 0 if unset", () => {
// Arrange
// Act
const result = getSessionKeyValue();
//Assert
expect(result).toBe(0);
});
});
describe("setSessionKeyIfUnset", () => {
test("Should set the cookie if not previously set", () => {
// Arrange
// Act
setSessionKeyIfUnset("54321");
const result = getSessionKeyValue();
// Assert
expect(result).toBe("54321");
});
test("Should not set the cookie if previously set", () => {
// Arrange
setupCookies({});
// Act
setSessionKeyIfUnset("54321");
const result = getSessionKeyValue();
// Assert
expect(result).toBe("12345");
});
});
});
describe("Session Id", () => {
describe("getSessionIdValue", () => {
test("Should return session id", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionIdValue();
//Assert
expect(result).toBe("cba0c3d1-3c1b-4305-bb56-31aa50f58e27");
});
test("Should return 0s if unset", () => {
// Arrange
// Act
const result = getSessionIdValue();
//Assert
expect(result).toBe("00000000-0000-0000-0000-000000000000");
});
});
describe("setSessionIdIfUnset", () => {
test("Should set the cookie if not previously set", () => {
// Arrange
// Act
setSessionIdIfUnset("f01f463a-a02c-4d1a-8aaf-6ca920ae5f02");
const result = getSessionIdValue();
// Assert
expect(result).toBe("f01f463a-a02c-4d1a-8aaf-6ca920ae5f02");
});
test("Should not set the cookie if previously set", () => {
// Arrange
setupCookies({});
// Act
setSessionIdIfUnset("f01f463a-a02c-4d1a-8aaf-6ca920ae5f02");
const result = getSessionIdValue();
// Assert
expect(result).toBe("cba0c3d1-3c1b-4305-bb56-31aa50f58e27");
});
});
});
describe("isCookieSet", () => {
test("Returns true if cookie is set and unexpired", () => {
// Arrange // Arrange
setupCookies({}); setupCookies({});
// Act // Act
const result = getSessionIdValue(); const result = isCookieSet(cookieNames.SESSION_ID);
//Assert // Assert
expect(result).toBe("cba0c3d1-3c1b-4305-bb56-31aa50f58e27"); expect(result).toBe(true);
});
test("Returns false if never set", () => {
// Arrange
// Act
const result = isCookieSet(cookieNames.SESSION_ID);
// Assert
expect(result).toBe(false);
});
test("Returns false if cookie is expired", () => {
// Arrange
setupCookies({});
// Act
setCookieProperties(
{
[cookieNames.SESSION_ID]: "test",
},
{ maxAge: 0 }
);
const result = isCookieSet(cookieNames.SESSION_ID);
// Assert
expect(result).toBe(false);
});
});
describe("refreshCookieExpiration", () => {
test("Preserves value", () => {
// Arrange
setupCookies({});
// Act
const result1 = getSessionIdValue();
refreshCookieExpiration(cookieNames.SESSION_ID, 1000000);
const result2 = getSessionIdValue();
// Assert
expect(result1).toBe(result2);
});
test("Does not set cookie if not already set", () => {
// Arrange
// Act
refreshCookieExpiration(cookieNames.SESSION_ID, 1000000);
const result = isCookieSet(cookieNames.SESSION_ID);
expect(result).toBe(false);
}); });
}); });
}); });

View file

@ -100,7 +100,8 @@ export const cookies = {
someOtherCookie: "{}", someOtherCookie: "{}",
dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe", dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
sid: "cba0c3d1-3c1b-4305-bb56-31aa50f58e27", sid: "cba0c3d1-3c1b-4305-bb56-31aa50f58e27",
skey: "12345", [cookieNames.FUNNEL_SESSION_KEY]: "12345",
[cookieNames.FUNNEL_USER_ID]: "11aec5e8-92ba-4dc9-a8b6-179a916d8d7a",
}; };
// Removes test cookies for testing cookie-helper and order-helper // Removes test cookies for testing cookie-helper and order-helper
@ -138,6 +139,14 @@ export function setupCookies({ funnelCookieValue = "", includeHeritageCookie = t
}); });
} }
export function setupCrypto(mockValue) {
global.crypto = {
randomUUID: jest.fn(),
};
global.crypto.randomUUID.mockImplementation(() => mockValue);
}
// Private methods // Private methods
function setupBaseMixinDispatchStoreAction(mockData) { function setupBaseMixinDispatchStoreAction(mockData) {
if (mockData.actionList !== undefined) { if (mockData.actionList !== undefined) {

View file

@ -300,7 +300,7 @@ export default {
return serviceLocationReqs && scheduleReqs && customerReqs; return serviceLocationReqs && scheduleReqs && customerReqs;
}, },
forwardButtonAction() { forwardButtonAction() {
window.location.assign("//www.safelite.com/"); window.location.assign(location.protocol + "//" + location.host);
}, },
}, },
components: { components: {

View file

@ -48,6 +48,7 @@
class="mb-4" class="mb-4"
v-model="techNotes" v-model="techNotes"
cmsWidgetName="TextAreaContentWidget" cmsWidgetName="TextAreaContentWidget"
textAreaLabelCopy="Notes for your technician"
maxLength="150" /> maxLength="150" />
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" /> <textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />

View file

@ -532,7 +532,7 @@ export default {
} }
cartItems.forEach((item) => { cartItems.forEach((item) => {
if (item.name !== null) { if (item.name !== null && item.category != "promos") {
lineItems.push(`${item.name}|${(item.salesTax + item.subTotal).toFixed(2)}|1`); lineItems.push(`${item.name}|${(item.salesTax + item.subTotal).toFixed(2)}|1`);
} }
}); });

View file

@ -252,6 +252,7 @@ export default {
this.$emit("updated-mobile-fee-part", mobileFeePart); this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data); this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase); this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
// update the page level model // update the page level model
this.$emit("update:modelValue", this.internalModel); this.$emit("update:modelValue", this.internalModel);

View file

@ -83,6 +83,7 @@
@updated-mobile-fee-part="setMobileFeePart" @updated-mobile-fee-part="setMobileFeePart"
@updated-serviceability="setServiceabilityDetails" @updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase" @updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile"
validationRules="mobile-location-required" validationRules="mobile-location-required"
ref="mobileLocationQuestions" ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget" linkWidgetName="MobileLocationLinkWidget"
@ -128,6 +129,7 @@ import contentGroupModal from "@/fmg-components/content-group-modal/content-grou
// Supporting files // Supporting files
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
@ -181,6 +183,7 @@ export default {
zipContainsMilitaryBase: false, zipContainsMilitaryBase: false,
zipCodeCtu: null, zipCodeCtu: null,
shopProviderData: null, shopProviderData: null,
navigatingForward: false,
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -374,6 +377,9 @@ export default {
this.zipContainsMilitaryBase = val; this.zipContainsMilitaryBase = val;
} }
}, },
setCtuForMobile(val) {
this.zipCodeCtu = val;
},
setMobileFeePart(mobileFeePart) { setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart; this.mobileFeePart = mobileFeePart;
}, },
@ -472,6 +478,33 @@ export default {
} }
}, },
async forwardButtonAction() { async forwardButtonAction() {
this.navigatingForward = true;
// clear items not needed for appointmentType
if (this.selectedAppointmentType == AppointmentTypeStrings.IN_SHOP) {
// if we have a provider.zipCodeCtu that's different than the servicelocation.zipCodeCtu then they
// may have selected a shop in a different ctu. change the servicelocation zip/ctu if different
if (this.zipCodeCtu != this.selectedProvider.address.zipCodeCtu) {
this.zipCodeCtu = this.selectedProvider.address.zipCodeCtu;
this.zipCode = this.selectedProvider.address.zipCode;
}
this.city = null;
this.streetAddress = null;
}
if (
this.selectedAppointmentType == AppointmentTypeStrings.MOBILE &&
this.selectedProvider &&
this.selectedProvider.address
) {
this.selectedProvider.address.streetAddress = null;
this.selectedProvider.address.city = null;
this.selectedProvider.address.state = null;
this.selectedProvider.address.zipCode = null;
this.selectedProvider.address.zipCodeCtu = null;
}
await this.dispatchStoreAction( await this.dispatchStoreAction(
this.storeActions.SAVE_SERVICE_LOCATION, this.storeActions.SAVE_SERVICE_LOCATION,
{ {
@ -507,16 +540,18 @@ export default {
watch: { watch: {
zipCode: { zipCode: {
handler(newValue) { handler(newValue) {
getShopProviderData(this.zipCode).then(async (result) => { if (!this.navigatingForward) {
this.shopProviderData = result.data; getShopProviderData(this.zipCode).then(async (result) => {
if (this.selectedAppointmentType === "Mobile") { this.shopProviderData = result.data;
this.selectedProvider = new Provider( if (this.selectedAppointmentType === "Mobile") {
this.shopProviderData.mobileProviderNumber this.selectedProvider = new Provider(
); this.shopProviderData.mobileProviderNumber
} else { );
this.selectedProvider = new Provider(); } else {
} this.selectedProvider = new Provider();
}); }
});
}
}, },
}, },
selectedAppointmentType: { selectedAppointmentType: {

View file

@ -1,9 +1,15 @@
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { import {
setCookieProperties,
getDeviceIdValue, getDeviceIdValue,
getSessionIdValue, getSessionIdValue,
getSessionKeyValue, getSessionKeyValue,
getUserIdValue,
regenerateDeviceId,
regenerateUserId,
refreshSessionExpiration,
areAllSessionCookiesSet,
setSessionIdIfUnset,
setSessionKeyIfUnset,
} from "@/helpers/heritage-integration/cookie-helper"; } from "@/helpers/heritage-integration/cookie-helper";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
@ -15,7 +21,6 @@ import {
GaEvents, GaEvents,
ValueToLogTypes, ValueToLogTypes,
} from "@/constants/analytics"; } from "@/constants/analytics";
import { cookieNames } from "@/constants/cookie-names";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
@ -27,10 +32,12 @@ export default {
return getPageNameByQueryString(); return getPageNameByQueryString();
}, },
logPageView(pageEvent) { async logPageView(pageEvent) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
await this.validateSession();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getUserIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: getSessionIdValue(), sessionId: getSessionIdValue(),
@ -42,14 +49,15 @@ export default {
parentAccountNumber: store.getters.order.payment.parentAccountNumber, parentAccountNumber: store.getters.order.payment.parentAccountNumber,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false); await baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false);
}, },
logCustomEvent(category, action, label, value) { async logCustomEvent(category, action, label, value) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
await this.validateSession();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getUserIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: getSessionIdValue(), sessionId: getSessionIdValue(),
@ -63,10 +71,14 @@ export default {
parentAccountNumber: store.getters.order.payment.parentAccountNumber, parentAccountNumber: store.getters.order.payment.parentAccountNumber,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false); await baseMixin.methods.dispatchStoreAction(
storeActions.LOG_CUSTOM_EVENT,
payload,
false
);
}, },
pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) { async pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
const labelToLog = getValueToLog(label, valueToLogType); const labelToLog = getValueToLog(label, valueToLogType);
@ -82,11 +94,11 @@ export default {
pushToDataLayerIfDefined(eventToBePushed); pushToDataLayerIfDefined(eventToBePushed);
if (pushToLogApp) { if (pushToLogApp) {
this.logCustomEvent(category, action, labelToLog, undefined); await this.logCustomEvent(category, action, labelToLog, undefined);
} }
}, },
pushPageViewToGA() { async pushPageViewToGA() {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
const pageViewEvent = { const pageViewEvent = {
event: GaEvents.PAGE_VIEW_EVENT, event: GaEvents.PAGE_VIEW_EVENT,
@ -96,7 +108,7 @@ export default {
pushToDataLayerIfDefined(pageViewEvent); pushToDataLayerIfDefined(pageViewEvent);
this.logPageView(analyticsPageEvents.ENTRY); await this.logPageView(analyticsPageEvents.ENTRY);
}, },
pushExperimentsToDataLayer() { pushExperimentsToDataLayer() {
@ -136,15 +148,21 @@ export default {
}, },
async initSession() { async initSession() {
const sid = getSessionIdValue(); regenerateDeviceId();
const skey = getSessionKeyValue(); regenerateUserId();
const referrer =
applicationConfig.CURRENT_ENVIRONMENT != "Localhost" ? document.referrer : null;
var payload = { const userId = getUserIdValue(); // cookieNames.FUNNEL_USER_ID
userId: getDeviceIdValue(), const deviceId = getDeviceIdValue(); // cookieNames.DXDEV
sessionId: sid, const sessionId = getSessionIdValue(); // cookieNames.SESSION_ID
userAgent: navigator.userAgent, const userAgent = navigator.userAgent; // navigator.userAgent
const referrer =
applicationConfig.CURRENT_ENVIRONMENT != "Localhost" ? document.referrer : null; // see above
const payload = {
userId: userId,
deviceId: deviceId,
sessionId: sessionId,
userAgent: userAgent,
referrer: referrer, referrer: referrer,
}; };
@ -155,30 +173,26 @@ export default {
); );
if (response?.data) { if (response?.data) {
if (response?.data.sessionKey && skey === 0) { if (response.data.sessionKey) {
setCookieProperties( setSessionKeyIfUnset(response.data.sessionKey);
{ [cookieNames.SESSION_KEY]: response?.data.sessionKey },
{
useDefaultFunnelCookieAttributes: false,
}
);
} }
if (response?.data.sessionId && sid === "00000000-0000-0000-0000-000000000000") {
setCookieProperties( if (response.data.sessionId) {
{ [cookieNames.SESSION_ID]: response?.data.sessionId }, setSessionIdIfUnset(response.data.sessionId);
{
maxAge: 60 * 30, // 30 minutes
}
);
} }
} }
}, },
noSession() { noSession() {
return ( return !areAllSessionCookiesSet();
getSessionKeyValue() === 0 || },
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
); async validateSession() {
if (this.noSession()) {
await this.initSession();
}
refreshSessionExpiration();
}, },
removeParamsFromEndpoint(endpoint) { removeParamsFromEndpoint(endpoint) {

View file

@ -1,5 +1,10 @@
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js"; import {
setupMocksForJsFiles,
setupCookies,
setupCrypto,
removeAllTestCookies,
} from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { import {
analyticsPageEvents, analyticsPageEvents,
@ -10,9 +15,20 @@ import {
ValueToLogTypes, ValueToLogTypes,
} from "@/constants/analytics"; } from "@/constants/analytics";
import store from "@/store"; import store from "@/store";
import {
getDeviceIdValue,
getSessionIdValue,
getSessionKeyValue,
getUserIdValue,
} from "@/helpers/heritage-integration/cookie-helper";
describe("analyticsMixin.js", () => { describe("analyticsMixin.js", () => {
test("logPageView: calls dispatch with type and payload", () => { beforeEach(() => {
removeAllTestCookies();
setupCrypto("7e4727f3-9a3d-4c59-9cb3-6f4121b5ea94");
});
test("logPageView: calls dispatch with type and payload", async () => {
const type = ""; const type = "";
const payload = {}; const payload = {};
@ -21,6 +37,9 @@ describe("analyticsMixin.js", () => {
{ {
actionName: storeActions.LOG_PAGE_VIEW, actionName: storeActions.LOG_PAGE_VIEW,
}, },
{
actionName: storeActions.INITIALIZE_SESSION,
},
], ],
}; };
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
@ -31,27 +50,35 @@ describe("analyticsMixin.js", () => {
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) }); setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
analyticsMixin.methods.logPageView(type, payload); await analyticsMixin.methods.logPageView(type, payload);
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("logCustomEvent: calls dispatch with type and payload", () => { test("logCustomEvent: calls dispatch with type and payload", async () => {
const mockData = { const mockData = {
actionList: [ actionList: [
{ {
actionName: storeActions.LOG_CUSTOM_EVENT, actionName: storeActions.LOG_CUSTOM_EVENT,
}, },
{
actionName: storeActions.INITIALIZE_SESSION,
},
], ],
}; };
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
analyticsMixin.methods.logCustomEvent("someCat", "someAction", "someLabel", "someVal"); await analyticsMixin.methods.logCustomEvent(
"someCat",
"someAction",
"someLabel",
"someVal"
);
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("pushEventToGA, should call dataLayer push and logCustomEvent too", () => { test("pushEventToGA, should call dataLayer push and logCustomEvent too", async () => {
// Arrange // Arrange
window.dataLayer = []; window.dataLayer = [];
const mockData = { const mockData = {
@ -59,6 +86,9 @@ describe("analyticsMixin.js", () => {
{ {
actionName: storeActions.LOG_CUSTOM_EVENT, actionName: storeActions.LOG_CUSTOM_EVENT,
}, },
{
actionName: storeActions.INITIALIZE_SESSION,
},
], ],
}; };
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
@ -73,14 +103,14 @@ describe("analyticsMixin.js", () => {
}); });
// Act // Act
analyticsMixin.methods.pushEventToGA("category", "action", "label", true); await analyticsMixin.methods.pushEventToGA("category", "action", "label", true);
// Assert // Assert
expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer)); expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", () => { test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", async () => {
// Arrange // Arrange
window.dataLayer = []; window.dataLayer = [];
var expectedDataLayer = []; var expectedDataLayer = [];
@ -93,8 +123,21 @@ describe("analyticsMixin.js", () => {
path: "/fmg/?fmgPage=", path: "/fmg/?fmgPage=",
}); });
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act // Act
analyticsMixin.methods.pushEventToGA( await analyticsMixin.methods.pushEventToGA(
"category", "category",
"action", "action",
"1111122222333333", "1111122222333333",
@ -106,7 +149,7 @@ describe("analyticsMixin.js", () => {
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer)); 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", () => { test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 logs only the last 3 characters for a 3 character string", async () => {
// Arrange // Arrange
window.dataLayer = []; window.dataLayer = [];
var expectedDataLayer = []; var expectedDataLayer = [];
@ -119,8 +162,21 @@ describe("analyticsMixin.js", () => {
path: "/fmg/?fmgPage=", path: "/fmg/?fmgPage=",
}); });
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act // Act
analyticsMixin.methods.pushEventToGA( await analyticsMixin.methods.pushEventToGA(
"category", "category",
"action", "action",
"111", "111",
@ -296,4 +352,120 @@ describe("analyticsMixin.js", () => {
//Assert //Assert
expect(gaLabels).toEqual(GaLabels); expect(gaLabels).toEqual(GaLabels);
}); });
describe("initSession", () => {
test("Generates random values for userId and deviceId if not present", async () => {
// Arrange
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
data: {
sessionKey: "12345",
sessionId: "4f1eba4a-4dd5-4144-9a6b-1363c1e5e54f",
},
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act
await analyticsMixin.methods.initSession();
const userId = getUserIdValue();
const deviceId = getDeviceIdValue();
// Assert
expect(userId).not.toBe("00000000-0000-0000-0000-000000000000");
expect(deviceId).not.toBe("00000000-0000-0000-0000-000000000000");
});
test("Pulls sessionId and sessionKey from api if not set", async () => {
// Arrange
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
data: {
sessionKey: "54321",
sessionId: "4f1eba4a-4dd5-4144-9a6b-1363c1e5e54f",
},
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act
await analyticsMixin.methods.initSession();
const sessionId = getSessionIdValue();
const sessionKey = getSessionKeyValue();
// Assert
expect(sessionId).toBe("4f1eba4a-4dd5-4144-9a6b-1363c1e5e54f");
expect(sessionKey).toBe("54321");
});
test("Does not overwrite values that are already set", async () => {
// Arrange
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
data: {
sessionKey: "54321",
sessionId: "4f1eba4a-4dd5-4144-9a6b-1363c1e5e54f",
},
},
],
};
const mocks = setupMocksForJsFiles(mockData);
setupCookies({});
// Act
await analyticsMixin.methods.initSession();
const userId = getUserIdValue();
const deviceId = getDeviceIdValue();
const sessionId = getSessionIdValue();
const sessionKey = getSessionKeyValue();
// Assert
expect(userId).toBe("11aec5e8-92ba-4dc9-a8b6-179a916d8d7a");
expect(deviceId).toBe("21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe");
expect(sessionId).toBe("cba0c3d1-3c1b-4305-bb56-31aa50f58e27");
expect(sessionKey).toBe("12345");
});
test("Calls dispatchStoreAction", async () => {
// Arrange
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
data: {
sessionKey: "12345",
sessionId: "4f1eba4a-4dd5-4144-9a6b-1363c1e5e54f",
},
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act
await analyticsMixin.methods.initSession();
// Assert
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
});
});
}); });

View file

@ -38,11 +38,7 @@ const routes = [
async beforeEnter(to, from, next) { async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string. // If we have no query string, or we don't have the FmgPage query string.
try { try {
if (analyticsMixin.methods.noSession()) { await analyticsMixin.methods.validateSession();
await analyticsMixin.methods.initSession();
} else {
updateSessionIdCookie();
}
if (getFunnelCookie()?.SuppressConceptFunnel) { if (getFunnelCookie()?.SuppressConceptFunnel) {
await navigateToHeritageFunnel({ shouldSaveSession: false }); await navigateToHeritageFunnel({ shouldSaveSession: false });

View file

@ -1076,7 +1076,7 @@ export const actions = {
sessionKey: sessionKey, sessionKey: sessionKey,
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.ANALYTICS_APPLICATION_NAME,
action: action, action: action,
event: event, event: event,
shouldUseSessionId: shouldUseSessionId, shouldUseSessionId: shouldUseSessionId,
@ -1124,7 +1124,7 @@ export const actions = {
sessionKey: sessionKey, sessionKey: sessionKey,
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.ANALYTICS_APPLICATION_NAME,
category: category, category: category,
action: action, action: action,
label: label, label: label,
@ -1151,11 +1151,11 @@ export const actions = {
} }
); );
}, },
initializeSession(context, { userId, sessionId, userAgent, referrer }) { initializeSession(context, { userId, deviceId, sessionId, userAgent, referrer }) {
var payload = { var payload = {
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.ANALYTICS_APPLICATION_NAME,
userId: userId, userId: userId,
deviceId: userId, deviceId: deviceId,
sessionId: sessionId, sessionId: sessionId,
userAgent: userAgent, userAgent: userAgent,
operatorId: "WEB", operatorId: "WEB",
@ -2495,15 +2495,7 @@ export const actions = {
isVinOptionalVehicle(context) { isVinOptionalVehicle(context) {
//Optional for carIds with only a single windshield //Optional for carIds with only a single windshield
if ( if (singleWindshieldCarIds.find((item) => item === context.state.order.vehicle.carId)) {
singleWindshieldCarIds.find((item) => item === context.state.order.vehicle.carId) &&
context.state.order.damage.glassToReplace.length == 1 &&
context.state.order.damage.glassToReplace.find(
(glassToReplace) =>
glassToReplace.glassLocation.toLowerCase() ===
damageLocationsSelected.WINDSHIELD.toLowerCase()
)
) {
return true; return true;
} }
//Optional for specific YMMSs //Optional for specific YMMSs

View file

@ -3329,7 +3329,7 @@ describe("isVinOptionalVehicle", () => {
"CR00062396", "CR00062396",
"make3", "make3",
[{ glassLocation: "windshield" }, { glassLocation: "driver" }], [{ glassLocation: "windshield" }, { glassLocation: "driver" }],
false, true,
], ],
["CR00066428", "make4", [{ glassLocation: "rear" }], false], ["CR00066428", "make4", [{ glassLocation: "rear" }], false],
]; ];

View file

@ -28,4 +28,58 @@ describe("loader.vue", () => {
loaderPosition: "left", loaderPosition: "left",
}); });
}); });
describe("Blocking interaction on page", () => {
test("Does capture clicks if enabled (default)", async () => {
// Arrange
const div = document.createElement("div");
div.id = "parent";
document.body.appendChild(div);
const parentClickFn = jest.fn();
div.addEventListener("click", parentClickFn);
const wrapper = shallowMount(loader, {
props: {},
attachTo: "#parent",
});
// Act
await wrapper.trigger("click");
// Assert
expect(parentClickFn).not.toBeCalled();
// Cleanup
document.body.removeChild(div);
});
test("Does not capture clicks if disabled", async () => {
// Arrange
const div = document.createElement("div");
div.id = "parent";
document.body.appendChild(div);
const parentClickFn = jest.fn();
div.addEventListener("click", parentClickFn);
const wrapper = shallowMount(loader, {
props: {
allowPageInteraction: true,
},
attachTo: "#parent",
});
// Act
await wrapper.trigger("click");
// Assert
expect(parentClickFn).toBeCalled();
// Cleanup
document.body.removeChild(div);
});
});
}); });