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 #### 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). 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) `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/helpers/unit-test-helper.js",
"!src/layouts/component-test/component-test.vue", "!src/layouts/component-test/component-test.vue",
"!src/layouts/form-test/form-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. ], //! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 85, statements: 87,
}, },
}, },
}; };

View file

@ -5,7 +5,6 @@
<style lang="scss"> <style lang="scss">
@import "./node_modules/bootstrap/scss/bootstrap"; @import "./node_modules/bootstrap/scss/bootstrap";
@import "@/styles/common-styles.scss"; @import "@/styles/common-styles.scss";
@import "@/styles/common-button-styles.scss";
@import "@/styles/common-list-styles.scss";
@import "@/styles/common-typography-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"; import buttonBack from "./button-back";
describe("back button", () => { describe("back button", () => {
test("renders a button", () => { test("renders a button", () => {
// Arrange // Arrange
const myFunction = () => {}; const myFunction = () => {};
@ -14,10 +13,9 @@ describe("back button", () => {
backButtonAccessibleText: "something", backButtonAccessibleText: "something",
}, },
}); });
// Assert // Assert
expect(wrapper.find("button").exists()).toBe(true); expect(wrapper.find("button").exists()).toBe(true);
wrapper.unmount(); wrapper.unmount();
}); });
}); });

View file

@ -1,13 +1,38 @@
<template> <template>
<button <button
@click="handleClick" @click="handleClick"
class="back-button-wrapper p-2" class="back-button-wrapper"
:aria-label="backButtonAccessibleText" :aria-label="backButtonAccessibleText"
> >
<div class="button-back"> <svg
<div class="arrow-left"></div> xmlns="http://www.w3.org/2000/svg"
<div class="box"></div> width="34px"
</div> 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> </button>
</template> </template>
@ -19,71 +44,39 @@ export default {
type: String, type: String,
required: true, required: true,
default: "", default: "",
} },
}, },
methods: { methods: {
handleClick() { handleClick() {
this.$emit('click-event'); this.$emit("click-event");
} },
} },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.back-button-wrapper { .back-button-wrapper {
padding: 0;
position: relative;
top: -1px;
margin-left: 2px;
border: none; border: none;
background: transparent; background: none;
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;
.button-back { &:focus {
display: inline-flex; outline: none;
position: relative;
height: 18px; svg .outer {
width: 23px; fill: $white;
overflow: hidden; stroke-width: 2.5px;
// Account for padding while still being inline with heading text (for cases where text wraps to 2 lines) stroke: $blue;
margin-bottom: -2px; stroke-linecap: round;
.arrow-left { stroke-linejoin: round;
position: absolute; transform: translate(-2.7px, -1.8px) scale(1.2);
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-visible {
outline: none;
}
} }
</style> </style>

View file

@ -13,9 +13,7 @@ describe("buttonQuestion.vue", () => {
wrapper.vm.chooseAnswer("2021"); wrapper.vm.chooseAnswer("2021");
// Assert // Assert
expect(wrapper.find(".text-center").text()).toEqual( expect(wrapper.find(".text-center").text()).toEqual("Question Text");
"Question Text"
);
const buttonButtons = wrapper.findAllComponents('[data-test="button"]'); const buttonButtons = wrapper.findAllComponents('[data-test="button"]');
expect(buttonButtons.length).toBe(3); expect(buttonButtons.length).toBe(3);
expect(wrapper.props().modelValue).toBe("2020"); expect(wrapper.props().modelValue).toBe("2020");

View file

@ -1,9 +1,7 @@
<template> <template>
<div :class="isOverflowScrollable ? 'button_question' : ''"> <div :class="isOverflowScrollable ? 'button_question' : ''">
<div v-if="questionText" class="mt-6 mb-4 d-flex"> <div v-if="questionText" class="mt-6 mb-4 d-flex">
<span class="text-center fs-6 fw-bold w-100">{{ <span class="text-center fs-6 fw-bold w-100">{{ questionText }}</span>
questionText
}}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :class="getFieldSetClasses" role="radiogroup" :aria-labelledby="groupName ? groupName + '-radio-group' : ''"> <fieldset class="w-100" :class="getFieldSetClasses" role="radiogroup" :aria-labelledby="groupName ? groupName + '-radio-group' : ''">
@ -48,8 +46,8 @@ export default {
name: "buttonQuestion", name: "buttonQuestion",
props: { props: {
buttonType: { buttonType: {
type: String, type: String,
default: 'listButton' default: "listButton",
}, },
isMultiSelect: Boolean, isMultiSelect: Boolean,
groupName: String, groupName: String,
@ -57,20 +55,20 @@ export default {
answers: Array, answers: Array,
textPosition: { textPosition: {
type: String, type: String,
default: 'text-center' default: "text-center",
}, },
loaderEnabled: Boolean, loaderEnabled: Boolean,
loaderColor: { loaderColor: {
type: String, type: String,
default: 'blue' default: "blue",
}, },
loaderPosition: { loaderPosition: {
type: String, type: String,
default: 'right' default: "right",
}, },
sizeInRem: { sizeInRem: {
type: [String, Number], type: [String, Number],
default: 1.5 default: 1.5,
}, },
isRequired: Boolean, isRequired: Boolean,
isOverflowScrollable: Boolean, isOverflowScrollable: Boolean,
@ -78,17 +76,19 @@ export default {
modelValue: [Array, String], modelValue: [Array, String],
}, },
computed: { computed: {
getFieldSetClasses(){ getFieldSetClasses() {
return this.isOverflowScrollable ? 'container-fluid overflow-scroll position-absolute px-5 pt-1 py-0' : ''; return this.isOverflowScrollable
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
: "";
}, },
getComponentWrapperClasses() { getComponentWrapperClasses() {
let classes; let classes;
switch(this.buttonType) { switch (this.buttonType) {
case 'listButton': case "listButton":
classes = 'w-100' classes = "w-100";
break; break;
case 'listButtonHorizontal': case "listButtonHorizontal":
classes = 'd-flex flex-row p-0' classes = "d-flex flex-row p-0";
break; break;
case 'listCard': case 'listCard':
classes = 'row justify-content-center g-2' classes = 'row justify-content-center g-2'
@ -135,7 +135,7 @@ export default {
components: { components: {
listButton, listButton,
listButtonHorizontal, listButtonHorizontal,
listCard listCard,
}, },
}; };
</script> </script>

View file

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

View file

@ -1,64 +1,69 @@
<template> <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" /> <img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
</div> </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" /> <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> </template>
<script> <script>
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import eventBus from "@/helpers/event-bus/event-bus"; import eventBus from "@/helpers/event-bus/event-bus";
import { import { globalEvents } from "@/constants/events";
globalEvents,
} from "@/constants/events";
export default { export default {
name: "funnel-header", name: "funnel-header",
data() { data() {
return { return {
imageSrc: "", imageSrc: "",
displayGlobalAlert: false, displayGlobalAlert: false,
globalAlertMessage: { globalAlertMessage: {
isDismissible: false, isDismissible: false,
messageCopy: "", messageCopy: "",
messageHeadline: "", messageHeadline: "",
type: "", type: "",
}, },
}; };
}, },
methods: { methods: {
initializeComponent(cmsContent) { initializeComponent(cmsContent) {
this.imageSrc = cmsContent.LogoImage; 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;
}
}, },
},
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.funnel-header { .funnel-header {
height: 56px; height: 56px;
} }
.logo-image { .logo-image {
max-width: 78px; max-width: 78px;
} }
</style> </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 () => { it("Should render the 'text' data value as a span value for the header span text value.", async () => {
// Act // Act
const wrapper = shallowMount(FunnelSubHeader); const wrapper = shallowMount(FunnelSubHeader);
await wrapper.setData ({ await wrapper.setData({
text: "FunnelSubHeader Content", text: "FunnelSubHeader Content",
}); });
wrapper.vm.initializeComponent(cmsContent); wrapper.vm.initializeComponent(cmsContent);
@ -18,5 +18,5 @@ describe("FunnelSubHeader.vue", () => {
//Mock CMS content //Mock CMS content
const cmsContent = { const cmsContent = {
HeaderText: 'FunnelSubHeader Content' HeaderText: "FunnelSubHeader Content",
}; };

View file

@ -1,6 +1,8 @@
<template> <template>
<div class="current_car_info-text"> <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"> <h5 class="text-center fw-normal mb-0">
<span> <span>
{{ text }} {{ text }}
@ -10,14 +12,16 @@
:backButtonAccessibleText="backButtonAccessibleText" :backButtonAccessibleText="backButtonAccessibleText"
@click-event="clickEvent" @click-event="clickEvent"
/> />
</h5> </h5>
</div> </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"> <p class="text-center small fw-normal mb-0">
<span> <span>
{{ subText }} {{ subText }}
</span> </span>
</p> </p>
</div> </div>
</div> </div>
</template> </template>
@ -29,9 +33,9 @@ export default {
name: "FunnelSubHeader", name: "FunnelSubHeader",
data() { data() {
return { return {
text: '', text: "",
subText: '' subText: "",
} };
}, },
props: { props: {
hasBackButton: Boolean, hasBackButton: Boolean,
@ -42,27 +46,23 @@ export default {
}, },
methods: { methods: {
clickEvent() { clickEvent() {
this.$emit('click-event'); this.$emit("click-event");
}, },
initializeComponent(cmsContent) { initializeComponent(cmsContent) {
this.text = cmsContent.HeaderText; this.text = cmsContent.HeaderText;
this.subText = cmsContent.HeaderSubText; this.subText = cmsContent.HeaderSubText;
} },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
h5 { h5 {
color: $gray-550; color: $gray-550;
line-height: 32px;
button { button {
border: none; color: inherit;
background: none;
color: inherit;
}
}
.back-button-wrapper {
margin-left: 0;
} }
}
</style> </style>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,12 +4,11 @@ import { dispatch } from "@/store";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
dispatch: jest.fn(), dispatch: jest.fn(),
state: { 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", () => { it("Should return data from CMS", () => {
// Arrange // Arrange
const cmsMockData = { const cmsMockData = {
@ -34,9 +33,9 @@ describe("cms-content-helper.js", () => {
}, },
], ],
}; };
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
// Act // Act
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // 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", () => { it("Should replace strings for global state", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
@ -60,21 +58,18 @@ describe("cms-content-helper.js", () => {
}, },
], ],
}; };
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual( expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019");
"2019"
);
}); });
}); });
}); });
describe("cms-content-helper.js", () => { describe("cms-content-helper.js", () => {
it("Should replace strings for global state, and leave others the same", () => { it("Should replace strings for global state, and leave others the same", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
{ {
@ -91,21 +86,21 @@ describe("cms-content-helper.js", () => {
}, },
], ],
}; };
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019"); 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", () => { it("Should replace strings for global state in nested objects", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
{ {
@ -119,22 +114,23 @@ describe("cms-content-helper.js", () => {
Model: { Model: {
OtherObjectInside: { OtherObjectInside: {
ExampleText: "{globalState:order.vehicle.make}", ExampleText: "{globalState:order.vehicle.make}",
} },
}, },
}, },
], ],
}; };
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019"); 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"; import { storeActions } from "@/constants/store-actions.js";
export default { export default {
// Adds event to the bus given its category, subcategory, and eventValue; // Adds event to the bus given its category, subcategory, and eventValue;
addEventToBus(category, subCategory, eventValue) { addEventToBus(category, subCategory, eventValue) {
store.commit(storeActions.ADD_EVENT_TO_BUS, { category: category, subCategory: subCategory, eventValue: 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. // Finds event on the bus, removes the item, and returns its value to the caller.
readAndPopEventFromBus(category, subCategory) { readAndPopEventFromBus(category, subCategory) {
const event = store.getters.eventBusItem(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. // Finds the event on the bus and returns its value to the caller, does not remove it.
readEventFromBus(category, subCategory) { readEventFromBus(category, subCategory) {
const event = store.getters.eventBusItem(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"; import store from "@/store";
describe("event-bus.js", () => { 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 = { it("Puts item on bus and then take it off", () => {
isDismissible: true, // Arrange / Act
messageCopy: 'You can get a quote by starting on this page.', eventBus.addEventToBus(
messageHeadline: 'We\'re sorry, something went wrong.', globalEvents.Categories.GLOBAL_ALERT,
type: globalEventTypes.Danger 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 expect(store.state.applicationUser.eventBus.length).toEqual(1);
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
// Arrange / Act
const eventValue = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
// Assert // Assert
expect(store.getters.eventBusItem(globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND)).toEqual(event); expect(eventValue).toEqual(event);
expect(store.state.applicationUser.eventBus.length).toEqual(1); expect(store.state.applicationUser.eventBus.length).toEqual(0);
});
// Arrange / Act it("Reads event from bus, should have event value.", () => {
const eventValue = eventBus.readAndPopEventFromBus(globalEvents.Categories.GLOBAL_ALERT, globalEvents.SubCategories.PAGE_NOT_FOUND); // Arrange / Act
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
// Assert // Assert
expect(eventValue).toEqual(event); expect(
eventBus.readEventFromBus(
expect(store.state.applicationUser.eventBus.length).toEqual(0); globalEvents.Categories.GLOBAL_ALERT,
}); globalEvents.SubCategories.PAGE_NOT_FOUND
)
it("Reads event from bus, should have event value.", () => { ).toEqual(event);
// 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);
});
});

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,125 +1,201 @@
import { shallowMount, flushPromises } from "@vue/test-utils"; // Components
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import vehicleDamage from "@/layouts/vehicle-damage/vehicle-damage.vue"; import vehicleDamage from "@/layouts/vehicle-damage/vehicle-damage.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ 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", () => { describe("vehicle-damage.vue", () => {
test("Page header is initailized with api data", async (done) => { test("Page header is initailized with api data", async (done) => {
//Arrange //Arrange
const pageHeaderWidgetHeaderText = "Select Damage"; const pageHeaderWidgetHeaderText = "Select Damage";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText }); const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText); expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done(); done();
}); });
}); });
}); });
describe("vehicle-damage.vue", () => { describe("vehicle-damage.vue", () => {
test("Page logo image is initailized with api data", async (done) => { test("Page logo image is initailized with api data", async (done) => {
//Arrange
//Arrange const SiteHeaderWidget = {
const SiteHeaderWidget = { LogoImage:
LogoImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", };
} const { wrapper, apiPromise } = setupMocks({
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget}); 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();
});
}); });
});
describe("vehicle-damage.vue", () => { //Act
test("Vehicle image is initailized with api data", async (done) => { vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
//Arrange { query: { fmgPage: "vehicle-damage" } },
const VehicleBannerWidget = { undefined,
GenericVehicleImage: (c) => c(wrapper.vm)
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", );
}
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget}); //Assert
apiPromise.finally(() => {
//Act expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
vehicleDamage.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-damage" } }, undefined, (c) => c(wrapper.vm)); SiteHeaderWidget
);
//Assert done();
apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
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({ function setupMocks({
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
}) { }) {
//Mock api responses
//Mock api responses baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn(); baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation(() => {
baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation(() => { return Promise.resolve({
return Promise.resolve({ data: ['Front Window', 'Back Window', 'Side Window'] }); data: ["Front Window", "Back Window", "Side Window"],
}); });
const apiResponses = { });
cmsContent: { const apiResponses = {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, cmsContent: {
VehicleBannerWidget: FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
{ VehicleBannerWidget: {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "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",
},
}, },
damageOptions: ['Front Window', 'Back Window', 'Side Window'], FunnelHeaderWidget: {
}; LogoImage:
const apiPromise = Promise.resolve(apiResponses); "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
settleAllPromises.mockImplementation(() => apiPromise); },
},
//Mock damage initialize methods damageOptions: ["Front Window", "Back Window", "Side Window"],
funnelHeader.methods = { };
initializeComponent: jest.fn(),
}; const apiPromise = Promise.resolve(apiResponses);
vehicleBanner.methods = {
initializeComponent: jest.fn(), settleAllPromises.mockImplementation(() => apiPromise);
}; fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
funnelSubHeader.methods = {
initializeComponent: jest.fn(), //Mock damage initialize methods
}; funnelHeader.methods = {
const mountOptions = getMountOptions(mountOptionsMockData); initializeComponent: jest.fn(),
const wrapper = shallowMount(vehicleDamage, mountOptions); };
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent; vehicleBanner.methods = {
const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" }); initializeComponent: jest.fn(),
vehicleBannerWrapper.vm.initializeComponent = vehicleBanner.methods.initializeComponent; };
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeader.methods.initializeComponent; funnelSubHeader.methods = {
initializeComponent: jest.fn(),
return { wrapper, apiPromise }; };
}
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> <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" /> <funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" /> <vehicleBanner ref="vehicleBanner" />
<funnelSubHeader <funnelSubHeader
@ -22,24 +22,28 @@ import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
export default ({ export default {
name: "vehicle-damage", name: "vehicle-damage",
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const damageOptionsPromise = baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_DAMAGE_OPTIONS, {carId: store.getters.vehicle.carId}); const damageOptionsPromise =
baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: store.getters.vehicle.carId }
);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
{ {
resultKey: "cmsContent", resultKey: "cmsContent",
promise: cmsContentPromise, promise: cmsContentPromise,
}, },
{ {
resultKey: "damageOptions", resultKey: "damageOptions",
promise: damageOptionsPromise promise: damageOptionsPromise,
}, },
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
@ -60,7 +64,11 @@ export default ({
console.log(resultMap) console.log(resultMap)
}); });
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
}, },
components: { components: {
funnelHeader, funnelHeader,
@ -68,5 +76,5 @@ export default ({
funnelSubHeader, funnelSubHeader,
replaceOptionsQuestion, replaceOptionsQuestion,
}, },
}); };
</script> </script>

View file

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

View file

@ -1,5 +1,6 @@
<template> <template>
<buttonQuestion class="radioQuestion" <buttonQuestion
class="radioQuestion"
isOverflowScrollable isOverflowScrollable
:questionText="questionText" :questionText="questionText"
:answers="makes" :answers="makes"
@ -23,7 +24,7 @@ export default {
return { return {
questionText: null, questionText: null,
makes: Array, makes: Array,
} };
}, },
props: { props: {
modelValue: String, modelValue: String,
@ -33,17 +34,20 @@ export default {
}, },
methods: { methods: {
loadInitialData() { 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) { initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText; this.questionText = cmsContent.QuestionText;
this.makes = initialData; this.makes = initialData;
} },
}, },
watch: { watch: {
modelValue(val) { modelValue(val) {
this.$emit("update:modelValue", val); this.$emit("update:modelValue", val);
} },
}, },
}; };
</script> </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 funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue"; 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. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(), settleAllPromises: jest.fn(),
})); }));
beforeEach(() => {
jest.resetModules();
});
describe("vehicle-make.vue", () => { describe("vehicle-make.vue", () => {
test("Make question component is initized with api data", async (done) => { test("Make question component is initized with api data", async (done) => {
//Arrange //Arrange
const vehicleMakeQuestionCmsContent = { QuestionText: "What make is your vehicle?" }; const vehicleMakeQuestionCmsContent = {
QuestionText: "What make is your vehicle?",
};
const makeQuestionInitialData = ["honda", "ford", "dodge"]; const makeQuestionInitialData = ["honda", "ford", "dodge"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks({
vehicleMakeQuestionCmsContent: vehicleMakeQuestionCmsContent, vehicleMakeQuestionCmsContent: vehicleMakeQuestionCmsContent,
makeQuestionInitialData: makeQuestionInitialData, makeQuestionInitialData: makeQuestionInitialData,
} ); });
//Act //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleMakeQuestionCmsContent, makeQuestionInitialData); expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(
vehicleMakeQuestionCmsContent,
makeQuestionInitialData
);
done(); done();
}); });
}); });
@ -37,17 +64,25 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => { describe("vehicle-make.vue", () => {
test("Page header is initailized with api data", async (done) => { test("Page header is initailized with api data", async (done) => {
//Arrange //Arrange
const pageHeaderWidgetHeaderText = "Select a make to get started"; const pageHeaderWidgetHeaderText = "Select a make to get started";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText }); const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText); expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done(); done();
}); });
}); });
@ -55,20 +90,28 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => { describe("vehicle-make.vue", () => {
test("Page logo image is initailized with api data", async (done) => { test("Page logo image is initailized with api data", async (done) => {
//Arrange //Arrange
const SiteHeaderWidget = { const SiteHeaderWidget = {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "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 //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget); expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done(); done();
}); });
}); });
@ -76,20 +119,28 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => { describe("vehicle-make.vue", () => {
test("Vehicle image is initailized with api data", async (done) => { test("Vehicle image is initailized with api data", async (done) => {
//Arrange //Arrange
const VehicleBannerWidget = { const VehicleBannerWidget = {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "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 //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget); expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done(); done();
}); });
}); });
@ -97,19 +148,23 @@ describe("vehicle-make.vue", () => {
describe("vehicle-make.vue", () => { describe("vehicle-make.vue", () => {
test("BackButtonAction triggers a router.navigate change", async (done) => { test("BackButtonAction triggers a router.navigate change", async (done) => {
//Arrange //Arrange
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a make to get started", pageHeaderWidgetHeaderText: "Select a make to get started",
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigate: jest.fn() navigate: jest.fn(),
}, },
}, },
}); });
//Act //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(); wrapper.vm.backButtonAction();
await nextTick(); 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({ function setupMocks({
vehicleMakeQuestionCmsContent = {}, vehicleMakeQuestionCmsContent = {},
makeQuestionInitialData = {}, makeQuestionInitialData = {},
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
}) { }) {
//Mock api responses //Mock api responses
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleMakeQuestion: vehicleMakeQuestionCmsContent, VehicleMakeQuestion: vehicleMakeQuestionCmsContent,
VehicleBannerWidget: VehicleBannerWidget: {
{ GenericVehicleImage:
GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", },
}, FunnelHeaderWidget: {
FunnelHeaderWidget: LogoImage:
{ "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
LogoImage: },
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
}, },
makeQuestionInitialData: makeQuestionInitialData, makeQuestionInitialData: makeQuestionInitialData,
}; };
const apiPromise = Promise.resolve(apiResponses); const apiPromise = Promise.resolve(apiResponses);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
//Mock make question methods //Mock make question methods
@ -163,16 +239,23 @@ function setupMocks({
funnelSubHeader.methods = { funnelSubHeader.methods = {
initializeComponent: jest.fn(), initializeComponent: jest.fn(),
}; };
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleMake, mountOptions); const wrapper = shallowMount(vehicleMake, mountOptions);
const makeQuestionWrapper = wrapper.findComponent({ name: "makeQuestion" }); const makeQuestionWrapper = wrapper.findComponent({ name: "makeQuestion" });
makeQuestionWrapper.vm.initializeComponent = makeQuestion.methods.initializeComponent; makeQuestionWrapper.vm.initializeComponent =
makeQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" }); const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent; funnelHeaderWrapper.vm.initializeComponent =
funnelHeader.methods.initializeComponent;
const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" }); const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
vehicleBannerWrapper.vm.initializeComponent = vehicleBanner.methods.initializeComponent; vehicleBannerWrapper.vm.initializeComponent =
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" }); vehicleBanner.methods.initializeComponent;
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeader.methods.initializeComponent; const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -4,11 +4,11 @@
<div class="select-car"> <div class="select-car">
<div class="select-car-form rounded text-center"> <div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" /> <vehicleBanner ref="vehicleBanner" />
<funnelSubHeader <funnelSubHeader
ref="funnelSubHeader" ref="funnelSubHeader"
:hasBackButton="true" :hasBackButton="true"
backButtonAccessibleText="Change Vehicle Year" backButtonAccessibleText="Change Vehicle Year"
@click-event="backButtonAction" @click-event="backButtonAction"
/> />
<makeQuestion v-model="selectedMake" ref="makeQuestion" /> <makeQuestion v-model="selectedMake" ref="makeQuestion" />
</div> </div>
@ -25,6 +25,7 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default { export default {
name: "vehicle-make", name: "vehicle-make",
data() { data() {
@ -53,7 +54,6 @@ export default {
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.$refs.funnelSubHeader.initializeComponent( vm.$refs.funnelSubHeader.initializeComponent(
@ -77,6 +77,9 @@ export default {
// route to move backwards // route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
arePagePrerequisitesValid() {
return store.getters.vehicle.year !== null;
},
}, },
watch: { watch: {

View file

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

View file

@ -1,5 +1,6 @@
<template> <template>
<buttonQuestion class="radioQuestion" <buttonQuestion
class="radioQuestion"
isOverflowScrollable isOverflowScrollable
:questionText="questionText" :questionText="questionText"
:answers="models" :answers="models"
@ -23,7 +24,7 @@ export default {
return { return {
questionText: null, questionText: null,
models: Array, models: Array,
} };
}, },
props: { props: {
modelValue: String, modelValue: String,
@ -33,17 +34,20 @@ export default {
}, },
methods: { methods: {
loadInitialData() { 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) { initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText; this.questionText = cmsContent.QuestionText;
this.models = initialData; this.models = initialData;
} },
}, },
watch: { watch: {
modelValue(val) { modelValue(val) {
this.$emit("update:modelValue", val); this.$emit("update:modelValue", val);
} },
}, },
}; };
</script> </script>

View file

@ -1,30 +1,60 @@
import { shallowMount, flushPromises } from "@vue/test-utils"; // Components
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import vehicleModel from "@/layouts/vehicle-model/vehicle-model.vue"; import vehicleModel from "@/layouts/vehicle-model/vehicle-model.vue";
import modelQuestion from "@/layouts/vehicle-model/model-question/model-question"; import modelQuestion from "@/layouts/vehicle-model/model-question/model-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
// Supporting files
import { settleAllPromises } from "@/helpers/layout-helper.js"; 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 { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ 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: {
make: "Acura",
},
},
}));
describe("vehicle-model.vue", () => { describe("vehicle-model.vue", () => {
test("Model question component is initized with api data", async (done) => { test("Model question component is initized with api data", async (done) => {
//Arange //Arange
const buttonQuestionContent = { QuestionText: "What model is your vehicle?" }; const buttonQuestionContent = {
QuestionText: "What model is your vehicle?",
};
const modelQuestionInitialData = ["accord", "civic", "insight"]; const modelQuestionInitialData = ["accord", "civic", "insight"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks({
buttonQuestionContent: buttonQuestionContent, buttonQuestionContent: buttonQuestionContent,
modelQuestionInitialData: modelQuestionInitialData, modelQuestionInitialData: modelQuestionInitialData,
} ); });
//Act //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(buttonQuestionContent, modelQuestionInitialData); expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(
buttonQuestionContent,
modelQuestionInitialData
);
done(); done();
}); });
}); });
@ -33,12 +63,21 @@ describe("vehicle-model.vue", () => {
test("Page header is initailized with api data", async (done) => { test("Page header is initailized with api data", async (done) => {
//Arrange //Arrange
const pageHeaderWidgetHeaderText = "Select a model to get started"; const pageHeaderWidgetHeaderText = "Select a model to get started";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText }); const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText); expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done(); done();
}); });
}); });
@ -49,13 +88,22 @@ describe("vehicle-model.vue", () => {
const SiteHeaderWidget = { const SiteHeaderWidget = {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "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 //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget); expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done(); done();
}); });
}); });
@ -66,13 +114,22 @@ describe("vehicle-model.vue", () => {
const VehicleBannerWidget = { const VehicleBannerWidget = {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "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 //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget); expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done(); done();
}); });
}); });
@ -80,16 +137,21 @@ describe("vehicle-model.vue", () => {
describe("vehicle-model.vue", () => { describe("vehicle-model.vue", () => {
test("BackButtonAction triggers a router.navigate change", async (done) => { test("BackButtonAction triggers a router.navigate change", async (done) => {
//Arrange //Arrange
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a model to get started", pageHeaderWidgetHeaderText: "Select a model to get started",
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigate: jest.fn() navigate: jest.fn(),
}, },
}, },
}); });
//Act //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(); wrapper.vm.backButtonAction();
await nextTick(); await nextTick();
//Assert //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({ function setupMocks({
buttonQuestionContent = {}, buttonQuestionContent = {},
modelQuestionInitialData = {}, modelQuestionInitialData = {},
@ -110,21 +193,22 @@ function setupMocks({
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleModelQuestion: buttonQuestionContent, VehicleModelQuestion: buttonQuestionContent,
VehicleBannerWidget: VehicleBannerWidget: {
{ GenericVehicleImage:
GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", },
}, FunnelHeaderWidget: {
FunnelHeaderWidget: LogoImage:
{ "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
LogoImage: },
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
}, },
modelQuestionInitialData: modelQuestionInitialData, modelQuestionInitialData: modelQuestionInitialData,
}; };
const apiPromise = Promise.resolve(apiResponses); const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock model question methods //Mock model question methods
modelQuestion.methods = { modelQuestion.methods = {
loadInitialData: jest.fn(), loadInitialData: jest.fn(),
@ -141,13 +225,24 @@ function setupMocks({
}; };
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleModel, mountOptions); const wrapper = shallowMount(vehicleModel, mountOptions);
const modelQuestionWrapper = wrapper.findComponent({ name: "modelQuestion" }); const modelQuestionWrapper = wrapper.findComponent({ name: "modelQuestion" });
modelQuestionWrapper.vm.initializeComponent = modelQuestion.methods.initializeComponent; modelQuestionWrapper.vm.initializeComponent =
modelQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" }); const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent; funnelHeaderWrapper.vm.initializeComponent =
funnelHeader.methods.initializeComponent;
const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" }); const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
vehicleBannerWrapper.vm.initializeComponent = vehicleBanner.methods.initializeComponent; vehicleBannerWrapper.vm.initializeComponent =
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" }); vehicleBanner.methods.initializeComponent;
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeader.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -25,6 +25,8 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default { export default {
name: "vehicle-model", name: "vehicle-model",
data() { data() {
@ -77,6 +79,9 @@ export default {
// route to move backwards // route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
arePagePrerequisitesValid() {
return store.getters.vehicle.make !== null;
},
}, },
watch: { watch: {

View file

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

View file

@ -1,5 +1,6 @@
<template> <template>
<buttonQuestion class="radioQuestion" <buttonQuestion
class="radioQuestion"
isOverflowScrollable isOverflowScrollable
:questionText="questionText" :questionText="questionText"
:answers="styles" :answers="styles"
@ -23,7 +24,7 @@ export default {
return { return {
questionText: null, questionText: null,
styles: Array, styles: Array,
} };
}, },
props: { props: {
modelValue: String, modelValue: String,
@ -33,17 +34,24 @@ export default {
}, },
methods: { methods: {
loadInitialData() { 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) { initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText; this.questionText = cmsContent.QuestionText;
this.styles = initialData; this.styles = initialData;
} },
}, },
watch: { watch: {
modelValue(val) { modelValue(val) {
this.$emit("update:modelValue", val); this.$emit("update:modelValue", val);
} },
}, },
}; };
</script> </script>

View file

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

View file

@ -25,6 +25,8 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default { export default {
name: "vehicle-style", name: "vehicle-style",
data() { data() {
@ -78,17 +80,31 @@ export default {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
setVehicle() { 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: { watch: {
selectedStyle(style) { selectedStyle(style) {
this.$store.commit(this.storeMutations.UPDATE_STYLE, style); this.$store.commit(this.storeMutations.UPDATE_STYLE, style);
this.setVehicle().then(() => { this.setVehicle().then(() => {
this.$router.navigate(this.navigationScenarios.SELECTED_STYLE, this.$route); this.$router.navigate(
this.navigationScenarios.SELECTED_STYLE,
this.$route
);
}); });
} },
}, },
components: { components: {

View file

@ -15,21 +15,30 @@ jest.mock("@/helpers/layout-helper.js", () => ({
describe("vehicle-year.vue", () => { describe("vehicle-year.vue", () => {
test("Year question component is initized with api data", async (done) => { test("Year question component is initized with api data", async (done) => {
//Arrange //Arrange
const vehicleYearQuestionCmsContent = { QuestionText: "What year is your vehicle?" }; const vehicleYearQuestionCmsContent = {
QuestionText: "What year is your vehicle?",
};
const yearQuestionInitialData = ["2023", "2022", "2021"]; const yearQuestionInitialData = ["2023", "2022", "2021"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks({
vehicleYearQuestionCmsContent: vehicleYearQuestionCmsContent, vehicleYearQuestionCmsContent: vehicleYearQuestionCmsContent,
yearQuestionInitialData: yearQuestionInitialData, yearQuestionInitialData: yearQuestionInitialData,
} ); });
//Act //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleYearQuestionCmsContent, yearQuestionInitialData); expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(
vehicleYearQuestionCmsContent,
yearQuestionInitialData
);
done(); done();
}); });
}); });
@ -37,20 +46,28 @@ describe("vehicle-year.vue", () => {
describe("vehicle-year.vue", () => { describe("vehicle-year.vue", () => {
test("Page logo image is initailized with api data", async (done) => { test("Page logo image is initailized with api data", async (done) => {
//Arrange //Arrange
const SiteHeaderWidget = { const SiteHeaderWidget = {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "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 //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget); expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(
SiteHeaderWidget
);
done(); done();
}); });
}); });
@ -58,47 +75,73 @@ describe("vehicle-year.vue", () => {
describe("vehicle-year.vue", () => { describe("vehicle-year.vue", () => {
test("Vehicle image is initailized with api data", async (done) => { test("Vehicle image is initailized with api data", async (done) => {
//Arrange //Arrange
const VehicleBannerWidget = { const VehicleBannerWidget = {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "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 //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 //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget); expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(
VehicleBannerWidget
);
done(); 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({ function setupMocks({
vehicleYearQuestionCmsContent = {}, vehicleYearQuestionCmsContent = {},
yearQuestionInitialData = {}, yearQuestionInitialData = {},
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
}) { }) {
//Mock api responses //Mock api responses
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: { HeaderText: pageHeaderWidgetHeaderText }, FunnelSubHeaderWidget: { HeaderText: pageHeaderWidgetHeaderText },
VehicleYearQuestion: vehicleYearQuestionCmsContent, VehicleYearQuestion: vehicleYearQuestionCmsContent,
VehicleBannerWidget: VehicleBannerWidget: {
{ GenericVehicleImage:
GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", },
}, FunnelHeaderWidget: {
FunnelHeaderWidget: LogoImage:
{ "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
LogoImage: },
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
}, },
yearQuestionInitialData: yearQuestionInitialData, yearQuestionInitialData: yearQuestionInitialData,
}; };
@ -122,13 +165,19 @@ function setupMocks({
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleYear, mountOptions); const wrapper = shallowMount(vehicleYear, mountOptions);
const yearQuestionWrapper = wrapper.findComponent({ name: "yearQuestion" }); const yearQuestionWrapper = wrapper.findComponent({ name: "yearQuestion" });
yearQuestionWrapper.vm.initializeComponent = yearQuestion.methods.initializeComponent; yearQuestionWrapper.vm.initializeComponent =
yearQuestion.methods.initializeComponent;
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" }); const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeader.methods.initializeComponent; funnelHeaderWrapper.vm.initializeComponent =
funnelHeader.methods.initializeComponent;
const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" }); const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
vehicleBannerWrapper.vm.initializeComponent = vehicleBanner.methods.initializeComponent; vehicleBannerWrapper.vm.initializeComponent =
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" }); vehicleBanner.methods.initializeComponent;
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeader.methods.initializeComponent; const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -4,9 +4,7 @@
<div class="select-car"> <div class="select-car">
<div class="select-car-form rounded text-center"> <div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" /> <vehicleBanner ref="vehicleBanner" />
<funnelSubHeader <funnelSubHeader ref="funnelSubHeader" />
ref="funnelSubHeader"
/>
<yearQuestion v-model="selectedYear" ref="yearQuestion" /> <yearQuestion v-model="selectedYear" ref="yearQuestion" />
</div> </div>
</div> </div>
@ -80,6 +78,11 @@ export default {
); );
}, },
}, },
methods: {
arePagePrerequisitesValid() {
return true;
},
},
components: { components: {
yearQuestion, yearQuestion,
funnelHeader, funnelHeader,

View file

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

View file

@ -1,5 +1,6 @@
<template> <template>
<buttonQuestion class="radioQuestion" <buttonQuestion
class="radioQuestion"
isOverflowScrollable isOverflowScrollable
:questionText="questionText" :questionText="questionText"
:answers="years" :answers="years"
@ -7,6 +8,7 @@
textPosition="text-start" textPosition="text-start"
:loaderEnabled="true" :loaderEnabled="true"
v-model="modelValue" v-model="modelValue"
isRequired=true
/> />
</template> </template>
@ -21,7 +23,7 @@ export default {
return { return {
questionText: null, questionText: null,
years: Array, years: Array,
} };
}, },
props: { props: {
modelValue: String, modelValue: String,
@ -32,17 +34,20 @@ export default {
}, },
methods: { methods: {
loadInitialData() { loadInitialData() {
return baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.GET_VEHICLE_YEARS, {}); return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_VEHICLE_YEARS,
{}
);
}, },
initializeComponent(cmsContent, initialData) { initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText; this.questionText = cmsContent.QuestionText;
this.years = initialData; this.years = initialData;
} },
}, },
watch: { watch: {
modelValue(val) { modelValue(val) {
this.$emit("update:modelValue", val); this.$emit("update:modelValue", val);
} },
}, },
}; };
</script> </script>

View file

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

View file

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

View file

@ -1,12 +1,14 @@
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
import { widgetNames } from "@/constants/widget-names.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"; import store from "@/store";
describe("baseMixin.js", () => { describe("baseMixin.js", () => {
test("dispatchNonblockingStoreAction: calls dispatch with type and payload", () => { test("dispatchNonblockingStoreAction: calls dispatch with type and payload", () => {
const mixIn = getMixInInstance({}); const mixIn = getMixInInstance({});
const type = ''; const type = "";
const payload = {}; const payload = {};
mixIn.methods.dispatchNonBlockingStoreAction(type, payload); mixIn.methods.dispatchNonBlockingStoreAction(type, payload);
@ -16,14 +18,59 @@ describe("baseMixin.js", () => {
test("dispatchNonblockingStoreAction: calls dispatch with type and payload, handles Uri encode", () => { test("dispatchNonblockingStoreAction: calls dispatch with type and payload, handles Uri encode", () => {
const mixIn = getMixInInstance({}); const mixIn = getMixInInstance({});
const type = ''; const type = "";
const payload = { make: "Alfa Romeo/Chrysler" }; const payload = { make: "Alfa Romeo/Chrysler" };
mixIn.methods.dispatchNonBlockingStoreAction(type, payload, true); mixIn.methods.dispatchNonBlockingStoreAction(type, payload, true);
expect(store.dispatch).toBeCalledWith(type, payload); 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 }) { function getMixInInstance({ isDispatchSuccess = true }) {
// Mock Store // Mock Store
const storeDispatch = jest.fn(); const storeDispatch = jest.fn();

View file

@ -35,16 +35,26 @@ const routes = [
}, },
{ {
path: "/", path: "/",
name: "root",
async beforeEnter(to, from, next) { async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string. // If we have no query string, or we don't have the FmgPage query string.
if (to.query.fmgPage === undefined) { if (to.query.fmgPage === undefined) {
await GoToFunnelStartOn404(next); await GoToFunnelStartOn404(next);
} else { } else {
try { try {
// If we already have our route, go to it. // If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) { 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. // 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, component: routeData[0].component,
}); });
// Assign current query string parameters, as well as our fmgPage one. // Call the next components arePagePrerequisitesValid method before load.
next({ name: routeData[0].name, query: Object.assign(to.query, { fmgPage: routeData[0].name }) }); // 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) { } catch (error) {
console.log(error); console.log(error);
// If we don't have a route, go to our 404 page. // If we don't have a route, go to our 404 page.
await GoToFunnelStartOn404(next); await GoToFunnelStartOn404(next);
} }
} }
}, },
}, },
@ -97,7 +120,7 @@ router.navigate = (
if (matchingScenarioMap.destinationFmgPageValue !== undefined) { 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. // 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({ router.push({
path: "/", name: "root",
query: Object.assign(optionalQuery, { query: Object.assign(optionalQuery, {
fmgPage: matchingScenarioMap.destinationFmgPageValue, fmgPage: matchingScenarioMap.destinationFmgPageValue,
}), }),
@ -133,7 +156,9 @@ function navigateToUrl(url) {
// Get route information by page name. // 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. // This will reach out to the Cms and there is a 1:1 relationship between page names and route names.
async function GetRouteInfoFromPageName(pageName) { 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); const jsonFromResponse = JSON.parse(response.data.Result);
let routeData = []; let routeData = [];
@ -151,7 +176,6 @@ async function GetRouteInfoFromPageName(pageName) {
// Go to our start page on a 404. // Go to our start page on a 404.
async function GoToFunnelStartOn404(next) { async function GoToFunnelStartOn404(next) {
const apiResponse = await store.dispatch(storeActions.GET_HOMEPAGE_NAME); const apiResponse = await store.dispatch(storeActions.GET_HOMEPAGE_NAME);
const homepageName = apiResponse.data.Result; const homepageName = apiResponse.data.Result;
@ -161,14 +185,14 @@ async function GoToFunnelStartOn404(next) {
globalEvents.SubCategories.PAGE_NOT_FOUND, globalEvents.SubCategories.PAGE_NOT_FOUND,
{ {
isDismissible: true, isDismissible: true,
messageCopy: 'You can get a quote by starting on this page.', messageCopy: "You can get a quote by starting on this page.",
messageHeadline: 'We\'re sorry, something went wrong.', messageHeadline: "We're sorry, something went wrong.",
type: globalEventTypes.Danger type: globalEventTypes.Danger,
} }
); );
next({ next({
path: '/', path: "/",
query: { fmgPage: homepageName }, query: { fmgPage: homepageName },
}); });
} }

View file

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

View file

@ -1,6 +1,6 @@
import { createStore } from "vuex"; import { createStore } from "vuex";
import { endpoints } from "@/constants/endpoints.js"; import { endpoints } from "@/constants/endpoints.js";
import { storeMutations } from "@/constants/store-mutations" import { storeMutations } from "@/constants/store-mutations";
import createPersistedState from "vuex-persistedstate"; import createPersistedState from "vuex-persistedstate";
import globalMethods from "@/global-methods"; import globalMethods from "@/global-methods";
import eventBus from "../helpers/event-bus/event-bus"; import eventBus from "../helpers/event-bus/event-bus";
@ -19,43 +19,17 @@ export default createStore({
make: null, make: null,
model: null, model: null,
style: null, style: null,
vin: null,
licensePlate: null,
carId: null, carId: null,
category: null, category: null,
evoxImageId: null,
hasSplitWindshieldOption: null,
hasBackglassSliderOption: null,
registration: {
rawAddress: null,
zipCode: null,
firstName: null,
lastName: null,
licensePlate: null,
},
damage: { damage: {
isRepair: null, isRepair: null,
isReplacement: null,
numberOfChips: null, numberOfChips: null,
glassToReplace: null, glassToReplace: null,
problemGlassQuestionAnswers: null,
problemMoldingQuestionAnswers: null,
problemPropertyQuestionAnswers: null,
}, },
lineItems: null,
customer: {
emailAddress: null,
},
payment: {
isInsurance: null,
isCash: null,
},
referralSeqNum: null,
}, },
}, },
applicationUser: { applicationUser: {
experiments: null, eventBus: [],
eventBus: []
}, },
}, },
// See IMPORTANT note at top of "state" declaration. // See IMPORTANT note at top of "state" declaration.
@ -81,23 +55,30 @@ export default createStore({
state.applicationUser.eventBus.push(event); state.applicationUser.eventBus.push(event);
}, },
removeEventFromBus(state, eventData) { 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); const itemIndex = state.applicationUser.eventBus.indexOf(matchedEvent);
// If the item exists, remove it. // If the item exists, remove it.
if (itemIndex > -1) { if (itemIndex > -1) {
state.applicationUser.eventBus.splice(itemIndex, 1); state.applicationUser.eventBus.splice(itemIndex, 1);
} }
} },
}, },
getters: { getters: {
vehicle: state => state.order.vehicle, vehicle: (state) => state.order.vehicle,
eventBusItem: (state) => (eventCategory, eventSubCategory) => { 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; return matchedEvent !== undefined ? matchedEvent.eventValue : undefined;
}, },
eventBus: state => state.applicationUser.eventBus eventBus: (state) => state.applicationUser.eventBus,
}, },
actions: { actions: {
// Vehicle API Actions // Vehicle API Actions
@ -145,22 +126,24 @@ export default createStore({
payload: {}, payload: {},
}); });
}, },
setVehicle(context, {year, make, model, style}) { setVehicle(context, { year, make, model, style }) {
return globalMethods.callHttpClient({ return globalMethods
methods: endpoints.GetVehicle.method, .callHttpClient({
endpoint: `${endpoints.GetVehicle.url}/${year}/${make}/${model}/${style}`, methods: endpoints.GetVehicle.method,
payload: {}, endpoint: `${endpoints.GetVehicle.url}/${year}/${make}/${model}/${style}`,
}).then(response => { payload: {},
context.commit(storeMutations.UPDATE_VEHICLE, response.data); })
return response; .then((response) => {
}); context.commit(storeMutations.UPDATE_VEHICLE, response.data);
return response;
});
}, },
getDamageOptions(context, {carId}){ getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method, methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`, endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {}, payload: {},
}) });
}, },
// Content API Actions // Content API Actions
@ -177,8 +160,8 @@ export default createStore({
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetHomepageInfo.method, method: endpoints.GetHomepageInfo.method,
endpoint: endpoints.GetHomepageInfo.url, endpoint: endpoints.GetHomepageInfo.url,
}); });
}, },
getPageData(context, { pageName }) { getPageData(context, { pageName }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetPageData.method, method: endpoints.GetPageData.method,

View file

@ -152,7 +152,12 @@ describe("Actions", () => {
}); });
}); });
await store 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) => { .then((response) => {
returnData = response.data.Result; returnData = response.data.Result;
}); });
@ -174,7 +179,7 @@ describe("Actions", () => {
}); });
}); });
await store await store
.dispatch("lookupVehicleByVin", { vin: "12345678"}) .dispatch("lookupVehicleByVin", { vin: "12345678" })
.then((response) => { .then((response) => {
returnData = response.data.Result; returnData = response.data.Result;
}); });
@ -197,7 +202,7 @@ describe("Actions", () => {
}); });
}); });
await store await store
.dispatch("getEvoxImage", { relativeUrl: "evox_image.com"}) .dispatch("getEvoxImage", { relativeUrl: "evox_image.com" })
.then((response) => { .then((response) => {
returnData = response.data.Result; returnData = response.data.Result;
}); });
@ -244,8 +249,8 @@ describe("Mutations", () => {
// Arrange // Arrange
const carData = { const carData = {
carId: "123abc", carId: "123abc",
category: "car" category: "car",
} };
// Act // Act
store.commit("updateVehicle", carData); store.commit("updateVehicle", carData);
@ -256,48 +261,68 @@ describe("Mutations", () => {
it("Should add event onto bus and update state", () => { it("Should add event onto bus and update state", () => {
// Arrange // Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'}; const event = {
category: "TestCategoryOne",
subCategory: "TestSubCategoryOne",
eventValue: "TestEventValueOne",
};
// Act // Act
store.commit("addEventToBus", event); store.commit("addEventToBus", event);
//Assert //Assert
expect(store.state.applicationUser.eventBus[0].category).toBe('TestCategoryOne'); expect(store.state.applicationUser.eventBus[0].category).toBe(
expect(store.state.applicationUser.eventBus[0].subCategory).toBe('TestSubCategoryOne'); "TestCategoryOne"
expect(store.state.applicationUser.eventBus[0].eventValue).toBe('TestEventValueOne'); );
expect(store.state.applicationUser.eventBus[0].subCategory).toBe(
"TestSubCategoryOne"
);
expect(store.state.applicationUser.eventBus[0].eventValue).toBe(
"TestEventValueOne"
);
}); });
}); });
describe("Getters", () => { describe("Getters", () => {
it("Should validate vehicle getter", () => { it("Should validate vehicle getter", () => {
// Arrange // Arrange
const vehicle = store.getters.vehicle; const vehicle = store.getters.vehicle;
// Assert // Assert
expect(typeof vehicle).toBe('object'); expect(typeof vehicle).toBe("object");
}); });
it("Should get item from bus via getter", () => { it("Should get item from bus via getter", () => {
// Arrange // Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'}; const event = {
category: "TestCategoryOne",
subCategory: "TestSubCategoryOne",
eventValue: "TestEventValueOne",
};
// Act // Act
store.commit("addEventToBus", event) store.commit("addEventToBus", event);
// Assert // Assert
const returnedEventValue = store.getters.eventBusItem(event.category, event.subCategory); const returnedEventValue = store.getters.eventBusItem(
expect(returnedEventValue).toBe('TestEventValueOne'); event.category,
event.subCategory
);
expect(returnedEventValue).toBe("TestEventValueOne");
}); });
it("Should get eventbus from getter, should have length > 0", () => { it("Should get eventbus from getter, should have length > 0", () => {
// Arrange // Arrange
const event = {category: 'TestCategoryOne', subCategory: 'TestSubCategoryOne', eventValue: 'TestEventValueOne'}; const event = {
category: "TestCategoryOne",
subCategory: "TestSubCategoryOne",
eventValue: "TestEventValueOne",
};
// Act // Act
store.commit("addEventToBus", event); store.commit("addEventToBus", event);
//Assert //Assert
expect(store.getters.eventBus.length).toBeGreaterThan(0); 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 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 { .pointer {
cursor: 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> <template>
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag --> <!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
<div class="ui-checkbox d-flex"> <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"> <label class="d-flex align-items-center" :for="buttonID">
<p v-if="checkboxLabel" class="m-0">{{checkboxLabel}}</p> <p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span> <span v-if="screenReaderOnlyText" class="sr-only">{{
screenReaderOnlyText
}}</span>
</label> </label>
</div> </div>
</template> </template>
@ -18,15 +27,15 @@ export default {
tabIndex: Number, tabIndex: Number,
checkboxLabel: String, checkboxLabel: String,
screenReaderOnlyText: String, screenReaderOnlyText: String,
isRequired: Boolean isRequired: Boolean,
} },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.ui-checkbox { .ui-checkbox {
position: relative; position: relative;
input[type='checkbox'] { input[type="checkbox"] {
position: absolute !important; position: absolute !important;
height: 1px; height: 1px;
width: 1px; width: 1px;
@ -37,7 +46,7 @@ export default {
position: relative; position: relative;
} }
+ label::before { + label::before {
content: ''; content: "";
position: relative; position: relative;
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
@ -51,7 +60,7 @@ export default {
background: $blue; background: $blue;
} }
&:checked + label::after { &:checked + label::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 8px; top: 8px;
left: 2px; left: 2px;

View file

@ -3,7 +3,6 @@ import listButtonHorizontal from "./list-button-horizontal";
import { nextTick } from "vue"; import { nextTick } from "vue";
describe("list-button-horizontal.vue", () => { describe("list-button-horizontal.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
@ -36,7 +35,7 @@ describe("list-button-horizontal.vue", () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
buttonID: "List Card Checkbox" buttonID: "List Card Checkbox",
}, },
}); });
@ -44,14 +43,13 @@ describe("list-button-horizontal.vue", () => {
const label = wrapper.find("label"); const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox"); expect(label.attributes().for).toEqual("List Card Checkbox");
}); });
it("Should return screen reader text", async () => { it("Should return screen reader text", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { 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"); const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text"); expect(paragraph.text()).toEqual("Screen Reader Only Text");
}); });
it("Should return text alignment class", async () => { it("Should return text alignment class", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
textPosition: "text-center" textPosition: "text-center",
}, },
}); });
// Assert // Assert
const paragraph = wrapper.find("span.m-0"); 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 () => { it("Should return aria-required state", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
isRequired: true isRequired: true,
}, },
}); });
@ -89,14 +85,13 @@ describe("list-button-horizontal.vue", () => {
const input = wrapper.find("input"); const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true"); expect(input.attributes()["aria-required"]).toEqual("true");
}); });
it("Should return loader enabled true", async () => { it("Should return loader enabled true", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -111,7 +106,6 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub"); const loader = wrapper.find("loader-stub");
expect(loader.exists()).toBe(true); expect(loader.exists()).toBe(true);
}); });
it("Should return loader color", async () => { it("Should return loader color", async () => {
@ -119,7 +113,7 @@ describe("list-button-horizontal.vue", () => {
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
loaderColor: "blue", loaderColor: "blue",
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -133,8 +127,7 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub"); const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("blue"); expect(loader.attributes("class")).toContain("blue");
}); });
it("Should return loader position", async () => { it("Should return loader position", async () => {
@ -142,7 +135,7 @@ describe("list-button-horizontal.vue", () => {
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
loaderPosition: "right", loaderPosition: "right",
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -156,8 +149,7 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub"); 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 () => { it("Should return loader size in rem", async () => {
@ -165,7 +157,7 @@ describe("list-button-horizontal.vue", () => {
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
sizeInRem: 1, sizeInRem: 1,
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -179,8 +171,6 @@ describe("list-button-horizontal.vue", () => {
const loader = wrapper.find("loader-stub"); 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> </template>
<script> <script>
import { toRefs } from 'vue'; import { toRefs } from "vue";
import { useField } from 'vee-validate'; import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
export default { export default {
@ -30,11 +30,12 @@ export default {
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number, String], sizeInRem: [Number, String],
isRequired: Boolean, isRequired: Boolean,
value: { // Field initial value value: {
// Field initial value
type: String, type: String,
default: "" default: "",
}, },
modelValue: false modelValue: false,
}, },
data() { data() {
return { return {
@ -46,11 +47,11 @@ export default {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
handleClick(value) { handleClick(value) {
if(this.loaderEnabled){ if (this.loaderEnabled) {
this.displayLoader(); this.displayLoader();
} }
this.handleChange(value); this.handleChange(value);
} },
}, },
watch: { watch: {
modelValue(){ modelValue(){
@ -63,17 +64,20 @@ export default {
setup(props) { setup(props) {
const { groupName, value } = toRefs(props); const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio"; const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange, errorMessage } = useField(groupName, undefined, { const { checked, handleChange, errorMessage } = useField(
type: inputType, groupName,
checkedValue: value undefined,
}); {
type: inputType,
checkedValue: value,
}
);
return { return {
checked, checked,
handleChange, handleChange,
errorMessage, errorMessage,
}; };
}, },
}; };
</script> </script>

View file

@ -3,7 +3,6 @@ import listButton from "./list-button";
import { nextTick } from "vue"; import { nextTick } from "vue";
describe("list-button.vue", () => { describe("list-button.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
@ -36,7 +35,7 @@ describe("list-button.vue", () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
buttonID: "List Card Checkbox" buttonID: "List Card Checkbox",
}, },
}); });
@ -44,14 +43,13 @@ describe("list-button.vue", () => {
const label = wrapper.find("label"); const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox"); expect(label.attributes().for).toEqual("List Card Checkbox");
}); });
it("Should return screen reader text", async () => { it("Should return screen reader text", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { 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"); const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text"); expect(paragraph.text()).toEqual("Screen Reader Only Text");
}); });
it("Should return text alignment class", async () => { it("Should return text alignment class", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
textPosition: "text-center" textPosition: "text-center",
}, },
}); });
// Assert // Assert
const paragraph = wrapper.find("span.m-0"); 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 () => { it("Should return aria-required state", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
isRequired: true isRequired: true,
}, },
}); });
@ -89,14 +85,13 @@ describe("list-button.vue", () => {
const input = wrapper.find("input"); const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true"); expect(input.attributes()["aria-required"]).toEqual("true");
}); });
it("Should return loader enabled true", async () => { it("Should return loader enabled true", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -111,7 +106,6 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub"); const loader = wrapper.find("loader-stub");
expect(loader.exists()).toBe(true); expect(loader.exists()).toBe(true);
}); });
it("Should return loader color", async () => { it("Should return loader color", async () => {
@ -119,7 +113,7 @@ describe("list-button.vue", () => {
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
loaderColor: "blue", loaderColor: "blue",
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -133,8 +127,7 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub"); const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("blue"); expect(loader.attributes("class")).toContain("blue");
}); });
it("Should return loader position", async () => { it("Should return loader position", async () => {
@ -142,7 +135,7 @@ describe("list-button.vue", () => {
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
loaderPosition: "right", loaderPosition: "right",
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -156,8 +149,7 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub"); 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 () => { it("Should return loader size in rem", async () => {
@ -165,7 +157,7 @@ describe("list-button.vue", () => {
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
sizeInRem: 1, sizeInRem: 1,
loaderEnabled: true loaderEnabled: true,
}, },
}); });
@ -179,8 +171,6 @@ describe("list-button.vue", () => {
const loader = wrapper.find("loader-stub"); 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"> <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"> <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 class="m-0" :class="textPosition">{{ buttonLabel }}</span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">{{buttonLabelSubCopy}}</span> <span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">{{
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span> buttonLabelSubCopy
<loader v-if="isLoaderDisplayed && !isMultiSelect" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" /> }}</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> </label>
</div> </div>
</template> </template>
<script> <script>
import { toRefs } from 'vue'; import { toRefs } from "vue";
import { useField } from 'vee-validate'; import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
export default { export default {
name: "listButton", name: "listButton",
props: { props: {
isMultiSelect: Boolean, isMultiSelect: Boolean,
groupName: String, groupName: String,
buttonLabel: [Number,String], buttonLabel: [Number, String],
buttonID: [Number,String], buttonID: [Number, String],
isRequired: Boolean, isRequired: Boolean,
textPosition: String, textPosition: String,
buttonLabelSubCopy: String, buttonLabelSubCopy: String,
@ -29,9 +37,10 @@ export default {
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number,String], sizeInRem: [Number,String],
value: { // Field initial value value: {
// Field initial value
type: [String, Number], type: [String, Number],
default: "" default: "",
}, },
modelValue: false modelValue: false
}, },
@ -45,11 +54,11 @@ export default {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
handleClick(value) { handleClick(value) {
if(this.loaderEnabled){ if (this.loaderEnabled) {
this.displayLoader(); this.displayLoader();
} }
this.handleChange(value); this.handleChange(value);
} },
}, },
watch: { watch: {
modelValue(){ modelValue(){
@ -62,10 +71,14 @@ export default {
setup(props) { setup(props) {
const { groupName, value } = toRefs(props); const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio"; const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange, errorMessage } = useField(groupName, undefined, { const { checked, handleChange, errorMessage } = useField(
type: inputType, groupName,
checkedValue: value undefined,
}); {
type: inputType,
checkedValue: value,
}
);
return { return {
checked, checked,
handleChange, handleChange,
@ -83,6 +96,40 @@ export default {
position: static; //override bootstrap position: static; //override bootstrap
height: 0; 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> </style>

View file

@ -2,7 +2,6 @@ import { shallowMount } from "@vue/test-utils";
import listCard from "./list-card"; import listCard from "./list-card";
describe("list-card.vue", () => { describe("list-card.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act // Act
const wrapper = shallowMount(listCard, { const wrapper = shallowMount(listCard, {
@ -31,7 +30,7 @@ describe("list-card.vue", () => {
buttonID: "List Card Checkbox", buttonID: "List Card Checkbox",
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 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"); const paragraph = wrapper.find("p");
expect(paragraph.text()).toEqual("Windshield"); expect(paragraph.text()).toEqual("Windshield");
}); });
it("Should return secondary (sub) label text", async () => { it("Should return secondary (sub) label text", async () => {
@ -52,7 +50,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", 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)"); const paragraph = wrapper.find("p:nth-of-type(2)");
expect(paragraph.text()).toEqual("Test"); expect(paragraph.text()).toEqual("Test");
}); });
it("Should return value used for various text settings including the label 'for' and input id", async () => { 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", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test" buttonLabelSubCopy: "Test",
}, },
}); });
@ -81,7 +78,6 @@ describe("list-card.vue", () => {
const label = wrapper.find("label"); const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox"); expect(label.attributes().for).toEqual("List Card Checkbox");
}); });
it("Should return input group name used for radio or checkbox", async () => { it("Should return input group name used for radio or checkbox", async () => {
@ -94,7 +90,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test" buttonLabelSubCopy: "Test",
}, },
}); });
@ -102,7 +98,6 @@ describe("list-card.vue", () => {
const input = wrapper.find("input"); const input = wrapper.find("input");
expect(input.attributes().name).toEqual("radio 1"); expect(input.attributes().name).toEqual("radio 1");
}); });
it("Should return aria-required state", async () => { it("Should return aria-required state", async () => {
@ -115,7 +110,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true isRequired: true,
}, },
}); });
@ -123,7 +118,6 @@ describe("list-card.vue", () => {
const input = wrapper.find("input"); const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true"); expect(input.attributes()["aria-required"]).toEqual("true");
}); });
it("Should return flex row classes if isWide is true", async () => { 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="!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> <p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4">{{buttonLabelSubCopy}}</p>
<div v-if="isWide" class="order-2"> <div v-if="isWide" class="order-2">
<p class="m-0 small">{{buttonLabel}}</p> <p class="m-0 small">{{ buttonLabel }}</p>
<p v-if="buttonLabelSubCopy" class="m-0 fs-7">{{buttonLabelSubCopy}}</p> <p v-if="buttonLabelSubCopy" class="m-0 fs-7">
{{ buttonLabelSubCopy }}
</p>
</div> </div>
</label> </label>
</div> </div>
@ -17,8 +19,8 @@
</template> </template>
<script> <script>
import { toRefs } from 'vue'; import { toRefs } from "vue";
import { useField } from 'vee-validate'; import { useField } from "vee-validate";
export default { export default {
name: "listCard", name: "listCard",
@ -27,33 +29,34 @@ export default {
isMultiSelect: Boolean, //Defines use as checkbox isMultiSelect: Boolean, //Defines use as checkbox
isWide: Boolean, isWide: Boolean,
//end must choose //end must choose
buttonImage: String,//Required: File name of image buttonImage: String, //Required: File name of image
buttonImageId: String, buttonImageId: String,
buttonLabel: String,//Required: Label text buttonLabel: String, //Required: Label text
isRequired: Boolean, //Required: is aria-required required or not? 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. altText: String, //Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
buttonID: String,//Required: Unique buttonID: String, //Required: Unique
groupName: String,//Rquired: Unique groupName: String, //Rquired: Unique
buttonLabelSubCopy: String,//Optional: sub text buttonLabelSubCopy: String, //Optional: sub text
value: { // Field initial value value: {
// Field initial value
type: String, type: String,
default: "" default: "",
}, },
colLength: String, colLength: String,
modelValue: false modelValue: false,
}, },
computed: { computed: {
getLabelClasses(){ getLabelClasses() {
if(this.isWide) { if (this.isWide) {
let classes = "flex-row py-r ps-3 pe-8"; let classes = "flex-row py-r ps-3 pe-8";
if(this.buttonLabelSubCopy) { if (this.buttonLabelSubCopy) {
classes += " checkboxTop" classes += " checkboxTop";
} }
return classes return classes;
} else { } else {
return "flex-column pt-4 pb-2"; return "flex-column pt-4 pb-2";
} }
} },
}, },
watch: { watch: {
modelValue(){ modelValue(){
@ -78,10 +81,12 @@ export default {
<style lang="scss"> <style lang="scss">
.list-card { .list-card {
border: 1px solid $gray-500; border: 1px solid $gray-500;
&.invalid { //Red border if invalid &.invalid {
//Red border if invalid
border: 1px solid $red; 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; height: auto;
width: 6.5rem; width: 6.5rem;
margin-bottom: 3rem; margin-bottom: 3rem;
@ -90,7 +95,7 @@ export default {
box-shadow: 0px 0px 0px 6px $blue-100; box-shadow: 0px 0px 0px 6px $blue-100;
border: 1px solid transparent; border: 1px solid transparent;
} }
input[type='checkbox'], input[type="checkbox"],
input[type="radio"] { input[type="radio"] {
opacity: 0; opacity: 0;
width: 0; width: 0;
@ -107,12 +112,12 @@ export default {
} }
&:focus + label { &:focus + label {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
border-radius: .5rem; border-radius: 0.5rem;
} }
&:checked + label { &:checked + label {
background: $blue-100; background: $blue-100;
box-shadow: 0 0 0 1px $blue; box-shadow: 0 0 0 1px $blue;
border-radius: .5rem; border-radius: 0.5rem;
} }
+ label::before { + label::before {
content: ""; content: "";
@ -138,7 +143,7 @@ export default {
background: $blue; background: $blue;
} }
&:checked + label::after { &:checked + label::after {
content: ''; content: "";
position: absolute; position: absolute;
margin: 3.2rem 0 0 0; margin: 3.2rem 0 0 0;
border-left: 2px solid $white; border-left: 2px solid $white;
@ -168,18 +173,18 @@ export default {
margin-bottom: 0; margin-bottom: 0;
width: 5.5rem; width: 5.5rem;
} }
input[type='checkbox'], input[type="checkbox"],
input[type="radio"] { input[type="radio"] {
+ label::before { + label::before {
content: ""; content: "";
position: relative; position: relative;
margin: 0 .5rem 0 0; margin: 0 0.5rem 0 0;
order: 1; order: 1;
} }
&:checked + label::after { &:checked + label::after {
content: ''; content: "";
margin: -0.25rem 0 0 0; margin: -0.25rem 0 0 0;
left: .875rem; left: 0.875rem;
} }
+ label { + label {
min-height: 48px; min-height: 48px;

View file

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