Merge pull request #538 from Safelite/rlsmerge/release2022.06.09-to-develop

Rlsmerge/release2022.06.09 to develop
This commit is contained in:
katieoh-safelite 2022-06-08 08:03:09 -04:00 committed by GitHub
commit 602768b187
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2862 additions and 1845 deletions

View file

@ -26,7 +26,7 @@ resources:
type: github type: github
name: Safelite/AzureDevOps name: Safelite/AzureDevOps
endpoint: Safelite endpoint: Safelite
ref: refs/tags/t5.4.0 ref: refs/tags/t5.5.19
variables: variables:
- group: Digital-Infrastructure - group: Digital-Infrastructure
@ -67,9 +67,10 @@ stages:
- template: templates/digital/step-build-vue.yml@AzureDevOps - template: templates/digital/step-build-vue.yml@AzureDevOps
parameters: parameters:
buildOutputDir: dist buildOutputDir: dist
environment: Dev
- template: templates/digital/step-deploy-vue.yml@AzureDevOps - template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters: parameters:
artifactName: vueDist artifactName: vueDistDev
awsProfile: $(devDeploymentProfile) awsProfile: $(devDeploymentProfile)
outputPath: /fmg/ outputPath: /fmg/
deployBuckets: deployBuckets:
@ -109,9 +110,10 @@ stages:
- template: templates/digital/step-build-vue.yml@AzureDevOps - template: templates/digital/step-build-vue.yml@AzureDevOps
parameters: parameters:
buildOutputDir: dist buildOutputDir: dist
environment: Qa
- template: templates/digital/step-deploy-vue.yml@AzureDevOps - template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters: parameters:
artifactName: vueDist artifactName: vueDistQa
awsProfile: $(qaDeploymentProfile) awsProfile: $(qaDeploymentProfile)
outputPath: /fmg/ outputPath: /fmg/
deployBuckets: deployBuckets:
@ -131,7 +133,7 @@ stages:
# Prod Build/Deploy # Prod Build/Deploy
- stage: Prod - stage: Prod
condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] ) condition: succeeded('Qa')
variables: variables:
- group: FixMyGlassProd - group: FixMyGlassProd
jobs: jobs:
@ -150,9 +152,10 @@ stages:
- template: templates/digital/step-build-vue.yml@AzureDevOps - template: templates/digital/step-build-vue.yml@AzureDevOps
parameters: parameters:
buildOutputDir: dist buildOutputDir: dist
environment: Prod
- template: templates/digital/step-deploy-vue.yml@AzureDevOps - template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters: parameters:
artifactName: vueDist artifactName: vueDistProd
awsProfile: $(prodDeploymentProfile) awsProfile: $(prodDeploymentProfile)
outputPath: /fmg/ outputPath: /fmg/
deployBuckets: deployBuckets:
@ -168,4 +171,8 @@ stages:
indexDeployVariables: indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) __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__) __VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId) cfDistributionId: $(cfDistributionId)
- template: templates/digital/auto-tag.yml@AzureDevOps
parameters:
userName: SafeliteAzureDevops
userEmail: githubazuredevops@safelite.com

View file

@ -23,10 +23,6 @@ module.exports = {
"!src/layouts/address-lookup/address-lookup.vue", "!src/layouts/address-lookup/address-lookup.vue",
"!src/layouts/address-lookup/customer-questions/customer-questions.vue", "!src/layouts/address-lookup/customer-questions/customer-questions.vue",
"!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue", "!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue",
"!src/layouts/address-vehicles/address-vehicles.vue",
"!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue",
"!src/common-components/dropdown-question/dropdown-question.vue",
"!src/common-components/textbox-question/textbox-question.vue",
"!src/ux-components/alert\alert.vue", "!src/ux-components/alert\alert.vue",
"!src/helpers/validation-rules.js", "!src/helpers/validation-rules.js",
// END // END

View file

@ -0,0 +1,170 @@
import { shallowMount } from "@vue/test-utils";
import dropdownQuestion from "./dropdown-question";
// Mock CMS content
const questionText = "Question Text";
const mockMixin = {
methods: {
getCmsContent: jest.fn().mockImplementation(()=> {
return questionText;
})
}
}
// TODO: Remove the following from dropdown-question.vue -> :class="(errors && errors.length) || hasError ? 'has-error' : ''"
// It is not being used.
describe("dropdownQuestion.vue", () => {
it("Should render a select input", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
},
mixins: [mockMixin]
});
wrapper.getCmsContent = jest.fn();
// Act
const select = wrapper.find("select");
// Assert
expect(select.exists()).toBe(true);
});
it("Should render the 'questionText' data value as the label text.", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
},
mixins: [mockMixin]
});
// Act
const label = wrapper.find("label");
// Assert
expect(label.text()).toContain(questionText);
});
it("Should render the 'questionText' data value with '⁠' after the first character of each word in the label text when disableAutoFill is true.", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
disableAutoFill: true,
},
mixins: [mockMixin]
});
// Mock CMS content ...
// Trust me, the below instance of the string "Question Text" actually has the ⁠ in it. You just can't see it
// Don't believe me? Copy and paste it into Google. Then inspect the search field element in Dev Tools,
// you will see "Q⁠uestion T⁠ext"
const expectedQuestionText = "Question Text";
// Act
const label = wrapper.find("label");
// Assert
expect(label.text()).toContain(expectedQuestionText);
});
it("Should return input id as the id of the select field", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
inputId: "input ID",
options: {},
},
mixins: [mockMixin]
});
// Act
const select = wrapper.find("select");
// Assert
expect(select.attributes().id).toEqual("input ID");
});
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
},
mixins: [mockMixin]
});
// Act
const label = wrapper.find("label");
// Assert
expect(label.attributes("aria-label")).toContain(questionText);
});
it("Should return aria-disabled state as disabled", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
isDisabled: true,
},
mixins: [mockMixin]
});
// Act
const select = wrapper.find("select");
// Assert
expect(select.attributes("aria-disabled")).toEqual("true");
});
it("Should emit new value when modelValue is changed", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
modelValue: "val",
},
mixins: [mockMixin]
});
// Act
await wrapper.find("select").setValue("val2");
// Assert
expect(wrapper.emitted()).toHaveProperty('change')
});
it("Should call this.handleChange with new value when selectedOption is changed", async () => {
// Arrange
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
modelValue: 0,
},
mixins: [mockMixin]
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
// Act
wrapper.vm.$options.watch.selectedOption.call(wrapper.vm, 1);
// Assert
expect(wrapper.vm.handleChange).toHaveBeenCalled;
});
});

View file

@ -1,131 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import dropdownQuestion from "./dropdown-question";
import { nextTick } from "vue";
import { maska } from 'maska';
describe("dropdownQuestion.vue", () => {
it("Should return aria-disabled state", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
isDisabled: true,
},
});
// Assert
const input = wrapper.find("select");
// Expect
expect(input.attributes()["aria-disabled"]).toEqual("true");
});
it("Should render a select input", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
name: "test",
label: "unit test label",
},
});
// Assert
const input = wrapper.find("select");
expect(input.exists()).toBe(true);
});
it("Should return input id", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
inputId: "input ID",
},
});
// Assert
const input = wrapper.find("select");
// Expect
expect(input.attributes().id).toEqual("input ID");
});
it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => {
// Arrange
//Mock CMS content
const questionText = "Question Text";
const cmsContent = {
QuestionText: questionText,
};
// Act
const wrapper = shallowMount(dropdownQuestion, {
global: {
directives: {
maska: maska,
}
},
});
await wrapper.setData({
questionText: questionText,
});
wrapper.vm.initializeComponent(cmsContent);
// Assert
expect(wrapper.find("label").text()).toContain(questionText);
wrapper.unmount();
});
it("Should render the 'questionText' data value with '⁠' after the first character as the label text when disableAutoFill is true.", async () => {
// Arrange
//Mock CMS content
const originalQuestionText = "Question Text";
// Trust me, the below instance of the string "Question Text" actually has the ⁠ in it. You just can't see it
// Don't believe me? Copy it and paste it into Google. Then inspect the search field element in Dev Tools,
// you will see "Q&uestion Text"
const expectedQuestionText = "Question Text";
const cmsContent = {
QuestionText: originalQuestionText,
};
// Act
const wrapper = shallowMount(dropdownQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
disableAutoFill: true,
},
});
await wrapper.setData({
questionText: originalQuestionText,
});
wrapper.vm.initializeComponent(cmsContent);
// Assert
expect(wrapper.vm.$el.children[0].innerHTML).toBe(expectedQuestionText);
wrapper.unmount();
});
it("Should emit new value when modelValue is changed", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
modelValue: "val",
},
});
await wrapper.find("select").setValue("val2");
// Assert
expect(wrapper.emitted()).toHaveProperty('change')
});
});

View file

@ -3,30 +3,6 @@ import funnelFooter from "./funnel-footer";
describe("funnel-footer.vue", () => { describe("funnel-footer.vue", () => {
it("Should return footer-link class", async () => {
// Act
const r = {
test:"testing",
clientHeight: 10,
offsetHeight: 24,
};
global.document.querySelector = jest.fn().mockImplementation(()=> {
return r;
});
const wrapper = mount(funnelFooter, {
mixins: [mockMixin]
});
// Assert
const link = wrapper.find("a");
// Expect
expect(link.attributes('class')).toContain("footer");
});
it("Should emit ForwardClicked on button click", async () => { it("Should emit ForwardClicked on button click", async () => {
// Act // Act
const wrapper = mount(funnelFooter, { const wrapper = mount(funnelFooter, {
@ -53,7 +29,7 @@ describe("funnel-footer.vue", () => {
mixins: [mockMixin] mixins: [mockMixin]
}); });
wrapper.vm.updateButtonText('newText'); wrapper.vm.updateButtonText('newText');
// Assert // Assert
expect(wrapper.componentVM.customButtontext).toBe('newText'); expect(wrapper.componentVM.customButtontext).toBe('newText');
}); });
@ -65,4 +41,4 @@ const mockMixin = {
getCmsContent: jest.fn(), getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=>80) getFooterInfoBoxHeight: jest.fn(()=>80)
} }
} }

View file

@ -1,21 +1,7 @@
<template> <template>
<div class="footer pt-4"> <div class="row" :style="`padding-bottom: ${paddingHeight}px`"></div>
<div class="container-fluid g-2"> <footer class="footer container-fluid fixed-bottom g-5 bg-light py-4" id="infoBox">
<div class="row"> <div class="row d-flex flex-row-reverse align-items-center vw-100">
<div class="col-12 d-flex justify-content-center pb-2 fs-7">
&copy;&nbsp;{{new Date().getFullYear()}}&nbsp;Safelite Group
</div>
</div>
<div class="row" :style="`padding-bottom: ${paddingHeight}px`">
<div class="col d-flex justify-content-center justify-content-around text-center">
<textLink linkType="footer" text="Terms of use" href="https://www.safelite.com/terms-of-use" />
<textLink linkType="footer" text="Privacy policy" href="https://www.safelite.com/safelite-group-privacy-policy" />
<textLink linkType="footer" text="Do not sell my information" href="https://privacyportal-cdn.onetrust.com/dsarwebform/d3b95a93-e22e-4d4d-a806-482052406557/9e371601-eae3-4338-9430-b90b9036022b.html" />
</div>
</div>
</div>
<div class="container-fluid fixed-bottom g-5 bg-light py-4" id="infoBox">
<div class="row d-flex flex-row-reverse align-items-center">
<div class="col button-col d-flex" id="stacked"> <div class="col button-col d-flex" id="stacked">
<buttonMain <buttonMain
ref="buttonMain" ref="buttonMain"
@ -26,6 +12,8 @@
:aria-disabled="isForwardActionDisabled" :aria-disabled="isForwardActionDisabled"
:isDisabled="isForwardActionDisabled" :isDisabled="isForwardActionDisabled"
@click-event="buttonClick" @click-event="buttonClick"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
/> />
</div> </div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break"> <div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
@ -34,11 +22,12 @@
:text="backLink" :text="backLink"
@click-event="linkClick" @click-event="linkClick"
href="javascript:void(0)" href="javascript:void(0)"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
/> />
</div> </div>
</div> </div>
</div> </footer>
</div>
</template> </template>
<script> <script>
@ -80,7 +69,7 @@ export default {
}, },
buttonText(){ buttonText(){
return this.customButtontext ? this.customButtontext : this.getCmsContent(this.cmsWidgetName, 'ForwardButtonText'); return this.customButtontext ? this.customButtontext : this.getCmsContent(this.cmsWidgetName, 'ForwardButtonText');
} },
}, },
methods: { methods: {
onResize() { onResize() {
@ -112,7 +101,6 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.footer { .footer {
display: flex; display: flex;
margin-top: auto;
a { a {
display: flex; display: flex;
justify-content: center; justify-content: center;

View file

@ -4,6 +4,7 @@
v-if="imageSrc" v-if="imageSrc"
> >
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" /> <img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
<menuModal/>
</div> </div>
<alert <alert
class="position-absolute rounded-0 w-100 border-0 shadow-sm" class="position-absolute rounded-0 w-100 border-0 shadow-sm"
@ -19,6 +20,7 @@
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import eventBus from "@/helpers/event-bus/event-bus"; import eventBus from "@/helpers/event-bus/event-bus";
import { globalEvents } from "@/constants/events"; import { globalEvents } from "@/constants/events";
import menuModal from "@/common-components/funnel-header/menu-modal/menu-modal";
export default { export default {
name: "funnel-header", name: "funnel-header",
@ -43,6 +45,7 @@ export default {
}, },
components: { components: {
alert, alert,
menuModal,
}, },
mounted() { mounted() {
// Check if alert event is on the bus // Check if alert event is on the bus
@ -50,7 +53,6 @@ export default {
globalEvents.Categories.GLOBAL_ALERT, globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND globalEvents.SubCategories.PAGE_NOT_FOUND
); );
// If alert event is on the bus, then display the alert // If alert event is on the bus, then display the alert
if (alertEvent !== undefined) { if (alertEvent !== undefined) {
this.displayGlobalAlert = true; this.displayGlobalAlert = true;
@ -70,5 +72,6 @@ export default {
} }
.alert { .alert {
left: 0; left: 0;
top: 72px;
} }
</style> </style>

View file

@ -0,0 +1,120 @@
<template>
<div class="menu-modal-container">
<button class="menu-button" type="button" :class="[isActive ? 'active' : '']" data-bs-toggle="modal" data-bs-target="#footerModal">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</button>
</div>
<!-- Modal -->
<div class="modal fade" id="footerModal" tabindex="-1" aria-labelledby="footerModalLabel" aria-hidden="true" :style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header visually-hidden">
<h5 class="modal-title" id="footerModalLabel">Footer Navigation</h5>
</div>
<div class="modal-body d-flex flex-column">
<textLink linkType="navigation" text="Terms of use" href="https://www.safelite.com/terms-of-use" target="_blank" />
<textLink linkType="navigation" text="Privacy policy" href="https://www.safelite.com/safelite-group-privacy-policy" target="_blank" />
<textLink linkType="navigation" text="Do not sell my information" href="https://privacyportal-cdn.onetrust.com/dsarwebform/d3b95a93-e22e-4d4d-a806-482052406557/9e371601-eae3-4338-9430-b90b9036022b.html" target="_blank" />
</div>
<div class="modal-footer d-flex justify-content-start">
&copy; {{new Date().getFullYear()}} Safelite Group
</div>
</div>
</div>
</div>
</template>
<script>
import textLink from "@/ux-components/text-link/text-link";
export default {
name: 'menuModal',
data() {
return {
isActive: false,
currentFooterAndHeaderHeight: 0,
};
},
mounted() {
const myModalEl = document.getElementById('footerModal');
var self = this;
myModalEl.addEventListener('hide.bs.modal', function (event) {
self.isActive = false;
});
myModalEl.addEventListener('show.bs.modal', function (event) {
self.currentFooterAndHeaderHeight = self.getFooterInfoBoxHeight() + 72;
self.isActive = true;
document.querySelector('.page-container-grouped-styles').scrollTo({
top: 0, behavior: 'smooth'
});
})
},
components: {
textLink,
}
}
</script>
<style lang="scss">
.menu-modal-container {
position: absolute;
padding: 1.47rem 1rem 1.47rem 1.47rem;
right: 0;
button {
border: none;
&.menu-button {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.2);
background-color: $white;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;//Required to prevent 'squish' on iPhone
z-index: 1056;
.bar1,
.bar2,
.bar3 {
width: 14px;
height: 2px;
background-color: $blue-400;
margin: 1px 0;
transition: 0.25s;
}
&.active .bar1 {
transform: rotate(-45deg) translate(-3px, 3px);
}
&.active .bar2 {opacity: 0;}
&.active .bar3 {
transform: rotate(45deg) translate(-3px, -3px);
}
}
}
}
.modal {
max-width: 576px;
left: auto;
height: calc(100% - 72px);
top: 72px;
border-top: 1px solid $gray-300;
.modal-body {
padding: 2rem;
}
.modal-fullscreen {
width: 100vw;
max-width: 576px;
}
.navigation-link {
margin-bottom: 1.5rem;
}
.modal-footer {
border-top: none;
padding: 2rem;
}
//.modal-backdrop styles are in common-styles.scss
}
</style>

View file

@ -3,7 +3,7 @@
<div class="row g-2 h-100 d-flex align-items-center"> <div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden"> <div class="container-fluid overflow-hidden">
<div class="row h-100"> <div class="row h-100">
<div class="col text-center tagbg mb-2"> <div class="col text-center tagbg mb-4">
<div class="spinner-border text-danger" role="status"> <div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
@ -12,17 +12,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="text-container slide"> <div class="text-container slide">
<p>Finishing up <p>Assessing your damage
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Generating your quote
<span class="dot-1">.</span> <span class="dot-1">.</span>
<span class="dot-2">.</span> <span class="dot-2">.</span>
<span class="dot-3">.</span> <span class="dot-3">.</span>
@ -32,7 +22,17 @@
<span class="dot-2">.</span> <span class="dot-2">.</span>
<span class="dot-3">.</span> <span class="dot-3">.</span>
</p> </p>
<p>Assessing your damage <p>Generating your quote
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Finishing up
<span class="dot-1">.</span> <span class="dot-1">.</span>
<span class="dot-2">.</span> <span class="dot-2">.</span>
<span class="dot-3">.</span> <span class="dot-3">.</span>
@ -130,39 +130,39 @@ export default {
p { p {
margin: 0 175px; margin: 0 175px;
transform: translateX(-2346px); transform: translateX(180px);
} }
@keyframes slide-in { @keyframes slide-in {
0% { 0% {
transform: translateX(-2346px) transform: translateX(180px)
} }
11% { 11% {
transform: translateX(-2010px) transform: translateX(-80px)
} }
22% { 22% {
transform: translateX(-2010px) transform: translateX(-80px)
} }
33% { 33% {
transform: translateX(-1490px) transform: translateX(-600px)
} }
44% { 44% {
transform: translateX(-1490px) transform: translateX(-600px)
} }
55% { 55% {
transform: translateX(-985px) transform: translateX(-1110px)
} }
66% { 66% {
transform: translateX(-985px) transform: translateX(-1110px)
} }
77% { 77% {
transform: translateX(-490px) transform: translateX(-1600px)
} }
88% { 88% {
transform: translateX(-490px) transform: translateX(-1600px)
} }
100% { 100% {
transform: translateX(-40px) transform: translateX(-2050px)
} }
} }

View file

@ -1,53 +0,0 @@
<template>
<div class="menu-button" :class="[isActive ? 'active' : '']" @click="toggleClass()">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</div>
</template>
<script>
export default {
name: 'menuButton',
data() {
return {
isActive: false,
};
},
methods: {
toggleClass: function(event){
this.isActive = !this.isActive;
}
}
}
</script>
<style lang="scss">
.menu-button {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
box-shadow: 0px 0px 8px 2px rgba(0,0,0,0.2);
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.bar1,
.bar2,
.bar3 {
width: 14px;
height: 2px;
background-color: $blue-400;
margin: 1px 0;
transition: 0.25s;
}
&.active .bar1 {
transform: rotate(-45deg) translate(-3px, 3px);
}
&.active .bar2 {opacity: 0;}
&.active .bar3 {
transform: rotate(45deg) translate(-3px, -3px);
}
}
</style>

View file

@ -1,139 +1,61 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import textboxQuestion from "./textbox-question"; import textboxQuestion from "./textbox-question";
import { maska } from 'maska';
// Mock CMS content
const questionText = "Question Text";
const mockMixin = {
methods: {
getCmsContent: jest.fn().mockImplementation(()=> {
return questionText;
})
}
}
const maska = jest.fn();
describe("textboxQuestion.vue", () => { describe("textboxQuestion.vue", () => {
it("Should return aria-disabled state", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
isDisabled: true,
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes()["aria-disabled"]).toEqual("true");
});
it("Should render a text input", async () => { it("Should render a text input", async () => {
// Act // Arrange
const wrapper = shallowMount(textboxQuestion, { const wrapper = shallowMount(textboxQuestion, {
global: { global: {
directives: { directives: {
maska: maska, maska: maska,
} }
}, },
propsData: { mixins: [mockMixin]
name: "test",
label: "unit test label",
},
}); });
// Assert wrapper.getCmsContent = jest.fn();
// Act
const input = wrapper.find("input"); const input = wrapper.find("input");
// Assert
expect(input.exists()).toBe(true); expect(input.exists()).toBe(true);
}); });
it("Should return input id", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
inputId: "input ID",
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().id).toEqual("input ID");
});
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
// Arrange
//Mock CMS content
const questionText = "Question Text";
const cmsContent = {
QuestionText: questionText,
};
// Act
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
});
await wrapper.setData({
questionText: questionText,
});
wrapper.vm.initializeComponent(cmsContent);
// Assert
expect(wrapper.find("label").attributes('aria-label')).toBe(questionText);
wrapper.unmount();
});
it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => { it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => {
// Arrange // Arrange
//Mock CMS content
const questionText = "Question Text";
const cmsContent = {
QuestionText: questionText,
};
// Act
const wrapper = shallowMount(textboxQuestion, { const wrapper = shallowMount(textboxQuestion, {
global: { global: {
directives: { directives: {
maska: maska, maska: maska,
} }
}, },
mixins: [mockMixin]
}); });
await wrapper.setData({
questionText: questionText,
});
wrapper.vm.initializeComponent(cmsContent);
// Assert
expect(wrapper.find("label").text()).toContain(questionText);
wrapper.unmount();
});
it("Should render the 'questionText' data value with '&NoBreak;' after the first character as the label text when disableAutoFill is true.", async () => {
// Arrange
//Mock CMS content
const originalQuestionText = "Question Text";
// Trust me, the below instance of the string "Question Text" actually has the &NoBreak; in it. You just can't see it
// Don't believe me? Copy it and paste it into Google. Then inspect the search field element in Dev Tools,
// you will see "Q&uestion Text"
const expectedQuestionText = "Question Text";
const cmsContent = {
QuestionText: originalQuestionText,
};
// Act // Act
const label = wrapper.find("label");
// Assert
expect(label.text()).toContain(questionText);
});
it("Should render the 'questionText' data value with '&NoBreak;' after the first character of each word in the label text when disableAutoFill is true.", async () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, { const wrapper = shallowMount(textboxQuestion, {
global: { global: {
directives: { directives: {
@ -143,15 +65,84 @@ describe("textboxQuestion.vue", () => {
propsData: { propsData: {
disableAutoFill: true, disableAutoFill: true,
}, },
mixins: [mockMixin]
}); });
await wrapper.setData({
questionText: originalQuestionText, // Mock CMS content ...
}); // Trust me, the below instance of the string "Question Text" actually has the &NoBreak; in it. You just can't see it
wrapper.vm.initializeComponent(cmsContent); // Don't believe me? Copy and paste it into Google. Then inspect the search field element in Dev Tools,
// you will see "Q&NoBreak;uestion T&NoBreak;ext"
const expectedQuestionText = "Question Text";
// Act
const label = wrapper.find("label");
// Assert // Assert
expect(wrapper.vm.$el.children[0].innerHTML).toBe(expectedQuestionText); expect(label.text()).toContain(expectedQuestionText);
wrapper.unmount();
});
it("Should return input id as the id of the input field", async () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
inputId: "input ID",
},
mixins: [mockMixin]
});
// Act
const input = wrapper.find("input");
// Assert
expect(input.attributes().id).toEqual("input ID");
});
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
mixins: [mockMixin]
});
// Act
const label = wrapper.find("label");
// Assert
expect(label.attributes("aria-label")).toContain(questionText);
});
it("Should return aria-disabled state as disabled", async () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
isDisabled: true,
},
mixins: [mockMixin]
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes("aria-disabled")).toEqual("true");
}); });
@ -166,6 +157,7 @@ describe("textboxQuestion.vue", () => {
propsData: { propsData: {
modelValue: "val", modelValue: "val",
}, },
mixins: [mockMixin]
}); });
await wrapper.find("input").setValue("val2"); await wrapper.find("input").setValue("val2");
@ -175,4 +167,33 @@ describe("textboxQuestion.vue", () => {
}); });
it("Should call this.handleChange with new value when this.semiAggressiveValidation = true, the value is changed, and the new value is valid", async () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
}
},
propsData: {
options: {},
modelValue: "foo",
semiAggressiveValidation: true,
},
mixins: [mockMixin]
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
wrapper.vm.validate = jest.fn().mockImplementation(() => {
return true;
});
// Act
wrapper.vm.$options.watch.value.call(wrapper.vm, "bar");
// Assert
expect(wrapper.vm.handleChange).toHaveBeenCalled;
});
}); });

View file

@ -1,7 +1,7 @@
const dynamicStrings = { const dynamicStrings = {
GLOBAL_STATE: "globalState", GLOBAL_STATE: "globalState",
CUSTOM: "custom", CUSTOM: "custom",
ROUTER_LINK: "routerLink" ROUTER_LINK: "routerLink:"
}; };
export { dynamicStrings }; export { dynamicStrings };

View file

@ -79,6 +79,10 @@ const endpoints = {
url: "/analytics/api/v1/analytics/log-custom-event", url: "/analytics/api/v1/analytics/log-custom-event",
method: "POST", method: "POST",
}, },
InitializeSession:{
url: "/analytics/api/v1/analytics/initialize",
method: "POST",
},
GetExperimentsByUser: { GetExperimentsByUser: {
url: "/analytics/api/v1/analytics/get-experiments", url: "/analytics/api/v1/analytics/get-experiments",
method: "GET", method: "GET",

View file

@ -21,6 +21,7 @@ const storeActions = {
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure", LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
LOG_PAGE_VIEW: "logPageView", LOG_PAGE_VIEW: "logPageView",
LOG_CUSTOM_EVENT: "logCustomEvent", LOG_CUSTOM_EVENT: "logCustomEvent",
INITIALIZE_SESSION: "initializeSession",
GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser", GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser",
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration", UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration",

View file

@ -120,4 +120,37 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have something else like a number, boolean, etc. just return it // If we have something else like a number, boolean, etc. just return it
return widgetModel[key]; return widgetModel[key];
} }
export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK);
}
export function splitCopyOnCMSPlaceHolder(copy){
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
}
export function getRouterLinkRouteFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
}
export function getRouterLinkDisplayTextFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
}
// Copy returned from the CMS that has newlines will return blocks wrapped in
// <p ... >...</p>
// This function returns an array of each paragraph, works with or without html
// attributes present
export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
}

View file

@ -104,6 +104,14 @@ export function getSessionIdValue(){
return '00000000-0000-0000-0000-000000000000'; return '00000000-0000-0000-0000-000000000000';
} }
export function setCookieProperties(properties) {
if (typeof properties == "object") {
Object.keys(properties).forEach(key => {
document.cookie = `${key}=${properties[key]}`;
});
}
}
/* /*
=========================== ===========================
= PRIVATE FUNCTIONS = = PRIVATE FUNCTIONS =

View file

@ -11,8 +11,8 @@ import baseMixin from "@/mixins/base-mixin";
export async function loadOrderIfPresent() { export async function loadOrderIfPresent() {
const funnelCookie = getFunnelCookie(); const funnelCookie = getFunnelCookie();
// Do nothing if there is no cookie or no correlation id. // Do nothing if there is no cookie, correlation id, or referral number.
if (funnelCookie == null || funnelCookie.ReferralCorrelationId == null) { if (funnelCookie == null || funnelCookie.ReferralCorrelationId == null || !funnelCookie.ReferralNumber) {
return null; return null;
} }

View file

@ -18,7 +18,8 @@ describe("loadOrderIfPresent", () => {
const testCookieValue = { const testCookieValue = {
ShouldResetState: testShouldResetState, ShouldResetState: testShouldResetState,
ReferralCorrelationId: "xxx" ReferralCorrelationId: "xxx",
ReferralNumber: "12345"
} }
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=${JSON.stringify(testCookieValue)}; path=/; ${cookieHelper.getCookieDomainValue()}`; document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=${JSON.stringify(testCookieValue)}; path=/; ${cookieHelper.getCookieDomainValue()}`;
@ -33,7 +34,11 @@ describe("loadOrderIfPresent", () => {
test("ShouldResetState == true => reset store", () => { test("ShouldResetState == true => reset store", () => {
// Arrange // Arrange
cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie").mockReturnValueOnce({ ShouldResetState: true, ReferralCorrelationId: "xxx-xxx-xxx" }); cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie").mockReturnValueOnce({
ShouldResetState: true,
ReferralCorrelationId: "xxx-xxx-xxx",
ReferralNumber: "12345"
});
const mockData = { const mockData = {
actionList: [{ actionList: [{

View file

@ -24,14 +24,13 @@ export function getMountOptions(mockData) {
mocks.logEvent = jest.fn(); mocks.logEvent = jest.fn();
mocks.pushExperimentsToDataLayer = jest.fn(); mocks.pushExperimentsToDataLayer = jest.fn();
mocks.prependActionToMethod = jest.fn(); mocks.prependActionToMethod = jest.fn();
mocks.dispatchStoreAction = jest.fn(); mocks.dispatchStoreAction = jest.fn();
mocks.dispatchStoreAction.mockImplementation((actionName) => { mocks.dispatchStoreAction.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList.filter( let actionFilterResult = mockData.actionList.filter(
(x) => x.actionName == actionName (x) => x.actionName == actionName
); );
if (actionFilterResult.length > 0 && actionFilterResult.length === 1) { if (actionFilterResult.length === 1) {
return Promise.resolve({ return Promise.resolve({
data: actionFilterResult[0].data, data: actionFilterResult[0].data,
}); });

View file

@ -0,0 +1,504 @@
// Components
import addressLookup from "@/layouts/address-lookup/address-lookup.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
import router from "@/router";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { mount, flushPromises, shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { maska } from 'maska';
import { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
import { validate } from "vee-validate";
import { isGlassAvailableForCarId } from "@/helpers/damage-helper.js"
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { EXPECTATION_FAILED } from "http-status-codes";
// Mock our module for promises.
// jest.mock("@/helpers/layout-helper.js", () => ({
// settleAllPromises: jest.fn(),
// }));
// // Mock fetchCmsContentForPage
// jest.mock("@/helpers/cms-content-helper", () => ({
// fetchCmsContentForPage: jest.fn(),
// }));
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: jest.fn().mockImplementation(() => true),
getDamageString: jest.fn()
}));
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateAfterSaveToHeritageFunnel: jest.fn()
}));
// const questionText = "Question Text";
// const mockMixin = {
// methods: {
// getCmsContent: jest.fn().mockImplementation(() => {
// return questionText;
// })
// }
// }
describe("address-lookup.vue", () => {
describe("registration and service zips", () => {
describe("if registration zip is serviceable", () => {
test("if registration address is provided => update service address on successful continue", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup,{
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
}
})
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
});
});
describe("if registration zip is not serviceable", () => {
test("if registration address is provided and user clicks continue => show non-serviceable zip alert", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: false
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
}
})
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).exists()).toBe(false);
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.displayNonServiceableZipAlert).toBe(true);
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).exists()).toBe(true);
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true);
});
test("if registration address is provided user clicks continue => show service zip field on continue click", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: false
}
);
expect(wrapper.vm.showServiceZipField).toBeFalsy();
expect(wrapper.findComponent({ ref: "serviceZip" }).exists()).toBe(false);
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
}
})
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.showServiceZipField).toBe(true);
expect(wrapper.findComponent({ ref: "serviceZip" }).isVisible()).toBe(true);
});
test("if registration address, service zip are provided, and user clicks continue => don't update service address", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: false
}
);
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
}
})
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(baseMixin.methods.dispatchStoreAction).not.toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
});
test("if registration address, service zip are provided, and user clicks continue => both zips are saved and are different", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
if (value == "43215") {
data = {
isServiceable: false
};
}
else {
data = {
isServiceable: true
}
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
}]
}
}
return Promise.resolve({ data });
})
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
}
})
await wrapper.vm.forwardButtonAction();
await wrapper.setData({
serviceZipCode: "12345"
})
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(store.getters.order.serviceLocation.zipCode).not.toEqual(store.getters.vehicle.registration.zipCode);
expect(store.getters.vehicle.registration.zipCode).toEqual("43215");
expect(store.getters.order.serviceLocation.zipCode).toEqual("12345");
});
});
});
describe.skip("initialization", () => {
test("Page header is initialized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select Damage";
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
addressLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "address-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
test("Customer Questions component is initialized with api data", async (done) => {
//Arrange
const StreetAddressQuestionWidget = { QuestionText: "test" };
const CityQuestionWidget = { QuestionText: "test" };
const StateQuestionWidget = { QuestionText: "test" };
const ZipQuestionWidget = { QuestionText: "test" };
const FirstNameQuestionWidget = { QuestionText: "test" };
const LastNameQuestionWidget = { QuestionText: "test" };
const EmailAddressQuestionWidget = { QuestionText: "test" };
const AlertVerificationWarningWidget = { HeaderText: "test", BodyText: "test" };
const AlertNoMatchWarningWidget = { HeaderText: "test", BodyText: "test" };
const widgets = [
StreetAddressQuestionWidget,
CityQuestionWidget,
StateQuestionWidget,
ZipQuestionWidget,
AlertVerificationWarningWidget,
AlertNoMatchWarningWidget,
FirstNameQuestionWidget,
LastNameQuestionWidget,
EmailAddressQuestionWidget,
];
const { wrapper, apiPromise } = setupMocks({
cmsContent: widgets,
});
//Act
addressLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "address-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(customerQuestions.methods.initializeComponent).toHaveBeenCalledWith(
widgets
);
done();
});
});
});
});
function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressResponse }) {
store.commit(storeMutations.RESET_STATE);
const wrapper = shallowMount(addressLookup, getMountOptions({
...mountOptions,
actionList: [
{
actionName: storeActions.VALIDATE_ZIP,
data: {
isServiceable: isZipServiceable
}
},
{
actionName: storeActions.LOOKUP_VIN_BY_ADDRESS,
data: lookupVinbyAddressResponse ? lookupVinbyAddressResponse : {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
}]
}
}
],
router: {
navigate: jest.fn(),
navigateAfterSave: jest.fn()
},
}));
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
// jest.mock("@/store", () => ({
// commit: jest.fn(),
// dispatch: jest.fn(),
// getters: {
// vehicle: {
// carId: "CARID"
// }
// },
// }));
return { wrapper };
}
// function setupMocks({
// pageHeaderWidgetHeaderText = {},
// mountOptionsMockData = {
// router: {
// navigate: jest.fn(),
// },
// store: {
// getters: {
// vehicle: {},
// },
// },
// },
// }) {
// //Mock api responses
// baseMixin.methods.dispatchStoreAction = jest.fn();
// const apiResponses = {
// cmsContent: {
// FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
// VehicleBannerWidget: {
// GenericVehicleImage:
// "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
// },
// FunnelHeaderWidget: {
// LogoImage:
// "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
// },
// StreetAddressQuestionWidget: {
// QuestionText:
// "test"
// },
// CityQuestionWidget: {
// QuestionText:
// "test"
// },
// StateQuestionWidget: {
// QuestionText:
// "test"
// },
// ZipQuestionWidget: {
// QuestionText:
// "test"
// },
// FirstNameQuestionWidget: {
// QuestionText:
// "test"
// },
// LastNameQuestionWidget: {
// QuestionText:
// "test"
// },
// EmailAddressQuestionWidget: {
// QuestionText:
// "test"
// },
// AlertVerificationWarningWidget: {
// HeaderText:
// "test",
// BodyText:
// "test",
// },
// AlertNoMatchWarningWidget: {
// HeaderText:
// "test",
// BodyText:
// "test",
// },
// },
// };
// const apiPromise = Promise.resolve(apiResponses);
// settleAllPromises.mockImplementation(() => apiPromise);
// fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
// //Mock damage initialize methods
// funnelHeader.methods = {
// initializeComponent: jest.fn(),
// };
// vehicleBanner.methods = {
// initializeComponent: jest.fn(),
// };
// funnelSubHeader.methods = {
// initializeComponent: jest.fn(),
// };
// funnelFooter.methods = {
// initializeComponent: jest.fn(),
// };
// customerQuestions.methods = {
// initializeComponent: jest.fn(),
// };
// addressQuestions.methods = {
// initializeComponent: jest.fn(),
// setupAddressLookup: jest.fn(),
// };
// const mountOptions = getMountOptions(mountOptionsMockData);
// mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
// mountOptions.global.directives = {
// maska: maska
// };
// const wrapper = mount(addressLookup, mountOptions);
// const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
// funnelHeaderWrapper.vm.initializeComponent =
// funnelHeader.methods.initializeComponent;
// const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
// vehicleBannerWrapper.vm.initializeComponent =
// vehicleBanner.methods.initializeComponent;
// const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
// funnelSubHeaderWrapper.vm.initializeComponent =
// funnelSubHeader.methods.initializeComponent;
// const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" });
// funnelFooterWrapper.vm.initializeComponent =
// funnelFooter.methods.initializeComponent;
// const customerQuestionsWrapper = wrapper.findComponent({ name: "customerQuestions" });
// customerQuestionsWrapper.vm.initializeComponent =
// customerQuestions.methods.initializeComponent;
// const addressQuestionsWrapper = wrapper.findComponent({ name: "addressQuestions" });
// addressQuestionsWrapper.vm.initializeComponent =
// addressQuestions.methods.initializeComponent;
// addressQuestionsWrapper.vm.setupAddressLookup =
// addressQuestions.methods.setupAddressLookup;
// return { wrapper, apiPromise };
// }

View file

@ -1,244 +0,0 @@
// Components
import addressLookup from "@/layouts/address-lookup/address-lookup.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { mount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { maska } from 'maska';
import { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
import { validate } from "vee-validate";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
describe("address-lookup.vue", () => {
test("Page header is initialized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select Damage";
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
addressLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "address-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
test("Customer Questions component is initialized with api data", async (done) => {
//Arrange
const StreetAddressQuestionWidget = { QuestionText: "test" };
const CityQuestionWidget = { QuestionText: "test" };
const StateQuestionWidget = { QuestionText: "test" };
const ZipQuestionWidget = { QuestionText: "test" };
const FirstNameQuestionWidget = { QuestionText: "test" };
const LastNameQuestionWidget = { QuestionText: "test" };
const EmailAddressQuestionWidget = { QuestionText: "test" };
const AlertVerificationWarningWidget = { HeaderText: "test", BodyText: "test" };
const AlertNoMatchWarningWidget = { HeaderText: "test", BodyText: "test" };
const widgets = [
StreetAddressQuestionWidget,
CityQuestionWidget,
StateQuestionWidget,
ZipQuestionWidget,
AlertVerificationWarningWidget,
AlertNoMatchWarningWidget,
FirstNameQuestionWidget,
LastNameQuestionWidget,
EmailAddressQuestionWidget,
];
const { wrapper, apiPromise } = setupMocks({
cmsContent: widgets,
});
//Act
addressLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "address-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(customerQuestions.methods.initializeComponent).toHaveBeenCalledWith(
widgets
);
done();
});
});
});
function setupMocks({
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {
router: {
navigate: jest.fn(),
},
store: {
getters: {
vehicle: {},
},
},
},
}) {
//Mock api responses
baseMixin.methods.dispatchStoreAction = jest.fn();
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
StreetAddressQuestionWidget: {
QuestionText:
"test"
},
CityQuestionWidget: {
QuestionText:
"test"
},
StateQuestionWidget: {
QuestionText:
"test"
},
ZipQuestionWidget: {
QuestionText:
"test"
},
FirstNameQuestionWidget: {
QuestionText:
"test"
},
LastNameQuestionWidget: {
QuestionText:
"test"
},
EmailAddressQuestionWidget: {
QuestionText:
"test"
},
AlertVerificationWarningWidget: {
HeaderText:
"test",
BodyText:
"test",
},
AlertNoMatchWarningWidget: {
HeaderText:
"test",
BodyText:
"test",
},
},
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock damage initialize methods
funnelHeader.methods = {
initializeComponent: jest.fn(),
};
vehicleBanner.methods = {
initializeComponent: jest.fn(),
};
funnelSubHeader.methods = {
initializeComponent: jest.fn(),
};
funnelFooter.methods = {
initializeComponent: jest.fn(),
};
customerQuestions.methods = {
initializeComponent: jest.fn(),
};
addressQuestions.methods = {
initializeComponent: jest.fn(),
setupAddressLookup: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
mountOptions.global.directives = {
maska: maska
};
const wrapper = mount(addressLookup, mountOptions);
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent =
funnelHeader.methods.initializeComponent;
const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" });
funnelFooterWrapper.vm.initializeComponent =
funnelFooter.methods.initializeComponent;
const customerQuestionsWrapper = wrapper.findComponent({ name: "customerQuestions" });
customerQuestionsWrapper.vm.initializeComponent =
customerQuestions.methods.initializeComponent;
const addressQuestionsWrapper = wrapper.findComponent({ name: "addressQuestions" });
addressQuestionsWrapper.vm.initializeComponent =
addressQuestions.methods.initializeComponent;
addressQuestionsWrapper.vm.setupAddressLookup =
addressQuestions.methods.setupAddressLookup;
return { wrapper, apiPromise };
}

View file

@ -329,8 +329,8 @@ export default {
{ {
licenseLastName: lastName, licenseLastName: lastName,
licenseStreetAddress: streetAddress, licenseStreetAddress: streetAddress,
licenseZip: zip, licenseZip: zip,
licenseState: state licenseState: state,
}, false }, false
); );
}, },

View file

@ -0,0 +1,63 @@
import { shallowMount } from "@vue/test-utils";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
describe("addressVehiclesQuestion.vue", () => {
it("Should return content for differentVehicleAlertHeader", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.differentVehicleAlertHeader).toEqual('FoundWindshieldTestReturn');
});
it("Should return content for differentVehicleAlertBody", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
propsData: {
vehicles: ["1", "2"],
modelValue: ["1", "2"],
}
});
// Assert
expect(wrapper.vm.differentVehicleAlertBody).toEqual('FoundWindshieldTestReturn');
});
it("Should emit a modelValue change when setting selectedVehicleVin", async () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
propsData: {
vehicles: ["1", "2"],
modelValue: ["1", "2"],
}
});
// Act
const localThis = { $emit: jest.fn() }
addressVehiclesQuestion.computed.selectedVehicleVin.set.call(localThis, 'newValue');
// Assert
expect(localThis.$emit).toBeCalledWith("update:modelValue", "newValue");
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn((contentName) => {
if (contentName === "FoundWindshield") {
return 'FoundWindshieldTestReturn';
}
return null;
}),
vehicles: jest.fn(() => {
return [{ vehicle: "test" }];
})
}
}

View file

@ -1,113 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
// import store from "@/store";
// jest.mock("@/store", () => { return {}; }, { virtual: true });
describe("addressVehiclesQuestion.vue", () => {
it("Should include button-question component", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
propsData: {
// vehicles: [
// {
// "Name": "testname",
// "Text": "testtext",
// }
// ]
}
});
// console.log('wrapper.html: ', wrapper.html());
// Assert
const buttonQuestion = wrapper.find('button-question-stub');
expect(buttonQuestion).toBe;
});
it("on initialize should pass in questionText", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
propsData: {},
});
//Act
addressVehiclesQuestion.methods.initializeComponent.call(wrapper.vm, {QuestionText: 'Testing question text'});
// console.log('wrapper.html: ', wrapper.html());
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
// Assert
expect(wrapper.vm.questionText).toEqual('Testing question text');
});
// it("Alert should show if prop isCarIdDifferent is true", () => {
// // Arrange
// const wrapper = shallowMount(addressVehiclesQuestion, setupMountOptions({
// propsData: {
// isCarIdDifferent: true,
// }
// }));
// //Act
// const alert = wrapper.find('alert');
// console.log('wrapper.html: ', wrapper.html());
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
// // Assert
// // expect(wrapper.vm.questionText).toEqual('Testing question text');
// });
});
function setupMountOptions(mountOptionsMockData = {}) {
// //Mock store
// store.dispatch = jest.fn(() => {});
// store.getters = {};
// const mockMixin = {
// methods: {
// getCmsContent: jest.fn().mockImplementation(() => {
// return '';
// }),
// getDamageString: jest.fn().mockImplementation(() => {
// return '';
// })
// },
// store: {
// dispatch: store.dispatch,
// getters: store.getters,
// },
// }
const mockGetCmsContent = jest.fn();
mockGetCmsContent((cmsWidget, field) => {
return field
});
const defaultMountOptions = {
// route: { query: { fmgPage: 'page-name' } },
mixins: {
methods: {
getCmsContent: mockGetCmsContent,
},
},
global: {
mocks: {
store: {
dispatch: store.dispatch,
getters: store.getters,
},
},
},
};
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
console.log('what are allMountOptions??? ', allMountOptions);
return allMountOptions;
}

View file

@ -0,0 +1,354 @@
// Components
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
// Mock our module for promises.
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: () => {
return false;
},
}));
describe("addressVehicles.vue", () => {
test("Should return true for valid page requisites if carId / zipCode / emailAddress / pageData exists", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$router.navigate = jest.fn();
// Act
const result = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(result).toBe(true);
wrapper.unmount();
});
test("Should return false for valid page requisites if carId is missing", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$router.navigate = jest.fn();
// Act
wrapper.vm.$store.getters.order.vehicle.carId = null;
const result = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(result).toBe(false);
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should navigate to CLICKED_BACK if backButtonAction is run", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$router.navigate = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
});
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toBeCalled();
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should run several related methods if forwardButtonAction is run", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
data: {
carId: "456"
}
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {});
wrapper.vm.navigateForward = jest.fn().mockImplementation(()=> {});
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
});
await wrapper.vm.forwardButtonAction();
wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.updateCustomerInfo).toBeCalled();
expect(wrapper.vm.navigateForward).toBeCalled();
wrapper.unmount();
});
test("Should return out of forwardButtonAction if lookupVin returns with an error", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
error: "there is an error"
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {});
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
});
await wrapper.vm.forwardButtonAction();
wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.forwardButtonAction).toReturn;
wrapper.unmount();
});
test("Should send dispatch reset if carId is different and selected glass not available for vehicle on updateCustomerInfo", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
data: {
carId: "456"
}
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: false,
isCarIdDifferent: true,
});
await wrapper.vm.updateCustomerInfo(wrapper.vm.selectedVehicle.vin, wrapper.vm.selectedVehicle.vehicle);
//Assert
expect(store.dispatch).toBeCalledWith("resetDamageAndDependencies");
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should send dispatch store action if lookupVin is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
await wrapper.vm.lookupVin('1234567890');
//Assert
expect(store.dispatch).toBeCalledWith("lookupVehicleByVin", {"vin": "1234567890"});
wrapper.unmount();
});
test("If selectedVehicleVin changes, then should update isCarIdDifferent", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isCarIdDifferent: false,
});
await wrapper.vm.resetDependentState();
//Assert
expect(wrapper.vm.isCarIdDifferent).toBe(true);
wrapper.unmount();
});
test("If selectedVehicleVin changes, then text on funnel footer should be updated", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isCarIdDifferent: false,
});
await wrapper.vm.resetDependentState();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toBeCalled();
wrapper.unmount();
});
test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$router.navigateAfterSave = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: false,
isCarIdDifferent: true,
});
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.$router.navigateAfterSave).toBeCalledTimes(1);
wrapper.unmount();
});
test("Should navigate to navigateAfterSaveToHeritageFunnel if carId is not different on navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: true,
isCarIdDifferent: false,
});
await wrapper.vm.navigateForward();
//Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toBeCalledTimes(1);
wrapper.unmount();
});
});
function setupMocks({
// modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
// dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => {});
store.getters = {
pageData: jest.fn((pageName) => {
// console.log('pageName: ', pageName) // address-vehicles
return [
{
vehicle: {
"carId": "CR00069309",
"category": "SUV",
"year": 2020,
"make": "Hyundai",
"model": "Santa Fe",
"style": "4 door utility",
"imageUrl": "https://dbhdyzvm8lm25.cloudfront.net/color_0320_032/MY2020/13769/13769_cc0320_032_WW8.jpg",
"imageVifNumber": "13769",
"imageVifColor": "white"
},
vin: "5NMS3CADXLH233004"
},
];
}),
order: {
vehicle: {
carId: "123",
},
serviceLocation: {
zipCode: "12345"
},
customer: {
emailAddress: "qw@er.ty"
}
},
damage: {
glassToReplace: "Windshield"
},
vehicle: {
carId: "456",
}
};
// baseMixin.methods.dispatchStoreAction = jest.fn();
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
router: {
navigate: jest.fn(),
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn((contentName) => {
if (contentName === "FoundMultipleVehicles") {
return 'FoundMultipleVehiclesTestReturn';
}
if (contentName === "ProvideVinAlert") {
return 'ProvideVinAlertTestReturn';
}
return null;
}),
// dispatchStoreAction: jest.fn(() => {
// console.log("23424243")
// }),
// isGlassAvailableForCarId: () => {
// console.log("%%%%%%%%%%%%%%%")
// return Promise.resolve(true)
// }
// lookupVin: jest.fn(() => Promise.resolve(lookupVinResponse)),
},
computed: {
dynamicStrings() {
return {ROUTER_LINK: "routerLink:"}
}
}
}
// mountOptions.propsData = {
// modelValue: modelValueProp,
// };
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(addressVehicles, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper };
}

View file

@ -1,26 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
describe("addressVehicles.vue", () => {
it("Should include address-vehicles-question component", () => {
// Arrange
const wrapper = shallowMount(addressVehicles, {
propsData: {
vehicles: [
{
"Name": "testname",
"Text": "testtext",
}
]
}
});
// console.log('wrapper.html: ', wrapper.html());
// Assert
const addressVehiclesQuestion = wrapper.find('address-vehicles-question-stub');
expect(addressVehiclesQuestion).toBe;
});
});

View file

@ -28,8 +28,8 @@
/> />
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length"> <div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy"> <span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="text-body"> <span v-if="doesCopyContainRouterLink(copy)" class="text-body">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link> <router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
</span> </span>
<span v-else class="m-0 text-body" v-html="copy"></span> <span v-else class="m-0 text-body" v-html="copy"></span>
</span> </span>
@ -68,6 +68,10 @@ import { required } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { isGlassAvailableForCarId } from "@/helpers/damage-helper"; import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
import { doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper"
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED)); defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED));
@ -116,7 +120,7 @@ export default {
}, },
splitAlertProvideVinBodyForLink() { splitAlertProvideVinBodyForLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed // Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.AlertProvideVinBody.split(/{(.*?)}/g); return this.splitCopyOnCMSPlaceHolder(this.AlertProvideVinBody);
}, },
VehiclesForQuestions() { VehiclesForQuestions() {
const vehiclesData = this.VehiclesFromApi; const vehiclesData = this.VehiclesFromApi;
@ -145,6 +149,10 @@ export default {
}, },
}, },
methods: { methods: {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
if ( if (
store.getters.order.vehicle.carId store.getters.order.vehicle.carId
@ -162,8 +170,10 @@ export default {
async forwardButtonAction() { async forwardButtonAction() {
const vinLookup = await this.lookupVin(this.selectedVehicle.vin).catch(() => { const vinLookup = await this.lookupVin(this.selectedVehicle.vin).catch(() => {
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
return;
}); });
if (!vinLookup) {
return;
}
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId); this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId);
this.updateCustomerInfo(this.selectedVehicle.vin, this.selectedVehicle.vehicle); this.updateCustomerInfo(this.selectedVehicle.vin, this.selectedVehicle.vehicle);
this.navigateForward(); this.navigateForward();
@ -210,7 +220,7 @@ export default {
}, },
watch: { watch: {
selectedVehicleVin(vehicleVin) { selectedVehicleVin() {
// does this vehicle match the previously selected carId? // does this vehicle match the previously selected carId?
this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId; this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId;
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`); this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`);

View file

@ -7,7 +7,7 @@
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<menuButton/> <menuModal/>
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
@ -1089,7 +1089,7 @@
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question"; import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question";
import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information"; import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information";
import menuButton from "@/common-components/menu-button/menu-button"; import menuModal from "@/common-components/funnel-header/menu-modal/menu-modal";
export default { export default {
name: "App", name: "App",
components: { components: {
@ -1104,7 +1104,7 @@
textboxQuestion, textboxQuestion,
dropdownQuestion, dropdownQuestion,
vinInformation, vinInformation,
menuButton, menuModal,
}, },
data() { data() {
return { return {

View file

@ -6,9 +6,10 @@ import { settleAllPromises } from "@/helpers/layout-helper.js";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper"; import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount, flushPromises } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store"; import store from "@/store";
jest.mock('@/assets/img/loader.gif', () => 'loader.gif') jest.mock('@/assets/img/loader.gif', () => 'loader.gif')
@ -24,416 +25,502 @@ jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(), fetchCmsContentForPage: jest.fn(),
})); }));
// Mock Store
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
order: {
customer: { emailAddress: "test@test.com"},
serviceLocation: {zip: "11111"},
},
vehicle: {
carId: "TESTID",
registration: {
licensePlate: "TESTPLATE",
zipCode: "12345",
},
},
eventBusItem: jest.fn(),
damage: {
glassToReplace: []
},
},
}));
describe("license-plate-lookup.vue", () => { describe("license-plate-lookup.vue", () => {
test("CarId set, arePagePrerequisitesValid should be true ", async () => { describe("get values from store", () => {
//Arrange test("getLicensePlateFromStore returns store license plate", async () => {
const { wrapper } = setupMocks({}); // Arrange
const { wrapper } = setupMocks({});
const mockLicensePlate = "TESTPLATE";
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, mockLicensePlate);
//Act // Act
licensePlateLookup.beforeRouteEnter.call( const licensePlate = wrapper.vm.getLicensePlateFromStore();
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); // Assert
await nextTick(); expect(licensePlate).toEqual(mockLicensePlate);
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("license-plate-lookup.vue", () => {
test("BackButtonAction triggers a router.navigate change", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("getLicensePlateFromStore returns store license plate", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const licensePlate = wrapper.vm.getLicensePlateFromStore();
// Assert
expect(licensePlate).toEqual("TESTPLATE");
});
});
describe("license-plate-lookup.vue", () => {
test("getRegistrationZipFromStore returns store registration zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const registrationZip = wrapper.vm.getRegistrationZipFromStore();
// Assert
expect(registrationZip).toEqual("12345");
});
});
describe("license-plate-lookup.vue", () => {
test("getEmailFromStore returns store customer email", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const customerEmail = wrapper.vm.getEmailFromStore();
// Assert
expect(customerEmail).toEqual("test@test.com");
});
});
describe("license-plate-lookup.vue", () => {
test("getServiceZipFromStore returns store service zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const serviceZip = wrapper.vm.getServiceZipFromStore();
// Assert
expect(serviceZip).toEqual("11111");
});
});
describe("license-plate-lookup.vue", () => {
test("Navigate forward should be called and isCarId should be set to false when data entered matches store data on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: true}};
}); });
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; test("getRegistrationZipFromStore returns store registration zip", async () => {
}); // Arrange
const vinLookup = {data: {vehicle: {carId: "TESTID"}}} const { wrapper } = setupMocks({});
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => { const mockRegistrationZip = "12345";
return {catch: () => vinLookup}; store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, mockRegistrationZip);
// ACT
const registrationZip = wrapper.vm.getRegistrationZipFromStore();
// Assert
expect(registrationZip).toEqual(mockRegistrationZip);
}); });
wrapper.vm.navigateForward = jest.fn();
//Act test("getEmailFromStore returns store customer email", async () => {
licensePlateLookup.beforeRouteEnter.call( // Arrange
wrapper.vm, const { wrapper } = setupMocks({});
{ query: { fmgPage: "license-plate-lookup" } }, const mockEmail = "test@test.com";
undefined, store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, mockEmail);
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert // ACT
expect(wrapper.vm.navigateForward).toHaveBeenCalled(); const customerEmail = wrapper.vm.getEmailFromStore();
expect(wrapper.vm.isCarIdDifferent).toEqual(false);
});
});
describe("license-plate-lookup.vue", () => { // Assert
test("Function should stop and datam isRegistrationZipServicable should be set to false when service zip entered returns false on forwardButtonAction click", async () => { expect(customerEmail).toEqual(mockEmail);
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: false}};
}); });
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert test("getServiceZipFromStore returns store service zip", async () => {
expect(wrapper.vm.isRegistrationZipServicable).toEqual(false); // Arrange
}); const { wrapper } = setupMocks({});
}); const mockServiceZip = "11111";
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, mockServiceZip);
describe("license-plate-lookup.vue", () => { // ACT
test("Function should stop and datam isCarIdDifferent should be set to true when carId entered doesn't match store carId or previously entered carId on forwardButtonAction click", async () => { const serviceZip = wrapper.vm.getServiceZipFromStore();
// Arrange // Assert
const { wrapper } = setupMocks({}); expect(serviceZip).toEqual(mockServiceZip);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: true}};
}); });
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { });
return '';
}); describe("navigation", () => {
const vinLookup = {data: {vehicle: {carId: "TESTID1"}}} test("BackButtonAction triggers a router.navigate change", async () => {
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return {catch: () => vinLookup}; //Arrange
const { wrapper } = setupMocks({});
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
}); });
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
//Act describe("on forwardButtonAction click", () => {
licensePlateLookup.beforeRouteEnter.call( test("Navigate forward should be called and isCarIdDifferent should be set to false when data entered matches store data on forwardButtonAction click", async () => {
wrapper.vm, // Arrange
{ query: { fmgPage: "license-plate-lookup" } }, const { wrapper } = setupMocks({});
undefined, const mockCarId = "TESTID";
(c) => c(wrapper.vm) store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
); store.commit(storeMutations.UPDATE_CAR_ID, mockCarId);
await wrapper.vm.forwardButtonAction();
//Assert wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
expect(wrapper.vm.isCarIdDifferent).toEqual(true); return { data: { isServiceable: true } };
}); });
}); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.navigateForward = jest.fn();
describe("license-plate-lookup.vue", () => { //Act
test("Navigate forward should be called and isCarId should be set to true when carId entered matches previously entered carId and rest of data entered matches store data on forwardButtonAction click", async () => { licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
// Arrange await wrapper.vm.forwardButtonAction();
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => { //Assert
return {data: {isServiceable: true}}; expect(wrapper.vm.isCarIdDifferent).toEqual(false);
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
test("Function should stop and datam isRegistrationZipServicable should be set to false when service zip entered returns false on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: false } };
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.isRegistrationZipServicable).toEqual(false);
});
test("Function should stop and datam isCarIdDifferent should be set to true when carId entered doesn't match store carId or previously entered carId on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: true } };
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
const vinLookup = { data: { vehicle: { carId: "TESTID1" } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.isCarIdDifferent).toEqual(true);
});
test("Navigate forward should be called and isCarId should be set to true when carId entered matches previously entered carId and rest of data entered matches store data on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: true } };
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
const vinLookup = { data: { vehicle: { carId: "TESTID1" } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.previouslyEnteredCarId = "TESTID1";
wrapper.vm.navigateForward = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
expect(wrapper.vm.isCarIdDifferent).toEqual(true);
});
}); });
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; describe("navigateForward", () => {
}); test("NavigateAfterSave should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => {
const vinLookup = {data: {vehicle: {carId: "TESTID1"}}}
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => { // Arrange
return {catch: () => vinLookup}; const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = true;
wrapper.vm.isSelectedGlassAvailableForVehicle = false;
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
store.dispatch = jest.fn();
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled();
});
test("navigateAfterSaveToHeritageFunnel should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = false;
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
await wrapper.vm.navigateForward();
//Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toHaveBeenCalled();
});
}); });
wrapper.vm.previouslyEnteredCarId = "TESTID1";
wrapper.vm.navigateForward = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
expect(wrapper.vm.isCarIdDifferent).toEqual(true);
}); });
});
describe("license-plate-lookup.vue", () => { describe("button text", () => {
test("Button Text should revert to initial value when licensePlate textfield has new text", async () => { test("Button Text should revert to initial value when licensePlate textfield has new text", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
//Act //Act
wrapper.vm.licensePlate = "NEWPLATE"; wrapper.vm.licensePlate = "NEWPLATE";
wrapper.vm.getCmsContent = jest.fn(); wrapper.vm.getCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
//Assert //Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled(); expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("Button Text should revert to initial value when registrationZip textfield has new text", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.registrationZip = "55555";
wrapper.vm.getCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("Button Text should revert to initial value when serviceZip textfield has new text", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.serviceZip = "55555";
wrapper.vm.getCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("Dispatch reset damage and dependencies should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when updateCustomerInfo is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = true;
wrapper.vm.isSelectedGlassAvailableForVehicle = false;
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
}); });
store.commit = jest.fn();
store.dispatch = jest.fn();
const vehicleInfo = {year: "2020", make: "honda", model: "civic", style: "2 door", carId: "TestId", category: "testCat", imageUrl: "image.jpg", imageVifNumber: "123", imageColor: "blue"} test("Button Text should revert to initial value when registrationZip textfield has new text", async () => {
await wrapper.vm.updateCustomerInfo('vin', vehicleInfo, 'registrationState');
//Assert // Arrange
expect(store.dispatch).toHaveBeenCalled(); const { wrapper } = setupMocks({});
});
});
describe("license-plate-lookup.vue", () => { //Act
test("NavigateAfterSave should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => { wrapper.vm.registrationZip = "55555";
wrapper.vm.getCmsContent = jest.fn();
await wrapper.vm.$nextTick();
// Arrange //Assert
const { wrapper } = setupMocks({}); expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
//Act
wrapper.vm.isCarIdDifferent = true;
wrapper.vm.isSelectedGlassAvailableForVehicle = false;
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
}); });
store.dispatch = jest.fn();
await wrapper.vm.navigateForward(); test("Button Text should revert to initial value when serviceZip textfield has new text", async () => {
//Assert // Arrange
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled(); const { wrapper } = setupMocks({});
});
});
describe("license-plate-lookup.vue", () => { //Act
test("navigateAfterSaveToHeritageFunnel should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => { wrapper.vm.serviceZip = "55555";
wrapper.vm.getCmsContent = jest.fn();
await wrapper.vm.$nextTick();
// Arrange //Assert
const { wrapper } = setupMocks({}); expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
//Act
wrapper.vm.isCarIdDifferent = false;
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
}); });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn(); })
await wrapper.vm.navigateForward();
//Assert describe("saving registrationZip and serviceZip on continue", () => {
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toHaveBeenCalled(); test("registrationZip is serviceable and vehicle match is found => sets service zip/state to registration zip/state", async () => {
}); // Arrange
const { wrapper } = setupMocks({});
const mockCarId = "TESTID";
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId)
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.validateZip = jest.fn().mockImplementation((zip) => {
if (zip)
return { data: { isServiceable: true, state: "OH" } };
return
});
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => new Promise(resolve => resolve(vinLookup)));
await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(store.getters.order.serviceLocation.zipCode).toEqual(store.getters.vehicle.registration.zipCode);
expect(store.getters.vehicle.registration.zipCode).toEqual("00000");
expect(store.getters.order.serviceLocation.zipCode).toEqual("00000");
})
test("vehicle match is found but registrationZip is not serviceable => shows service zip/state field", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: false, state: "XX" } };
});
await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Assert
const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']");
expect(serviceZipField.exists()).toBe(true);
expect(serviceZipField.isVisible()).toBe(true);
})
test("registrationZip is not serviceable so serviceZip field is shown, continue clicked => user cannot continue", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: false, state: "XX" } };
});
await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
await wrapper.vm.forwardButtonAction();
wrapper.vm.$router.navigateAfterSave = jest.fn();
// At this point, serviceZip field is shown
// Act
// Continue without entering anything into service zip field
await wrapper.vm.forwardButtonAction();
// Assert
const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']");
expect(serviceZipField.exists()).toBe(true);
expect(serviceZipField.isVisible()).toBe(true); 3
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).not.toHaveBeenCalled();
expect(wrapper.vm.$router.navigateAfterSave).not.toHaveBeenCalled();
});
test("registrationZip is not serviceable so serviceZip field is shown, user enters serviceZip => user can continue", async () => {
// Arrange
const { wrapper } = setupMocks({});
const registrationZip = "00000";
const serviceZip = "99999";
const mockCarId = "TestCarId";
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId);
wrapper.vm.validateZip = jest.fn().mockImplementation((zip) => {
return { data: { isServiceable: zip == registrationZip ? false : true, state: "XX" } };
});
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.navigateForward = jest.fn();
await wrapper.setData({ registrationZip: registrationZip });
await wrapper.vm.forwardButtonAction();
// At this point, serviceZip field is shown
await wrapper.setData({ serviceZip: serviceZip });
// Act
// Continue after entering input into service zip field
await wrapper.vm.forwardButtonAction();
// Assert
const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']");
expect(serviceZipField.exists()).toBe(true);
expect(serviceZipField.isVisible()).toBe(true);
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
test("registrationZip is not serviceable so serviceZip field is shown, user enters serviceZip => service and registration zips/states saved", async () => {
// Arrange
const { wrapper } = setupMocks({});
const registrationZip = "00000";
const serviceZip = "99999";
const mockCarId = "TestCarId";
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId);
wrapper.vm.validateZip = jest.fn().mockImplementation((zip) => {
return { data: { isServiceable: zip == registrationZip ? false : true, state: "XX" } };
});
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.navigateForward = jest.fn();
await wrapper.setData({ registrationZip: registrationZip });
await wrapper.vm.forwardButtonAction();
// At this point, serviceZip field is shown
await wrapper.setData({ serviceZip: serviceZip });
// Act
// Continue after entering value into service zip field
await wrapper.vm.forwardButtonAction();
// Assert
expect(store.getters.vehicle.registration.zipCode).toEqual(registrationZip);
expect(store.getters.order.serviceLocation.zipCode).toEqual(serviceZip);
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
})
describe("miscellaneous", () => {
test("CarId set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.commit(storeMutations.UPDATE_CAR_ID, "TESTCARID");
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("Dispatch reset damage and dependencies should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when updateCustomerInfo is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = true;
wrapper.vm.isSelectedGlassAvailableForVehicle = false;
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
store.commit = jest.fn();
store.dispatch = jest.fn();
const vehicleInfo = { year: "2020", make: "honda", model: "civic", style: "2 door", carId: "TestId", category: "testCat", imageUrl: "image.jpg", imageVifNumber: "123", imageColor: "blue" }
await wrapper.vm.updateCustomerInfo('vin', vehicleInfo, 'registrationState');
//Assert
expect(store.dispatch).toHaveBeenCalled();
})
test("dispatch non blocking store action called on validate zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.validateZip("12345");
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
test("dispatch non blocking store action called on lookup vin", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.lookupVin("zzz123fqsfwg");
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
})
}); });
describe("license-plate-lookup.vue", () => {
test("dispatch non blocking store action called on validate zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.validateZip("12345");
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("dispatch non blocking store action called on lookup vin", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.lookupVin("zzz123fqsfwg");
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
});
function setupMocks({ function setupMocks({
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = { mountOptionsMockData = {
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
}, },
licensePlate: "TESTPLATE",
registrationZip: "12345"
}, },
}) { }) {
store.commit(storeMutations.RESET_STATE);
//Mock api responses //Mock api responses
baseMixin.methods.dispatchStoreAction = jest.fn(); baseMixin.methods.dispatchStoreAction = jest.fn();
const apiResponses = { const apiResponses = {
@ -455,13 +542,16 @@ function setupMocks({
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
const wrapper = shallowMount(licensePlateLookup, mountOptions); const wrapper = shallowMount(licensePlateLookup, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -220,19 +220,18 @@ export default {
return store.getters.order.customer.emailAddress; return store.getters.order.customer.emailAddress;
}, },
getServiceZipFromStore() { getServiceZipFromStore() {
return store.getters.order.serviceLocation.zip; return store.getters.order.serviceLocation.zipCode;
}, },
backButtonAction() { backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
async forwardButtonAction() { async forwardButtonAction() {
//Call zip validation services //Call zip validation services
const registrationZipValidationPromise = this.validateZip(this.registrationZip); const registrationZipValidationPromise = this.validateZip(this.registrationZip);
const serviceZipValidationPromise = this.serviceZip ? this.validateZip(this.serviceZip) : null; const serviceZipValidationPromise = this.serviceZip ? this.validateZip(this.serviceZip) : null;
const registrationZipValidationResults = await registrationZipValidationPromise; const registrationZipValidationResults = await registrationZipValidationPromise;
const serviceZipValidationResults = serviceZipValidationPromise !== null ? (await serviceZipValidationPromise) : registrationZipValidationResults; const serviceZipValidationResults = serviceZipValidationPromise !== null ? (await serviceZipValidationPromise) : registrationZipValidationResults;
//Handle service zip validations //Handle service zip validations
if (!serviceZipValidationResults.data.isServiceable) { if (!serviceZipValidationResults.data.isServiceable) {
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
@ -255,6 +254,7 @@ export default {
this.isCarIdDifferent = false; this.isCarIdDifferent = false;
return; return;
}); });
this.isCarIdDifferent = this.isCarIdDifferent =
vinLookup.data.vehicle.carId !== store.getters.vehicle.carId; vinLookup.data.vehicle.carId !== store.getters.vehicle.carId;

File diff suppressed because it is too large Load diff

View file

@ -405,7 +405,7 @@ export default {
shouldDisplayVehicleChangeAlert() { shouldDisplayVehicleChangeAlert() {
return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]; return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT];
}, },
shouldHideBackButton(){ shouldHideBackButton() {
return this.$store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration; return this.$store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
} }
}, },

View file

@ -1,52 +1,42 @@
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper"; import { setCookieProperties, getDeviceIdValue, getSessionIdValue, getSessionKeyValue } 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";
import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents } from "@/constants/analytics"; import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents } from "@/constants/analytics";
import { cookieNames } from "@/constants/cookie-names";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
export default { export default {
methods: { methods: {
logPageView(pageEvent) { logPageView(pageEvent) {
// if the user does not have a session id from the content site, do not log.
const sid = getSessionIdValue();
if (sid === '00000000-0000-0000-0000-000000000000' || sid == null) {
return;
}
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: sid, sessionId: getSessionIdValue(),
action: '', action: '',
event: pageEvent, event: pageEvent,
shouldUseSessionId: true, shouldUseSessionId: false,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false); baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false);
}, },
logCustomEvent(category, action, label, value) { logCustomEvent(category, action, label, value) {
// if the user does not have a session id from the content site, do not log.
const sid = getSessionIdValue();
if (sid === '00000000-0000-0000-0000-000000000000' || sid == null) {
return;
}
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: sid, sessionId: getSessionIdValue(),
category: category, category: category,
action: action, action: action,
label: label, label: label,
value: value, value: value,
shouldUseSessionId: true, shouldUseSessionId: false,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false); baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false);
@ -66,7 +56,7 @@ export default {
pushToDataLayerIfDefined(eventToBePushed); pushToDataLayerIfDefined(eventToBePushed);
if (pushToLogApp) { if (pushToLogApp) {
this.logCustomEvent(category, action, label, undefined); this.logCustomEvent(category, action, label, undefined);
} }
}, },
@ -116,6 +106,32 @@ export default {
return baseMethod.apply(object, arguments); return baseMethod.apply(object, arguments);
}; };
}, },
async initSession() {
const sid = getSessionIdValue();
const skey = getSessionKeyValue();
var payload = {
userId: getDeviceIdValue(),
sessionId: sid,
userAgent: navigator.userAgent,
referrer: document.referrer,
};
const response = await baseMixin.methods.dispatchStoreAction(storeActions.INITIALIZE_SESSION, payload, false);
if (response.data) {
if (response.data.sessionKey && skey === 0) {
setCookieProperties({ [cookieNames.SESSION_KEY]: response.data.sessionKey});
}
if (response.data.sessionId && sid === '00000000-0000-0000-0000-000000000000') {
setCookieProperties({ [cookieNames.SESSION_ID]: response.data.sessionId});
}
}
},
noSession() {
return getSessionKeyValue() === 0 || getSessionIdValue() === '00000000-0000-0000-0000-000000000000';
}
}, },
computed: { computed: {
analyticsPageEvents() { analyticsPageEvents() {

View file

@ -1,6 +1,7 @@
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js"; import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents } from "@/constants/analytics";
describe("analyticsMixin.js", () => { describe("analyticsMixin.js", () => {
test("logPageView: calls dispatch with type and payload", () => { test("logPageView: calls dispatch with type and payload", () => {
@ -26,9 +27,6 @@ describe("analyticsMixin.js", () => {
}); });
test("logCustomEvent: calls dispatch with type and payload", () => { test("logCustomEvent: calls dispatch with type and payload", () => {
const type = "";
const payload = {};
const mockData = { const mockData = {
actionList: [{ actionList: [{
actionName: storeActions.LOG_CUSTOM_EVENT actionName: storeActions.LOG_CUSTOM_EVENT
@ -36,7 +34,7 @@ describe("analyticsMixin.js", () => {
} }
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
analyticsMixin.methods.logCustomEvent(type, payload); analyticsMixin.methods.logCustomEvent("someCat", "someAction", "someLabel", "someVal");
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
@ -126,4 +124,37 @@ describe("analyticsMixin.js", () => {
//Assert //Assert
expect(obj!=null); expect(obj!=null);
}); });
test("analyticsPageEvents returns constants analyticsPageEvents", () => {
//Act
const analyticsPE = analyticsMixin.computed.analyticsPageEvents();
//Assert
expect(analyticsPE).toEqual(analyticsPageEvents);
});
test("GaActions returns constants GaActions", () => {
//Act
const gaActions = analyticsMixin.computed.GaActions();
//Assert
expect(gaActions).toEqual(GaActions);
});
test("GaCategories returns constants GaCategories", () => {
//Act
const gaCategories = analyticsMixin.computed.GaCategories();
//Assert
expect(gaCategories).toEqual(GaCategories);
});
test("GaLabels returns constants GaLabels", () => {
//Act
const gaLabels = analyticsMixin.computed.GaLabels();
//Assert
expect(gaLabels).toEqual(GaLabels);
});
}); });

View file

@ -5,6 +5,7 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
import { vehicleCategories } from "@/constants/vehicle-categories.js"; import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { routerParams } from "@/router/router-constants/router-params"; import { routerParams } from "@/router/router-constants/router-params";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings";
export default { export default {
data() { data() {
@ -43,9 +44,9 @@ export default {
} }
}, },
getFooterInfoBoxHeight() { getFooterInfoBoxHeight() {
const footerInfoBox = document.querySelector(".footer #infoBox"); const footerInfoBox = document.querySelector(".footer#infoBox");
return footerInfoBox ? footerInfoBox.offsetHeight : 0; return footerInfoBox ? footerInfoBox.offsetHeight : 0;
} },
}, },
computed: { computed: {
storeActions() { storeActions() {
@ -65,6 +66,9 @@ export default {
}, },
queryStrings(){ queryStrings(){
return queryStrings; return queryStrings;
},
dynamicStrings(){
return dynamicStrings;
} }
}, },
}; };

View file

@ -40,6 +40,10 @@ 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.initSession();
}
// If the saved session has timed out, clear the session, execute 404 logic. // If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
await GoToFunnelStartOn404(next); await GoToFunnelStartOn404(next);

View file

@ -509,6 +509,26 @@ export const actions = {
}); });
}, },
initializeSession(context, { userId, sessionId, userAgent, referrer }) {
var payload = {
applicationName: 'SafeliteDotCom',
userId: userId,
deviceId: userId,
sessionId: sessionId,
userAgent: userAgent,
operatorId: "WEB",
userName: "SafeliteConceptFunnel",
referrer: referrer
};
return globalMethods.callHttpClient({
method: endpoints.InitializeSession.method,
endpoint: endpoints.InitializeSession.url,
payload: payload,
logApiCall: false
});
},
GetExperimentsByUser(context, { userId }){ GetExperimentsByUser(context, { userId }){
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetExperimentsByUser.method, method: endpoints.GetExperimentsByUser.method,

View file

@ -642,7 +642,7 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.logPageView(context, { userId: "userId", sessionKey: "sessionKey", pageName: "pageName", sessionId: "sessionId", pageEvent: pageEvent, shouldUseSessionId: true }); const response = await actions.logPageView(context, { userId: "userId", sessionKey: "sessionKey", pageName: "pageName", sessionId: "sessionId", pageEvent: pageEvent, shouldUseSessionId: false });
expect(response).toEqual({}); expect(response).toEqual({});
}); });
@ -663,7 +663,22 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.logCustomEvent(context, { userId: "userId", sessionKey: "sessionKey", pageName: "pageName", sessionId: "sessionId", customEvent: customEvent, shouldUseSessionId: true }); const response = await actions.logCustomEvent(context, { userId: "userId", sessionKey: "sessionKey", pageName: "pageName", sessionId: "sessionId", customEvent: customEvent, shouldUseSessionId: false });
expect(response).toEqual({});
});
it("initializeSession action, should return nothing", async () => {
// Arrange
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ });
});
// Assert
const response = await actions.initializeSession(context, { userId: "userId", sessionId: "", userAgent: "", referrer: "", shouldUseSessionId: false });
expect(response).toEqual({}); expect(response).toEqual({});
}); });

View file

@ -47,4 +47,13 @@ body {
.page-container-grouped-styles { .page-container-grouped-styles {
@extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5; @extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5;
} }
}
//Footer modal backdrop adjustments for positioning
.modal-backdrop {
left: 50%;
transform: translateX(-50%);
max-width: 576px;
height: calc(100% - 72px);
}
}

View file

@ -180,3 +180,7 @@ $box-shadow-inset: inset 0 1px 2px rgba($black, .075);
$alert-bg-scale: -90%; $alert-bg-scale: -90%;
$alert-border-scale: -100%; $alert-border-scale: -100%;
$alert-color-scale: 40%; $alert-color-scale: 40%;
//Modal animation
$modal-fade-transform: translate(0, 0);
$modal-backdrop-opacity: 0;

View file

@ -1,23 +1,18 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import alert from "./alert"; import alert from "./alert";
describe("alert.vue", () => { describe("alert.vue", () => {
it("Should add class 'alert-dismissible' if isDismissible is true", async () => { it("Should add class 'alert-dismissible' if isDismissible is true", async () => {
// Arrange // Arrange
const wrapper = shallowMount(alert, { const wrapper = shallowMount(alert, setupMocks({
propsData: { propsData: {
isDismissible: true isDismissible: true,
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
}, },
computed: { }));
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin]
});
const wrapperDiv = wrapper.find('div'); const wrapperDiv = wrapper.find('div');
@ -27,19 +22,13 @@ describe("alert.vue", () => {
it("Should add specified alert class", async () => { it("Should add specified alert class", async () => {
// Arrange // Arrange
const wrapper = shallowMount(alert, { const wrapper = shallowMount(alert, setupMocks({
propsData: { propsData: {
alertClass: 'warning' alertClass: 'warning',
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
}, },
computed: { }));
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin]
});
const wrapperDiv = wrapper.find('div'); const wrapperDiv = wrapper.find('div');
@ -49,24 +38,171 @@ describe("alert.vue", () => {
it("Should update alert Headline to manualHeadline datam entered and alert copy to manualCopy datam entered when no cmsWidgetName entered", async () => { it("Should update alert Headline to manualHeadline datam entered and alert copy to manualCopy datam entered when no cmsWidgetName entered", async () => {
// Arrange // Arrange
const wrapper = shallowMount(alert, { const wrapper = shallowMount(alert, setupMocks({}));
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
mixins: [mockMixin]
});
// Assert // Assert
expect(wrapper.vm.alertHeadline).toBe("testHeader"); expect(wrapper.vm.alertHeadline).toBe("testHeader");
expect(wrapper.vm.alertCopy).toBe("testCopy"); expect(wrapper.vm.alertCopy).toBe("testCopy");
}); });
it("Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () =>{
// Arrange & Act
const wrapper = shallowMount(alert, setupMocks({
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy with a {routerLink: testName, testLink} inside of it'
},
stubs: ['router-link'],
}));
// Assert
expect(wrapper.find('router-link').exists()).toBe(true);
});
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () =>{
// Arrange & Act
const wrapper = shallowMount(alert, setupMocks({
propsData: {
manualHeadline: 'testHeader',
manualCopy: '<p>testCopy with a {routerLink: testName, testLink} inside of it</p><p>and two paragraphs</p>'
},
stubs: ['router-link'],
}));
// Assert
expect(wrapper.findAll('p').length === 3).toBe(true);
});
it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (out of view top)", () => {
// Arrange
var viewPortHeight = 200;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: -100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).toHaveBeenCalled();
});
it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (bottom is hidden behind footer)", () => {
// Arrange
var viewPortHeight = 240;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: 100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).toHaveBeenCalled();
});
it("Should not call scrollIntoView() when the clientBoundingRect is not entirely in the viewport but 'shouldScrollToOnMount' is false", () => {
// Arrange
var viewPortHeight = 200;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: -100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({
propsData: {
shouldScrollToOnMount: false,
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).not.toHaveBeenCalled();
});
it("Should not call scrollIntoView() when the clientBoundingRect is entirely in the viewport", () => {
// Arrange
var viewPortHeight = 500;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: 100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).not.toHaveBeenCalled();
});
}); });
const mockMixin = { const mockMixin = {
methods: { methods: {
getCmsContent: jest.fn(), getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=> 80), getFooterInfoBoxHeight: jest.fn(()=> 50),
},
computed: {
dynamicStrings: jest.fn(()=> {
return {ROUTER_LINK: 'routerLink:'}
})
} }
}
function setUpViewPort(height) {
Object.defineProperty(global.window, 'innerHeight', {
writable: true,
configurable: true,
value: height,
});
Object.defineProperty(window.document.documentElement, 'clientHeight', {
writable: true,
configurable: true,
value: height
});
}
function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = {
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
mixins: [mockMixin]
};
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
return allMountOptions;
} }

View file

@ -8,7 +8,7 @@
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph"> <template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
<p class="m-0 text-body small" v-if="!doesCopyContainRouterLink(paragraph)" v-html="paragraph"></p> <p class="m-0 text-body small" v-if="!doesCopyContainRouterLink(paragraph)" v-html="paragraph"></p>
<p class="m-0 text-body small" v-else> <p class="m-0 text-body small" v-else>
<template v-for="copy in splitCopyForRouterLink(paragraph)" :key="copy"> <template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
<span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span> <span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
<span v-else> <span v-else>
<router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link> <router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
@ -36,6 +36,11 @@
</template> </template>
<script> <script>
import { doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
splitCMSCopyOnParagraphTag } from "@/helpers/cms-content-helper"
export default { export default {
name: "alert", name: "alert",
@ -65,36 +70,19 @@ export default {
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy; return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
}, },
splitAlertCopyForParagraphTag(){ splitAlertCopyForParagraphTag(){
// splits the alertCopy on <p ... > (with or without attributes) and </p> return splitCMSCopyOnParagraphTag(this.alertCopy);
// filter removes empty strings that are a result of string.split with regex
return this.alertCopy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
}, },
}, },
methods: { methods: {
doesCopyContainRouterLink(copy) { doesCopyContainRouterLink,
return copy.includes('routerLink:'); splitCopyOnCMSPlaceHolder,
}, getRouterLinkRouteFromCopy,
splitCopyForRouterLink(copy){ getRouterLinkDisplayTextFromCopy,
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
},
getRouterLinkRouteFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
},
getRouterLinkDisplayTextFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
},
ensureAlertIsInViewPort() { ensureAlertIsInViewPort() {
if (this.shouldScrollToOnMount && this.$el.style.display != 'none') { if (this.shouldScrollToOnMount && this.$el.style.display != 'none') {
var footerHeight = this.getFooterInfoBoxHeight(); var footerHeight = this.getFooterInfoBoxHeight();
if (!this.isAlertInViewport(footerHeight)) { if (!this.isAlertInViewport(footerHeight)) {
this.scrollContainerToAlert(footerHeight); this.$el.scrollIntoView(true); // 'true' attempts to scroll element to top of viewport
} }
} }
}, },
@ -106,14 +94,6 @@ export default {
rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight) rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight)
); );
}, },
scrollContainerToAlert(footerHeight) {
// alert position on page + height of alert + footer height
var scrollToHeight = this.$el.scrollHeight + this.$el.offsetHeight + footerHeight;
// find the div wrapped by the form element - this is the scrollable container
// should be a more future-proof selector in case of CSS class changes
var pageContainerScrollable = document.querySelector('form > div');
pageContainerScrollable.scrollTo(0, scrollToHeight);
},
}, },
mounted() { mounted() {
this.ensureAlertIsInViewPort(); this.ensureAlertIsInViewPort();