Merge branch 'feature/CSR-6' into feature/CSR-121_button-click-function

This commit is contained in:
Max 2021-11-30 16:12:04 -05:00
commit 09396149c5
15 changed files with 223 additions and 212 deletions

View file

@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import vehicleBanner from './vehicle-banner';
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import { nextTick } from 'vue';
describe('vehicleBanner', () => {
const $store = {
@ -44,7 +45,7 @@ describe('vehicleBanner', () => {
]
};
test('renders the blurrycar image when vehicleImageSrc is not present', async () => {
test('renders the blurrycar image when vehicleImageSrc is not present', () => {
// Arrange
const mountOptions = getMountOptions(actionList);
mountOptions.global.mocks = {
@ -60,7 +61,7 @@ describe('vehicleBanner', () => {
wrapper.unmount();
});
test('does not render the blurrycar image when vehicleImageSrc exists', async () => {
test('does not render the blurrycar image when vehicleImageSrc exists', () => {
// Arrange
const mountOptions = getMountOptions(actionList);
mountOptions.global.mocks = {
@ -77,9 +78,7 @@ describe('vehicleBanner', () => {
}
});
// Assert
setTimeout(() => {
expect(wrapper.find('img').attributes('class')).not.toContain('blurrycar');
}, 500);
expect(wrapper.find('img').attributes('class')).not.toContain('blurrycar');
wrapper.unmount();
});
@ -98,13 +97,12 @@ describe('vehicleBanner', () => {
}
});
// Assert
setTimeout(() => {
expect(wrapper.find('img').attributes('class')).not.toContain('blurrycar');
}, 500);
await nextTick();
expect(wrapper.find('img').attributes('class')).not.toContain('blurrycar');
wrapper.unmount();
});
test('does not render the blurrycar image when a YMMS object is found', async () => {
test('receives a carId when a YMMS object is found', async () => {
// Arrange
$store.state.order.vehicle.year = "2019"
$store.state.order.vehicle.make = "acura"
@ -120,14 +118,12 @@ describe('vehicleBanner', () => {
...mountOptions
});
// Assert
setTimeout(() => {
expect(wrapper.find('img').attributes('class')).not.toContain('blurrycar');
}, 500);
await nextTick();
expect(wrapper.vm.$data.carId.length).toBeGreaterThan(0);
wrapper.unmount();
});
test('does not render the blurrycar image when a VIN is found', async () => {
test('receives a carId when a VIN is found', async () => {
// Arrange
$store.state.order.vehicle.vin = "1J4GW58S4XC541166";
const mountOptions = getMountOptions(actionList);
@ -140,9 +136,9 @@ describe('vehicleBanner', () => {
...mountOptions
});
// Assert
setTimeout(() => {
expect(wrapper.find('img').attributes('class')).not.toContain('blurrycar');
}, 500);
await nextTick();
expect(wrapper.vm.$data.carId.length).toBeGreaterThan(0);
wrapper.unmount();
});
});

View file

@ -1,5 +1,5 @@
<template>
<div class="vehicle_banner mb-3">
<div class="vehicle_banner mb-3 text-center">
<img
class="vehicle-image img-fluid"
:src="vehicleImageSrc"
@ -53,14 +53,14 @@ export default {
methods: {
getCarIdWithYmms(ymms) { // retrieve carId with YMMS object
this.dispatchNonBlockingStoreAction(this.storeActions.LOOKUP_VEHICLE_BY_YMMS, ymms).then((response) => {
const carId = response.data[0].CarId;
this.getVehicleImage(carId)
this.carId = response.data[0].CarId;
this.getVehicleImage(this.carId)
});
},
getCarIdWithVin(vin) { // retrieve carId with VIN string
this.dispatchNonBlockingStoreAction(this.storeActions.LOOKUP_VEHICLE_BY_VIN, vin).then((response) => {
const carId = response.data[0].CarId;
this.getVehicleImage(carId)
this.carId = response.data[0].CarId;
this.getVehicleImage(this.carId)
});
},
getVehicleImage(carId) { // look up evox with carId

View file

@ -0,0 +1,30 @@
import { storeActions } from "@/constants/store-actions.js";
import { widgetNames } from "@/constants/widget-names.js";
import store from "@/store";
export function fetchCmsContentForPage(fmgPage) {
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
const pageDataFromCms = {
isCmsContentReady: false
};
response.data.Result.forEach((widget) => {
if (Object.values(widgetNames).includes(widget.Type)) {
// If we already have this widget, push it on the collection
if (widget.Type in pageDataFromCms) {
pageDataFromCms[widget.Type].push(widget.Model);
return;
}
pageDataFromCms[widget.Type] = [widget.Model];
}
});
// Our 'Page' is ready because we have data now
pageDataFromCms.isCmsContentReady = true;
return pageDataFromCms;
});
}

View file

@ -0,0 +1,29 @@
export function settleAllPromises(layoutPromiseTable) {
// Pull our keys out of the promise 'table'
const promiseNames = Object.entries(layoutPromiseTable);
return Promise.allSettled(promiseNames.map(e => e[1]).map(n => n.promise))
.then(results => {
const resultMap = {};
// Build a map of the results
for (let i = 0; i < results.length; ++i) {
const promiseName = promiseNames[i][1].key;
// Some Promises like the cms content call don't have a 'data' field
// when returned, so other promises do. Map the results to the object
// so that the object is the return data.
if (results[i].value.data === undefined) {
resultMap[promiseName] = results[i].value
} else {
resultMap[promiseName] = results[i].value.data;
}
}
return resultMap;
});
}

View file

@ -1,7 +1,7 @@
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
export function getMountOptions(mockData, cmsMockData = null) {
export function getMountOptions(mockData) {
// Define our mocks to attached to the 'global' object for Vue/Jest.
const mocks = {};
@ -18,16 +18,9 @@ export function getMountOptions(mockData, cmsMockData = null) {
}
});
if(cmsMockData){
mocks.GetContentFromCms = jest.fn();
mocks.GetContentFromCms.mockImplementation(() =>
{
return cmsMockData;
});
}
// Mock store actions from js file
mocks.storeActions = storeActions;
const global = {
mocks: mocks,
plugins: [store]

View file

@ -37,6 +37,9 @@
<div class="col my-3 d-flex align-items-center">
<buttonPrimary
buttonText="Primary"
loaderColor="white"
loaderPosition="right"
sizeInRem="1"
/>
</div>
</div>
@ -44,6 +47,9 @@
<div class="col my-3 d-flex align-items-center">
<buttonSecondary
buttonText="Secondary"
loaderColor="white"
loaderPosition="right"
sizeInRem="1"
/>
</div>
</div>
@ -227,7 +233,7 @@
</div>
</div>
<div class="row my-3">
<div class="d-flex align-items-center">
<div class="col">
<vehicleBanner
passedCarId=""
/>

View file

@ -1,27 +1,56 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from 'vue'
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn()
}));
describe("vehicle-year.vue", () => {
test("vehicle-year.vue should render data from CMS", async () => {
// Arrange
const cmsMockData = {
PageHeaderWidget: [{ HeaderText: "Select a year to get started" }],
RadioQuestionWidget: [{ QuestionText: "What year is your vehicle?" }],
// Our mock data for our call to settleAllPromises
const mockData = {
getPageContent: {
PageHeaderWidget: [{ HeaderText: "Select a year to get started" }],
RadioQuestionWidget: [{ QuestionText: "What year is your vehicle?" }],
isCmsContentReady: true,
},
getVehicleYear: [2023, 2022, 2021]
}
const mountOptions = getMountOptions({}, cmsMockData);
// our router information needed.
const to = {
query: {
fmgPage: 'vehicle-year'
}
};
const mountOptions = getMountOptions(mockData);
// our mock implementation of settleAllPromises
settleAllPromises.mockImplementation(() => { return Promise.resolve(mockData);});
// Act
const wrapper = shallowMount(vehicleYear, mountOptions);
await flushPromises();
// Call our beforeRouteEnter on the component.
// This passes (c) => c(wrapper.vm) so that next can be called and our
// data can be set.
vehicleYear.beforeRouteEnter.call(wrapper.vm, to, undefined, (c) => c(wrapper.vm));
await nextTick(); // Wait for the DOM to update.
// Assert
const header = await wrapper.find(".Header");
expect(header.attributes("text")).toEqual("Select a year to get started");
const yearQuestion = await wrapper.findComponent({name: 'year-question'});
const yearQuestion = wrapper.findComponent({ name: 'year-question' });
expect(yearQuestion.attributes("questiontext")).toEqual("What year is your vehicle?");
});
});

View file

@ -1,37 +1,62 @@
<template v-if="isCmsContentReady">
<div class="select-car">
<template>
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner />
<pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" />
<yearQuestion :questionText="radioQuestionWidgets.QuestionText" />
<pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" />
<yearQuestion :questionText="radioQuestionWidgets.QuestionText" :years="vehicleYears" />
</div>
</div>
</div>
</template>
<script>
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import pageHeader from "@/ux-components/header/header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
export default {
name: "vehicle-year",
data() {
return {
pageHeaderWidgets: {},
radioQuestionWidgets: {},
};
},
async created() {
const pageContent = await this.GetContentFromCms();
name: "vehicle-year",
data() {
return {
pageHeaderWidgets: {},
radioQuestionWidgets: {},
vehicleYears: [],
};
},
computed: {},
beforeRouteEnter(to, from, next) {
this.pageHeaderWidgets = pageContent.PageHeaderWidget[0];
this.radioQuestionWidgets = pageContent.RadioQuestionWidget[0];
},
components: {
yearQuestion,
pageHeader,
vehicleBanner,
},
const contentPromise = fetchCmsContentForPage(to.query.fmgPage);
const getVehicleYearPromise = store.dispatch(storeActions.GET_VEHICLE_YEARS, {});
const promiseResultMap = [
{
resultKey: "getPageContent",
promise: contentPromise,
},
{
resultKey: "getVehicleYear",
promise: getVehicleYearPromise,
},
];
settleAllPromises(promiseResultMap).then((resultMap) => {
// Call our next function to transition to the next page.
next((vm) => {
vm.pageHeaderWidgets = resultMap.getPageContent.PageHeaderWidget[0];
vm.radioQuestionWidgets = resultMap.getPageContent.RadioQuestionWidget[0];
vm.vehicleYears = resultMap.getVehicleYear;
});
});
},
components: {
yearQuestion,
pageHeader,
},
};
</script>

View file

@ -13,16 +13,9 @@ export default {
name: "year-question",
props: {
questionText: String,
},
data() {
return {
years: [],
};
years: Array
},
created() {
this.dispatchNonBlockingStoreAction(this.storeActions.GET_VEHICLE_YEARS, {}).then((response) => {
this.years = response.data;
});
},
components: {
radioQuestion,

View file

@ -24,12 +24,11 @@ export default {
return this.$store.dispatch(type, payload);
},
GetContentFromCms() {
return this.dispatchNonBlockingStoreAction(this.storeActions.GET_PAGE_DATA,{ pageName: this.$route.query.fmgPage }).then((response) => {
const pageDataFromCms = {};
response.data.Result.forEach((widget) => {
if (Object.values(this.widgetNames).includes(widget.Type)) {
// If we already have this widget, push it on the collection
@ -39,10 +38,10 @@ export default {
}
pageDataFromCms[widget.Type] = [widget.Model];
}
});
// Our 'Page' is ready because we have data now
this.isCmsContentReady = true;

View file

@ -23,60 +23,6 @@ describe("baseMixin.js", () => {
expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
});
test('GetContentFromCms: Should return mapped data and call dispatchNonblockingStoreAction', async () => {
const mixIn = getMixInInstance({});
// Mock dispatch action
mixIn.methods.dispatchNonBlockingStoreAction = jest.fn();
mixIn.methods.dispatchNonBlockingStoreAction.mockImplementation((action) => {
if (action === 'getPageData') {
return Promise.resolve({
data: {
Result: [
{
Type: "PageConfigWidget",
Model: {
LayoutNames: [
"None",
"vehicle-year"
],
LayoutStyle: "vehicle-year"
}
},
{
Type: "RadioQuestionWidget",
Model: {
"QuestionText": "What year is your vehicle?"
}
},
{
Type: "RadioQuestionWidget",
Model: {
"QuestionText": "This is a second radio question"
}
},
{
Type: "PageHeaderWidget",
Model: {
"HeaderText": "Select a year to get started"
}
}
]
},
});
}
});
const result = await mixIn.methods.GetContentFromCms();
await flushPromises();
expect(mixIn.methods.dispatchNonBlockingStoreAction).toBeCalledWith(storeActions.GET_PAGE_DATA, { pageName: 'test-page'});
expect(result.PageHeaderWidget[0].HeaderText).toEqual('Select a year to get started');
});
})
function getMixInInstance({ isDispatchSuccess = true }) {

View file

@ -16,23 +16,8 @@
&:focus-visible { // Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
}
&.button-loader {
padding: 0 40px 0 12px;
&:after {
content: url(../../assets/img/icons/button-spinner-white.svg);
position: absolute;
right: 14px;
width: 16px;
height: 16px;
margin-left: 12px;
animation: rotation 1s infinite linear;
@keyframes rotation {
100% {
transform:rotate(360deg);
}
}
}
color: $white;
@include blue-gradient;
}
&:disabled {
background: $gray-100 !important;
@ -59,39 +44,8 @@
&:focus-visible { // Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
}
&.button-loader {
padding: 0 40px 0 12px;
&:after {
content: url(../../assets/img/icons/button-spinner-blue.svg);
position: absolute;
right: 14px;
width: 16px;
height: 16px;
margin-left: 12px;
animation: rotation 1s infinite linear;
@keyframes rotation {
100% {
transform:rotate(360deg);
}
}
}
&:focus:hover {
&:after {
content: url(../../assets/img/icons/button-spinner-white.svg);
position: absolute;
right: 14px;
width: 16px;
height: 16px;
margin-left: 12px;
animation: rotation 1s infinite linear;
@keyframes rotation {
100% {
transform:rotate(360deg);
}
}
}
}
color: $white;
@include blue-gradient;
}
&:disabled {
background: transparent;
@ -130,22 +84,5 @@
color: $danger;
}
}
&.button-loader {
padding: 0 40px 0 12px;
&:after {
content: url(../../assets/img/icons/button-spinner-blue.svg);
position: absolute;
right: 14px;
width: 16px;
height: 16px;
margin-left: 12px;
animation: rotation 1s infinite linear;
@keyframes rotation {
100% {
transform:rotate(360deg);
}
}
}
}
}
}

View file

@ -1,10 +1,10 @@
<template>
<div class="alert fade show text-center mb-0" role="alert"
<div class="alert fade show text-center mb-0 py-2 px-3" role="alert"
:class="[ isDismissible ? 'alert-dismissible' : '', this.alertClass ]"
>
<p class="m-0 fw-bold small alert-heading">{{alertHeadline}}</p>
<p class="m-0 text-body small">{{alertCopy}}</p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close p-2" data-bs-dismiss="alert" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.7 23.7" xml:space="preserve">
<path d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z"/>
</svg>
@ -39,10 +39,14 @@ export default {
.btn-close {
background: none;
opacity: 1;
width: .75rem;
height: .75rem;
}
&.alert-dismissible {
button {
display: flex;
top: 2px;
right: 2px;
}
}
&.alert-info {

View file

@ -3,29 +3,41 @@
:disabled="isDisabled"
:aria-disabled="isDisabled"
class="btn btn-primary d-flex align-items-center"
v-on:click="showLoader()"
v-bind:class="[this.isLoading ? 'button-loader' : 'not-loading']"
>
@click='displayComponent'
>
{{ this.buttonText }}
<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
},
data() {
return {
isLoading: false,
display: false,
};
},
methods: {
showLoader() {
this.isLoading = true;
displayComponent() {
this.display = true;
},
},
components: {
loader,
},
};
</script>

View file

@ -3,29 +3,41 @@
:disabled="isDisabled"
:aria-disabled="isDisabled"
class="btn btn-secondary d-flex align-items-center"
v-on:click="showLoader()"
v-bind:class="[this.isLoading ? 'button-loader' : 'not-loading']"
>
@click='displayComponent'
>
{{ this.buttonText }}
<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: "buttonSecondary",
name: "buttonPrimary",
props: {
buttonText: String,
isDisabled: Boolean,
loaderColor: String,
loaderPosition: String,
sizeInRem: Number
},
data() {
return {
isLoading: false,
isDisabled: false,
display: false,
};
},
methods: {
showLoader() {
this.isLoading = true;
displayComponent() {
this.display = true;
},
},
components: {
loader,
},
};
</script>