Merge branch 'develop' into feature/CSR-267

This commit is contained in:
Max 2022-01-31 15:34:50 -05:00
commit 02470efdb5
69 changed files with 2877 additions and 1922 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: 87,
},
},
};

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,13 +1,38 @@
<template>
<button
@click="handleClick"
class="back-button-wrapper p-2"
class="back-button-wrapper"
:aria-label="backButtonAccessibleText"
>
<div class="button-back">
<div class="arrow-left"></div>
<div class="box"></div>
</div>
<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
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"
/>
</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
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"
/>
</g>
</svg>
</button>
</template>
@ -19,71 +44,39 @@ export default {
type: String,
required: true,
default: "",
}
},
},
methods: {
handleClick() {
this.$emit('click-event');
}
}
this.$emit("click-event");
},
},
};
</script>
<style lang="scss">
.back-button-wrapper {
padding: 0;
position: relative;
top: -1px;
margin-left: 2px;
border: none;
background: transparent;
text-decoration: none;
border-bottom: none;
// Account for padding while still being inline with heading text (for cases where text wraps to 2 lines)
margin-bottom: -.75rem;
margin-top: -.75rem;
background: none;
.button-back {
display: inline-flex;
position: relative;
height: 18px;
width: 23px;
overflow: hidden;
// Account for padding while still being inline with heading text (for cases where text wraps to 2 lines)
margin-bottom: -2px;
.arrow-left {
position: absolute;
width: 18px;
height: 18px;
left: 4px;
background-color: $gray-550;
transform: rotate(45deg);
border-radius: 4px;
}
.box {
content: "";
position: absolute;
background: $gray-550;
height: 100%;
width: 11px;
display: flex;
justify-content: center;
align-items: center;
left: 12px;
border-radius: 5px;
&:before, &:after {
position: absolute;
left: 1px;
top: 3px;
content: " ";
height: 12px;
width: 2px;
background-color: $white;
border-radius: 2px;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
&:focus {
outline: none;
svg .outer {
fill: $white;
stroke-width: 2.5px;
stroke: $blue;
stroke-linecap: round;
stroke-linejoin: round;
transform: translate(-2.7px, -1.8px) scale(1.2);
}
}
&:focus-visible {
outline: none;
}
}
</style>

View file

@ -13,9 +13,7 @@ describe("buttonQuestion.vue", () => {
wrapper.vm.chooseAnswer("2021");
// Assert
expect(wrapper.find(".text-center").text()).toEqual(
"Question Text"
);
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");

View file

@ -1,9 +1,7 @@
<template>
<div :class="isOverflowScrollable ? 'button_question' : ''">
<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' : ''">
@ -48,8 +46,8 @@ export default {
name: "buttonQuestion",
props: {
buttonType: {
type: String,
default: 'listButton'
type: String,
default: "listButton",
},
isMultiSelect: Boolean,
groupName: String,
@ -57,20 +55,20 @@ 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,
@ -78,17 +76,19 @@ export default {
modelValue: [Array, 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 justify-content-center g-2'
@ -135,7 +135,7 @@ export default {
components: {
listButton,
listButtonHorizontal,
listCard
listCard,
},
};
</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,6 +1,8 @@
<template>
<div class="current_car_info-text">
<div class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
<div
class="d-flex align-items-center justify-content-center container-fluid overflow-hidden"
>
<h5 class="text-center fw-normal mb-0">
<span>
{{ text }}
@ -10,14 +12,16 @@
:backButtonAccessibleText="backButtonAccessibleText"
@click-event="clickEvent"
/>
</h5>
</h5>
</div>
<div class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
<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,9 +33,9 @@ export default {
name: "FunnelSubHeader",
data() {
return {
text: '',
subText: ''
}
text: "",
subText: "",
};
},
props: {
hasBackButton: Boolean,
@ -42,27 +46,23 @@ export default {
},
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;
h5 {
color: $gray-550;
line-height: 32px;
button {
border: none;
background: none;
color: inherit;
}
}
.back-button-wrapper {
margin-left: 0;
button {
color: inherit;
}
}
</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

@ -7,7 +7,7 @@ describe("vehicleBanner", () => {
// Act
const wrapper = shallowMount(vehicleBanner);
await wrapper.setData ({
await wrapper.setData({
vehicleImageSrc: "image_url",
});
wrapper.vm.initializeComponent(cmsContent);
@ -20,5 +20,5 @@ describe("vehicleBanner", () => {
//Mock CMS content
const cmsContent = {
GenericVehicleImage: 'image_url'
};
GenericVehicleImage: "image_url",
};

View file

@ -13,13 +13,13 @@ export default {
name: "vehicle-banner",
data() {
return {
vehicleImageSrc: '',
}
vehicleImageSrc: "",
};
},
methods: {
initializeComponent(cmsContent) {
this.vehicleImageSrc = cmsContent.GenericVehicleImage;
}
},
},
};
</script>

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

@ -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

@ -2,24 +2,31 @@ import store from "@/store";
import { storeActions } from "@/constants/store-actions.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(storeActions.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(storeActions.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

@ -8,45 +8,51 @@
<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

@ -1,125 +1,201 @@
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";
// 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";
// 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", () => ({
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);
});
});
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({
data: ["Front Window", "Back Window", "Side Window"],
});
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: ["Front Window", "Back Window", "Side Window"],
};
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 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 };
}

View file

@ -1,5 +1,5 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative">
<div class="container-fluid shadow rounded-3 p-0 position-relative">
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" />
<funnelSubHeader
@ -22,24 +22,28 @@ 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});
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
},
];
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "damageOptions",
promise: damageOptionsPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
@ -60,7 +64,11 @@ export default ({
console.log(resultMap)
});
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
},
components: {
funnelHeader,
@ -68,5 +76,5 @@ export default ({
funnelSubHeader,
replaceOptionsQuestion,
},
});
};
</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,5 +1,6 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="makes"
@ -23,7 +24,7 @@ export default {
return {
questionText: null,
makes: Array,
}
};
},
props: {
modelValue: String,
@ -33,17 +34,20 @@ export default {
},
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

@ -7,29 +7,56 @@ 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 { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
jest.mock("@/store", () => ({
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(),
}));
beforeEach(() => {
jest.resetModules();
});
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 +64,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 +90,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 +119,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 +148,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 +176,53 @@ 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();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
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 +239,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

@ -4,11 +4,11 @@
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" />
<funnelSubHeader
ref="funnelSubHeader"
:hasBackButton="true"
backButtonAccessibleText="Change Vehicle Year"
@click-event="backButtonAction"
<funnelSubHeader
ref="funnelSubHeader"
:hasBackButton="true"
backButtonAccessibleText="Change Vehicle Year"
@click-event="backButtonAction"
/>
<makeQuestion v-model="selectedMake" ref="makeQuestion" />
</div>
@ -25,6 +25,7 @@ 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 store from "@/store";
export default {
name: "vehicle-make",
data() {
@ -53,7 +54,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(
@ -77,6 +77,9 @@ export default {
// route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.year !== null;
},
},
watch: {

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,5 +1,6 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="models"
@ -23,7 +24,7 @@ export default {
return {
questionText: null,
models: Array,
}
};
},
props: {
modelValue: String,
@ -33,17 +34,20 @@ export default {
},
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,60 @@
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, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
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: {
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 +63,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 +88,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 +114,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 +137,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 +161,27 @@ 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);
});
});
function setupMocks({
buttonQuestionContent = {},
modelQuestionInitialData = {},
@ -110,21 +193,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 +225,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

@ -25,6 +25,8 @@ 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 store from "@/store";
export default {
name: "vehicle-model",
data() {
@ -77,6 +79,9 @@ export default {
// route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
arePagePrerequisitesValid() {
return store.getters.vehicle.make !== null;
},
},
watch: {

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,5 +1,6 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="styles"
@ -23,7 +24,7 @@ export default {
return {
questionText: null,
styles: Array,
}
};
},
props: {
modelValue: String,
@ -33,17 +34,24 @@ export default {
},
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

@ -25,6 +25,8 @@ 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 store from "@/store";
export default {
name: "vehicle-style",
data() {
@ -78,17 +80,31 @@ export default {
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;
},
},
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.navigate(
this.navigationScenarios.SELECTED_STYLE,
this.$route
);
});
}
},
},
components: {

View file

@ -15,21 +15,30 @@ jest.mock("@/helpers/layout-helper.js", () => ({
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 +46,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,47 +75,73 @@ 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);
});
});
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,
};
@ -122,13 +165,19 @@ function setupMocks({
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

@ -4,9 +4,7 @@
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" />
<funnelSubHeader
ref="funnelSubHeader"
/>
<funnelSubHeader ref="funnelSubHeader" />
<yearQuestion v-model="selectedYear" ref="yearQuestion" />
</div>
</div>
@ -80,6 +78,11 @@ export default {
);
},
},
methods: {
arePagePrerequisitesValid() {
return true;
},
},
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,5 +1,6 @@
<template>
<buttonQuestion class="radioQuestion"
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
:questionText="questionText"
:answers="years"
@ -7,6 +8,7 @@
textPosition="text-start"
:loaderEnabled="true"
v-model="modelValue"
isRequired=true
/>
</template>
@ -21,7 +23,7 @@ export default {
return {
questionText: null,
years: Array,
}
};
},
props: {
modelValue: String,
@ -32,17 +34,20 @@ export default {
},
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

@ -6,8 +6,7 @@ import { widgetNames } from "@/constants/widget-names.js";
export default {
data() {
return {
};
return {};
},
methods: {
dispatchNonBlockingStoreAction(type, payload, encodePayload = true) {

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 arePagePrerequisitesValid = router
.getRoutes()
.filter((x) => x.name === to.query.fmgPage)[0]
.components.default.methods.arePagePrerequisitesValid();
if (!arePagePrerequisitesValid) {
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 (!nextComponent.default.methods.arePagePrerequisitesValid()) {
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);
}
}
},
},
@ -97,7 +120,7 @@ router.navigate = (
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.
router.push({
path: "/",
name: "root",
query: Object.assign(optionalQuery, {
fmgPage: matchingScenarioMap.destinationFmgPageValue,
}),
@ -133,7 +156,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 +176,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,14 +185,14 @@ 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 },
});
}

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,6 +1,6 @@
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";
@ -19,43 +19,17 @@ export default createStore({
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: []
eventBus: [],
},
},
// See IMPORTANT note at top of "state" declaration.
@ -81,23 +55,30 @@ export default createStore({
state.applicationUser.eventBus.push(event);
},
removeEventFromBus(state, eventData) {
const matchedEvent = state.applicationUser.eventBus.find(({ category, subCategory }) => category === eventData.category && subCategory === eventData.subCategory);
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,
vehicle: (state) => state.order.vehicle,
eventBusItem: (state) => (eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(({ category, subCategory }) => category === eventCategory && subCategory === eventSubCategory);
const matchedEvent = state.applicationUser.eventBus.find(
({ category, subCategory }) =>
category === eventCategory && subCategory === eventSubCategory
);
return matchedEvent !== undefined ? matchedEvent.eventValue : undefined;
},
eventBus: state => state.applicationUser.eventBus
eventBus: (state) => state.applicationUser.eventBus,
},
actions: {
// Vehicle API Actions
@ -145,22 +126,24 @@ export default createStore({
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;
});
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}){
getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
})
});
},
// Content API Actions
@ -177,8 +160,8 @@ export default createStore({
return globalMethods.callHttpClient({
method: endpoints.GetHomepageInfo.method,
endpoint: endpoints.GetHomepageInfo.url,
});
},
});
},
getPageData(context, { pageName }) {
return globalMethods.callHttpClient({
method: endpoints.GetPageData.method,

View file

@ -152,7 +152,12 @@ describe("Actions", () => {
});
});
await store
.dispatch("lookupVehicleByYmms", { year: "2018", make: "Honda", model: "Civic", style: "2 Door"})
.dispatch("lookupVehicleByYmms", {
year: "2018",
make: "Honda",
model: "Civic",
style: "2 Door",
})
.then((response) => {
returnData = response.data.Result;
});
@ -174,7 +179,7 @@ describe("Actions", () => {
});
});
await store
.dispatch("lookupVehicleByVin", { vin: "12345678"})
.dispatch("lookupVehicleByVin", { vin: "12345678" })
.then((response) => {
returnData = response.data.Result;
});
@ -197,7 +202,7 @@ describe("Actions", () => {
});
});
await store
.dispatch("getEvoxImage", { relativeUrl: "evox_image.com"})
.dispatch("getEvoxImage", { relativeUrl: "evox_image.com" })
.then((response) => {
returnData = response.data.Result;
});
@ -244,8 +249,8 @@ describe("Mutations", () => {
// Arrange
const carData = {
carId: "123abc",
category: "car"
}
category: "car",
};
// Act
store.commit("updateVehicle", carData);
@ -256,48 +261,68 @@ describe("Mutations", () => {
it("Should add event onto bus and update state", () => {
// Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'};
const event = {
category: "TestCategoryOne",
subCategory: "TestSubCategoryOne",
eventValue: "TestEventValueOne",
};
// Act
store.commit("addEventToBus", event);
//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(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"
);
});
});
describe("Getters", () => {
it("Should validate vehicle getter", () => {
// Arrange
const vehicle = store.getters.vehicle;
// Assert
expect(typeof vehicle).toBe('object');
expect(typeof vehicle).toBe("object");
});
it("Should get item from bus via getter", () => {
// Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'};
const event = {
category: "TestCategoryOne",
subCategory: "TestSubCategoryOne",
eventValue: "TestEventValueOne",
};
// Act
store.commit("addEventToBus", event)
store.commit("addEventToBus", event);
// Assert
const returnedEventValue = store.getters.eventBusItem(event.category, event.subCategory);
expect(returnedEventValue).toBe('TestEventValueOne');
const returnedEventValue = store.getters.eventBusItem(
event.category,
event.subCategory
);
expect(returnedEventValue).toBe("TestEventValueOne");
});
it("Should get eventbus from getter, should have length > 0", () => {
// Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'};
// Arrange
const event = {
category: "TestCategoryOne",
subCategory: "TestSubCategoryOne",
eventValue: "TestEventValueOne",
};
// Act
store.commit("addEventToBus", event);
// Act
store.commit("addEventToBus", event);
//Assert
expect(store.getters.eventBus.length).toBeGreaterThan(0);
//Assert
expect(store.getters.eventBus.length).toBeGreaterThan(0);
});
});

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,114 @@
<template>
<button
:disabled="isDisabled"
:aria-disabled="isDisabled"
class="btn d-flex align-items-center py-3 px-4"
:class="isPrimary ? 'btn-primary' : 'btn-secondary'"
@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],
},
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;
&: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

@ -11,8 +11,8 @@
</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,11 +30,12 @@ export default {
loaderPosition: String,
sizeInRem: [Number, String],
isRequired: Boolean,
value: { // Field initial value
value: {
// Field initial value
type: String,
default: ""
default: "",
},
modelValue: false
modelValue: false,
},
data() {
return {
@ -46,11 +47,11 @@ export default {
this.isLoaderDisplayed = true;
},
handleClick(value) {
if(this.loaderEnabled){
if (this.loaderEnabled) {
this.displayLoader();
}
this.handleChange(value);
}
},
},
watch: {
modelValue(){
@ -63,17 +64,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>

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

@ -3,24 +3,32 @@
<input :type="isMultiSelect ? 'checkbox' : 'radio'" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" :data-focus-target="groupName" v-model="modelValue">
<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]" />
<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,9 +37,10 @@ export default {
loaderColor: String,
loaderPosition: String,
sizeInRem: [Number,String],
value: { // Field initial value
value: {
// Field initial value
type: [String, Number],
default: ""
default: "",
},
modelValue: false
},
@ -45,11 +54,11 @@ export default {
this.isLoaderDisplayed = true;
},
handleClick(value) {
if(this.loaderEnabled){
if (this.loaderEnabled) {
this.displayLoader();
}
this.handleChange(value);
}
},
},
watch: {
modelValue(){
@ -62,10 +71,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,
@ -83,6 +96,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, {
@ -31,7 +30,7 @@ describe("list-card.vue", () => {
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg"
buttonImage: "windshield-damage.svg",
},
});
@ -39,7 +38,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 +50,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
buttonLabelSubCopy: "Test",
},
});
@ -60,7 +58,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 +70,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
buttonLabelSubCopy: "Test",
},
});
@ -81,7 +78,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 +90,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
buttonLabelSubCopy: "Test",
},
});
@ -102,7 +98,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 +110,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true
isRequired: true,
},
});
@ -123,7 +118,6 @@ 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 () => {

View file

@ -8,8 +8,10 @@
<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>
@ -17,8 +19,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",
@ -27,33 +29,34 @@ 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: false
modelValue: 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: {
modelValue(){
@ -78,10 +81,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;
@ -90,7 +95,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;
@ -107,12 +112,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: "";
@ -138,7 +143,7 @@ export default {
background: $blue;
}
&:checked + label::after {
content: '';
content: "";
position: absolute;
margin: 3.2rem 0 0 0;
border-left: 2px solid $white;
@ -168,18 +173,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,84 @@
<template>
<!-- Navigation Text Link -->
<a
v-if="navigation"
@click="handleClick"
class="navigation-link"
:href="href"
>{{ text }}</a
>
<!-- Footer Text Link -->
<a
v-else-if="footer"
@click="handleClick"
class="footer-link"
:href="href"
target="_blank"
>{{ text }}</a
>
<!-- Small Text Link -->
<a v-else-if="textSmall" @click="handleClick" class="small" :href="href">{{
text
}}</a>
<!-- Default Text Link -->
<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;
}
&.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: 0.75rem;
&:hover {
border-bottom: 1px solid $gray-500;
padding: 0 0 2px 0;
}
}
}
</style>