Merge branch 'develop' into feature/CSR-254

This commit is contained in:
Adam Caouette 2022-02-03 17:43:51 -05:00
commit 30ae4fcd8b
82 changed files with 4734 additions and 2340 deletions

View file

@ -6,11 +6,8 @@ This Repository is for Fix My Glass front end Vue and documentation associated w
#### Routing
We are using [Vue Router](https://next.router.vuejs.org/) 4.x for our front end routing. We are using a runtime dynamic routing solution. Some more information about dynamic routing can be found on the [official Vue Router documentation](https://next.router.vuejs.org/guide/advanced/dynamic-routing.html).
Here is a high level overview of how our routing works:
![FixMyGlass - Routing](https://user-images.githubusercontent.com/44780237/139876801-7d5c495a-f361-4234-a8ec-69126a357f80.png)
`RetainStructureAndGoTo404(to, next)` - Since our 404 path on our router is `path: '/:pathMatch(.*)*'` (meaning that we don't have a dedicated 404 route) we retain the information entered by the user in the url bar and just serve back our `NotFound` component on Vue. For example if a user was to type in `.../fmg/fmgPage=junkPage` Vue will retain this url in the browser and return our `NotFound` component. This could be especially useful for analytics and reporting.
`Next()` - Is a function that is native to Vue Router in which we call in order to send the user where want them to go based on some logic we have[. You can read more about Navigation Guards and the `Next()` function on the Vue Router documentation. ](https://next.router.vuejs.org/guide/advanced/navigation-guards.html)
`GetRouteInfoFromPageName(PageName)` - This method typically takes the query string value of `fmgPage` and passes it into a call to our Content Service. The service will pull information on the page; matching page name passed to page name in our Cms. If we have a match, we will compile the route information and send it to our router to process. If we do not have a that page in our Cms, the user will be served back a 404 page while retaining the structure.
`GetRouteInfoFromPageName(PageName)` - This method typically takes the query string value of `fmgPage` and passes it into a call to our Content Service. The service will pull information on the page; matching page name passed to page name in our Cms. If we have a match, we will compile the route information and send it to our router to process. If we do not have a that page in our Cms, the user will be served back a 404 page while retaining the structure.
`GoToFunnelStartOn404(next)` - When a user tries to go to a page that doesn't exist in the CMS, they will be put back to the 'Start' of the funnel which is whatever page is marked as the homepage on Sitefinity.

View file

@ -13,12 +13,12 @@ module.exports = {
"!src/helpers/unit-test-helper.js",
"!src/layouts/component-test/component-test.vue",
"!src/layouts/form-test/form-test.vue",
"!src/layouts/address-poc/address-poc.vue"
"!src/layouts/address-poc/address-poc.vue",
], //! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 85,
statements: 89,
},
},
};

View file

@ -49,8 +49,7 @@
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/prettier"
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"

View file

@ -5,7 +5,6 @@
<style lang="scss">
@import "./node_modules/bootstrap/scss/bootstrap";
@import "@/styles/common-styles.scss";
@import "@/styles/common-button-styles.scss";
@import "@/styles/common-list-styles.scss";
@import "@/styles/common-typography-styles.scss";
</style>
@import "@/styles/common-error-styles.scss";
</style>

View file

@ -2,7 +2,6 @@ import { shallowMount } from "@vue/test-utils";
import buttonBack from "./button-back";
describe("back button", () => {
test("renders a button", () => {
// Arrange
const myFunction = () => {};
@ -14,10 +13,9 @@ describe("back button", () => {
backButtonAccessibleText: "something",
},
});
// Assert
expect(wrapper.find("button").exists()).toBe(true);
wrapper.unmount();
});
});

View file

@ -1,23 +1,36 @@
<template>
<button
@click="handleClick"
class="back-button-wrapper"
class="back-button-wrapper"
:aria-label="backButtonAccessibleText"
>
<svg xmlns="http://www.w3.org/2000/svg" width="34px" height="30px" viewBox="-2.2 -6 30 30">
<svg
xmlns="http://www.w3.org/2000/svg"
width="34px"
height="30px"
viewBox="-2.2 -6 30 30"
>
<g id="Layer_3">
<path class="outer" fill="#FFFFFF" d="M19.934-1.434h-8.486c-1.683,0-2.999,0.528-4.021,1.614L1.685,6.146C0.78,7.079,0.338,8.027,0.338,9.041
<path
class="outer"
fill="#FFFFFF"
d="M19.934-1.434h-8.486c-1.683,0-2.999,0.528-4.021,1.614L1.685,6.146C0.78,7.079,0.338,8.027,0.338,9.041
c0,1.028,0.444,1.979,1.359,2.908l5.753,5.938c1.054,1.093,2.403,1.647,4.007,1.647l8.475,0.01c2.861,0,4.568-1.692,4.568-4.528
V3.094C24.5,0.259,22.793-1.434,19.934-1.434L19.934-1.434z"/>
V3.094C24.5,0.259,22.793-1.434,19.934-1.434L19.934-1.434z"
/>
</g>
<g id="Layer_2">
<path class="inner" fill="#727676" d="M19.934,17.989c2.041,0,3.066-1.006,3.066-3.028V3.039c0-2.022-1.025-3.028-3.066-3.028h-8.486
<path
class="inner"
fill="#727676"
d="M19.934,17.989c2.041,0,3.066-1.006,3.066-3.028V3.039c0-2.022-1.025-3.028-3.066-3.028h-8.486
c-1.143,0-2.129,0.292-2.929,1.143L2.766,7.13C2.131,7.784,1.838,8.37,1.838,8.985c0,0.605,0.283,1.201,0.928,1.855l5.762,5.947
c0.811,0.84,1.787,1.191,2.93,1.191L19.934,17.989z M10.773,13.253c-0.439,0-0.801-0.371-0.801-0.82
c0-0.215,0.078-0.42,0.235-0.566l2.861-2.861l-2.861-2.862c-0.156-0.156-0.234-0.352-0.234-0.566c0-0.458,0.362-0.801,0.801-0.801
c0.224,0,0.41,0.069,0.556,0.225l2.881,2.872l2.891-2.881c0.166-0.166,0.353-0.234,0.566-0.234c0.439,0,0.801,0.352,0.801,0.801
c0,0.225-0.078,0.4-0.234,0.576l-2.871,2.871l2.861,2.852c0.146,0.156,0.234,0.352,0.234,0.576c0,0.449-0.361,0.82-0.811,0.82
c-0.225,0-0.43-0.098-0.586-0.244l-2.852-2.871l-2.852,2.871C11.203,13.165,10.999,13.253,10.773,13.253z"/>
c-0.225,0-0.43-0.098-0.586-0.244l-2.852-2.871l-2.852,2.871C11.203,13.165,10.999,13.253,10.773,13.253z"
/>
</g>
</svg>
</button>
@ -31,13 +44,13 @@ export default {
type: String,
required: true,
default: "",
}
},
},
methods: {
handleClick() {
this.$emit('click-event');
}
}
this.$emit("click-event");
},
},
};
</script>
@ -46,6 +59,9 @@ export default {
padding: 0;
position: relative;
top: -1px;
margin-left: 2px;
border: none;
background: none;
&:focus {
outline: none;
@ -60,7 +76,7 @@ export default {
}
}
&:focus-visible {
outline: none;
outline: none;
}
}
</style>

View file

@ -2,22 +2,83 @@ import { shallowMount } from "@vue/test-utils";
import buttonQuestion from "@/common-components/button-question/button-question";
describe("buttonQuestion.vue", () => {
it("Should render the 'questionText' prop value as a span value for the button question and the 'answer' values should render as text values for button components.", async () => {
it("Should show overflow classes on fieldset if isOverflowScrollable is true", async () => {
// Act
const wrapper = shallowMount(buttonQuestion);
await wrapper.setProps({
questionText: "Question Text",
answers: ["2023", "2022", "2021"],
modelValue: "2020",
isOverflowScrollable: true,
});
wrapper.vm.chooseAnswer("2021");
// Assert
expect(wrapper.find(".text-center").text()).toEqual(
"Question Text"
);
const buttonButtons = wrapper.findAllComponents('[data-test="button"]');
expect(buttonButtons.length).toBe(3);
expect(wrapper.props().modelValue).toBe("2020");
const fieldSet = wrapper.find('fieldset');
expect(fieldSet.classes()).toContain("overflow-scroll");
});
});
describe("buttonQuestion.vue", () => {
it("Fieldset classes should contain row if button type is listCard", async () => {
// Act
const wrapper = shallowMount(buttonQuestion);
await wrapper.setProps({
buttonType: "listCard",
});
// Assert
const Div = wrapper.find('fieldset div');
expect(Div.classes()).toContain("row");
});
});
describe("buttonQuestion.vue", () => {
it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", async () => {
// Act
const wrapper = shallowMount(buttonQuestion);
await wrapper.setProps({
buttonType: "listButtonHorizontal",
});
// Assert
const Div = wrapper.find('fieldset div');
expect(Div.classes()).toContain("d-flex");
});
});
describe("buttonQuestion.vue", () => {
it("Should trigger event modelValue change on select", async () => {
// Act
const wrapper = shallowMount(buttonQuestion);
await wrapper.setData({
modelValueAnswers: ["Windshield"],
chosenAnswer: "Windshield"
});
wrapper.setValue({ answer: wrapper.vm.chosenAnswer });
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{answer: "Windshield"}]);
});
});
describe("buttonQuestion.vue", () => {
it("Should trigger event modelValue change with an array of string values on select if checked is true and multiple options are chosen", async () => {
// Act
const wrapper = shallowMount(buttonQuestion);
await wrapper.setData({
modelValueAnswers: ["Windshield", "BackDoor"]
});
wrapper.vm.handleCheckedChanged(true);
// Assert
expect(typeof wrapper.emitted()["update:modelValue"][0]).toEqual('object');
});
});
describe("buttonQuestion.vue", () => {
it("Should not trigger event modelValue change on select if checked is false", async () => {
// Act
const wrapper = shallowMount(buttonQuestion);
wrapper.setData({
modelValueAnswers: ["Front-door"]
})
const val = {isChecked : false, buttonId: "Front-door"}
wrapper.vm.handleCheckedChanged(val);
// Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([undefined]);
});
});

View file

@ -2,18 +2,19 @@
<div :class="isOverflowScrollable ? 'button_question' : ''">
I AM THE BUTTON QUESTION, groupName is: {{groupName}}
<div v-if="questionText" class="mt-6 mb-4 d-flex">
<span class="text-center fs-6 fw-bold w-100">{{
questionText
}}</span>
<span class="text-center fs-6 fw-bold w-100">{{ questionText }}</span>
</div>
<div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :class="getFieldSetClasses" role="radiogroup" :aria-labelledby="groupName ? groupName + '-radio-group' : ''">
<legend class="sr-only">{{groupName}}</legend>
<div :class="getComponentWrapperClasses">
<component :is="buttonType" v-for="answer in answers" :key="answer"
@mouseup="chooseAnswer(answer.Name ? answer.Name : answer)"
@keyup.space="chooseAnswer(answer.Name ? answer.Name : answer)"
<component
:is="buttonType"
v-for="answer in answers"
:key="answer.Name ? answer.Name : answer"
@isCheckedChanged="handleCheckedChanged"
:buttonID="answer.Name ? answer.Name : answer"
:value="answer.Name ? answer.Name : answer"
:buttonLabel="answer.Text ? answer.Text : answer"
:buttonLabelSubCopy="answer.SubText"
:textPosition="textPosition"
@ -29,7 +30,8 @@
:buttonImageId="answer.ImageId"
:altText="answer.Name ? answer.Name : answer"
screenReaderOnlyText="(opens new window)"
:value="answer.Name ? answer.Name : answer"
:colLength="this.answers.length < 3 ? '' : '-4'"
v-model="selectedValues"
data-test="button"
/>
</div>
@ -51,8 +53,8 @@ export default {
name: "buttonQuestion",
props: {
buttonType: {
type: String,
default: 'listButton'
type: String,
default: "listButton",
},
isMultiSelect: Boolean,
groupName: String,
@ -60,47 +62,68 @@ export default {
answers: Array,
textPosition: {
type: String,
default: 'text-center'
default: "text-center",
},
loaderEnabled: Boolean,
loaderColor: {
type: String,
default: 'blue'
default: "blue",
},
loaderPosition: {
type: String,
default: 'right'
type: String,
default: "right",
},
sizeInRem: {
type: [String, Number],
default: 1.5
type: [String, Number],
default: 1.5,
},
isRequired: Boolean,
isOverflowScrollable: Boolean,
isWide: Boolean,
modelValue: String,
modelValue: Array,
validationRules: Array,
name: String,
value: String,
},
computed: {
getFieldSetClasses(){
return this.isOverflowScrollable ? 'container-fluid overflow-scroll position-absolute px-5 pt-1 py-0' : '';
getFieldSetClasses() {
return this.isOverflowScrollable
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
: "";
},
getComponentWrapperClasses() {
let classes;
switch(this.buttonType) {
case 'listButton':
classes = 'w-100'
switch (this.buttonType) {
case "listButton":
classes = "w-100";
break;
case 'listButtonHorizontal':
classes = 'd-flex flex-row p-0'
case "listButtonHorizontal":
classes = "d-flex flex-row p-0";
break;
case 'listCard':
classes = 'row g-2'
classes = 'row justify-content-center g-2'
break;
}
return classes;
},
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
},
data(){
return {
modelValueAnswers: [],
}
},
mounted(){
if(Array.isArray(this.answers) && this.answers.length === 1) {
this.modelValueAnswers.push(typeof(this.answers[0]) === 'object' ? this.answers[0].Name : this.answers[0]);
this.$emit("update:modelValue", this.modelValueAnswers);
}
},
methods: {
@ -108,11 +131,17 @@ export default {
console.log('method: chooseAnswer, answer: ', answer);
this.$emit("update:modelValue", answer);
},
handleCheckedChanged(val) {
// Add or remove item to array of data to emit
val.isChecked ? this.modelValueAnswers.push(val.buttonId) : this.modelValueAnswers.splice(this.modelValueAnswers.indexOf(val.buttonId), 1);
const answersToEmit = this.modelValueAnswers.length ? this.modelValueAnswers : undefined;
this.$emit("update:modelValue", answersToEmit);
},
},
components: {
listButton,
listButtonHorizontal,
listCard
listCard,
},
setup(props) {
console.log('button-question running setup');

View file

@ -0,0 +1,60 @@
import { mount } from "@vue/test-utils";
import funnelFooter from "./funnel-footer";
describe("funnel-footer.vue", () => {
it("Should return footer-link class", async () => {
// Act
try {
global.document.getElementById = jest.fn().mockImplementation(()=> {
return {}
});
const wrapper = mount(funnelFooter, {
propsData: {
footer: true
},
});
// Assert
const link = wrapper.find("a");
// Expect
expect(link.attributes('class')).toContain("footer-link");
expect(global.document.getElementById).toBeCalled();
} catch(error) {
console.log(error);
}
});
it("Should return link text", async () => {
// Act
const wrapper = mount(funnelFooter, {
propsData: {
text: "Terms of use",
},
});
// Assert
const link = wrapper.find("a");
expect(link.text()).toEqual("Terms of use");
});
it("Should return class btn-primary", async () => {
// Act
const wrapper = mount(funnelFooter, {
propsData: {
isPrimary: true,
},
});
// Assert
const input = wrapper.find("button");
// Expect
expect(input.attributes("class")).toContain("btn-primary");
});
});

View file

@ -1,22 +1,86 @@
<!-- Simple implementation of an input field -->
<template>
<div class="d-block w-50 mb-2">
<h3>I AM FUNNEL FOOTER</h3>
<button
class="mb-6"
:class="isDisabled && 'form-test-invalid'"
:aria-disabled="isDisabled"
>
Get your estimate
</button>
<div class="container-fluid g-2 footer">
<div class="row">
<div class="col-12 d-flex justify-content-center pb-2 fs-7">
&copy;&nbsp;<span id="years"></span>&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
footer=true
text="Terms of use"
href="https://www.safelite.com/terms-of-use"
/>
<textLink
footer=true
text="Privacy policy"
href="https://www.safelite.com/safelite-group-privacy-policy"
/>
<textLink
footer=true
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-4 bg-light py-4" id="infoBox">
<div class="row">
<div class="col">
<buttonMain
isPrimary
buttonText="Review Appointment Details"
loaderColor="white"
sizeInRem="1"
isFloat
:class="isDisabled && 'form-test-invalid'"
:aria-disabled="isDisabled"
/>
<textLink
navigation=true
text="back"
/>
</div>
</div>
</div>
</template>
<script>
import textLink from "@/ux-components/text-link/text-link";
import buttonMain from "@/ux-components/button-main/button-main";
export default {
name: "funnelFooter",
props: {
isDisabled: Boolean,
isDisabled: Boolean,
},
components: {
textLink,
buttonMain
},
data() {
return {
paddingHeight: 0
}
},
mounted() {
this.paddingHeight = document.getElementById("infoBox").offsetHeight;
this.$nextTick(() => {
window.addEventListener('resize', this.onResize);
})
setTimeout(function(){ // Give it a moment to set the date
document.getElementById('years').innerHTML += new Date().getFullYear();
}, 100);
},
beforeUnmount() {
window.removeEventListener('resize', this.onResize);
},
methods: {
onResize() {
this.paddingHeight = document.getElementById("infoBox").offsetHeight;
}
}
};
</script>

View file

@ -21,5 +21,5 @@ describe("funnelHeader", () => {
//Mock CMS content
const cmsContent = {
imageSrc: 'image_url'
};
imageSrc: "image_url",
};

View file

@ -1,64 +1,69 @@
<template>
<div class="funnel-header d-flex justify-content-center align-items-center flex-column" v-if="imageSrc">
<div
class="funnel-header d-flex justify-content-center align-items-center flex-column"
v-if="imageSrc"
>
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
</div>
<alert class="position-absolute rounded-0 w-100 border-0 shadow-sm" v-if="displayGlobalAlert" :alertClass="globalAlertMessage.type" :alertHeadline="globalAlertMessage.messageHeadline" :alertCopy="globalAlertMessage.messageCopy" v-bind:isDismissible="globalAlertMessage.isDismissible" />
</div>
<alert
class="position-absolute rounded-0 w-100 border-0 shadow-sm"
v-if="displayGlobalAlert"
:alertClass="globalAlertMessage.type"
:alertHeadline="globalAlertMessage.messageHeadline"
:alertCopy="globalAlertMessage.messageCopy"
v-bind:isDismissible="globalAlertMessage.isDismissible"
/>
</template>
<script>
import alert from "@/ux-components/alert/alert";
import eventBus from "@/helpers/event-bus/event-bus";
import {
globalEvents,
} from "@/constants/events";
import { globalEvents } from "@/constants/events";
export default {
name: "funnel-header",
data() {
return {
imageSrc: "",
displayGlobalAlert: false,
globalAlertMessage: {
isDismissible: false,
messageCopy: "",
messageHeadline: "",
type: "",
},
};
},
methods: {
initializeComponent(cmsContent) {
this.imageSrc = cmsContent.LogoImage;
},
},
components: {
alert,
},
mounted() {
// Check if alert event is on the bus
const alertEvent = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
// If alert event is on the bus, then display the alert
if (alertEvent !== undefined) {
this.displayGlobalAlert = true;
this.globalAlertMessage = alertEvent;
}
name: "funnel-header",
data() {
return {
imageSrc: "",
displayGlobalAlert: false,
globalAlertMessage: {
isDismissible: false,
messageCopy: "",
messageHeadline: "",
type: "",
},
};
},
methods: {
initializeComponent(cmsContent) {
this.imageSrc = cmsContent.LogoImage;
},
},
components: {
alert,
},
mounted() {
// Check if alert event is on the bus
const alertEvent = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
// If alert event is on the bus, then display the alert
if (alertEvent !== undefined) {
this.displayGlobalAlert = true;
this.globalAlertMessage = alertEvent;
}
},
};
</script>
<style lang="scss" scoped>
.funnel-header {
height: 56px;
height: 56px;
}
.logo-image {
max-width: 78px;
max-width: 78px;
}
</style>

View file

@ -5,7 +5,7 @@ describe("FunnelSubHeader.vue", () => {
it("Should render the 'text' data value as a span value for the header span text value.", async () => {
// Act
const wrapper = shallowMount(FunnelSubHeader);
await wrapper.setData ({
await wrapper.setData({
text: "FunnelSubHeader Content",
});
wrapper.vm.initializeComponent(cmsContent);
@ -18,5 +18,5 @@ describe("FunnelSubHeader.vue", () => {
//Mock CMS content
const cmsContent = {
HeaderText: 'FunnelSubHeader Content'
};
HeaderText: "FunnelSubHeader Content",
};

View file

@ -1,7 +1,7 @@
<template>
<div class="current_car_info-text">
<div class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
<h5 class="text-center fw-normal mb-0">
<h5 class="text-center fw-normal mb-0" :class="hasSubText ? 'dark-header' : 'light-header'">
<span>
{{ text }}
</span>
@ -10,14 +10,14 @@
:backButtonAccessibleText="backButtonAccessibleText"
@click-event="clickEvent"
/>
</h5>
</h5>
</div>
<div class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
<p class="text-center small fw-normal mb-0">
<span>
{{ subText }}
</span>
</p>
</p>
</div>
</div>
</template>
@ -29,38 +29,48 @@ export default {
name: "FunnelSubHeader",
data() {
return {
text: '',
subText: ''
}
text: "",
subText: "",
};
},
props: {
hasBackButton: Boolean,
backButtonAccessibleText: String,
hasSubText: Boolean
},
components: {
buttonBack,
},
methods: {
clickEvent() {
this.$emit('click-event');
this.$emit("click-event");
},
initializeComponent(cmsContent) {
this.text = cmsContent.HeaderText;
this.subText = cmsContent.HeaderSubText;
}
},
},
};
</script>
<style lang="scss">
h5 {
color: $gray-550;
line-height: 32px;
h5 {
line-height: 32px;
button {
border: none;
background: none;
color: inherit;
}
button {
color: inherit;
}
}
.dark-header {
color: $black;
}
.light-header {
color: $gray-550;
}
p.small {
color: $gray-550
}
</style>

View file

@ -2,7 +2,6 @@ import { shallowMount } from "@vue/test-utils";
import textInput from "./text-input";
describe("text-input.vue", () => {
it("Should render a text input", async () => {
// Act
const wrapper = shallowMount(textInput, {
@ -24,7 +23,7 @@ describe("text-input.vue", () => {
propsData: {
name: "test",
label: "unit test label",
isRequired: true
isRequired: true,
},
});
@ -32,7 +31,5 @@ describe("text-input.vue", () => {
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
});
});

View file

@ -16,7 +16,7 @@
:aria-labelledby="name"
class="d-flex justify-content-center py-3 px-4"
>
<span class="m-0">{{label}}</span>
<span class="m-0">{{ label }}</span>
</label>
</div>
<div v-show="errorMessage" class="row px-3 form-test-error">
@ -25,7 +25,7 @@
</template>
<script>
import { useField } from 'vee-validate';
import { useField } from "vee-validate";
export default {
name: "textInput",
@ -49,16 +49,16 @@ export default {
isRequired: Boolean,
},
setup(props) {
const {
value: inputValue,
errorMessage,
handleBlur,
handleChange,
meta,
const {
value: inputValue,
errorMessage,
handleBlur,
handleChange,
meta,
} = useField(props.name, undefined, {
initialValue: props.value,
initialValue: props.value,
});
return {
handleChange,
handleBlur,

View file

@ -1,24 +1,120 @@
import { shallowMount } from "@vue/test-utils";
import vehicleBanner from "./vehicle-banner";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("vehicleBanner", () => {
test("renders the blurrycar image", async () => {
// Arrange
// Act
const wrapper = shallowMount(vehicleBanner);
await wrapper.setData ({
vehicleImageSrc: "image_url",
});
const { wrapper, cmsContent } = setupMocks({ displayGenericVehicleImageProp: true, imageUrlValue: "NULL" });
//Act
wrapper.vm.initializeComponent(cmsContent);
// Assert
expect(wrapper.find("img").attributes("class")).toContain("blurrycar");
expect(wrapper.vm.vehicleImageToDisplay).toEqual( "image_url");
expect(wrapper.find("img").attributes("class")).toContain("vehicle-image");
wrapper.unmount();
});
});
//Mock CMS content
const cmsContent = {
GenericVehicleImage: 'image_url'
};
describe("vehicleBanner", () => {
test("renders expected vehicle image", async () => {
// Arrange
const { wrapper, cmsContent } = setupMocks({ displayGenericVehicleImageProp: false, imageUrlValue: "url_to_vehicle_image" });
//Act
wrapper.vm.initializeComponent(cmsContent);
// Assert
expect(wrapper.vm.vehicleImageToDisplay).toEqual( "url_to_vehicle_image");
expect(wrapper.find("img").attributes("class")).toContain("vehicle-image");
wrapper.unmount();
});
});
describe("vehicleBanner", () => {
test("should render car icon when imageUrl is null and category is default", async () => {
// Arrange
const { wrapper, cmsContent } = setupMocks({ displayGenericVehicleImageProp: false, imageUrlValue: "NULL" });
//Act
wrapper.vm.initializeComponent(cmsContent);
var iconUrl = wrapper.vm.vehicleImageToDisplay;
// Assert
expect(iconUrl).toEqual( "car_icon_url");
wrapper.unmount();
});
});
const params = [["CAR", "car_icon_url"],
["TRUCK", "truck_icon_url"],
["VAN", "van_icon_url"],
["COMMERCIAL VAN", "commercial_van_icon_url"],
["SUV", "suv_icon_url"],
["OTHER", "car_icon_url"]];
describe("vehicleBanner", () => {
test.each(params)("renders an icon instead of an image for %s and %s", async (category, expectedIcon) => {
// Arrange
const { wrapper, cmsContent } = setupMocks({ displayGenericVehicleImageProp: false, imageUrlValue: expectedIcon, categoryValue: category });
//Act
wrapper.vm.initializeComponent(cmsContent);
var vehicleIcon = wrapper.vm.getUnmatchedVehicleIcon();
// Assert
expect(wrapper.vm.carUnmatchedVehicleIcon).toEqual(cmsContent.CarUnmatchedVehicleIcon);
expect(wrapper.vm.truckUnmatchedVehicleIcon).toEqual(cmsContent.TruckUnmatchedVehicleIcon);
expect(wrapper.vm.vanUnmatchedVehicleIcon).toEqual(cmsContent.VanUnmatchedVehicleIcon);
expect(wrapper.vm.commercialUnmatchedVehicleIcon).toEqual(cmsContent.CommercialVanUnmatchedVehicleIcon);
expect(wrapper.vm.suvUnmatchedVehicleIcon).toEqual(cmsContent.SuvUnmatchedVehicleIcon);
expect(store.getters.vehicle.category).toEqual(category);
expect(wrapper.vm.vehicleImageToDisplay).toEqual(vehicleIcon);
expect(wrapper.find("img").attributes("class")).toContain("vehicle-image");
wrapper.unmount();
});
});
function setupMocks({
displayGenericVehicleImageProp,
imageUrlValue,
categoryValue = "CAR"
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: { category: categoryValue, imageUrl: imageUrlValue } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
mountOptions.propsData = { displayGenericVehicleImage: displayGenericVehicleImageProp };
const wrapper = shallowMount(vehicleBanner, mountOptions);
//Mock CMS content
const cmsContent = {
GenericVehicleImage: "image_url",
CarUnmatchedVehicleIcon: "car_icon_url",
TruckUnmatchedVehicleIcon: "truck_icon_url",
VanUnmatchedVehicleIcon: "van_icon_url",
CommercialVanUnmatchedVehicleIcon: "commercial_van_icon_url",
SuvUnmatchedVehicleIcon: "suv_icon_url",
};
return { wrapper, cmsContent };
}

View file

@ -1,8 +1,8 @@
<template>
<div class="vehicle_banner mb-3 text-center">
<img
class="vehicle-image img-fluid blurrycar"
:src="vehicleImageSrc"
class="vehicle-image img-fluid"
:src="vehicleImageToDisplay"
alt=""
/>
</div>
@ -11,21 +11,70 @@
<script>
export default {
name: "vehicle-banner",
props: {
displayGenericVehicleImage: {
type: Boolean,
required: true
}
},
data() {
return {
vehicleImageSrc: '',
genericVehicleImage: '',
carUnmatchedVehicleIcon: '',
truckUnmatchedVehicleIcon: '',
vanUnmatchedVehicleIcon: '',
commercialUnmatchedVehicleIcon: '',
suvUnmatchedVehicleIcon: ''
}
},
computed: {
vehicleImageToDisplay(){
if (this.displayGenericVehicleImage){
return this.genericVehicleImage;
}
if (this.$store.getters.vehicle.imageUrl === null || this.$store.getters.vehicle.imageUrl === "NULL"){
return this.getUnmatchedVehicleIcon();
}
return this.$store.getters.vehicle.imageUrl;
}
},
methods: {
initializeComponent(cmsContent) {
this.vehicleImageSrc = cmsContent.GenericVehicleImage;
this.genericVehicleImage = cmsContent.GenericVehicleImage;
this.carUnmatchedVehicleIcon = cmsContent.CarUnmatchedVehicleIcon;
this.truckUnmatchedVehicleIcon = cmsContent.TruckUnmatchedVehicleIcon;
this.vanUnmatchedVehicleIcon = cmsContent.VanUnmatchedVehicleIcon;
this.commercialUnmatchedVehicleIcon = cmsContent.CommercialVanUnmatchedVehicleIcon;
this.suvUnmatchedVehicleIcon = cmsContent.SuvUnmatchedVehicleIcon;
},
getUnmatchedVehicleIcon(){
switch(this.$store.getters.vehicle.category){
case this.vehicleCategories.CAR:
return this.carUnmatchedVehicleIcon;
case this.vehicleCategories.SUV:
return this.suvUnmatchedVehicleIcon;
case this.vehicleCategories.TRUCK:
return this.truckUnmatchedVehicleIcon;
case this.vehicleCategories.VAN:
return this.vanUnmatchedVehicleIcon;
case this.vehicleCategories.COMMERCIALVAN:
return this.commercialUnmatchedVehicleIcon;
default:
return this.carUnmatchedVehicleIcon;
}
}
},
}
};
</script>
<style lang="scss" scoped>
.vehicle-image {
max-width: 290px;
object-fit: cover;
width: 100%;
height: 118px;
}
</style>

View file

@ -1,17 +1,17 @@
const globalEvents = {
Categories: {
GLOBAL_ALERT: "GLOBAL_ALERT",
},
SubCategories: {
PAGE_NOT_FOUND: "PAGE_NOT_FOUND",
}
}
Categories: {
GLOBAL_ALERT: "GLOBAL_ALERT",
},
SubCategories: {
PAGE_NOT_FOUND: "PAGE_NOT_FOUND",
},
};
const globalEventTypes = {
Success: "alert-success",
Warning: "alert-warning",
Info: "alert-info",
Danger: "alert-danger",
}
Success: "alert-success",
Warning: "alert-warning",
Info: "alert-info",
Danger: "alert-danger",
};
export {globalEvents, globalEventTypes}
export { globalEvents, globalEventTypes };

View file

@ -11,9 +11,12 @@ const storeActions = {
GET_EVOX_IMAGE: "getEvoxImage",
LOOKUP_VEHICLE_BY_YMMS: "lookupVehicleByYmms",
LOOKUP_VEHICLE_BY_VIN: "lookupVehicleByVin",
// EVENT BUS
ADD_EVENT_TO_BUS: "addEventToBus",
REMOVE_EVENT_FROM_BUS: "removeEventFromBus",
// DEPENDENCY MUTATIONS
RESET_VEHICLE_AND_DEPS: "resetVehicleAndDependencies",
RESET_DAMAGE_AND_DEPS: "resetDamageAndDependencies",
RESET_REGISTRATION_AND_DEPS: "resetRegistrationAndDependencies",
RESET_PARTS_AND_DEPS: "resetPartsAndDependencies",
};
export { storeActions };

View file

@ -1,9 +1,26 @@
const storeMutations = {
// VEHICLE MUTATIONS
UPDATE_YEAR: "updateYear",
UPDATE_MAKE: "updateMake",
UPDATE_MODEL: "updateModel",
UPDATE_STYLE: "updateStyle",
UPDATE_VEHICLE: "updateVehicle",
UPDATE_CAR_ID: "updateCarId",
UPDATE_VEHICLE_CATEGORY: "updateVehicleCategory",
UPDATE_VEHICLE_IMAGE_URL: "updateVehicleImageUrl",
UPDATE_VEHICLE_IMAGE_VIF_NUMBER: "updateVehicleImageVifNumber",
UPDATE_VEHICLE_IMAGE_COLOR: "updateVehicleImageColor",
// EVENT BUS MUTATIONS
ADD_EVENT_TO_BUS: "addEventToBus",
REMOVE_EVENT_FROM_BUS: "removeEventFromBus",
// DEPENDENCY MUTATIONS
RESET_VEHICLE_AND_DEPS: "resetVehicleAndDependencies",
RESET_DAMAGE_AND_DEPS: "resetDamageAndDependencies",
RESET_REGISTRATION_AND_DEPS: "resetRegistrationAndDependencies",
RESET_PARTS_AND_DEPS: "resetPartsAndDependencies",
};
export { storeMutations };

View file

@ -0,0 +1,11 @@
const vehicleCategories = {
CAR: "CAR",
TRUCK: "TRUCK",
VAN: "VAN",
COMMERCIALVAN: "COMMERCIAL VAN",
SUV: "SUV",
MOTORHOME: "MOTOR HOME",
SEMI: "SEMI",
};
export { vehicleCategories };

View file

@ -9,20 +9,26 @@ export function fetchCmsContentForPage(fmgPage) {
const pageDataFromCms = {};
response.data.Result.forEach((widget) => {
let widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Name);
let widgetWithReplacements = findAndReplaceGlobalStateValues(
widget.Model,
widget.Name
);
// If we already have this widget, push it on the collection
if (widgetWithReplacements.Name in pageDataFromCms) {
pageDataFromCms[widgetWithReplacements.Name].push(widgetWithReplacements.Model);
pageDataFromCms[widgetWithReplacements.Name].push(
widgetWithReplacements.Model
);
return;
}
pageDataFromCms[widgetWithReplacements.Name] = [widgetWithReplacements.Model];
pageDataFromCms[widgetWithReplacements.Name] = [
widgetWithReplacements.Model,
];
});
Object.keys(pageDataFromCms).forEach((key) => {
if (pageDataFromCms[key].length === 1){
if (pageDataFromCms[key].length === 1) {
pageDataFromCms[key] = pageDataFromCms[key][0];
}
});
@ -33,31 +39,29 @@ export function fetchCmsContentForPage(fmgPage) {
// Function to convert a string, into a matching global state item.
function mapStringToState(str) {
// Pull all matches out of the string.
const regexExp = new RegExp('{(.*?):(.*?)}', 'g');
const regexExp = new RegExp("{(.*?):(.*?)}", "g");
const matches = [...str.matchAll(regexExp)];
// Our final string value that will be built from the matches.
let stringBuilder = '';
let stringBuilder = "";
for (const match of matches) {
// Reset store state for each match.
let storeState = store.state;
for (const s of match[2].split('.')) {
for (const s of match[2].split(".")) {
if (storeState[s] != undefined) {
storeState = storeState[s];
} else {
return ''; // if we can't map our string to state data, return an empty string.
return ""; // if we can't map our string to state data, return an empty string.
}
}
const stringWithReplacement = str.replace(match[0], storeState);
// If we still have values we need to substitute, call this function again.
if(stringWithReplacement.includes('{globalState:')) {
if (stringWithReplacement.includes("{globalState:")) {
return mapStringToState(stringWithReplacement);
}
@ -71,15 +75,16 @@ function mapStringToState(str) {
// Parent function for processWidgetItemForReplacement. This will loop through the parent
// object and pass any objects that need additional processing to the processWidgetItemForReplacement function.
function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
const objWithReplacements = {
Name: widgetName,
Model: {}
Model: {},
};
Object.keys(widgetModel).forEach(key => {
let modelWithReplacements = processWidgetItemForReplacement(widgetModel, key);
Object.keys(widgetModel).forEach((key) => {
let modelWithReplacements = processWidgetItemForReplacement(
widgetModel,
key
);
objWithReplacements.Model[key] = modelWithReplacements;
});
@ -91,16 +96,19 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
// This is a recursive function, it will call itself until it runs out of items to iterate on given the object.
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === 'string') {
if (widgetModel[key].includes('{globalState:')) {
if (typeof widgetModel[key] === "string") {
if (widgetModel[key].includes("{globalState:")) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
return widgetModel[key];
}
// If we have an object. array, etc
if (typeof widgetModel[key] === 'object' && Object.keys(widgetModel[key]).length) {
Object.keys(widgetModel[key]).forEach(item => {
if (
typeof widgetModel[key] === "object" &&
Object.keys(widgetModel[key]).length
) {
Object.keys(widgetModel[key]).forEach((item) => {
processWidgetItemForReplacement(widgetModel[key], item);
});
@ -109,5 +117,4 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have something else like a number, boolean, etc. just return it
return widgetModel[key];
}
}

View file

@ -4,12 +4,11 @@ import { dispatch } from "@/store";
jest.mock("@/store", () => ({
dispatch: jest.fn(),
state: {
order: { vehicle: { year: "2019", make: "Acura" } }
}
order: { vehicle: { year: "2019", make: "Acura" } },
},
}));
describe("cms-content-helper.js", () => {
describe("cms-content-helper.js", () => {
it("Should return data from CMS", () => {
// Arrange
const cmsMockData = {
@ -34,9 +33,9 @@ describe("cms-content-helper.js", () => {
},
],
};
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
// Act
fetchCmsContentForPage("testPage").then((response) => {
// Assert
@ -45,10 +44,9 @@ describe("cms-content-helper.js", () => {
);
});
});
});
describe("cms-content-helper.js", () => {
describe("cms-content-helper.js", () => {
it("Should replace strings for global state", () => {
const cmsMockData = {
Result: [
@ -60,21 +58,18 @@ describe("cms-content-helper.js", () => {
},
],
};
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
fetchCmsContentForPage("testPage").then((response) => {
// Assert
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual(
"2019"
);
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019");
});
});
});
describe("cms-content-helper.js", () => {
describe("cms-content-helper.js", () => {
it("Should replace strings for global state, and leave others the same", () => {
const cmsMockData = {
Result: [
{
@ -91,21 +86,21 @@ describe("cms-content-helper.js", () => {
},
],
};
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
fetchCmsContentForPage("testPage").then((response) => {
// Assert
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019");
expect(response.VehicleYearQuestion.ExampleText).toEqual("My widget value!");
expect(response.VehicleYearQuestion.ExampleText).toEqual(
"My widget value!"
);
});
});
});
describe("cms-content-helper.js", () => {
describe("cms-content-helper.js", () => {
it("Should replace strings for global state in nested objects", () => {
const cmsMockData = {
Result: [
{
@ -119,22 +114,23 @@ describe("cms-content-helper.js", () => {
Model: {
OtherObjectInside: {
ExampleText: "{globalState:order.vehicle.make}",
}
},
},
},
],
};
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
fetchCmsContentForPage("testPage").then((response) => {
// Assert
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019");
expect(response.VehicleMakeQuestion.OtherObjectInside.ExampleText).toEqual("Acura");
expect(
response.VehicleMakeQuestion.OtherObjectInside.ExampleText
).toEqual("Acura");
});
});
});
test.todo("String cannot be mapped to global state");
test.todo("String cannot be mapped to global state");

View file

@ -1,25 +1,32 @@
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js";
export default {
// Adds event to the bus given its category, subcategory, and eventValue;
addEventToBus(category, subCategory, eventValue) {
store.commit(storeActions.ADD_EVENT_TO_BUS, { category: category, subCategory: subCategory, eventValue: eventValue });
},
// Adds event to the bus given its category, subcategory, and eventValue;
addEventToBus(category, subCategory, eventValue) {
store.commit(storeMutations.ADD_EVENT_TO_BUS, {
category: category,
subCategory: subCategory,
eventValue: eventValue,
});
},
// Finds event on the bus, removes the item, and returns its value to the caller.
readAndPopEventFromBus(category, subCategory) {
const event = store.getters.eventBusItem(category, subCategory);
// Finds event on the bus, removes the item, and returns its value to the caller.
readAndPopEventFromBus(category, subCategory) {
const event = store.getters.eventBusItem(category, subCategory);
store.commit(storeActions.REMOVE_EVENT_FROM_BUS, { category: category, subCategory: subCategory });
store.commit(storeMutations.REMOVE_EVENT_FROM_BUS, {
category: category,
subCategory: subCategory,
});
return event;
},
return event;
},
// Finds the event on the bus and returns its value to the caller, does not remove it.
readEventFromBus(category, subCategory) {
const event = store.getters.eventBusItem(category, subCategory);
// Finds the event on the bus and returns its value to the caller, does not remove it.
readEventFromBus(category, subCategory) {
const event = store.getters.eventBusItem(category, subCategory);
return event;
},
}
return event;
},
};

View file

@ -3,48 +3,57 @@ import eventBus from "@/helpers/event-bus/event-bus";
import store from "@/store";
describe("event-bus.js", () => {
let event = {
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: globalEventTypes.Danger,
};
let event = {
isDismissible: true,
messageCopy: 'You can get a quote by starting on this page.',
messageHeadline: 'We\'re sorry, something went wrong.',
type: globalEventTypes.Danger
};
it("Puts item on bus and then take it off", () => {
// Arrange / Act
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
it("Puts item on bus and then take it off", () => {
// Assert
expect(
store.getters.eventBusItem(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
)
).toEqual(event);
// Arrange / Act
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
expect(store.state.applicationUser.eventBus.length).toEqual(1);
// Arrange / Act
const eventValue = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
// Assert
expect(store.getters.eventBusItem(globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND)).toEqual(event);
// Assert
expect(eventValue).toEqual(event);
expect(store.state.applicationUser.eventBus.length).toEqual(1);
expect(store.state.applicationUser.eventBus.length).toEqual(0);
});
// Arrange / Act
const eventValue = eventBus.readAndPopEventFromBus(globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND);
it("Reads event from bus, should have event value.", () => {
// Arrange / Act
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
// Assert
expect(eventValue).toEqual(event);
expect(store.state.applicationUser.eventBus.length).toEqual(0);
});
it("Reads event from bus, should have event value.", () => {
// Arrange / Act
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
// Assert
expect(eventBus.readEventFromBus(globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND)).toEqual(event);
});
});
// Assert
expect(
eventBus.readEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
)
).toEqual(event);
});
});

View file

@ -1,6 +1,7 @@
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
export function getMountOptions(mockData) {
// Define our mocks to attached to the 'global' object for Vue/Jest.
@ -23,6 +24,7 @@ export function getMountOptions(mockData) {
mocks.storeActions = storeActions;
mocks.storeMutations = storeMutations;
mocks.navigationScenarios = navigationScenarios;
mocks.vehicleCategories = vehicleCategories;
// Mock $store and $router when accessing this.$store/$router
mocks.$store = mockData.store;

View file

@ -0,0 +1,38 @@
const rules = {
// required: (value) => {
// console.log('value is... ', value);
// if (!value) {
// return 'PASSWORD IS REQUIRED';
// }
// },
// lengthOf3: (value) => {
// console.log('value is... ', value);
// if (value.length < 3) {
// return 'PASSWORD MUST BE AT LEAST 3 CHARS LONG';
// }
// }
required: {
test: (value) => {
// console.log('required value is... ', value);
if (!value || value.length < 1) {
// console.log('return false')
return false;
}
// console.log('no if, return true')
return true;
},
error: 'ssdfgPASSWORD IS REQUIRED'
},
lengthOf3: {
test: (value) => {
console.log('lengthof3 value is... ', value);
if (value.length < 3) {
return false;
}
return true;
},
error: 'sfdgfPASSWORD MUST BE AT LEAST 3 CHARS LONG'
}
};
export { rules };

View file

@ -2,51 +2,57 @@
<div class="container-fluid container-shadow p-2 rounded-3">
<div class="row my-3">
<div class="col">
<vehicleBanner ref="vehicleBanner" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="autocomplete">Home address</label>
<input ref="autocomplete"
id="autocomplete"
placeholder="Search"
class="form-control search-location"
onfocus="value = ''"
type="text" />
<input
ref="autocomplete"
id="autocomplete"
placeholder="Search"
class="form-control search-location"
onfocus="value = ''"
type="text"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
export default {
name: "App",
components: {
vehicleBanner
vehicleBanner,
},
mounted() {
this.$refs.vehicleBanner.initializeComponent({GenericVehicleImage: 'https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3'});
this.$refs.vehicleBanner.initializeComponent({
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
});
let self = this;
//AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo - localhost:8080
//AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w
this.$loadScript("https://maps.googleapis.com/maps/api/js?key=AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w&libraries=places")
//AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo - localhost:8080
//AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w
this.$loadScript(
"https://maps.googleapis.com/maps/api/js?key=AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w&libraries=places"
)
.then(() => {
// Script is loaded, do something
self.autocomplete = new window.google.maps.places.Autocomplete(
(self.$refs.autocomplete),
{
self.$refs.autocomplete,
{
componentRestrictions: { country: ["us"] },
fields: ["address_components"],
types: ['address']
types: ["address"],
}
);
self.autocomplete.addListener('place_changed', () => {
self.autocomplete.addListener("place_changed", () => {
let place = self.autocomplete.getPlace();
let address1 = "";
let postcode = "";
@ -67,10 +73,10 @@ export default {
break;
}
case "locality":
address1 += ', ' + component.long_name;
address1 += ", " + component.long_name;
break;
case "administrative_area_level_1": {
address1 += ', ' + component.short_name;
address1 += ", " + component.short_name;
break;
}
case "postal_code": {
@ -82,20 +88,17 @@ export default {
postcode = `${postcode}-${component.long_name}`;
break;
}
}
let addressField1 = document.querySelector("#autocomplete");
addressField1.value = address1 + ' ' + postcode;
}
});
addressField1.value = address1 + " " + postcode;
}
});
})
.catch(() => {
// Failed to fetch script
console.log("Unable to load Google Places API script")
console.log("Unable to load Google Places API script");
});
},
};
</script>

File diff suppressed because it is too large Load diff

View file

@ -1,117 +1,126 @@
<template>
<Form
:validation-schema="schema"
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
v-slot="{ values, meta }"
class="w-50"
>
<div class="row g-2 mt-6 mx-4">
<h2 class="mx-2 mt-4 mb-0">FORM VALIDATION TEST</h2>
<p class="mx-2">NOTE: This page is testing functionality only; styling will not match Figma mocks.</p>
</div>
<Form
:validation-schema="schema"
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
v-slot="{ values, meta }"
class="w-50"
>
<div class="row g-2 mt-6 mx-4">
<h2 class="mx-2 mt-4 mb-0">FORM VALIDATION TEST</h2>
<p class="mx-2">
NOTE: This page is testing functionality only; styling will not match
Figma mocks.
</p>
</div>
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">How many chips are we repairing? (Required)</h4>
<div
role="radiogroup"
aria-labelledby="demo-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo-group">
How many chips are we repairing?
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">
How many chips are we repairing? (Required)
</h4>
<listButtonHorizontal
groupName="demo"
aria-required="true"
buttonID="1"
buttonLabel="1"
value="1"
buttonLabelSubCopy=""
textPosition="text-center"
sizeInRem="1"
:totalInGroup="3"
:positionInGroup="1"
screenReaderOnlyText=" opens new window"
/>
<listButtonHorizontal
groupName="demo"
aria-required="true"
buttonID="2"
buttonLabel="2"
value="2"
buttonLabelSubCopy=""
textPosition="text-center"
sizeInRem="1"
:totalInGroup="3"
:positionInGroup="2"
screenReaderOnlyText=" opens new window"
/>
<listButtonHorizontal
groupName="demo"
aria-required="true"
buttonID="3"
buttonLabel="3"
value="3"
buttonLabelSubCopy=""
textPosition="text-center"
sizeInRem="1"
:totalInGroup="3"
:positionInGroup="3"
screenReaderOnlyText=" opens new window"
/>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo"></error-message>
</div>
</div>
<div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">Which windshield part needs fixed? (Required)</h4>
<fieldset>
<legend class="sr-only">Which windshield part needs fixed?</legend>
<listButton
isMultiSelect
buttonID="Single Windshield"
buttonLabel="Single Windshield"
value="Single Windshield"
groupName="demo2"
/>
<listButton
isMultiSelect
buttonID="Driver Side"
buttonLabel="Driver Side"
value="Driver Side"
groupName="demo2"
/>
<listButton
isMultiSelect
buttonID="Passenger Side"
buttonLabel="Passenger Side"
value="Passenger Side"
groupName="demo2"
/>
<div
role="radiogroup"
aria-labelledby="demo-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo-group">
How many chips are we repairing?
</h4>
<listButtonHorizontal
groupName="demo"
aria-required="true"
buttonID="1"
buttonLabel="1"
value="1"
buttonLabelSubCopy=""
textPosition="text-center"
sizeInRem="1"
:totalInGroup="3"
:positionInGroup="1"
screenReaderOnlyText=" opens new window"
/>
<listButtonHorizontal
groupName="demo"
aria-required="true"
buttonID="2"
buttonLabel="2"
value="2"
buttonLabelSubCopy=""
textPosition="text-center"
sizeInRem="1"
:totalInGroup="3"
:positionInGroup="2"
screenReaderOnlyText=" opens new window"
/>
<listButtonHorizontal
groupName="demo"
aria-required="true"
buttonID="3"
buttonLabel="3"
value="3"
buttonLabelSubCopy=""
textPosition="text-center"
sizeInRem="1"
:totalInGroup="3"
:positionInGroup="3"
screenReaderOnlyText=" opens new window"
/>
</div>
</fieldset>
<div class="row px-3 form-test-error">
<error-message name="demo"></error-message>
</div>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo2"></error-message>
</div>
</div>
<div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">demo3: Enter your VIN (Required)</h4>
<textInput
name="demo3"
type="text"
label="VIN (Required)"
:isRequired="true"
/>
<div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">
Which windshield part needs fixed? (Required)
</h4>
<fieldset>
<legend class="sr-only">Which windshield part needs fixed?</legend>
<listButton
isMultiSelect
buttonID="Single Windshield"
buttonLabel="Single Windshield"
value="Single Windshield"
groupName="demo2"
/>
<listButton
isMultiSelect
buttonID="Driver Side"
buttonLabel="Driver Side"
value="Driver Side"
groupName="demo2"
/>
<listButton
isMultiSelect
buttonID="Passenger Side"
buttonLabel="Passenger Side"
value="Passenger Side"
groupName="demo2"
/>
</fieldset>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo2"></error-message>
</div>
</div>
</div>
<!-- <div class="row g-2 mt-6 mx-4">
<div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">demo3: Enter your VIN (Required)</h4>
<textInput
name="demo3"
type="text"
label="VIN (Required)"
:isRequired="true"
/>
</div>
</div>
<!-- <div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">demo4: Enter some numbers (optional)</h4>
<textInput
@ -123,16 +132,11 @@
</div>
</div> -->
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">demo5: Where's your damage? (Required)</h4>
<div
aria-labelledby="demo5-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo5-group">
Where's your damage?
</h4>
<listCard
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">demo5: Where's your damage? (Required)</h4>
<div aria-labelledby="demo5-group" class="d-flex flex-row p-0">
<h4 class="sr-only" id="demo5-group">Where's your damage?</h4>
<listCard
isMultiSelect
buttonImage="windshield-damage.svg"
buttonLabel="Windshield"
@ -159,24 +163,25 @@
buttonID="List Card Checkbox c"
groupName="demo5"
/>
</div>
</div>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo5"></error-message>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo5"></error-message>
</div>
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">demo6: What's your windshield damage? (Required if demo5 "Windshield" is selected)</h4>
<div
role="radiogroup"
aria-labelledby="demo6-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo6-group">
What's your windshield damage?
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">
demo6: What's your windshield damage? (Required if demo5 "Windshield" is
selected)
</h4>
<listCard
<div
role="radiogroup"
aria-labelledby="demo6-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo6-group">What's your windshield damage?</h4>
<listCard
isRadio
buttonImage="windshield-damage.svg"
buttonLabel="Crack"
@ -196,14 +201,14 @@
buttonID="Chip Checkbox b"
groupName="demo6"
/>
</div>
</div>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo6"></error-message>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo6"></error-message>
</div>
<!-- <div class="row g-2 mt-6 mx-4">
<!-- <div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">Password (Required, enter any string)</h4>
<textInput
@ -227,24 +232,24 @@
</div>
</div> -->
<div class="row px-3 form-test-error" data-focus-target="demoX">
<error-message name="demoX"></error-message>
</div>
<div class="row px-3 form-test-error" data-focus-target="demoX">
<error-message name="demoX"></error-message>
</div>
<div class="g-2 mt-6 mx-4">
<button class="mb-6" :class="!meta.valid && 'form-test-invalid'">Get your estimate</button>
<p>Current Form Values:</p>
<pre>{{ values }}</pre>
<p>Is Form Valid? (Meta.valid):</p>
<pre>{{ meta.valid }}</pre>
</div>
</Form>
<div class="g-2 mt-6 mx-4">
<button class="mb-6" :class="!meta.valid && 'form-test-invalid'">
Get your estimate
</button>
<p>Current Form Values:</p>
<pre>{{ values }}</pre>
<p>Is Form Valid? (Meta.valid):</p>
<pre>{{ meta.valid }}</pre>
</div>
</Form>
</template>
<script>
import { Form, ErrorMessage } from 'vee-validate';
import { Form, ErrorMessage } from "vee-validate";
import * as Yup from "yup";
import listCard from "@/ux-components/list-card/list-card";
@ -256,62 +261,81 @@ export default {
name: "App",
data() {
const errorMessages = {
demo: 'Please select chip(s)',
demo2: 'Please select a windshield part',
demo3: 'You entered an invalid VIN. Please check to make sure that you inserted a 17 digit, alpha-numeric number. VINs do not contain the letters I, O or Q.',
demo4: 'Invalid Entry. Must enter only numbers, no letters.',
demo5: 'Please select damage location',
demo: "Please select chip(s)",
demo2: "Please select a windshield part",
demo3:
"You entered an invalid VIN. Please check to make sure that you inserted a 17 digit, alpha-numeric number. VINs do not contain the letters I, O or Q.",
demo4: "Invalid Entry. Must enter only numbers, no letters.",
demo5: "Please select damage location",
};
return {
schema: Yup.object().shape({
demo: Yup.string().required(errorMessages.demo),
demo2: Yup.array().required(errorMessages.demo2).min(1, errorMessages.demo2),
demo3: Yup.string()
.uppercase()
.required('Please enter a VIN.')
.min(17, errorMessages.demo3)
.max(17, errorMessages.demo3)
.matches('^[^IOQ]+$', errorMessages.demo3),
// demo4: Yup.string().matches('^[0-9]*$', errorMessages.demo4),
demo5: Yup.array().required(errorMessages.demo5).min(1, errorMessages.demo5),
demo6: Yup.string()
.when('demo5', function (demo5, schema, value) {
schema: Yup.object()
.shape({
demo: Yup.string().required(errorMessages.demo),
demo2: Yup.array()
.required(errorMessages.demo2)
.min(1, errorMessages.demo2),
demo3: Yup.string()
.uppercase()
.required("Please enter a VIN.")
.min(17, errorMessages.demo3)
.max(17, errorMessages.demo3)
.matches("^[^IOQ]+$", errorMessages.demo3),
// demo4: Yup.string().matches('^[0-9]*$', errorMessages.demo4),
demo5: Yup.array()
.required(errorMessages.demo5)
.min(1, errorMessages.demo5),
demo6: Yup.string().when("demo5", function (demo5, schema, value) {
if (demo5) {
const demo5String = demo5.toString();
if (demo5String.includes("Windshield")) {
// make demo6 required
return schema.required('You must pick an option for Windshield Damage.')
return schema.required(
"You must pick an option for Windshield Damage."
);
}
return schema;
}
return schema;
}),
demoX: Yup.string().notRequired(),
// PASSWORD EXAMPLE, NOT BEING USED NOW
// password: Yup.string().min(6).required(),
// passwordConfirmation: Yup.string()
// .required()
// .oneOf([Yup.ref("password")], "Passwords do not match"),
}).test('repair-replace-conflict',
null, // need to pass null as error message so it won't get added to list of errors
function(value) {
// whole form test
console.log('ALL FORM VALUES: ', value);
demoX: Yup.string().notRequired(),
// PASSWORD EXAMPLE, NOT BEING USED NOW
// password: Yup.string().min(6).required(),
// passwordConfirmation: Yup.string()
// .required()
// .oneOf([Yup.ref("password")], "Passwords do not match"),
})
.test(
"repair-replace-conflict",
null, // need to pass null as error message so it won't get added to list of errors
function (value) {
// whole form test
console.log("ALL FORM VALUES: ", value);
// test problem scenario... return new error if conditions are true, allow submit if not
if (value.demo5 && value.demo6) {
const demo5String = value.demo5.toString();
console.log('demo5String: ', demo5String);
console.log('demo6: ', value.demo6);
// test problem scenario... return new error if conditions are true, allow submit if not
if (value.demo5 && value.demo6) {
const demo5String = value.demo5.toString();
console.log("demo5String: ", demo5String);
console.log("demo6: ", value.demo6);
if (demo5String.includes("Windshield") && value.demo5.length > 1 && value.demo6 == "Chip") {
return this.createError({ name: 'demoX', message: "Sorry, at this time we can't schedule a repair and replace at the same time. Please schedule these appointments separately as they are different technicians. Thanks.", path: 'demoX' });
if (
demo5String.includes("Windshield") &&
value.demo5.length > 1 &&
value.demo6 == "Chip"
) {
return this.createError({
name: "demoX",
message:
"Sorry, at this time we can't schedule a repair and replace at the same time. Please schedule these appointments separately as they are different technicians. Thanks.",
path: "demoX",
});
}
return true;
}
return true;
}
return true;
}),
}
),
};
},
components: {
listCard,
@ -323,8 +347,10 @@ export default {
},
methods: {
onSubmit(values) {
window.alert('Success! Submitted values: ' + JSON.stringify(values, null ,2))
console.log('submitted: ', JSON.stringify(values, null ,2));
window.alert(
"Success! Submitted values: " + JSON.stringify(values, null, 2)
);
console.log("submitted: ", JSON.stringify(values, null, 2));
},
onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it
@ -334,25 +360,22 @@ export default {
// get errorEls and order by alpha
const errorEls = Object.keys(errors).sort();
const firstErrorEl = errorEls[0];
console.log('firstErrorEl: ', firstErrorEl);
console.log("firstErrorEl: ", firstErrorEl);
if (firstErrorEl) {
const qsString = "[data-focus-target='" + firstErrorEl + "']";
document.querySelector(qsString).focus();
}
}
},
},
};
</script>
<style lang="scss">
.form-test-error {
color: red;
font-weight: bold;
}
.form-test-invalid {
opacity: 0.5;
}
.has-error {
border: 1px solid red;
}
</style>
.form-test-error {
color: red;
font-weight: bold;
}
.form-test-invalid {
opacity: 0.5;
}
</style>

View file

@ -0,0 +1,83 @@
import { shallowMount } from "@vue/test-utils";
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
describe("damage-location-question.vue", () => {
test("Selected location option is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: ["Windshield"] });
const locationToSelect = ["Backseat"];
//Act
wrapper.setValue({ modelValue: locationToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{modelValue: ["Backseat"]}]);
});
});
describe("damage-location-question.vue", () => {
test("Answers to display filtered by data from api.", async () => {
//Arrange
const { wrapper, cmsContent, damageOptions } = setupMocks({ dataFromStoreApi: {
driverSideOptions: {
availableReplacementOptions: ['Quarter', 'Front']
},
windshieldOptions: {
availableReplacementOptions: ['Single']
},
backGlassOptions: {
availableReplacementOptions: []
}
}
});
//Act
damageLocationQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, damageOptions, "car-group");
//Assert
expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'car-Windshield' }, { Name: 'car-SideDoor' } ])
});
});
function setupMocks({
modelValueProp = ["Windshield", "SideDoor"],
isMultiSelect = false,
groupName = "damageQuestion",
cmsQuestionText = "CMS text goes here",
cmsAnswers = [{Name: "car-Windshield"}, {Name: "car-SideDoor"}, {Name: "car-RearWindow"}],
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp,
isMultiSelect: isMultiSelect,
};
const wrapper = shallowMount(damageLocationQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
groupName: groupName,
QuestionText: cmsQuestionText,
Answers: cmsAnswers,
};
const damageOptions = dataFromStoreApi;
return { wrapper, cmsContent, damageOptions };
}

View file

@ -1,15 +1,15 @@
<template>
<div class="damage-location-question">
<div class="replace-options-question">
I AM DAMAGE LOCATION QUESTION
groupName is: {{groupName}}
<buttonQuestion
v-if="isAvailable && answersToDisplayTemp.length > 1"
v-if="answersToDisplay.length > 1"
:questionText="questionText"
:isMultiSelect="isMultiSelect"
:answers="answersToDisplayTemp"
groupName="damageLocation"
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
:modelValue="modelValue"
v-model="selectedValues"
:validationRules="setValidationRules()"
/>
</div>
@ -26,45 +26,24 @@ export default ({
return {
questionText: String,
answersFromCms: Array,
answersToDisplay: Array,
answersToDisplayTemp: [
{
Name: 'windshield-damage',
Text: 'Windshield',
AnswerImageUrl: 'windshield-damage.svg',
ImageId: 'windshield-damage',
},
{
Name: 'side-window-damage-left-all',
Text: 'Side Door',
AnswerImageUrl: 'side-window-damage-left-all.svg',
ImageId: 'side-window-damage-left-all',
},
{
Name: 'back-glass-damage',
Text: 'Rear Window',
AnswerImageUrl: 'back-glass-damage.svg',
ImageId: 'back-glass-damage',
}
],
damageOptions: Array,
damageOptions: Object,
groupName: String,
}
},
props: {
isMultiSelect: Boolean,
modelValue: Array,
isAvailable: Boolean,
filterByVehicleCategory: Boolean,
modelValue: String,
validationRules: Array,
name: String,
},
methods: {
initializeComponent(cmsContent, damageOptions){
initializeComponent(cmsContent, damageOptions, groupName){
console.log('cmsContent: ', cmsContent)
console.log('damageOptions: ', damageOptions)
this.groupName = cmsContent.Name;
this.groupName = groupName;
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
this.damageOptions = damageOptions;
@ -89,18 +68,31 @@ export default ({
return rulesArray;
},
},
mounted(){
// console.log('this.answersFromCms: ', this.answersFromCms)
// this.answersToDisplay = this.answersFromCms.filter(ans => {
// const name = ans.Name.split('-');
// return this.filterByVehicleCategory ? name[0].toUpperCase() === store.getters.vehicle.category : name[0].toUpperCase() === store.getters.vehicle.category && this.damageOptions.includes(name[1]);
// });
// console.log('this.answersToDisplay: ', this.answersToDisplay)
},
watch: {
modelValue(val) {
this.$emit("update:modelValue", val);
}
computed: {
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
damageOptionsMap(){
return {
Windshield: true,
SideDoor: this.damageOptions.driverSideOptions.availableReplacementOptions || this.damageOptions.passengerSideOptions.availableReplacementOption ? true : false,
RearWindow: this.damageOptions.backGlassOptions.availableReplacementOption ? true : false,
}
},
answersToDisplay(){
return Array.isArray(this.answersFromCms)
? this.answersFromCms.filter(ans =>
{
const name = ans.Name.split('-');
return name[0].toUpperCase() === store.getters.vehicle.category && this.damageOptionsMap[name[1]];
})
: [];
},
},
components: {
buttonQuestion,

View file

@ -0,0 +1,76 @@
import { shallowMount } from "@vue/test-utils";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
describe("replace-options-question.vue", () => {
test("Selected damage option is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: ["Windshield"] });
const damageToSelect = ["Backseat"];
//Act
wrapper.setValue({ modelValue: damageToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{modelValue: ["Backseat"]}]);
});
});
describe("replace-options-question.vue", () => {
test("Answers to display filtered by data from api.", async () => {
//Arrange
const { wrapper, cmsContent, replaceOptions } = setupMocks({ dataFromStoreApi: ["Windshield", "FrontDoor"]});
//Act
replaceOptionsQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, replaceOptions, "car-group");
//Assert
expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'car-Windshield' }, { Name: 'car-FrontDoor' } ])
});
});
function setupMocks({
modelValueProp = ["Windshield"],
isAvailale = true,
isMultiSelect = false,
filterByVehicleCategory = false,
groupName = "damageQuestion",
cmsQuestionText = "CMS text goes here",
cmsAnswers = [{Name: "car-Windshield"}, {Name: "car-BackDoor"}, {Name: "car-FrontDoor"}],
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp,
isAvailable: isAvailale,
isMultiSelect: isMultiSelect,
filterByVehicleCategory: filterByVehicleCategory
};
const wrapper = shallowMount(replaceOptionsQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
groupName: groupName,
QuestionText: cmsQuestionText,
Answers: cmsAnswers,
};
const replaceOptions = dataFromStoreApi;
return { wrapper, cmsContent, replaceOptions };
}

View file

@ -0,0 +1,80 @@
<template>
<transition name="fade">
<div class="replace-options-question">
<buttonQuestion
v-if="isAvailable && answersToDisplay.length > 1"
:questionText="questionText"
:isMultiSelect="isMultiSelect"
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
v-model="selectedValues"
/>
</div>
</transition>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import store from "@/store";
export default ({
name: "replaceOptionsQuestion",
data(){
return {
questionText: String,
answersFromCms: Array,
replaceOptions: Array,
groupName: String,
}
},
props: {
isMultiSelect: Boolean,
isAvailable: Boolean,
filterByVehicleCategory: Boolean,
modelValue: Array,
},
methods: {
initializeComponent(cmsContent, replaceOptions, groupName){
this.groupName = groupName;
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
this.replaceOptions = replaceOptions;
}
},
computed: {
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
answersToDisplay(){
return Array.isArray(this.answersFromCms)
? this.answersFromCms.filter(ans =>
{
const name = ans.Name.split('-');
return this.filterByVehicleCategory ? name[0].toUpperCase() === store.getters.vehicle.category : name[0].toUpperCase() === store.getters.vehicle.category && this.replaceOptions.includes(name[1])
})
: [];
},
},
components: {
buttonQuestion,
}
})
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,125 +1,256 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
// Components
import vehicleDamage from "@/layouts/vehicle-damage/vehicle-damage.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
// Mock Store
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
vehicle: {
carId: "C00000000",
},
},
}));
describe("vehicle-damage.vue", () => {
test("Page header is initailized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select Damage";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText });
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
vehicleDamage.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-damage" } }, undefined, (c) => c(wrapper.vm));
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText);
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
});
describe("vehicle-damage.vue", () => {
test("Page logo image is initailized with api data", async (done) => {
//Arrange
const SiteHeaderWidget = {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget});
//Act
vehicleDamage.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-damage" } }, undefined, (c) => c(wrapper.vm));
//Assert
apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget);
done();
});
test("Page logo image is initailized with api data", async (done) => {
//Arrange
const SiteHeaderWidget = {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
};
const { wrapper, apiPromise } = setupMocks({
SiteHeaderWidget: SiteHeaderWidget,
});
});
describe("vehicle-damage.vue", () => {
test("Vehicle image is initailized with api data", async (done) => {
//Arrange
const VehicleBannerWidget = {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget});
//Act
vehicleDamage.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-damage" } }, undefined, (c) => c(wrapper.vm));
//Assert
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
done();
});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done();
});
});
});
describe("vehicle-damage.vue", () => {
test("Vehicle image is initailized with api data", async (done) => {
//Arrange
const VehicleBannerWidget = {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
};
const { wrapper, apiPromise } = setupMocks({
VehicleBannerWidget: VehicleBannerWidget,
});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done();
});
});
});
describe("vehicle-damage.vue", () => {
test("CarId set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("vehicle-damage.vue", () => {
test("Call invalidation, ResetPartsAndDeps should be called", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.dispatch).toBeCalledWith(storeActions.RESET_PARTS_AND_DEPS)
});
});
function setupMocks({
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation(() => {
return Promise.resolve({ data: ['Front Window', 'Back Window', 'Side Window'] });
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation(() => {
return Promise.resolve({
driverSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
}
});
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",
},
});
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
damageOptions: ['Front Window', 'Back Window', 'Side Window'],
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
//Mock damage initialize methods
funnelHeader.methods = {
initializeComponent: jest.fn(),
};
vehicleBanner.methods = {
initializeComponent: jest.fn(),
};
funnelSubHeader.methods = {
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleDamage, 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;
return { wrapper, apiPromise };
}
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
},
damageOptions: {
driverSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
}
},
};
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(),
};
const driverSideOptions = replaceOptionsQuestion
driverSideOptions.methods = {
initializeComponent: jest.fn(),
};
damageLocationQuestion.methods = {
initializeComponent: jest.fn(),
}
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleDamage, 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 driverSideOptionsWrapper = wrapper.findComponent({
name: "replaceOptionsQuestion",
});
driverSideOptionsWrapper.vm.initializeComponent =
replaceOptionsQuestion.methods.initializeComponent;
const damageLocationQuestionWrapper = wrapper.findComponent({
name: "damageLocationQuestion",
});
damageLocationQuestionWrapper.vm.initializeComponent =
damageLocationQuestion.methods.initializeComponent;
return { wrapper, apiPromise };
}

View file

@ -1,10 +1,11 @@
<template>
<div class="container-fluid shadow rounded-3 p-0 position-relative">
<div class="container-fluid shadow rounded-3 p-2 position-relative">
I AM VEHICLE DAMAGE
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader
ref="funnelSubHeader"
:hasSubText=true
/>
<Form
@submit="onSubmit"
@ -12,12 +13,12 @@
v-slot="{ values, meta }"
>
<damageLocationQuestion
ref="damageLocationQuestion"
ref="damageLocation"
name="test"
isAvailable
isMultiSelect
v-model="damageLocationQuestionData"
/>
<replaceOptionsQuestion ref="replaceOptionsQuestion" isAvailable />
<replaceOptionsQuestion ref="driverSideOptions" isAvailable isMultiSelect v-model="driverSideOptionsData" />
<funnel-footer
:isDisabled="!meta.valid"
/>
@ -50,28 +51,33 @@ import { Form, ErrorMessage } from 'vee-validate';
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { storeActions } from "@/constants/store-actions";
export default ({
name: "vehicle-damage",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const damageOptionsPromise = baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_DAMAGE_OPTIONS, {carId: store.getters.vehicle.carId});
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "damageOptions",
promise: damageOptionsPromise
},
];
export default {
name: "vehicle-damage",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const damageOptionsPromise =
baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: store.getters.vehicle.carId }
);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "damageOptions",
promise: damageOptionsPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
@ -86,15 +92,22 @@ export default ({
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.damageLocationQuestion.initializeComponent(
resultMap.cmsContent.DamageLocationQuestion
vm.$refs.driverSideOptions.initializeComponent(
resultMap.cmsContent.DriverSideReplaceOptionsQuestion, resultMap.damageOptions.driverSideOptions.availableReplacementOptions, 'DriverSideReplaceOptionsQuestion'
);
// vm.$refs.replaceOptionsQuestion.initializeComponent(
// resultMap.cmsContent.DriverSideReplaceOptionsQuestion, resultMap.damageOptions.driverSideOptions.availableReplacementOptions
// );
console.log(resultMap)
vm.$refs.damageLocation.initializeComponent(
resultMap.cmsContent.DamageLocationQuestion, resultMap.damageOptions, 'DamageLocationQuestion'
);
// for damageOptions and cms content data
//console.log(resultMap)
});
},
data(){
return {
damageLocationQuestionData: [],
driverSideOptionsData: [],
}
},
components: {
Form,
@ -102,11 +115,18 @@ export default ({
vehicleBanner,
funnelSubHeader,
damageLocationQuestion,
// replaceOptionsQuestion,
replaceOptionsQuestion,
funnelFooter,
// textInput,
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
resetDependentState() {
// Invokes
store.dispatch(storeActions.RESET_PARTS_AND_DEPS);
},
onSubmit(values) {
window.alert('Success! Submitted values: ' + JSON.stringify(values, null ,2))
console.log('submitted: ', JSON.stringify(values, null ,2));
@ -126,5 +146,5 @@ export default ({
}
}
},
});
};
</script>

View file

@ -2,11 +2,16 @@ import makeQuestion from "@/layouts/vehicle-make/make-question/make-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("make-question.vue", () => {
test("Selected make is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "honda" });
const makeToSelect = "ford";
@ -16,51 +21,65 @@ describe("make-question.vue", () => {
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{selectedMake: "ford"}]);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([
{ selectedMake: "ford" },
]);
});
});
describe("make-question.vue", () => {
test("CMS question text is used as radio question text.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What make is your vehicle?" });
const { wrapper, cmsContent } = setupMocks({
cmsQuestionText: "What make is your vehicle?",
});
//Act
makeQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("questiontext")).toBe("What make is your vehicle?");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("questiontext")).toBe(
"What make is your vehicle?"
);
});
});
describe("make-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["honda", "ford", "dodge"] });
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["honda", "ford", "dodge"],
});
//Act
const initialData = makeQuestion.methods.loadInitialData.call(wrapper.vm);
makeQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData);
makeQuestion.methods.initializeComponent.call(
wrapper.vm,
cmsContent,
initialData
);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("answers")).toBe("honda,ford,dodge");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe(
"honda,ford,dodge"
);
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019} };
store.getters = { vehicle: { year: 2019 } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
@ -76,7 +95,7 @@ function setupMocks({
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText
};
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}
}

View file

@ -1,12 +1,14 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="makes"
groupName="Choose Vehicle Make"
textPosition="text-start"
:loaderEnabled="true"
v-model="modelValue"
v-model="selectedValueAsArray"
isRequired=true
/>
</template>
@ -23,27 +25,37 @@ export default {
return {
questionText: null,
makes: Array,
}
};
},
props: {
modelValue: String,
},
computed: {
selectedValueAsArray: {
get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : null;
return modelValueAsArray;
},
set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
this.$emit("update:modelValue", newValueAsScalar);
}
}
},
components: {
buttonQuestion,
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_VEHICLE_MAKES, {year: store.getters.vehicle.year});
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_VEHICLE_MAKES,
{ year: store.getters.vehicle.year }
);
},
initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText;
this.makes = initialData;
}
},
watch: {
modelValue(val) {
this.$emit("update:modelValue", val);
}
},
},
};
</script>

View file

@ -1,35 +1,67 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
// Components
import vehicleMake from "@/layouts/vehicle-make/vehicle-make.vue";
import makeQuestion from "@/layouts/vehicle-make/make-question/make-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import store from "@/store";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
vehicle: {
year: 2019,
},
},
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
describe("vehicle-make.vue", () => {
test("Make question component is initized with api data", async (done) => {
//Arrange
const vehicleMakeQuestionCmsContent = { QuestionText: "What make is your vehicle?" };
const vehicleMakeQuestionCmsContent = {
QuestionText: "What make is your vehicle?",
};
const makeQuestionInitialData = ["honda", "ford", "dodge"];
const { wrapper, apiPromise } = setupMocks( {
vehicleMakeQuestionCmsContent: vehicleMakeQuestionCmsContent,
const { wrapper, apiPromise } = setupMocks({
vehicleMakeQuestionCmsContent: vehicleMakeQuestionCmsContent,
makeQuestionInitialData: makeQuestionInitialData,
} );
});
//Act
vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm));
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleMakeQuestionCmsContent, makeQuestionInitialData);
expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(
vehicleMakeQuestionCmsContent,
makeQuestionInitialData
);
done();
});
});
@ -37,17 +69,25 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => {
test("Page header is initailized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select a make to get started";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText });
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm));
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText);
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
@ -55,20 +95,28 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => {
test("Page logo image is initailized with api data", async (done) => {
//Arrange
const SiteHeaderWidget = {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget});
};
const { wrapper, apiPromise } = setupMocks({
SiteHeaderWidget: SiteHeaderWidget,
});
//Act
vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm));
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget);
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done();
});
});
@ -76,20 +124,28 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => {
test("Vehicle image is initailized with api data", async (done) => {
//Arrange
const VehicleBannerWidget = {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget});
};
const { wrapper, apiPromise } = setupMocks({
VehicleBannerWidget: VehicleBannerWidget,
});
//Act
vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm));
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done();
});
});
@ -97,19 +153,23 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => {
test("BackButtonAction triggers a router.navigate change", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks( {
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a make to get started",
mountOptionsMockData: {
router: {
navigate: jest.fn()
navigate: jest.fn(),
},
},
});
//Act
vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm));
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
await nextTick();
@ -121,32 +181,80 @@ describe("vehicle-make.vue", () => {
});
});
describe("vehicle-make.vue", () => {
test("Year set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("vehicle-make.vue", () => {
test("Year set, call invalidation, model, style, carId, category should be null", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_AND_DEPS)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_AND_DEPS)
});
});
function setupMocks({
vehicleMakeQuestionCmsContent = {},
makeQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleMakeQuestion: vehicleMakeQuestionCmsContent,
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",
},
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",
},
},
makeQuestionInitialData: makeQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
settleAllPromises.mockImplementation(() => apiPromise);
//Mock make question methods
@ -163,16 +271,23 @@ function setupMocks({
funnelSubHeader.methods = {
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleMake, mountOptions);
const makeQuestionWrapper = wrapper.findComponent({ name: "makeQuestion" });
makeQuestionWrapper.vm.initializeComponent = makeQuestion.methods.initializeComponent;
makeQuestionWrapper.vm.initializeComponent =
makeQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent;
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;
vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise };
}
}

View file

@ -3,7 +3,7 @@
<funnelHeader ref="funnelHeader" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
<funnelSubHeader
ref="funnelSubHeader"
:hasBackButton="true"
@ -25,11 +25,15 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
export default {
name: "vehicle-make",
data() {
return {
selectedMake: null,
selectedMake: String,
};
},
computed: {},
@ -53,7 +57,6 @@ export default {
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.$refs.funnelSubHeader.initializeComponent(
@ -75,14 +78,31 @@ export default {
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.year !== null;
},
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_MODEL, null);
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_AND_DEPS);
store.dispatch(storeActions.RESET_REGISTRATION_AND_DEPS);
},
},
watch: {
selectedMake(make) {
this.$store.commit(this.storeMutations.UPDATE_MAKE, make);
this.$router.navigate(
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_MAKE,
this.$route
);

View file

@ -2,11 +2,16 @@ import modelQuestion from "@/layouts/vehicle-model/model-question/model-question
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("model-question.vue", () => {
test("Selected model is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "Accord" });
const modelToSelect = "Civic";
@ -16,51 +21,69 @@ describe("model-question.vue", () => {
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{selectedModel: "Civic"}]);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([
{ selectedModel: "Civic" },
]);
});
});
describe("model-question.vue", () => {
test("CMS question text is used as radio question text.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What model is your vehicle?" });
const { wrapper, cmsContent } = setupMocks({
cmsQuestionText: "What model is your vehicle?",
});
//Act
modelQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null);
modelQuestion.methods.initializeComponent.call(
wrapper.vm,
cmsContent,
null
);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("questiontext")).toBe("What model is your vehicle?");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("questiontext")).toBe(
"What model is your vehicle?"
);
});
});
describe("model-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["accord", "civic", "insight"] });
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["accord", "civic", "insight"],
});
//Act
const initialData = modelQuestion.methods.loadInitialData.call(wrapper.vm);
modelQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData);
modelQuestion.methods.initializeComponent.call(
wrapper.vm,
cmsContent,
initialData
);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("answers")).toBe("accord,civic,insight");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe(
"accord,civic,insight"
);
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda'} };
store.getters = { vehicle: { year: 2019, make: "honda" } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
@ -76,7 +99,7 @@ function setupMocks({
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText
};
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}
}

View file

@ -1,12 +1,14 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="models"
groupName="Choose Vehicle Model"
textPosition="text-start"
:loaderEnabled="true"
v-model="modelValue"
v-model="selectedValueAsArray"
isRequired=true
/>
</template>
@ -23,27 +25,37 @@ export default {
return {
questionText: null,
models: Array,
}
};
},
props: {
modelValue: String,
},
computed: {
selectedValueAsArray: {
get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : null;
return modelValueAsArray;
},
set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
this.$emit("update:modelValue", newValueAsScalar);
}
}
},
components: {
buttonQuestion,
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_VEHICLE_MODELS, {year: store.getters.vehicle.year, make: store.getters.vehicle.make});
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_VEHICLE_MODELS,
{ year: store.getters.vehicle.year, make: store.getters.vehicle.make }
);
},
initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText;
this.models = initialData;
}
},
watch: {
modelValue(val) {
this.$emit("update:modelValue", val);
}
},
},
};
</script>

View file

@ -1,30 +1,65 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
// Components
import vehicleModel from "@/layouts/vehicle-model/vehicle-model.vue";
import modelQuestion from "@/layouts/vehicle-model/model-question/model-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
// Supporting files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
// 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(),
}));
// Mock Store
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
vehicle: {
make: "Acura",
},
},
}));
describe("vehicle-model.vue", () => {
test("Model question component is initized with api data", async (done) => {
//Arange
const buttonQuestionContent = { QuestionText: "What model is your vehicle?" };
const buttonQuestionContent = {
QuestionText: "What model is your vehicle?",
};
const modelQuestionInitialData = ["accord", "civic", "insight"];
const { wrapper, apiPromise } = setupMocks( {
buttonQuestionContent: buttonQuestionContent,
const { wrapper, apiPromise } = setupMocks({
buttonQuestionContent: buttonQuestionContent,
modelQuestionInitialData: modelQuestionInitialData,
} );
});
//Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(buttonQuestionContent, modelQuestionInitialData);
expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(
buttonQuestionContent,
modelQuestionInitialData
);
done();
});
});
@ -33,12 +68,21 @@ describe("vehicle-model.vue", () => {
test("Page header is initailized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select a model to get started";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText });
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText);
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
@ -49,13 +93,22 @@ describe("vehicle-model.vue", () => {
const SiteHeaderWidget = {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget});
};
const { wrapper, apiPromise } = setupMocks({
SiteHeaderWidget: SiteHeaderWidget,
});
//Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget);
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done();
});
});
@ -66,13 +119,22 @@ describe("vehicle-model.vue", () => {
const VehicleBannerWidget = {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget});
};
const { wrapper, apiPromise } = setupMocks({
VehicleBannerWidget: VehicleBannerWidget,
});
//Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done();
});
});
@ -80,16 +142,21 @@ describe("vehicle-model.vue", () => {
describe("vehicle-model.vue", () => {
test("BackButtonAction triggers a router.navigate change", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks( {
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a model to get started",
mountOptionsMockData: {
router: {
navigate: jest.fn()
navigate: jest.fn(),
},
},
});
//Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
await nextTick();
//Assert
@ -99,6 +166,54 @@ describe("vehicle-model.vue", () => {
});
});
});
describe("vehicle-model.vue", () => {
test("Make set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("vehicle-model.vue", () => {
test("Year set, call invalidation, style, carId, category should be null", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_AND_DEPS)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_AND_DEPS)
});
});
function setupMocks({
buttonQuestionContent = {},
modelQuestionInitialData = {},
@ -110,21 +225,22 @@ function setupMocks({
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleModelQuestion: buttonQuestionContent,
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",
},
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",
},
},
modelQuestionInitialData: modelQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock model question methods
modelQuestion.methods = {
loadInitialData: jest.fn(),
@ -141,13 +257,24 @@ function setupMocks({
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleModel, mountOptions);
const modelQuestionWrapper = wrapper.findComponent({ name: "modelQuestion" });
modelQuestionWrapper.vm.initializeComponent = modelQuestion.methods.initializeComponent;
modelQuestionWrapper.vm.initializeComponent =
modelQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent;
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;
vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise };
}
}

View file

@ -3,7 +3,7 @@
<funnelHeader ref="funnelHeader" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
<funnelSubHeader
ref="funnelSubHeader"
:hasBackButton="true"
@ -24,12 +24,16 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default {
name: "vehicle-model",
data() {
return {
selectedModel: null,
selectedModel: String,
};
},
computed: {},
@ -75,14 +79,30 @@ export default {
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.make !== null;
},
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_AND_DEPS);
store.dispatch(storeActions.RESET_REGISTRATION_AND_DEPS);
},
},
watch: {
selectedModel(model) {
this.$store.commit(this.storeMutations.UPDATE_MODEL, model);
this.$router.navigate(
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_MODEL,
this.$route
);

View file

@ -2,11 +2,16 @@ import styleQuestion from "@/layouts/vehicle-style/style-question/style-question
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("style-question.vue", () => {
test("Selected style is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "2 Door" });
const styleToSelect = "4 Door";
@ -16,51 +21,67 @@ describe("style-question.vue", () => {
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{selectedStyle: "4 Door"}]);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([
{ selectedStyle: "4 Door" },
]);
});
});
describe("style-question.vue", () => {
test("CMS question text is used as radio question text.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What style is your vehicle?" });
const { wrapper, cmsContent } = setupMocks({
cmsQuestionText: "What style is your vehicle?",
});
//Act
styleQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null);
styleQuestion.methods.initializeComponent.call(
wrapper.vm,
cmsContent,
null
);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("questiontext")).toBe("What style is your vehicle?");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("questiontext")).toBe(
"What style is your vehicle?"
);
});
});
describe("style-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["2 Door", "4 Door"] });
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["2 Door", "4 Door"],
});
//Act
const initialData = styleQuestion.methods.loadInitialData.call(wrapper.vm);
styleQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData);
styleQuestion.methods.initializeComponent.call(
wrapper.vm,
cmsContent,
initialData
);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe("2 Door,4 Door");
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc'} };
store.getters = { vehicle: { year: 2019, make: "honda", model: "civc" } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
@ -76,7 +97,7 @@ function setupMocks({
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText
};
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}
}

View file

@ -1,12 +1,14 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="styles"
groupName="Choose Vehicle Style"
textPosition="text-start"
:loaderEnabled="true"
v-model="modelValue"
v-model="selectedValueAsArray"
isRequired=true
/>
</template>
@ -23,27 +25,41 @@ export default {
return {
questionText: null,
styles: Array,
}
};
},
props: {
modelValue: String,
},
computed: {
selectedValueAsArray: {
get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : null;
return modelValueAsArray;
},
set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
this.$emit("update:modelValue", newValueAsScalar);
}
}
},
components: {
buttonQuestion,
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_VEHICLE_STYLES, {year: store.getters.vehicle.year, make: store.getters.vehicle.make, model: store.getters.vehicle.model});
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_VEHICLE_STYLES,
{
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
}
);
},
initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText;
this.styles = initialData;
}
},
watch: {
modelValue(val) {
this.$emit("update:modelValue", val);
}
},
},
};
</script>

View file

@ -1,36 +1,63 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
// Components
import vehicleStyle from "@/layouts/vehicle-style/vehicle-style.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import styleQuestion from "@/layouts/vehicle-style/style-question/style-question";
// Supporting files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
// 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(),
}));
// Mock Store
jest.mock("@/store", () => ({
getters: {
vehicle: {
model: "TL",
},
},
}));
describe("vehicle-style.vue", () => {
test("Style question component is initized with api data", async (done) => {
//Arrange
const vehicleStyleQuestionCmsContent = { QuestionText: "What style is your vehicle?" };
const vehicleStyleQuestionCmsContent = {
QuestionText: "What style is your vehicle?",
};
const styleQuestionInitialData = ["2 Door", "4 Door"];
const { wrapper, apiPromise } = setupMocks( {
vehicleStyleQuestionCmsContent: vehicleStyleQuestionCmsContent,
const { wrapper, apiPromise } = setupMocks({
vehicleStyleQuestionCmsContent: vehicleStyleQuestionCmsContent,
styleQuestionInitialData: styleQuestionInitialData,
} );
});
//Act
vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm));
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(styleQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleStyleQuestionCmsContent, styleQuestionInitialData);
expect(styleQuestion.methods.initializeComponent).toHaveBeenCalledWith(
vehicleStyleQuestionCmsContent,
styleQuestionInitialData
);
done();
});
});
@ -38,17 +65,25 @@ describe("vehicle-style.vue", () => {
describe("vehicle-style.vue", () => {
test("Page header is initailized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select a style to get started";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText });
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm));
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText);
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
@ -56,20 +91,28 @@ describe("vehicle-style.vue", () => {
describe("vehicle-style.vue", () => {
test("Page logo image is initailized with api data", async (done) => {
//Arrange
const SiteHeaderWidget = {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget});
};
const { wrapper, apiPromise } = setupMocks({
SiteHeaderWidget: SiteHeaderWidget,
});
//Act
vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm));
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget);
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done();
});
});
@ -77,20 +120,28 @@ describe("vehicle-style.vue", () => {
describe("vehicle-style.vue", () => {
test("Vehicle image is initailized with api data", async (done) => {
//Arrange
const VehicleBannerWidget = {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget});
};
const { wrapper, apiPromise } = setupMocks({
VehicleBannerWidget: VehicleBannerWidget,
});
//Act
vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm));
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done();
});
});
@ -98,19 +149,23 @@ describe("vehicle-style.vue", () => {
describe("vehicle-style.vue", () => {
test("BackButtonAction triggers a router.navigate change", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks( {
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a style to get started",
mountOptionsMockData: {
router: {
navigate: jest.fn()
navigate: jest.fn(),
},
},
});
//Act
vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm));
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
await nextTick();
@ -124,9 +179,8 @@ describe("vehicle-style.vue", () => {
describe("vehicle-style.vue", () => {
test("selectVehicle triggers a dispatchNonBlockingStoreAction commit", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks( {
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a style to get started",
mountOptionsMockData: {
store: {
@ -134,23 +188,28 @@ describe("vehicle-style.vue", () => {
getters: {
vehicle: {
year: 2020,
make: 'Honda',
model: 'Civic',
style: '2 Door',
}
}
make: "Honda",
model: "Civic",
style: "2 Door",
},
},
},
actionList: [
{
actionName: storeActions.SET_VEHICLE,
data: 'mockData'
}
]
data: "mockData",
},
],
},
});
});
//Act
vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm));
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setVehicle();
await nextTick();
@ -162,58 +221,92 @@ describe("vehicle-style.vue", () => {
});
});
describe("vehicle-style.vue", () => {
test("Model set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
function setupMocks({
vehicleStyleQuestionCmsContent = {},
styleQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleStyleQuestion: vehicleStyleQuestionCmsContent,
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",
},
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",
},
},
styleQuestionInitialData: styleQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock style question methods
styleQuestion.methods = {
loadInitialData: jest.fn(),
initializeComponent: jest.fn(),
};
funnelHeader.methods = {
initializeComponent: jest.fn(),
};
vehicleBanner.methods = {
initializeComponent: jest.fn(),
};
funnelSubHeader.methods = {
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleStyle, mountOptions);
const styleQuestionWrapper = wrapper.findComponent({ name: "styleQuestion" });
styleQuestionWrapper.vm.initializeComponent = styleQuestion.methods.initializeComponent;
styleQuestionWrapper.vm.initializeComponent =
styleQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent;
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;
vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise };
}
}

View file

@ -3,7 +3,7 @@
<funnelHeader ref="funnelHeader" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
<funnelSubHeader
ref="funnelSubHeader"
:hasBackButton="true"
@ -25,11 +25,14 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
export default {
name: "vehicle-style",
data() {
return {
selectedStyle: null,
selectedStyle: String,
};
},
computed: {},
@ -75,20 +78,42 @@ export default {
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
setVehicle() {
return this.dispatchNonBlockingStoreAction(this.storeActions.SET_VEHICLE, {year: this.$store.getters.vehicle.year, make: this.$store.getters.vehicle.make, model: this.$store.getters.vehicle.model, style: this.$store.getters.vehicle.style});
}
return this.dispatchNonBlockingStoreAction(
this.storeActions.SET_VEHICLE,
{
year: this.$store.getters.vehicle.year,
make: this.$store.getters.vehicle.make,
model: this.$store.getters.vehicle.model,
style: this.$store.getters.vehicle.style,
}
);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.model !== null;
},
resetDependentState() {
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_AND_DEPS);
store.dispatch(storeActions.RESET_REGISTRATION_AND_DEPS);
},
},
watch: {
selectedStyle(style) {
this.$store.commit(this.storeMutations.UPDATE_STYLE, style);
this.setVehicle().then(() => {
this.$router.navigate(this.navigationScenarios.SELECTED_STYLE, this.$route);
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_STYLE,
this.$route
);
});
}
},
},
components: {

View file

@ -1,35 +1,61 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue";
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import store from "@/store";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
}));
// 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("vehicle-year.vue", () => {
test("Year question component is initized with api data", async (done) => {
//Arrange
const vehicleYearQuestionCmsContent = { QuestionText: "What year is your vehicle?" };
const vehicleYearQuestionCmsContent = {
QuestionText: "What year is your vehicle?",
};
const yearQuestionInitialData = ["2023", "2022", "2021"];
const { wrapper, apiPromise } = setupMocks( {
vehicleYearQuestionCmsContent: vehicleYearQuestionCmsContent,
const { wrapper, apiPromise } = setupMocks({
vehicleYearQuestionCmsContent: vehicleYearQuestionCmsContent,
yearQuestionInitialData: yearQuestionInitialData,
} );
});
//Act
vehicleYear.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-year" } }, undefined, (c) => c(wrapper.vm));
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-year" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleYearQuestionCmsContent, yearQuestionInitialData);
expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(
vehicleYearQuestionCmsContent,
yearQuestionInitialData
);
done();
});
});
@ -37,20 +63,28 @@ describe("vehicle-year.vue", () => {
describe("vehicle-year.vue", () => {
test("Page logo image is initailized with api data", async (done) => {
//Arrange
const SiteHeaderWidget = {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget});
};
const { wrapper, apiPromise } = setupMocks({
SiteHeaderWidget: SiteHeaderWidget,
});
//Act
vehicleYear.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-year" } }, undefined, (c) => c(wrapper.vm));
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-year" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget);
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done();
});
});
@ -58,52 +92,108 @@ describe("vehicle-year.vue", () => {
describe("vehicle-year.vue", () => {
test("Vehicle image is initailized with api data", async (done) => {
//Arrange
const VehicleBannerWidget = {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget});
};
const { wrapper, apiPromise } = setupMocks({
VehicleBannerWidget: VehicleBannerWidget,
});
//Act
vehicleYear.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-year" } }, undefined, (c) => c(wrapper.vm));
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-year" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done();
});
});
});
describe("vehicle-year.vue", () => {
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("vehicle-year.vue", () => {
test("Year set, call invalidation, make, model, style, carId, category should be null", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-year" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_MAKE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_AND_DEPS)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_AND_DEPS)
});
});
function setupMocks({
vehicleYearQuestionCmsContent = {},
yearQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: { HeaderText: pageHeaderWidgetHeaderText },
VehicleYearQuestion: vehicleYearQuestionCmsContent,
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",
},
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",
},
},
yearQuestionInitialData: yearQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock year question methods
yearQuestion.methods = {
@ -119,16 +209,23 @@ function setupMocks({
funnelSubHeader.methods = {
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleYear, mountOptions);
const yearQuestionWrapper = wrapper.findComponent({ name: "yearQuestion" });
yearQuestionWrapper.vm.initializeComponent = yearQuestion.methods.initializeComponent;
yearQuestionWrapper.vm.initializeComponent =
yearQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent;
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;
vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise };
}
}

View file

@ -3,10 +3,8 @@
<funnelHeader ref="funnelHeader" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" />
<funnelSubHeader
ref="funnelSubHeader"
/>
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
<funnelSubHeader ref="funnelSubHeader" />
<yearQuestion v-model="selectedYear" ref="yearQuestion" />
</div>
</div>
@ -22,13 +20,15 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import eventBus from "@/helpers/event-bus/event-bus";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
export default {
name: "vehicle-year",
data() {
return {
selectedYear: null,
selectedYear: String,
};
},
computed: {},
@ -73,13 +73,32 @@ export default {
watch: {
selectedYear(year) {
this.$store.commit(this.storeMutations.UPDATE_YEAR, year);
this.$router.navigate(
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_YEAR,
this.$route
);
},
},
methods: {
arePagePrerequisitesValid() {
return true;
},
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_MAKE, null);
store.commit(storeMutations.UPDATE_MODEL, null);
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_AND_DEPS);
store.dispatch(storeActions.RESET_REGISTRATION_AND_DEPS);
}
},
components: {
yearQuestion,
funnelHeader,

View file

@ -2,11 +2,16 @@ import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("year-question.vue", () => {
test("Selected year is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "2020" });
const yearToSelect = "2021";
@ -16,48 +21,62 @@ describe("year-question.vue", () => {
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{modelValue: "2021"}]);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([
{ modelValue: "2021" },
]);
});
});
describe("year-question.vue", () => {
test("CMS question text is used as radio question text.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What year is your vehicle?" });
const { wrapper, cmsContent } = setupMocks({
cmsQuestionText: "What year is your vehicle?",
});
//Act
yearQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("questiontext")).toBe("What year is your vehicle?");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("questiontext")).toBe(
"What year is your vehicle?"
);
});
});
describe("year-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["2023", "2022", "2021"] });
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["2023", "2022", "2021"],
});
//Act
const initialData = yearQuestion.methods.loadInitialData.call(wrapper.vm);
yearQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData);
yearQuestion.methods.initializeComponent.call(
wrapper.vm,
cmsContent,
initialData
);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" });
expect(buttonQuestionComponent.attributes("answers")).toBe("2023,2022,2021");
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe(
"2023,2022,2021"
);
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
const mountOptions = getMountOptions({
@ -74,7 +93,7 @@ function setupMocks({
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText
};
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}
}

View file

@ -1,12 +1,14 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="years"
groupName="Choose Vehicle Year"
textPosition="text-start"
:loaderEnabled="true"
v-model="modelValue"
v-model="selectedValueAsArray"
isRequired=true
/>
</template>
@ -21,27 +23,38 @@ export default {
return {
questionText: null,
years: Array,
}
};
},
props: {
modelValue: String,
},
emits: ['update:modelValue'],
components: {
buttonQuestion,
},
computed: {
selectedValueAsArray: {
get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : null;
return modelValueAsArray;
},
set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null;
this.$emit("update:modelValue", newValueAsScalar);
}
}
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_VEHICLE_YEARS, {});
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_VEHICLE_YEARS,
{}
);
},
initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText;
this.years = initialData;
}
},
watch: {
modelValue(val) {
this.$emit("update:modelValue", val);
}
},
},
};
</script>
</script>

View file

@ -14,5 +14,4 @@ vueApp.use(store);
vueApp.use(LoadScript);
vueApp.mixin(baseMixin);
vueApp.mount("#app");

View file

@ -3,11 +3,11 @@ import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { widgetNames } from "@/constants/widget-names.js";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
export default {
data() {
return {
};
return {};
},
methods: {
dispatchNonBlockingStoreAction(type, payload, encodePayload = true) {
@ -32,6 +32,9 @@ export default {
widgetNames() {
return widgetNames;
},
vehicleCategories() {
return vehicleCategories;
},
},
};

View file

@ -1,12 +1,14 @@
import baseMixin from "@/mixins/base-mixin";
import { storeActions } from "@/constants/store-actions.js";
import { widgetNames } from "@/constants/widget-names.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import store from "@/store";
describe("baseMixin.js", () => {
test("dispatchNonblockingStoreAction: calls dispatch with type and payload", () => {
const mixIn = getMixInInstance({});
const type = '';
const type = "";
const payload = {};
mixIn.methods.dispatchNonBlockingStoreAction(type, payload);
@ -16,14 +18,59 @@ describe("baseMixin.js", () => {
test("dispatchNonblockingStoreAction: calls dispatch with type and payload, handles Uri encode", () => {
const mixIn = getMixInInstance({});
const type = '';
const type = "";
const payload = { make: "Alfa Romeo/Chrysler" };
mixIn.methods.dispatchNonBlockingStoreAction(type, payload, true);
expect(store.dispatch).toBeCalledWith(type, payload);
});
test("computed: storeActions should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
// Act
let storeActionsForTest = mixIn.computed.storeActions();
// Assert
expect(storeActionsForTest).toEqual(storeActions);
});
test("computed: storeMutations should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
// Act
let storeMutationsForTest = mixIn.computed.storeMutations();
// Assert
expect(storeMutationsForTest).toEqual(storeMutations);
});
test("computed: navigationScenarios should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
// Act
let navigationScenariosForTest = mixIn.computed.navigationScenarios();
// Assert
expect(navigationScenariosForTest).toEqual(navigationScenarios);
});
test("computed: widgetNames should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
// Act
let widgetNamesForTest = mixIn.computed.widgetNames();
// Assert
expect(widgetNamesForTest).toEqual(widgetNames);
});
});
function getMixInInstance({ isDispatchSuccess = true }) {
// Mock Store
const storeDispatch = jest.fn();

View file

@ -35,16 +35,26 @@ const routes = [
},
{
path: "/",
name: "root",
async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string.
if (to.query.fmgPage === undefined) {
await GoToFunnelStartOn404(next);
} else {
try {
// If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) {
return next({ name: to.query.fmgPage, query: to.query });
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
const component = router
.getRoutes()
.filter((x) => x.name === to.query.fmgPage)[0].components;
if (!arePagePrerequisitesValid(component)) {
await GoToFunnelStartOn404(next);
}
return next({ name: to.query.fmgPage, query: to.query, params: to.params });
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
@ -57,16 +67,29 @@ const routes = [
component: routeData[0].component,
});
// Assign current query string parameters, as well as our fmgPage one.
next({ name: routeData[0].name, query: Object.assign(to.query, { fmgPage: routeData[0].name }) });
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router
.getRoutes()
.filter((x) => x.name === routeData[0].name)[0]
.components.default();
// if (!arePagePrerequisitesValid(nextComponent)) {
// await GoToFunnelStartOn404(next);
// }
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { fmgPage: routeData[0].name }),
params: to.params
});
} catch (error) {
console.log(error);
// If we don't have a route, go to our 404 page.
await GoToFunnelStartOn404(next);
}
}
},
},
@ -79,25 +102,38 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}) => {
navigate(scenario, currentRoute, false, optionalQuery, optionalParams);
}
router.navigateAfterSave = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}) => {
navigate(scenario, currentRoute, true, optionalQuery, optionalParams);
}
// PRIVATE FUNCTIONS
// Navigate to the next route, depending on the scenario.
router.navigate = (
scenario,
currentRoute,
optionalQuery = {},
optionalParams = {}
) => {
function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, optionalParams = {}) {
if (!scenario) {
console.error("No scenario provided. Please review the routing table.");
return;
}
// Match our maps up and navigate if we have a destination.
const matchingScenarioMap = router.getNavigationMap(scenario, currentRoute);
const matchingScenarioMap = getNavigationMap(scenario, currentRoute);
if (matchingScenarioMap.destinationFmgPageValue !== undefined) {
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one.
// If we need to do invalidation
const currentComponent = currentRoute.matched[0].components;
if (invalidateOnSave) {
resetDependentState(currentComponent);
}
router.push({
path: "/",
name: "root",
query: Object.assign(optionalQuery, {
fmgPage: matchingScenarioMap.destinationFmgPageValue,
}),
@ -106,10 +142,10 @@ router.navigate = (
} else if (matchingScenarioMap.destinationUrl !== undefined) {
navigateToUrl(matchingScenarioMap.destinationUrl);
}
};
}
// Get navigation map depending on the scenario and the current 'page' you're on.
router.getNavigationMap = (scenario, currentRoute) => {
function getNavigationMap(scenario, currentRoute) {
const fmgPageValue = currentRoute.query.fmgPage;
const matchedQueryValue = routingTable
.filter(
@ -120,7 +156,7 @@ router.getNavigationMap = (scenario, currentRoute) => {
.map((m) => m.maps.filter((map) => map.scenario === scenario));
return matchedQueryValue[0][0];
};
}
//---------------------------------------------------------- Private Functions ----------------------------------------------------------
@ -133,7 +169,9 @@ function navigateToUrl(url) {
// Get route information by page name.
// This will reach out to the Cms and there is a 1:1 relationship between page names and route names.
async function GetRouteInfoFromPageName(pageName) {
const response = await store.dispatch(storeActions.GET_ROUTE_INFO_ACTION, { pageName: pageName });
const response = await store.dispatch(storeActions.GET_ROUTE_INFO_ACTION, {
pageName: pageName,
});
const jsonFromResponse = JSON.parse(response.data.Result);
let routeData = [];
@ -151,7 +189,6 @@ async function GetRouteInfoFromPageName(pageName) {
// Go to our start page on a 404.
async function GoToFunnelStartOn404(next) {
const apiResponse = await store.dispatch(storeActions.GET_HOMEPAGE_NAME);
const homepageName = apiResponse.data.Result;
@ -161,16 +198,26 @@ async function GoToFunnelStartOn404(next) {
globalEvents.SubCategories.PAGE_NOT_FOUND,
{
isDismissible: true,
messageCopy: 'You can get a quote by starting on this page.',
messageHeadline: 'We\'re sorry, something went wrong.',
type: globalEventTypes.Danger
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: globalEventTypes.Danger,
}
);
next({
path: '/',
path: "/",
query: { fmgPage: homepageName },
});
}
// Checks arePagePrerequisitesValid on the component passed in.
function arePagePrerequisitesValid(component) {
return component.default.methods.arePagePrerequisitesValid();
}
// Reset dependant state on route change.
function resetDependentState(component) {
return component.default.methods.resetDependentState();
}
export default router;

View file

@ -3,7 +3,6 @@ const navigationScenarios = {
SELECTED_MODEL: "SELECTED_MODEL",
SELECTED_MAKE: "SELECTED_MAKE",
SELECTED_STYLE: "SELECTED_STYLE",
VEHICLE_DAMAGE: "VEHICLE_DAMAGE",
CLICKED_BACK: "CLICKED_BACK",
};

View file

@ -21,7 +21,7 @@ const routingTable = [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR,
}
},
],
},
{
@ -34,7 +34,7 @@ const routingTable = [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
}
},
],
},
{
@ -47,9 +47,9 @@ const routingTable = [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL,
}
},
],
},
];
export { routingTable };
export { routingTable };

View file

@ -1,9 +1,245 @@
import { createStore } from "vuex";
import { endpoints } from "@/constants/endpoints.js";
import { storeMutations } from "@/constants/store-mutations"
import { storeMutations } from "@/constants/store-mutations";
import createPersistedState from "vuex-persistedstate";
import globalMethods from "@/global-methods";
import eventBus from "../helpers/event-bus/event-bus";
// Export State
export const state = {
order: {
vehicle: {
year: null,
make: null,
model: null,
style: null,
carId: null,
category: null,
imageUrl: null,
imageVifNumber: null,
imageColor: null,
},
damage: {
isRepair: null,
numberOfChips: null,
glassToReplace: null,
},
},
applicationUser: {
eventBus: [],
},
}
// Export Mutations
export const mutations = {
// VEHICLE MUTATIONS
updateYear(state, year) {
state.order.vehicle.year = year;
},
updateMake(state, make) {
state.order.vehicle.make = make;
},
updateModel(state, model) {
state.order.vehicle.model = model;
},
updateStyle(state, style) {
state.order.vehicle.style = style;
},
updateCarId(state, carId) {
state.order.vehicle.carId = carId;
},
updateVehicleCategory(state, category) {
state.order.vehicle.category = category;
},
updateVehicleImageUrl(state, imageUrl) {
state.order.vehicle.imageUrl = imageUrl;
},
updateVehicleImageVifNumber(state, imageVifNumber) {
state.order.vehicle.imageVifNumber = imageVifNumber;
},
updateVehicleImageColor(state, imageColor) {
state.order.vehicle.imageColor = imageColor;
},
// EVENT BUS MUTATIONS
addEventToBus(state, event) {
state.applicationUser.eventBus.push(event);
},
removeEventFromBus(state, eventData) {
const matchedEvent = state.applicationUser.eventBus.find(
({ category, subCategory }) =>
category === eventData.category &&
subCategory === eventData.subCategory
);
const itemIndex = state.applicationUser.eventBus.indexOf(matchedEvent);
// If the item exists, remove it.
if (itemIndex > -1) {
state.applicationUser.eventBus.splice(itemIndex, 1);
}
},
// DEPENDENCY MUTATIONS
resetVehicleAndDependencies(state) {
state.order.vehicle.year = null;
state.order.vehicle.make = null;
state.order.vehicle.model = null;
state.order.vehicle.style = null;
state.order.vehicle.carId = null;
state.order.vehicle.category = null;
},
resetDamageAndDependencies(state) {
state.order.damage.isRepair = null;
state.order.damage.numberOfChips = null;
state.order.damage.windshieldGlassToReplace = null;
state.order.damage.driverSideGlassToReplace = null;
state.order.damage.passengerSideGlassToReplace = null;
state.order.damage.rearGlassToReplace = null;
},
resetRegistrationAndDependencies(state) {
},
resetPartsAndDependencies(state) {
}
}
// Export Getters
export const getters = {
vehicle: (state) => state.order.vehicle,
eventBusItem: (state) => (eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(
({ category, subCategory }) =>
category === eventCategory && subCategory === eventSubCategory
);
return matchedEvent !== undefined ? matchedEvent.eventValue : undefined;
},
eventBus: (state) => state.applicationUser.eventBus,
}
// Export Actions
export const actions = {
// Vehicle API Actions
getVehicleYears(context) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleYears.method,
endpoint: endpoints.GetVehicleYears.url,
payload: {},
});
},
lookupVehicleByYmms(context, { year, make, model, style }) {
return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByYmms.method,
endpoint: `${endpoints.LookupVehicleByYmms.url}/${year}/${make}/${model}/${style}`,
payload: {},
});
},
lookupVehicleByVin(context, { vin }) {
return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByVin.method,
endpoint: endpoints.LookupVehicleByVin.url,
payload: {
vin: vin, // EX "1J4GW58S4XC541166"
},
});
},
getVehicleMakes(context, { year }) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleMakes.method,
endpoint: `${endpoints.GetVehicleMakes.url}/${year}`,
payload: {},
});
},
getVehicleModels(context, { year, make }) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleModels.method,
endpoint: `${endpoints.GetVehicleModels.url}/${year}/${make}`,
payload: {},
});
},
getVehicleStyles(context, { year, make, model }) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleStyles.method,
endpoint: `${endpoints.GetVehicleStyles.url}/${year}/${make}/${model}`,
payload: {},
});
},
setVehicle(context, { year, make, model, style }) {
return globalMethods
.callHttpClient({
methods: endpoints.GetVehicle.method,
endpoint: `${endpoints.GetVehicle.url}/${year}/${make}/${model}/${style}`,
payload: {},
})
.then((response) => {
context.commit(storeMutations.UPDATE_CAR_ID, response.data.carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, response.data.category);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, response.data.imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, response.data.imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, response.data.imageVifColor);
return response;
});
},
getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
});
},
// DEPENDENCY ACTIONS
resetVehicleAndDependencies(context) {
context.commit(storeMutations.RESET_VEHICLE_AND_DEPS);
context.commit(storeMutations.RESET_DAMAGE_AND_DEPS);
context.commit(storeMutations.RESET_REGISTRATION_AND_DEPS);
},
resetDamageAndDependencies(context) {
context.commit(storeMutations.RESET_DAMAGE_AND_DEPS);
context.commit(storeMutations.RESET_PARTS_AND_DEPS);
},
resetRegistrationAndDependencies(context) {
context.commit(storeMutations.RESET_REGISTRATION_AND_DEPS);
context.commit(storeMutations.RESET_PARTS_AND_DEPS)
},
resetPartsAndDependencies(context) {
context.commit(storeMutations.RESET_PARTS_AND_DEPS);
},
// Content API Actions
getRouteInfo(context, { pageName }) {
return globalMethods.callHttpClient({
method: endpoints.GetRouteInfo.method,
endpoint: endpoints.GetRouteInfo.url,
payload: {
pageName: pageName,
},
});
},
getHomepageName(context) {
return globalMethods.callHttpClient({
method: endpoints.GetHomepageInfo.method,
endpoint: endpoints.GetHomepageInfo.url,
});
},
getPageData(context, { pageName }) {
return globalMethods.callHttpClient({
method: endpoints.GetPageData.method,
endpoint: `${endpoints.GetPageData.url}/${pageName}`,
payload: {},
});
},
getEvoxImage(context, { relativeUrl }) {
return globalMethods.callHttpClient({
method: endpoints.GetPageData.method,
endpoint: relativeUrl,
payload: {},
});
},
}
export default createStore({
plugins: [createPersistedState()],
@ -12,186 +248,8 @@ export default createStore({
// * The CMS can reference the fields by name
// * Return users may have a previous "version" of the model, and we don't want
// them to have a breaking experience, because the model might have changed.
state: {
order: {
vehicle: {
year: null,
make: null,
model: null,
style: null,
vin: null,
licensePlate: null,
carId: null,
category: null,
evoxImageId: null,
hasSplitWindshieldOption: null,
hasBackglassSliderOption: null,
registration: {
rawAddress: null,
zipCode: null,
firstName: null,
lastName: null,
licensePlate: null,
},
damage: {
isRepair: null,
isReplacement: null,
numberOfChips: null,
glassToReplace: null,
problemGlassQuestionAnswers: null,
problemMoldingQuestionAnswers: null,
problemPropertyQuestionAnswers: null,
},
lineItems: null,
customer: {
emailAddress: null,
},
payment: {
isInsurance: null,
isCash: null,
},
referralSeqNum: null,
},
},
applicationUser: {
experiments: null,
eventBus: []
},
},
// See IMPORTANT note at top of "state" declaration.
mutations: {
updateYear(state, year) {
state.order.vehicle.year = year;
},
updateMake(state, make) {
state.order.vehicle.make = make;
},
updateModel(state, model) {
state.order.vehicle.model = model;
},
updateStyle(state, style) {
state.order.vehicle.style = style;
},
updateVehicle(state, data) {
state.order.vehicle.carId = data.carId;
state.order.vehicle.category = data.category;
},
addEventToBus(state, event) {
state.applicationUser.eventBus.push(event);
},
removeEventFromBus(state, eventData) {
const matchedEvent = state.applicationUser.eventBus.find(({ category, subCategory }) => category === eventData.category && subCategory === eventData.subCategory);
const itemIndex = state.applicationUser.eventBus.indexOf(matchedEvent);
// If the item exists, remove it.
if (itemIndex > -1) {
state.applicationUser.eventBus.splice(itemIndex, 1);
}
}
},
getters: {
vehicle: state => state.order.vehicle,
eventBusItem: (state) => (eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(({ category, subCategory }) => category === eventCategory && subCategory === eventSubCategory);
return matchedEvent !== undefined ? matchedEvent.eventValue : undefined;
},
eventBus: state => state.applicationUser.eventBus
},
actions: {
// Vehicle API Actions
getVehicleYears(context) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleYears.method,
endpoint: endpoints.GetVehicleYears.url,
payload: {},
});
},
lookupVehicleByYmms(context, { year, make, model, style }) {
return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByYmms.method,
endpoint: `${endpoints.LookupVehicleByYmms.url}/${year}/${make}/${model}/${style}`,
payload: {},
});
},
lookupVehicleByVin(context, { vin }) {
return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByVin.method,
endpoint: endpoints.LookupVehicleByVin.url,
payload: {
vin: vin, // EX "1J4GW58S4XC541166"
},
});
},
getVehicleMakes(context, { year }) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleMakes.method,
endpoint: `${endpoints.GetVehicleMakes.url}/${year}`,
payload: {},
});
},
getVehicleModels(context, { year, make }) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleModels.method,
endpoint: `${endpoints.GetVehicleModels.url}/${year}/${make}`,
payload: {},
});
},
getVehicleStyles(context, { year, make, model }) {
return globalMethods.callHttpClient({
method: endpoints.GetVehicleStyles.method,
endpoint: `${endpoints.GetVehicleStyles.url}/${year}/${make}/${model}`,
payload: {},
});
},
setVehicle(context, {year, make, model, style}) {
return globalMethods.callHttpClient({
methods: endpoints.GetVehicle.method,
endpoint: `${endpoints.GetVehicle.url}/${year}/${make}/${model}/${style}`,
payload: {},
}).then(response => {
context.commit(storeMutations.UPDATE_VEHICLE, response.data);
return response;
});
},
getDamageOptions(context, {carId}){
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
})
},
// Content API Actions
getRouteInfo(context, { pageName }) {
return globalMethods.callHttpClient({
method: endpoints.GetRouteInfo.method,
endpoint: endpoints.GetRouteInfo.url,
payload: {
pageName: pageName,
},
});
},
getHomepageName(context) {
return globalMethods.callHttpClient({
method: endpoints.GetHomepageInfo.method,
endpoint: endpoints.GetHomepageInfo.url,
});
},
getPageData(context, { pageName }) {
return globalMethods.callHttpClient({
method: endpoints.GetPageData.method,
endpoint: `${endpoints.GetPageData.url}/${pageName}`,
payload: {},
});
},
getEvoxImage(context, { relativeUrl }) {
return globalMethods.callMockHttpClient({
method: endpoints.GetPageData.method,
endpoint: relativeUrl,
payload: {},
});
},
},
state,
mutations,
getters,
actions,
});

View file

@ -1,303 +1,483 @@
import store from "./index";
import globalMethods from "@/global-methods";
import { mutations, state, actions, getters } from "@/store";
import { storeMutations } from "@/constants/store-mutations";
describe("Actions", () => {
it("Should return list of years retrieved", async () => {
// Mock global method
globalMethods.callHttpClient = jest.fn();
describe("Mutations", () => {
it("Updates vehicle year in state", () => {
// Arrange
let years = [];
const storeState = state;
// Act
mutations.updateYear(storeState, "2019");
// Assert
expect(storeState.order.vehicle.year).toEqual("2019");
});
it("Updates vehicle make in state", () => {
// Arrange
const storeState = state;
// Act
mutations.updateMake(storeState, "Acura");
// Assert
expect(storeState.order.vehicle.make).toEqual("Acura");
});
it("Updates vehicle model in state", () => {
// Arrange
const storeState = state;
// Act
mutations.updateModel(storeState, "ILX");
// Assert
expect(storeState.order.vehicle.model).toEqual("ILX");
});
it("Updates vehicle style in state", () => {
// Arrange
const storeState = state;
// Act
mutations.updateStyle(storeState, "4 DOOR SEDAN");
// Assert
expect(storeState.order.vehicle.style).toEqual("4 DOOR SEDAN");
});
it("Updates vehicle carId in state", () => {
// Arrange
const storeState = state;
// Act
mutations.updateCarId(storeState, "C0000001");
// Assert
expect(storeState.order.vehicle.carId).toEqual("C0000001");
});
it("Updates vehicle vehicle category in state", () => {
// Arrange
const storeState = state;
// Act
mutations.updateVehicleCategory(storeState, "CAR");
// Assert
expect(storeState.order.vehicle.category).toEqual("CAR");
});
it("Remove item to eventBus in state", () => {
// Arrange
const storeState = state;
const event = { category: "CategoryOne", subCategory: "SubCategoryOne" }
// Act / Assert
mutations.addEventToBus(storeState, event);
expect(storeState.applicationUser.eventBus).toEqual([event]);
// Act / Assert
mutations.removeEventFromBus(storeState, event);
expect(storeState.applicationUser.eventBus).toEqual([]);
});
it("Adds item to eventBus in state", () => {
// Arrange
const storeState = state;
// Act
mutations.addEventToBus(storeState, { EventOne: "ValueOne" });
// Assert
expect(storeState.applicationUser.eventBus).toEqual([{ EventOne: "ValueOne" }]);
});
it("resetVehicleAndDependencies, should set fields to null", () => {
// Arrange
const storeState = state;
mutations.updateYear(storeState, "2019");
mutations.updateMake(storeState, "Acura");
mutations.updateModel(storeState, "ILX");
mutations.updateStyle(storeState, "4 DOOR SEDAN");
mutations.updateCarId(storeState, "C0000001");
mutations.updateVehicleCategory(storeState, "CAR");
// Expect
expect(storeState.order.vehicle.year).toEqual("2019");
expect(storeState.order.vehicle.make).toEqual("Acura");
expect(storeState.order.vehicle.model).toEqual("ILX");
expect(storeState.order.vehicle.style).toEqual("4 DOOR SEDAN");
expect(storeState.order.vehicle.carId).toEqual("C0000001");
expect(storeState.order.vehicle.category).toEqual("CAR");
// Act
mutations.resetVehicleAndDependencies(storeState);
// Expect
expect(storeState.order.vehicle.year).toEqual(null);
expect(storeState.order.vehicle.make).toEqual(null);
expect(storeState.order.vehicle.model).toEqual(null);
expect(storeState.order.vehicle.style).toEqual(null);
expect(storeState.order.vehicle.carId).toEqual(null);
expect(storeState.order.vehicle.category).toEqual(null);
});
it("resetDamageAndDependencies, should set fields to null", () => {
// Arrange
const storeState = state;
storeState.order.damage = {
isRepair: true,
numberOfChips: 2,
windshieldGlassToReplace: "Front",
driverSideGlassToReplace: "Rear",
passengerSideGlassToReplace: "Rear",
rearGlassToReplace: "Slider"
}
// Expect
expect(storeState.order.damage.isRepair).toEqual(true);
expect(storeState.order.damage.numberOfChips).toEqual(2);
expect(storeState.order.damage.windshieldGlassToReplace).toEqual("Front");
expect(storeState.order.damage.driverSideGlassToReplace).toEqual("Rear");
expect(storeState.order.damage.passengerSideGlassToReplace).toEqual("Rear");
expect(storeState.order.damage.rearGlassToReplace).toEqual("Slider");
// Act
mutations.resetDamageAndDependencies(storeState);
// Expect
expect(storeState.order.damage.isRepair).toEqual(null);
expect(storeState.order.damage.numberOfChips).toEqual(null);
expect(storeState.order.damage.windshieldGlassToReplace).toEqual(null);
expect(storeState.order.damage.driverSideGlassToReplace).toEqual(null);
expect(storeState.order.damage.passengerSideGlassToReplace).toEqual(null);
expect(storeState.order.damage.rearGlassToReplace).toEqual(null);
});
});
describe("Actions", () => {
it("getVehicleYears action, should return years array", async () => {
// Arrange
const context = state;
// Act
globalMethods.callHttpClient = jest.fn();
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: [2023, 2022, 2021] });
});
await store.dispatch("getVehicleYears").then((response) => {
years = response.data;
});
// Assert
expect(years[0]).toBe(2023);
const response = await actions.getVehicleYears(context)
expect(response.data).toEqual([2023, 2022, 2021]);
});
it("Should return list of makes retrieved", async () => {
it("lookupVehicleByYmms action, should return car data", async () => {
// Arrange
let makes = [];
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["Baic", "Honda", "Ford"] });
});
await store.dispatch("getVehicleMakes", { year: 2023 }).then((response) => {
makes = response.data;
return Promise.resolve({ data: { carId: "C00000001" } });
});
// Assert
expect(makes[0]).toBe("Baic");
const response = await actions.lookupVehicleByYmms(context, "2019", "Acura", "ILX", "4 DOOR SEDAN")
expect(response.data).toEqual({ carId: "C00000001" });
});
it("Should return list of models retrieved", async () => {
it("lookupVehicleByVin action, should return car data", async () => {
// Arrange
let models = [];
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["BJ40 (MEX)", "Civic", "Accord"] });
return Promise.resolve({ data: { carId: "C00000001" } });
});
await store
.dispatch("getVehicleModels", { year: 2023, make: "Baic" })
.then((response) => {
models = response.data;
});
// Assert
expect(models[0]).toBe("BJ40 (MEX)");
const response = await actions.lookupVehicleByVin(context, "12345678901234567")
expect(response.data).toEqual({ carId: "C00000001" });
});
it("Should return list of styles retrieved", async () => {
it("getVehicleMakes action, should return makes list", async () => {
// Arrange
let styles = [];
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["4 DOOR UTILITY", "2 DOOR"] });
return Promise.resolve({ data: ["Acura", "Honda"] });
});
await store
.dispatch("getVehicleStyles", {
year: 2023,
make: "Baic",
model: "BJ40 (MEX)",
})
.then((response) => {
styles = response.data;
});
// Assert
expect(styles[0]).toBe("4 DOOR UTILITY");
const response = await actions.getVehicleMakes(context, "2019")
expect(response.data).toEqual(["Acura", "Honda"]);
});
it("Should return list of damage options retrieved", async () => {
it("getVehicleModels action, should return models list", async () => {
// Arrange
let damageOptions = [];
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["Front Window", "Rear Window"] });
return Promise.resolve({ data: ["ILX", "RDX"] });
});
await store
.dispatch("getDamageOptions", {
carId: "CR00070154",
})
.then((response) => {
damageOptions = response.data;
});
// Assert
expect(damageOptions[0]).toBe("Front Window");
const response = await actions.getVehicleModels(context, "2019", "Acura")
expect(response.data).toEqual(["ILX", "RDX"]);
});
it("Should return data from url retrieved", async () => {
it("getVehicleStyles action, should return models list", async () => {
// Arrange
let routeInfo = [];
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "Route Info Data",
},
});
return Promise.resolve({ data: { style: "4 DOOR SEDAN" } });
});
await store
.dispatch("getRouteInfo", { pageName: "vehicle-year" })
.then((response) => {
routeInfo = response.data.Result;
});
// Assert
expect(routeInfo).toBe("Route Info Data");
const response = await actions.getVehicleStyles(context, "2019", "Acura", "ILX")
expect(response.data).toEqual({ style: "4 DOOR SEDAN" });
});
it("Should return page data from url retrieved", async () => {
it("setVehicle action, should get vehicle data and set carId and vehicle category", async () => {
// Arrange
let pageData = [];
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "Page Info Data",
},
});
return Promise.resolve({ data: { carId: "C00000000", category: "CAR" } });
});
await store
.dispatch("getPageData", { pageName: "vehicle-year" })
.then((response) => {
pageData = response.data.Result;
});
// Assert
expect(pageData).toBe("Page Info Data");
const response = await actions.setVehicle(context, "C00000000")
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, "C00000000");
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, "CAR");
expect(response.data).toEqual({ carId: "C00000000", category: "CAR" });
});
it("Should return data from url retrieved", async () => {
it("getDamageOptions action", async () => {
// Arrange
let returnData = [];
const context = state;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "2018 Honda Civic",
},
});
return Promise.resolve({ data: ["Windshield", "DriversFrontDoor"] });
});
await store
.dispatch("lookupVehicleByYmms", { year: "2018", make: "Honda", model: "Civic", style: "2 Door"})
.then((response) => {
returnData = response.data.Result;
});
const response = await actions.getDamageOptions(context, "C00000000")
// Assert
expect(returnData).toBe("2018 Honda Civic");
expect(response.data).toEqual(["Windshield", "DriversFrontDoor"]);
});
it("Should return vehicle data from url retrieved", async () => {
it("resetVehicleAndDependencies action", async () => {
// Arrange
let returnData = [];
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
await actions.resetVehicleAndDependencies(context)
expect(commit).toBeCalledWith(storeMutations.RESET_VEHICLE_AND_DEPS);
expect(commit).toBeCalledWith(storeMutations.RESET_DAMAGE_AND_DEPS);
expect(commit).toBeCalledWith(storeMutations.RESET_REGISTRATION_AND_DEPS);
});
it("resetDamageAndDependencies action", async () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
await actions.resetDamageAndDependencies(context)
expect(commit).toBeCalledWith(storeMutations.RESET_DAMAGE_AND_DEPS);
expect(commit).toBeCalledWith(storeMutations.RESET_PARTS_AND_DEPS);
});
it("resetRegistrationAndDependencies action", async () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
await actions.resetRegistrationAndDependencies(context)
expect(commit).toBeCalledWith(storeMutations.RESET_REGISTRATION_AND_DEPS);
expect(commit).toBeCalledWith(storeMutations.RESET_PARTS_AND_DEPS);
});
it("resetPartsAndDependencies action", async () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
await actions.resetPartsAndDependencies(context)
expect(commit).toBeCalledWith(storeMutations.RESET_PARTS_AND_DEPS);
});
it("getRouteInfo action, returns route info", async () => {
// Arrange
const context = state;
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "2021 Honda Civic",
},
});
return Promise.resolve({ data: { Widget: "Data" } });
});
await store
.dispatch("lookupVehicleByVin", { vin: "12345678"})
.then((response) => {
returnData = response.data.Result;
});
// Assert
expect(returnData).toBe("2021 Honda Civic");
});
it("Should return vehicle image data from url retrieved", async () => {
// Arrange
let returnData = [];
// Act
globalMethods.callMockHttpClient = jest.fn();
globalMethods.callMockHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "2008_honda_civic.jpg",
},
});
const response = await actions.getRouteInfo(context, "vehicle-year")
expect(response.data).toEqual({ Widget: "Data" });
});
it("getHomepageName action, returns homepage name", async () => {
// Arrange
const context = state;
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: { Name: "vehicle-year" } });
});
await store
.dispatch("getEvoxImage", { relativeUrl: "evox_image.com"})
.then((response) => {
returnData = response.data.Result;
});
// Assert
expect(returnData).toBe("2008_honda_civic.jpg");
});
});
describe("Mutations", () => {
it("Should update the year property in the store", () => {
// Act
store.commit("updateYear", 2020);
const response = await actions.getHomepageName(context)
expect(response.data).toEqual({ Name: "vehicle-year" });
// Assert
expect(store.state.order.vehicle.year).toBe(2020);
});
it("Should update the make property in the store", () => {
// Act
store.commit("updateMake", "Honda");
it("getPageData action, returns page data", async () => {
// Assert
expect(store.state.order.vehicle.make).toBe("Honda");
});
it("Should update the model property in the store", () => {
// Act
store.commit("updateModel", "Civic");
// Assert
expect(store.state.order.vehicle.model).toBe("Civic");
});
it("Should update the style property in the store", () => {
// Act
store.commit("updateStyle", "2 Door");
// Assert
expect(store.state.order.vehicle.style).toBe("2 Door");
});
it("Should update the carID and category propertys in the store", () => {
// Arrange
const carData = {
carId: "123abc",
category: "car"
}
// Act
store.commit("updateVehicle", carData);
const context = state;
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: { Results: [{ Widget: "Data" }] } });
});
// Act
const response = await actions.getPageData(context, "vehicle-year")
expect(response.data).toEqual({ Results: [{ Widget: "Data" }] });
// Assert
expect(store.state.order.vehicle.carId).toBe("123abc");
expect(store.state.order.vehicle.category).toBe("car");
});
it("Should add event onto bus and update state", () => {
it("getEvoxImage action, returns image url", async () => {
// Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'};
const context = state;
globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: { imageUrl: "https://test.com" } });
});
// Act
store.commit("addEventToBus", event);
const response = await actions.getEvoxImage(context, { relativeUrl: "https://relativeurl.com" });
//Assert
expect(store.state.applicationUser.eventBus[0].category).toBe('TestCategoryOne');
expect(store.state.applicationUser.eventBus[0].subCategory).toBe('TestSubCategoryOne');
expect(store.state.applicationUser.eventBus[0].eventValue).toBe('TestEventValueOne');
expect(response.data).toEqual({ imageUrl: "https://test.com" });
});
});
describe("Getters", () => {
it("Should validate vehicle getter", () => {
it("Vehicle getter, should return vehicle data", () => {
// Arrange
const vehicle = store.getters.vehicle;
// Assert
expect(typeof vehicle).toBe('object');
});
it("Should get item from bus via getter", () => {
// Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'};
const storeState = state;
// Act
store.commit("addEventToBus", event)
mutations.updateYear(storeState, "2019");
mutations.updateMake(storeState, "Acura");
mutations.updateModel(storeState, "ILX");
// Assert
const returnedEventValue = store.getters.eventBusItem(event.category, event.subCategory);
expect(returnedEventValue).toBe('TestEventValueOne');
expect(getters.vehicle(storeState).year).toEqual("2019");
expect(getters.vehicle(storeState).make).toEqual("Acura");
expect(getters.vehicle(storeState).model).toEqual("ILX");
});
it("Should get eventbus from getter, should have length > 0", () => {
// Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'};
it("Get event bus item by event category and eventSubCategory", () => {
// Arrange
const storeState = state;
const event = { category: "CategoryOne", subCategory: "SubCategoryOne", eventValue: "EventValueOne" };
// Act
store.commit("addEventToBus", event);
// Act
mutations.addEventToBus(storeState, event);
// Assert
//expect(storeState.applicationUser.eventBus).toEqual([event]);
expect(getters.eventBusItem(storeState)(event.category, event.subCategory)).toEqual(event.eventValue);
//Assert
expect(store.getters.eventBus.length).toBeGreaterThan(0);
});
});
it("Get event bus", () => {
// Arrange
const storeState = state;
storeState.applicationUser.eventBus = [];
const event = { category: "CategoryOne", subCategory: "SubCategoryOne", eventValue: "EventValueOne" };
// Act
mutations.addEventToBus(storeState, event);
// Assert
expect(getters.eventBus(storeState)).toEqual([event]);
});
});

View file

@ -1,56 +0,0 @@
//Custom button styles
.btn {
&.btn-primary {
position: relative;
background: $blue-700;
@include blue-gradient;
border: none;
border-radius: $border-radius-lg;
color: $white;
transition: all 150ms linear;
&:hover {
background: linear-gradient(270deg, rgba(6,87,124,1) 0%, rgba(6,87,124,1) 100%);
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible { // Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}
&:disabled {
background: $gray-100 !important;
background: linear-gradient(270deg, $gray-100 0%, $gray-100 100%) !important;
color: $gray !important;
height: 48px;
border: none;
border-radius: $border-radius-lg;
}
}
&.btn-secondary {
position: relative;
background: transparent;
border: 1px solid $blue;
border-radius: $border-radius-lg;
color: $blue;
transition: all 150ms linear;
&:hover {
color: $white;
@include blue-gradient;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible { // Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}
&:disabled {
background: transparent;
color: $gray !important;
height: 48px;
border: 1px solid $gray-300;
border-radius: $border-radius-lg;
}
}
}

View file

@ -0,0 +1,10 @@
.has-error {
.list-button,
.list-card,
.list-button-horizontal {
color: $red;
label {
border: 1px solid $red;
}
}
}

View file

@ -1,44 +0,0 @@
.list-group {
&.list-button {
input[type="radio"],
input[type="checkbox"] {
opacity: 0;
position: fixed;
width: 0;
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue inset;
}
&:focus + label {
box-shadow: 0 0 0 2.5px $blue inset;
}
&:checked + label {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked + label p:first-child {
font-weight: 500;
}
}
label {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
&:hover {
@include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-100;
}
cursor: pointer;
}
+ p {
display: none;
}
}
}
}

View file

@ -10,41 +10,6 @@ body {
box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15); //Use instead of Bootstrap's helper
}
}
a {
color: $blue;
text-decoration: none;
border-bottom: 1px solid $blue;
line-height: 26px;
padding: 0 0 4px 0;
font-weight: 500;
&:hover {
color: $blue-700;
}
&.small {
line-height: 24px;
&:hover {
color: $blue-700;
}
}
&.navigation-link {
color: $black;
border-bottom: 1px solid $black;
line-height: 26px;
}
&.footer-link {
color: $gray-500;
text-decoration: none;
border-bottom: 1px solid transparent;
line-height: 20px;
padding: 0 0 2px 0;
font-weight: 400;
font-size: .75rem;
&:hover {
border-bottom: 1px solid $gray-500;
padding: 0 0 2px 0;
}
}
}
.pointer {
cursor: pointer;
}

View file

@ -0,0 +1,101 @@
import { shallowMount } from "@vue/test-utils";
import buttonMain from "./button-main";
import { nextTick } from "vue";
describe("buttonMain.vue", () => {
it("Should return btn-primary class", async () => {
// Act
const wrapper = shallowMount(buttonMain, {
propsData: {
isPrimary: true,
},
});
// Assert
const button = wrapper.find("button");
// Expect
expect(button.attributes("class")).toContain("btn-primary");
});
it("Should return aria-disabled state", async () => {
// Act
const wrapper = shallowMount(buttonMain, {
propsData: {
isDisabled: true,
},
});
// Assert
const button = wrapper.find("button");
// Expect
expect(button.attributes()["aria-disabled"]).toEqual("true");
});
it("Should return loader color", async () => {
// Act
const wrapper = shallowMount(buttonMain, {
propsData: {
loaderColor: "blue",
loaderEnabled: true,
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.clicked();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("blue");
});
it("Should return loader position", async () => {
// Act
const wrapper = shallowMount(buttonMain, {
propsData: {
loaderPosition: "right",
loaderEnabled: true,
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.clicked();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("right");
});
it("Should return loader size in rem", async () => {
// Act
const wrapper = shallowMount(buttonMain, {
propsData: {
sizeInRem: 1,
loaderEnabled: true,
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.clicked();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("style")).toContain("1rem");
});
});

View file

@ -0,0 +1,110 @@
<template>
<button :disabled="isDisabled" :aria-disabled="isDisabled" class="btn d-flex align-items-center py-3 px-4" :class="[isPrimary ? 'btn-primary' : 'btn-secondary',isFloat ? 'float-end' : '']" @click="clicked()">
<span class="m-0">{{ this.buttonText }}</span>
<loader
class="ms-2"
v-if="isLoaderDisplayed"
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]"
/>
</button>
</template>
<script>
import loader from "@/ux-components/loader/loader";
export default {
name: "buttonMain",
props: {
isPrimary: Boolean,
buttonText: String,
isDisabled: Boolean,
loaderColor: String,
loaderPosition: String,
sizeInRem: [Number, String],
isFloat: Boolean
},
data() {
return {
isLoaderDisplayed: false,
};
},
methods: {
clicked() {
this.isLoaderDisplayed = true;
this.$emit("click-event");
},
},
components: {
loader,
},
};
</script>
<style lang="scss">
.btn {
&.btn-primary {
position: relative;
background: $blue-700;
@include blue-gradient;
border: none;
border-radius: $border-radius-lg;
color: $white;
transition: all 150ms linear;
white-space: nowrap;
&:hover {
background: linear-gradient(
270deg,
rgba(6, 87, 124, 1) 0%,
rgba(6, 87, 124, 1) 100%
);
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}
&:disabled {
background: $gray-100 !important;
background: linear-gradient(
270deg,
$gray-100 0%,
$gray-100 100%
) !important;
color: $gray !important;
height: 48px;
border: none;
border-radius: $border-radius-lg;
}
}
&.btn-secondary {
position: relative;
background: transparent;
border: 1px solid $blue;
border-radius: $border-radius-lg;
color: $blue;
transition: all 150ms linear;
&:hover {
color: $white;
@include blue-gradient;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}
&:disabled {
background: transparent;
color: $gray !important;
height: 48px;
border: 1px solid $gray-300;
border-radius: $border-radius-lg;
}
}
}
</style>

View file

@ -1 +0,0 @@
test.todo("some test to be written in the future");

View file

@ -1,43 +0,0 @@
<template>
<button
:disabled="isDisabled"
:aria-disabled="isDisabled"
class="btn btn-primary d-flex align-items-center py-3 px-4"
@click="displayComponent"
>
<span class="m-0">{{ this.buttonText }}</span>
<loader
class="ms-2"
v-if="display"
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]"
/>
</button>
</template>
<script>
import loader from "@/ux-components/loader/loader";
export default {
name: "buttonPrimary",
props: {
buttonText: String,
isDisabled: Boolean,
loaderColor: String,
loaderPosition: String,
sizeInRem: [Number, String],
},
data() {
return {
display: false,
};
},
methods: {
displayComponent() {
this.display = true;
},
},
components: {
loader,
},
};
</script>

View file

@ -1 +0,0 @@
test.todo("some test to be written in the future");

View file

@ -1,43 +0,0 @@
<template>
<button
:disabled="isDisabled"
:aria-disabled="isDisabled"
class="btn btn-secondary d-flex align-items-center py-3 px-4"
@click="displayComponent"
>
<span class="m-0">{{ this.buttonText }}</span>
<loader
class="ms-2"
v-if="display"
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]"
/>
</button>
</template>
<script>
import loader from "@/ux-components/loader/loader";
export default {
name: "buttonPrimary",
props: {
buttonText: String,
isDisabled: Boolean,
loaderColor: String,
loaderPosition: String,
sizeInRem: [Number, String],
},
data() {
return {
display: false,
};
},
methods: {
displayComponent() {
this.display = true;
},
},
components: {
loader,
},
};
</script>

View file

@ -1 +1,78 @@
test.todo("some test to be written in the future");
import { shallowMount } from "@vue/test-utils";
import checkbox from "./checkbox";
import { nextTick } from "vue";
describe("checkbox.vue", () => {
it("Should return checkbox name", async () => {
// Act
const wrapper = shallowMount(checkbox, {
propsData: {
checkboxName: "Checkbox",
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().name).toEqual("Checkbox");
});
it("Should return checkbox id", async () => {
// Act
const wrapper = shallowMount(checkbox, {
propsData: {
buttonID: "Checkbox ID",
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().id).toEqual("Checkbox ID");
});
it("Should return tabindex value", async () => {
// Act
const wrapper = shallowMount(checkbox, {
propsData: {
tabIndex: "1",
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().tabindex).toEqual("1");
});
it("Should return label text", async () => {
// Act
const wrapper = shallowMount(checkbox, {
propsData: {
checkboxLabel: "label text",
},
});
// Assert
const paragraph = wrapper.find("p");
expect(paragraph.text()).toEqual("label text");
});
it("Should return label text", async () => {
// Act
const wrapper = shallowMount(checkbox, {
propsData: {
screenReaderOnlyText: "screenreader text",
},
});
// Assert
const paragraph = wrapper.find("span");
expect(paragraph.text()).toEqual("screenreader text");
});
});

View file

@ -1,10 +1,19 @@
<template>
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
<div class="ui-checkbox d-flex">
<input type="checkbox" aria-checked="false" :name="checkboxName" :id="buttonID" :tabindex="tabIndex" :aria-required="isRequired" />
<input
type="checkbox"
aria-checked="false"
:name="checkboxName"
:id="buttonID"
:tabindex="tabIndex"
:aria-required="isRequired"
/>
<label class="d-flex align-items-center" :for="buttonID">
<p v-if="checkboxLabel" class="m-0">{{checkboxLabel}}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
<p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{
screenReaderOnlyText
}}</span>
</label>
</div>
</template>
@ -18,15 +27,15 @@ export default {
tabIndex: Number,
checkboxLabel: String,
screenReaderOnlyText: String,
isRequired: Boolean
}
isRequired: Boolean,
},
};
</script>
<style lang="scss">
.ui-checkbox {
position: relative;
input[type='checkbox'] {
input[type="checkbox"] {
position: absolute !important;
height: 1px;
width: 1px;
@ -37,7 +46,7 @@ export default {
position: relative;
}
+ label::before {
content: '';
content: "";
position: relative;
display: inline-block;
margin-right: 10px;
@ -51,7 +60,7 @@ export default {
background: $blue;
}
&:checked + label::after {
content: '';
content: "";
position: absolute;
top: 8px;
left: 2px;

View file

@ -3,7 +3,6 @@ import listButtonHorizontal from "./list-button-horizontal";
import { nextTick } from "vue";
describe("list-button-horizontal.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
@ -36,7 +35,7 @@ describe("list-button-horizontal.vue", () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
buttonID: "List Card Checkbox"
buttonID: "List Card Checkbox",
},
});
@ -44,14 +43,13 @@ describe("list-button-horizontal.vue", () => {
const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox");
});
it("Should return screen reader text", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text"
screenReaderOnlyText: "Screen Reader Only Text",
},
});
@ -59,29 +57,27 @@ describe("list-button-horizontal.vue", () => {
const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text");
});
it("Should return text alignment class", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
textPosition: "text-center"
textPosition: "text-center",
},
});
// Assert
const paragraph = wrapper.find("span.m-0");
expect(paragraph.attributes('class')).toContain("text-center");
expect(paragraph.attributes("class")).toContain("text-center");
});
it("Should return aria-required state", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isRequired: true
isRequired: true,
},
});
@ -89,14 +85,13 @@ describe("list-button-horizontal.vue", () => {
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("Should return loader enabled true", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
loaderEnabled: true
loaderEnabled: true,
},
});
@ -111,7 +106,6 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.exists()).toBe(true);
});
it("Should return loader color", async () => {
@ -119,7 +113,7 @@ describe("list-button-horizontal.vue", () => {
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
loaderColor: "blue",
loaderEnabled: true
loaderEnabled: true,
},
});
@ -133,8 +127,7 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("blue");
expect(loader.attributes("class")).toContain("blue");
});
it("Should return loader position", async () => {
@ -142,7 +135,7 @@ describe("list-button-horizontal.vue", () => {
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
loaderPosition: "right",
loaderEnabled: true
loaderEnabled: true,
},
});
@ -156,8 +149,7 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("right");
expect(loader.attributes("class")).toContain("right");
});
it("Should return loader size in rem", async () => {
@ -165,7 +157,7 @@ describe("list-button-horizontal.vue", () => {
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
sizeInRem: 1,
loaderEnabled: true
loaderEnabled: true,
},
});
@ -179,8 +171,6 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.attributes('style')).toContain("1rem");
expect(loader.attributes("style")).toContain("1rem");
});
});

View file

@ -1,18 +1,52 @@
<template>
<div class="list-group list-button-horizontal d-flex flex-column w-100 mb-2" @mouseup="handleClick(value)" @keyup.space="handleClick(value)">
<input :type="isMultiSelect ? 'checkbox' : 'radio'" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" :data-focus-target="groupName" />
<label tabindex="-1" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4">
<span class="m-0" :class="textPosition">{{buttonLabel}}</span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">{{buttonLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
<loader v-if="isLoaderDisplayed && !isMultiSelect" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[loaderColor, loaderPosition]" />
<div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID"
:name="groupName"
:value="buttonID"
:aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue"
/>
<label
tabindex="-1"
:for="buttonID"
:aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4"
>
<span
class="m-0"
:class="textPosition"
>
{{buttonLabel}}
</span>
<span
v-if="buttonLabelSubCopy"
class="m-0 small"
:class="textPosition"
>
{{buttonLabelSubCopy}}
</span>
<span v-if="screenReaderOnlyText" class="sr-only">
{{screenReaderOnlyText}}
</span>
<loader
v-if="isLoaderDisplayed && !isMultiSelect"
:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
:class="[loaderColor, loaderPosition]"
/>
</label>
</div>
</template>
<script>
import { toRefs } from 'vue';
import { useField } from 'vee-validate';
import { toRefs } from "vue";
import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader";
export default {
@ -30,25 +64,39 @@ export default {
loaderPosition: String,
sizeInRem: [Number, String],
isRequired: Boolean,
value: { // Field initial value
value: {
// Field initial value
type: String,
default: ""
}
default: "",
},
modelValue: [Array, String],
},
data() {
return {
isLoaderDisplayed: false,
checkValue: Boolean,
};
},
created(){
this.checkValue = this.modelValue ? this.modelValue.includes(this.buttonID) : false;
},
methods: {
displayLoader() {
this.isLoaderDisplayed = true;
},
handleClick(value) {
if(this.loaderEnabled){
if (this.loaderEnabled) {
this.displayLoader();
}
this.handleChange(value);
},
},
watch: {
checkValue(newValue, oldValue){
const isInitialization = typeof(oldValue) !== 'function';
if (isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID + '' });
}
}
},
components: {
@ -57,17 +105,20 @@ export default {
setup(props) {
const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange, errorMessage } = useField(groupName, undefined, {
type: inputType,
checkedValue: value
});
const { checked, handleChange, errorMessage } = useField(
groupName,
undefined,
{
type: inputType,
checkedValue: value,
}
);
return {
checked,
handleChange,
errorMessage,
};
},
};
</script>
@ -97,6 +148,7 @@ export default {
background: $white;
transition: all 150ms linear;
border: 1px solid $gray-500;
border-radius: 0;
width: 100%;
&:hover {
box-shadow: 0 0 0 4px $blue-100;

View file

@ -3,7 +3,6 @@ import listButton from "./list-button";
import { nextTick } from "vue";
describe("list-button.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const wrapper = shallowMount(listButton, {
@ -36,7 +35,7 @@ describe("list-button.vue", () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
buttonID: "List Card Checkbox"
buttonID: "List Card Checkbox",
},
});
@ -44,14 +43,13 @@ describe("list-button.vue", () => {
const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox");
});
it("Should return screen reader text", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text"
screenReaderOnlyText: "Screen Reader Only Text",
},
});
@ -59,29 +57,27 @@ describe("list-button.vue", () => {
const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text");
});
it("Should return text alignment class", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
textPosition: "text-center"
textPosition: "text-center",
},
});
// Assert
const paragraph = wrapper.find("span.m-0");
expect(paragraph.attributes('class')).toContain("text-center");
expect(paragraph.attributes("class")).toContain("text-center");
});
it("Should return aria-required state", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isRequired: true
isRequired: true,
},
});
@ -89,14 +85,13 @@ describe("list-button.vue", () => {
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("Should return loader enabled true", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
loaderEnabled: true
loaderEnabled: true,
},
});
@ -111,7 +106,6 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.exists()).toBe(true);
});
it("Should return loader color", async () => {
@ -119,7 +113,7 @@ describe("list-button.vue", () => {
const wrapper = shallowMount(listButton, {
propsData: {
loaderColor: "blue",
loaderEnabled: true
loaderEnabled: true,
},
});
@ -133,8 +127,7 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("blue");
expect(loader.attributes("class")).toContain("blue");
});
it("Should return loader position", async () => {
@ -142,7 +135,7 @@ describe("list-button.vue", () => {
const wrapper = shallowMount(listButton, {
propsData: {
loaderPosition: "right",
loaderEnabled: true
loaderEnabled: true,
},
});
@ -156,8 +149,7 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("right");
expect(loader.attributes("class")).toContain("right");
});
it("Should return loader size in rem", async () => {
@ -165,7 +157,7 @@ describe("list-button.vue", () => {
const wrapper = shallowMount(listButton, {
propsData: {
sizeInRem: 1,
loaderEnabled: true
loaderEnabled: true,
},
});
@ -179,8 +171,6 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub");
expect(loader.attributes('style')).toContain("1rem");
expect(loader.attributes("style")).toContain("1rem");
});
});

View file

@ -1,26 +1,63 @@
<template>
<div class="list-group list-button d-flex flex-column w-100 mb-2" @mouseup="handleClick(value)" @keyup.space="handleClick(value)">
<input :type="isMultiSelect ? 'checkbox' : 'radio'" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" :data-focus-target="groupName">
<label tabindex="-1" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4">
<span class="m-0" :class="textPosition">{{ buttonLabel }}</span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">{{buttonLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
<loader v-if="isLoaderDisplayed && !isMultiSelect" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" />
<div
class="list-group list-button d-flex flex-column w-100 mb-2"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID"
:name="groupName"
:value="buttonID"
:aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue"
>
<label
tabindex="-1"
:for="buttonID"
:aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4"
>
<span
class="m-0"
:class="textPosition"
>
{{ buttonLabel }}
</span>
<span
v-if="buttonLabelSubCopy"
class="m-0 small"
:class="textPosition"
>
{{ buttonLabelSubCopy }}
</span>
<span
v-if="screenReaderOnlyText"
class="sr-only"
>
{{ screenReaderOnlyText }}
</span>
<loader
v-if="isLoaderDisplayed && !isMultiSelect"
:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
:class="[this.loaderColor, this.loaderPosition]"
/>
</label>
</div>
</template>
<script>
import { toRefs } from 'vue';
import { useField } from 'vee-validate';
import { toRefs } from "vue";
import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader";
export default {
name: "listButton",
props: {
isMultiSelect: Boolean,
groupName: String,
buttonLabel: [Number,String],
buttonID: [Number,String],
buttonLabel: [Number, String],
buttonID: [Number, String],
isRequired: Boolean,
textPosition: String,
buttonLabelSubCopy: String,
@ -29,25 +66,39 @@ export default {
loaderColor: String,
loaderPosition: String,
sizeInRem: [Number,String],
value: { // Field initial value
type: String,
default: ""
}
value: {
// Field initial value
type: [String, Number],
default: "",
},
modelValue: [Array, String],
},
data() {
return {
isLoaderDisplayed: false,
checkValue: Boolean,
};
},
created(){
this.checkValue = this.modelValue ? this.modelValue.includes(this.buttonID) : false;
},
methods: {
displayLoader() {
this.isLoaderDisplayed = true;
},
handleClick(value) {
if(this.loaderEnabled){
if (this.loaderEnabled) {
this.displayLoader();
}
this.handleChange(value);
},
},
watch: {
checkValue(newValue, oldValue){
const isInitialization = typeof(oldValue) !== 'function';
if (isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID + '' });
}
}
},
components: {
@ -56,10 +107,14 @@ export default {
setup(props) {
const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange, errorMessage } = useField(groupName, undefined, {
type: inputType,
checkedValue: value
});
const { checked, handleChange, errorMessage } = useField(
groupName,
undefined,
{
type: inputType,
checkedValue: value,
}
);
return {
checked,
handleChange,
@ -77,6 +132,40 @@ export default {
position: static; //override bootstrap
height: 0;
}
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue inset;
}
&:focus + label {
box-shadow: 0 0 0 2.5px $blue inset;
}
&:checked + label {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked + label p:first-child {
font-weight: 500;
}
}
label {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
&:hover {
@include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-100;
}
cursor: pointer;
}
+ p {
display: none;
}
}
}
</style>

View file

@ -2,7 +2,6 @@ import { shallowMount } from "@vue/test-utils";
import listCard from "./list-card";
describe("list-card.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
@ -13,6 +12,7 @@ describe("list-card.vue", () => {
groupID: "checkbox-demo-1",
groupName: "Checkbox 1",
buttonImage: "windshield-damage.svg",
modelValue: ["List Card Checkbox"],
},
});
@ -31,7 +31,8 @@ describe("list-card.vue", () => {
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg"
buttonImage: "windshield-damage.svg",
modelValue: ["List Card Checkbox"],
},
});
@ -39,7 +40,6 @@ describe("list-card.vue", () => {
const paragraph = wrapper.find("p");
expect(paragraph.text()).toEqual("Windshield");
});
it("Should return secondary (sub) label text", async () => {
@ -52,7 +52,8 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
buttonLabelSubCopy: "Test",
modelValue: ["List Card Checkbox"],
},
});
@ -60,7 +61,6 @@ describe("list-card.vue", () => {
const paragraph = wrapper.find("p:nth-of-type(2)");
expect(paragraph.text()).toEqual("Test");
});
it("Should return value used for various text settings including the label 'for' and input id", async () => {
@ -73,7 +73,8 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
buttonLabelSubCopy: "Test",
modelValue: ["List Card Checkbox"],
},
});
@ -81,7 +82,6 @@ describe("list-card.vue", () => {
const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox");
});
it("Should return input group name used for radio or checkbox", async () => {
@ -94,7 +94,8 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
buttonLabelSubCopy: "Test",
modelValue: ["List Card Checkbox"],
},
});
@ -102,7 +103,6 @@ describe("list-card.vue", () => {
const input = wrapper.find("input");
expect(input.attributes().name).toEqual("radio 1");
});
it("Should return aria-required state", async () => {
@ -115,7 +115,8 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true
isRequired: true,
modelValue: ["List Card Checkbox"],
},
});
@ -123,7 +124,71 @@ describe("list-card.vue", () => {
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("Should return flex row classes if isWide is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: true,
buttonLabelSubCopy: "",
modelValue: ["List Card Checkbox"],
},
});
// Assert
const label = wrapper.find("label");
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-r", "ps-3", "pe-8"]);
});
it("Should return flex row classes if isWide is true and checkboxTop if buttonLabelSubCopy is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: true,
buttonLabelSubCopy: "Button Subcopy",
modelValue: ["List Card Checkbox"],
},
});
// Assert
const label = wrapper.find("label");
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-r", "ps-3", "pe-8", "checkboxTop"]);
});
it("Should return flex column classes if isWide is false", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
},
});
// Assert
const label = wrapper.find("label");
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-column", "pt-4", "pb-2"]);
});
});

View file

@ -1,9 +1,11 @@
<template>
<!-- Heavily documented below -->
<div class="col">
<div :class="'col' + colLength">
I AM LIST CARD,
buttonID is: {{buttonID}}
<div class="list-card w-100 rounded-3 d-flex align-items-center h-100" :class="isWide ? 'horizontal' : ''">
<div class="list-card w-100 rounded-3 d-flex align-items-center h-100"
:class="isWide ? 'horizontal' : ''"
@mouseup="handleChange(value)"
@keyup.space="handleChange(value)">
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID"
@ -12,14 +14,34 @@
:aria-required="isRequired"
:data-focus-target="groupName"
@click="handleChange(value)"
/>
<label :for="buttonID" class="d-flex w-100 align-items-center px-2 h-100" :class="getLabelClasses" tabindex="-1">
<img :id="buttonImageId" :class="!isWide ? 'order-1' : 'ms-auto order-3'" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" :alt="altText" />
<p v-if="!isWide" class="small order-3" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'">{{buttonLabel}}</p>
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4">{{buttonLabelSubCopy}}</p>
v-model="checkValue"
/>
<label
:for="buttonID"
class="d-flex w-100 align-items-center px-2 h-100"
:class="getLabelClasses" tabindex="-1"
>
<img
:id="buttonImageId"
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
:src="buttonImage"
:alt="altText"
/>
<p
v-if="!isWide"
class="small order-3"
:class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'"
>
{{buttonLabel}}
</p>
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4">
{{buttonLabelSubCopy}}
</p>
<div v-if="isWide" class="order-2">
<p class="m-0 small">{{buttonLabel}}</p>
<p v-if="buttonLabelSubCopy" class="m-0 fs-7">{{buttonLabelSubCopy}}</p>
<p class="m-0 small">{{ buttonLabel }}</p>
<p v-if="buttonLabelSubCopy" class="m-0 fs-7">
{{ buttonLabelSubCopy }}
</p>
</div>
</label>
</div>
@ -27,8 +49,8 @@
</template>
<script>
import { toRefs } from 'vue';
import { useField } from 'vee-validate';
import { toRefs } from "vue";
import { useField } from "vee-validate";
export default {
name: "listCard",
@ -37,31 +59,50 @@ export default {
isMultiSelect: Boolean, //Defines use as checkbox
isWide: Boolean,
//end must choose
buttonImage: String,//Required: File name of image
buttonImage: String, //Required: File name of image
buttonImageId: String,
buttonLabel: String,//Required: Label text
buttonLabel: String, //Required: Label text
isRequired: Boolean, //Required: is aria-required required or not?
altText: String,//Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
buttonID: String,//Required: Unique
groupName: String,//Rquired: Unique
buttonLabelSubCopy: String,//Optional: sub text
value: { // Field initial value
altText: String, //Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
buttonID: String, //Required: Unique
groupName: String, //Rquired: Unique
buttonLabelSubCopy: String, //Optional: sub text
value: {
// Field initial value
type: String,
default: ""
default: "",
},
colLength: String,
modelValue: [Array, String],
validationRules: Array,
},
data(){
return {
checkValue: Boolean,
}
},
created(){
this.checkValue = this.modelValue ? this.modelValue.includes(this.buttonID) : false;
},
computed: {
getLabelClasses(){
if(this.isWide) {
getLabelClasses() {
if (this.isWide) {
let classes = "flex-row py-r ps-3 pe-8";
if(this.buttonLabelSubCopy) {
classes += " checkboxTop"
if (this.buttonLabelSubCopy) {
classes += " checkboxTop";
}
return classes
return classes;
} else {
return "flex-column pt-4 pb-2";
}
},
},
watch: {
checkValue(newValue, oldValue){
const isInitialization = typeof(oldValue) !== 'function';
if (isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID + '' });
}
}
},
// methods: {
@ -118,10 +159,12 @@ export default {
<style lang="scss">
.list-card {
border: 1px solid $gray-500;
&.invalid { //Red border if invalid
&.invalid {
//Red border if invalid
border: 1px solid $red;
}
img {// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
img {
// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
height: auto;
width: 6.5rem;
margin-bottom: 3rem;
@ -130,7 +173,7 @@ export default {
box-shadow: 0px 0px 0px 6px $blue-100;
border: 1px solid transparent;
}
input[type='checkbox'],
input[type="checkbox"],
input[type="radio"] {
opacity: 0;
width: 0;
@ -147,12 +190,12 @@ export default {
}
&:focus + label {
box-shadow: 0 0 0 2.5px $blue;
border-radius: .5rem;
border-radius: 0.5rem;
}
&:checked + label {
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
border-radius: .5rem;
border-radius: 0.5rem;
}
+ label::before {
content: "";
@ -178,7 +221,7 @@ export default {
background: $blue;
}
&:checked + label::after {
content: '';
content: "";
position: absolute;
margin: 3.2rem 0 0 0;
border-left: 2px solid $white;
@ -208,18 +251,18 @@ export default {
margin-bottom: 0;
width: 5.5rem;
}
input[type='checkbox'],
input[type="checkbox"],
input[type="radio"] {
+ label::before {
content: "";
position: relative;
margin: 0 .5rem 0 0;
margin: 0 0.5rem 0 0;
order: 1;
}
&:checked + label::after {
content: '';
content: "";
margin: -0.25rem 0 0 0;
left: .875rem;
left: 0.875rem;
}
+ label {
min-height: 48px;

View file

@ -1,9 +1,10 @@
<template>
<div class="loader"
role="alert"
aria-label="Loading new page"
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
v-bind:class="[this.loaderColor, this.loaderPosition]"
<div
class="loader"
role="alert"
aria-label="Loading new page"
v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]"
></div>
</template>

View file

@ -0,0 +1,65 @@
import { shallowMount } from "@vue/test-utils";
import textLink from "./text-link";
import { nextTick } from "vue";
describe("text-link.vue", () => {
it("Should return text-link type is navigation if prop navigation is true", async () => {
// Act
const wrapper = shallowMount(textLink, {
propsData: {
navigation: true,
},
});
// Assert
const paragraph = wrapper.find("a");
// Expect
expect(paragraph.attributes("class")).toContain("navigation-link");
});
it("Should return text-link type is footer if prop footer is true", async () => {
// Act
const wrapper = shallowMount(textLink, {
propsData: {
footer: true,
},
});
// Assert
const paragraph = wrapper.find("a");
// Expect
expect(paragraph.attributes("class")).toContain("footer-link");
});
it("Should return text-link type is textSmall if prop textSmall is true", async () => {
// Act
const wrapper = shallowMount(textLink, {
propsData: {
textSmall: true,
},
});
// Assert
const paragraph = wrapper.find("a");
// Expect
expect(paragraph.attributes("class")).toContain("small");
});
it("Should return text for href", async () => {
// Act
const wrapper = shallowMount(textLink, {
propsData: {
text: "This is link text",
},
});
// Assert
const paragraph = wrapper.find("a");
// Expect
expect(paragraph.text()).toEqual("This is link text");
});
});

View file

@ -0,0 +1,68 @@
<template>
<a v-if="navigation" @click="handleClick" class="navigation-link" :href="href">{{text}}</a>
<a v-else-if="footer" @click="handleClick" class="footer-link" :href="href" target="_blank">{{text}}</a>
<a v-else-if="textSmall" @click="handleClick" class="small" :href="href">{{text}}</a>
<a v-else @click="handleClick" :href="href">{{text}}</a>
</template>
<script>
export default {
name: "textLink",
props: {
navigation: Boolean,
footer: Boolean,
textSmall: Boolean,
text: String,
href: {
type: String,
default: `javascript:void(0)`, //Prevents page jump when value is empty or #
},
},
methods: {
handleClick() {
this.$emit("click-event");
},
},
};
</script>
<style lang="scss">
a {
color: $blue;
text-decoration: none;
border-bottom: 1px solid $blue;
line-height: 26px;
padding: 0 0 4px 0;
font-weight: 500;
&:hover {
color: $blue-700;
}
&.small {
line-height: 24px;
&:hover {
color: $blue-700;
}
}
&.navigation-link {
color: $black;
border-bottom: 1px solid $black;
line-height: 26px;
display: inline-flex;
text-transform: capitalize;
white-space: nowrap;
}
&.footer-link {
color: $gray-600;
text-decoration: none;
border-bottom: 1px solid transparent;
line-height: 20px;
padding: 0 0 2px 0;
font-weight: 400;
font-size: 0.75rem;
&:hover {
border-bottom: 1px solid $gray-500;
padding: 0 0 2px 0;
}
}
}
</style>