Merge pull request #87 from Safelite/feautre/CSR-224

CSR-224
This commit is contained in:
Frank Rua 2021-12-09 09:56:36 -05:00 committed by GitHub
commit aa073dc229
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 955 additions and 735 deletions

View file

@ -11,7 +11,7 @@ module.exports = {
"!src/constants/*.js", "!src/constants/*.js",
"!src/router/**/*.js", "!src/router/**/*.js",
"!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",
], //! 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: {

View file

@ -4,11 +4,11 @@ import radioQuestion from "@/common-components/radio-question/radio-question";
describe("radioQuestion.vue", () => { describe("radioQuestion.vue", () => {
it("Should render the 'questionText' prop value as a span value for the radio question and the 'answer' values should render as text values for radio components.", async () => { it("Should render the 'questionText' prop value as a span value for the radio question and the 'answer' values should render as text values for radio components.", async () => {
// Act // Act
const wrapper = shallowMount(radioQuestion) const wrapper = shallowMount(radioQuestion);
await wrapper.setProps({ await wrapper.setProps({
questionText: "Question Text", questionText: "Question Text",
answers: ["2023", "2022", "2021"], answers: ["2023", "2022", "2021"],
modelValue: "2020" modelValue: "2020",
}); });
wrapper.vm.chooseAnswer("2021"); wrapper.vm.chooseAnswer("2021");

View file

@ -6,19 +6,32 @@
}}</span> }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<div class="car_list overflow-scroll position-absolute container-fluid w-100 pt-1" role="radiogroup" aria-labelledby="select-year-radio-group"> <div
class="
car_list
overflow-scroll
position-absolute
container-fluid
w-100
pt-1
"
role="radiogroup"
aria-labelledby="select-year-radio-group"
>
<h3 class="visually-hidden" id="select-year-radio-group">*</h3> <h3 class="visually-hidden" id="select-year-radio-group">*</h3>
<radio v-for="answer in answers" :key="answer" <radio
:radioID="answer" v-for="answer in answers"
@click="chooseAnswer(answer)" :key="answer"
loaderColor="blue" :radioID="answer"
loaderPosition="right" @click="chooseAnswer(answer)"
sizeInRem="1.5" loaderColor="blue"
data-test="radio" loaderPosition="right"
groupName="radio-list" sizeInRem="1.5"
textPosition="text-start" data-test="radio"
:value="modelValue" groupName="radio-list"
/> textPosition="text-start"
:value="modelValue"
/>
</div> </div>
</div> </div>
</div> </div>
@ -31,12 +44,12 @@ export default {
props: { props: {
questionText: String, questionText: String,
answers: Array, answers: Array,
modelValue: String modelValue: String,
}, },
methods: { methods: {
chooseAnswer(answer) { chooseAnswer(answer) {
this.$emit('update:modelValue', answer); this.$emit("update:modelValue", answer);
} },
}, },
components: { components: {
radio, radio,

View file

@ -1,21 +1,19 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from "@vue/test-utils";
import siteHeader from './site-header'; import siteHeader from "./site-header";
describe('siteHeader', () => { describe("siteHeader", () => {
test("renders the logo image", () => {
// Arrange
test('renders the logo image', () => { // Act
// Arrange const wrapper = shallowMount(siteHeader, {
propsData: {
// Act imageSrc: "image_url",
const wrapper = shallowMount(siteHeader, { },
propsData: {
imageSrc: "image_url",
},
});
// Assert
expect(wrapper.find('img')).toBeTruthy();
wrapper.unmount();
}); });
// Assert
expect(wrapper.find("img")).toBeTruthy();
wrapper.unmount();
});
}); });

View file

@ -1,10 +1,9 @@
<template> <template>
<div class="funnel-header d-flex justify-content-center align-items-center" v-if="imageSrc"> <div
<img class="funnel-header d-flex justify-content-center align-items-center"
class="logo-image img-fluid" v-if="imageSrc"
:src="imageSrc" >
alt="Safelite logo" <img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
/>
</div> </div>
</template> </template>
@ -14,17 +13,17 @@ export default {
props: { props: {
imageSrc: { imageSrc: {
type: String, type: String,
required: true required: true,
} },
}, },
} };
</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

@ -1,21 +1,19 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from "@vue/test-utils";
import vehicleBanner from './vehicle-banner'; import vehicleBanner from "./vehicle-banner";
describe('vehicleBanner', () => { describe("vehicleBanner", () => {
test("renders the blurrycar image", () => {
// Arrange
test('renders the blurrycar image', () => { // Act
// Arrange const wrapper = shallowMount(vehicleBanner, {
propsData: {
// Act vehicleImageSrc: "image_url",
const wrapper = shallowMount(vehicleBanner, { },
propsData: {
vehicleImageSrc: "image_url",
},
});
// Assert
expect(wrapper.find('img').attributes('class')).toContain('blurrycar');
wrapper.unmount();
}); });
// Assert
expect(wrapper.find("img").attributes("class")).toContain("blurrycar");
wrapper.unmount();
});
}); });

View file

@ -14,14 +14,14 @@ export default {
props: { props: {
vehicleImageSrc: { vehicleImageSrc: {
type: String, type: String,
required: true required: true,
} },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.vehicle-image { .vehicle-image {
max-width: 290px; max-width: 290px;
} }
</style> </style>

View file

@ -1,5 +1,5 @@
const storeMutations = { const storeMutations = {
UPDATE_YEAR: "updateYear", UPDATE_YEAR: "updateYear",
} };
export { storeMutations }; export { storeMutations };

View file

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

View file

@ -2,52 +2,51 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { dispatch } from "@/store"; import { dispatch } from "@/store";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
dispatch: jest.fn() dispatch: jest.fn(),
})); }));
it("cms-content-helper: Should return data from CMS", () => { it("cms-content-helper: Should return data from CMS", () => {
// Arrange
const cmsMockData = {
Result: [
{
Type: "VehicleBannerWidget",
Model: {
ImageId: "28452dcb-7762-4cc9-ab09-7643d0b89203",
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
GenericVehicleImageFilePath:
"images/default-source/default-album/blurred-image.jpg",
},
},
{
Type: "PageHeaderWidget",
Model: {
HeaderText: "Select a year to get started",
},
},
{
Type: "PageHeaderWidget",
Model: {
HeaderText: "Select a model",
},
},
{
Type: "RadioQuestionWidget",
Model: {
QuestionText: "What year is your vehicle?",
},
},
],
};
// Arrange dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
const cmsMockData = {
Result: [
{
Type: "VehicleBannerWidget",
Model: {
ImageId: "28452dcb-7762-4cc9-ab09-7643d0b89203",
GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
GenericVehicleImageFilePath: "images/default-source/default-album/blurred-image.jpg"
}
},
{
Type: "PageHeaderWidget",
Model: {
"HeaderText": "Select a year to get started"
}
},
{
Type: "PageHeaderWidget",
Model: {
"HeaderText": "Select a model"
}
},
{
Type: "RadioQuestionWidget",
Model: {
"QuestionText": "What year is your vehicle?"
}
}
]
}
dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData }));
// Act
fetchCmsContentForPage('testPage').then((response) => {
// Assert
expect(response.isCmsContentReady).toBe(true);
expect(response.PageHeaderWidget[0].HeaderText).toEqual('Select a year to get started');
});
// Act
fetchCmsContentForPage("testPage").then((response) => {
// Assert
expect(response.PageHeaderWidget[0].HeaderText).toEqual(
"Select a year to get started"
);
});
}); });

View file

@ -1,29 +1,27 @@
export function settleAllPromises(promiseResultMap) { export function settleAllPromises(promiseResultMap) {
// Pull our keys out of the promise 'table'
const promiseNames = Object.entries(promiseResultMap);
// Pull our keys out of the promise 'table' return Promise.allSettled(
const promiseNames = Object.entries(promiseResultMap); promiseNames.map((e) => e[1]).map((n) => n.promise)
).then((results) => {
const resultMap = {};
return Promise.allSettled(promiseNames.map(e => e[1]).map(n => n.promise)) // Build a map of the results
.then(results => { for (let i = 0; i < results.length; ++i) {
const promiseName = promiseNames[i][1].resultKey;
const resultMap = {}; // 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.
// Build a map of the results if (results[i].value.data === undefined) {
for (let i = 0; i < results.length; ++i) { resultMap[promiseName] = results[i].value;
} else {
resultMap[promiseName] = results[i].value.data;
}
}
const promiseName = promiseNames[i][1].resultKey; return resultMap;
});
// 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,28 +1,25 @@
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
it("layout-helper: Should settle all promises and return mapped promise results", () => { it("layout-helper: Should settle all promises and return mapped promise results", () => {
// Arrange
const mockPromiseOne = Promise.resolve({ data: "test-data" });
const mockPromiseTwo = Promise.resolve({ data: "test-data-two" });
// Arrange const promiseResultMap = [
const mockPromiseOne = Promise.resolve({ data: "test-data" }); {
const mockPromiseTwo = Promise.resolve({ data: "test-data-two" }); resultKey: "MockResultOne",
promise: mockPromiseOne,
},
{
resultKey: "MockResultTwo",
promise: mockPromiseTwo,
},
];
const promiseResultMap = [ // Act
{ settleAllPromises(promiseResultMap).then((results) => {
resultKey: "MockResultOne", // Assert
promise: mockPromiseOne, expect(results.MockResultOne).toEqual("test-data");
}, expect(results.MockResultTwo).toEqual("test-data-two");
{ });
resultKey: "MockResultTwo", });
promise: mockPromiseTwo,
},
];
// Act
settleAllPromises(promiseResultMap).then(results => {
// Assert
expect(results.MockResultOne).toEqual('test-data');
expect(results.MockResultTwo).toEqual('test-data-two');
});
})

View file

@ -2,245 +2,265 @@
<div class="container-fluid container-shadow p-2 rounded-3"> <div class="container-fluid container-shadow p-2 rounded-3">
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Radio Card</h4> <h4 class="m-0 p-2 bg-light rounded">Radio Card</h4>
</div> </div>
</div> </div>
<div class="row g-2"> <div class="row g-2">
<radioCard <radioCard
radioLabel="Windshield" radioLabel="Windshield"
radioImage="windshield-damage.svg" radioImage="windshield-damage.svg"
altText="Windshield" altText="Windshield"
groupName="damageKey" groupName="damageKey"
radioID="windshield" radioID="windshield"
/> />
<radioCard <radioCard
radioLabel="Side Window" radioLabel="Side Window"
radioImage="side-window-damage.svg" radioImage="side-window-damage.svg"
altText="Side Window" altText="Side Window"
groupName="damageKey" groupName="damageKey"
radioID="sidewindow" radioID="sidewindow"
/> />
<radioCard <radioCard
radioLabel="Back Glass" radioLabel="Back Glass"
radioImage="back-glass-damage.svg" radioImage="back-glass-damage.svg"
altText="Back Glass" altText="Back Glass"
groupName="damageKey" groupName="damageKey"
radioID="backglass" radioID="backglass"
/> />
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Buttons</h4> <h4 class="m-0 p-2 bg-light rounded">Buttons</h4>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col my-3 d-flex align-items-center"> <div class="col my-3 d-flex align-items-center">
<buttonPrimary <buttonPrimary buttonText="Primary" loaderColor="white" sizeInRem="1" />
buttonText="Primary"
loaderColor="white"
sizeInRem="1"
/>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col my-3 d-flex align-items-center"> <div class="col my-3 d-flex align-items-center">
<buttonSecondary <buttonSecondary
buttonText="Secondary" buttonText="Secondary"
loaderColor="white" loaderColor="white"
sizeInRem="1" sizeInRem="1"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">List Button</h4> <h4 class="m-0 p-2 bg-light rounded">List Button</h4>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col my-3 d-flex align-items-center"> <div class="col my-3 d-flex align-items-center">
<listButton <listButton
buttonText="List Button" buttonText="List Button"
errorText="Test error message" errorText="Test error message"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Radio - Single-Line</h4> <h4 class="m-0 p-2 bg-light rounded">Radio - Single-Line</h4>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group --> <!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
<div role="radiogroup" aria-labelledby="demo-1-radio-group" class="col my-3 d-flex align-items-center flex-column"> <div
role="radiogroup"
aria-labelledby="demo-1-radio-group"
class="col my-3 d-flex align-items-center flex-column"
>
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. --> <!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
<h3 class="visually-hidden" id="demo-1-radio-group">Select Vehicle Year</h3> <h3 class="visually-hidden" id="demo-1-radio-group">
Select Vehicle Year
</h3>
<radio <radio
groupName="demo-1" groupName="demo-1"
ariaLabelBy="vehicle-year" ariaLabelBy="vehicle-year"
radioID="2021" radioID="2021"
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
<radio <radio
groupName="demo-1" groupName="demo-1"
ariaLabelBy="vehicle-year" ariaLabelBy="vehicle-year"
radioID="2020" radioID="2020"
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
<radio <radio
groupName="demo-1" groupName="demo-1"
ariaLabelBy="vehicle-year" ariaLabelBy="vehicle-year"
radioID="2019" radioID="2019"
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line</h4> <h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line</h4>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group --> <!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
<div role="radiogroup" aria-labelledby="demo-2-radio-group" class="col my-3 d-flex align-items-center flex-column"> <div
role="radiogroup"
aria-labelledby="demo-2-radio-group"
class="col my-3 d-flex align-items-center flex-column"
>
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. --> <!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
<h3 class="visually-hidden" id="demo-2-radio-group">Select Vehicle Year</h3> <h3 class="visually-hidden" id="demo-2-radio-group">
Select Vehicle Year
</h3>
<radio <radio
groupName="demo-2" groupName="demo-2"
ariaLabelBy="vehicle-make" ariaLabelBy="vehicle-make"
radioID="Chevrolet" radioID="Chevrolet"
radioLabelSubCopy="Test sub-headline" radioLabelSubCopy="Test sub-headline"
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
<radio <radio
groupName="demo-2" groupName="demo-2"
ariaLabelBy="vehicle-make" ariaLabelBy="vehicle-make"
radioID="Dodge" radioID="Dodge"
radioLabelSubCopy="Test sub-headline" radioLabelSubCopy="Test sub-headline"
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
<radio <radio
groupName="demo-2" groupName="demo-2"
ariaLabelBy="vehicle-make" ariaLabelBy="vehicle-make"
radioID="Ford" radioID="Ford"
radioLabelSubCopy="Test sub-headline" radioLabelSubCopy="Test sub-headline"
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line Centered</h4> <h4 class="m-0 p-2 bg-light rounded">Radio - Multi-Line Centered</h4>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group --> <!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
<div role="radiogroup" aria-labelledby="demo-3-radio-group" class="col my-3 d-flex align-items-center flex-column"> <div
role="radiogroup"
aria-labelledby="demo-3-radio-group"
class="col my-3 d-flex align-items-center flex-column"
>
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. --> <!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
<h3 class="visually-hidden" id="demo-3-radio-group">Multi-Line Centered</h3> <h3 class="visually-hidden" id="demo-3-radio-group">
Multi-Line Centered
</h3>
<radio <radio
groupName="demo-3" groupName="demo-3"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
radioID="Corvette" radioID="Corvette"
radioLabelSubCopy="Test sub-headline" radioLabelSubCopy="Test sub-headline"
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
<radio <radio
groupName="demo-3" groupName="demo-3"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
radioID="Testarosa" radioID="Testarosa"
radioLabelSubCopy="Test sub-headline" radioLabelSubCopy="Test sub-headline"
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
<radio <radio
groupName="demo-3" groupName="demo-3"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
radioID="S600" radioID="S600"
radioLabelSubCopy="Test sub-headline" radioLabelSubCopy="Test sub-headline"
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Radio Horizontal</h4> <h4 class="m-0 p-2 bg-light rounded">Radio Horizontal</h4>
</div> </div>
</div> </div>
<div class="row px-3"> <div class="row px-3">
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group --> <!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
<div role="radiogroup" aria-labelledby="demo-4-radio-group" class="d-flex flex-row p-0"> <div
role="radiogroup"
aria-labelledby="demo-4-radio-group"
class="d-flex flex-row p-0"
>
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. --> <!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. -->
<h3 class="visually-hidden" id="demo-4-radio-group">Select Vehicle Year</h3> <h3 class="visually-hidden" id="demo-4-radio-group">
Select Vehicle Year
</h3>
<radioHorizontal <radioHorizontal
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
radioID="1" radioID="1"
radioLabelSubCopy="" radioLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" v-bind:totalInGroup="3"
v-bind:positionInGroup="1" v-bind:positionInGroup="1"
/> />
<radioHorizontal <radioHorizontal
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
radioID="2" radioID="2"
radioLabelSubCopy="" radioLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" v-bind:totalInGroup="3"
v-bind:positionInGroup="2" v-bind:positionInGroup="2"
/> />
<radioHorizontal <radioHorizontal
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
radioID="3" radioID="3"
radioLabelSubCopy="" radioLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" v-bind:totalInGroup="3"
v-bind:positionInGroup="3" v-bind:positionInGroup="3"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Text Link</h4> <h4 class="m-0 p-2 bg-light rounded">Text Link</h4>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -250,19 +270,23 @@
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Typogrophy</h4> <h4 class="m-0 p-2 bg-light rounded">Typogrophy</h4>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<p>This is default body copy font size/weight</p> <p>This is default body copy font size/weight</p>
<p class="small">This is small body copy using <code>.small</code> class</p> <p class="small">
<p><small>This is also small using <code>&lt;small&gt;</code> tag</small></p> This is small body copy using <code>.small</code> class
</p>
<p>
<small>This is also small using <code>&lt;small&gt;</code> tag</small>
</p>
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Headings</h4> <h4 class="m-0 p-2 bg-light rounded">Headings</h4>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
@ -297,119 +321,123 @@
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Alerts</h4> <h4 class="m-0 p-2 bg-light rounded">Alerts</h4>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<alert <alert
alertClass="alert-success" alertClass="alert-success"
alertHeadline="Dismissible Alert" alertHeadline="Dismissible Alert"
alertCopy="This is an example of a DISMISSIBLE alert. It will fade away and content around it will shift when dismissed." alertCopy="This is an example of a DISMISSIBLE alert. It will fade away and content around it will shift when dismissed."
v-bind:isDismissible = "true" v-bind:isDismissible="true"
/> />
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<alert <alert
alertClass="alert-danger" alertClass="alert-danger"
alertHeadline="NON-Dismissible Alert" alertHeadline="NON-Dismissible Alert"
alertCopy="This is an example of a NON-DISMISSIBLE alert." alertCopy="This is an example of a NON-DISMISSIBLE alert."
v-bind:isDismissible = "false" v-bind:isDismissible="false"
/>
</div>
</div>
<div class="row my-2">
<div class="col">
<alert
alertClass="alert-warning"
alertHeadline="Warning Alert"
alertCopy="This is an example of a WARNING alert."
v-bind:isDismissible = "false"
/> />
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<alert <alert
alertClass="alert-info" alertClass="alert-warning"
alertHeadline="Info Alert" alertHeadline="Warning Alert"
alertCopy="This is an example of a INFO alert." alertCopy="This is an example of a WARNING alert."
v-bind:isDismissible = "false" v-bind:isDismissible="false"
/> />
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<alert <alert
alertClass="alert-danger" alertClass="alert-info"
alertHeadline="Danger Alert" alertHeadline="Info Alert"
alertCopy="This is an example of a DANGER alert." alertCopy="This is an example of a INFO alert."
v-bind:isDismissible = "false" v-bind:isDismissible="false"
/> />
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<alert <alert
alertClass="alert-success" alertClass="alert-danger"
alertHeadline="No Body Copy Alert" alertHeadline="Danger Alert"
alertCopy="" alertCopy="This is an example of a DANGER alert."
v-bind:isDismissible = "false" v-bind:isDismissible="false"
/>
</div>
</div>
<div class="row my-2">
<div class="col">
<alert
alertClass="alert-success"
alertHeadline="No Body Copy Alert"
alertCopy=""
v-bind:isDismissible="false"
/> />
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Site Header</h4> <h4 class="m-0 p-2 bg-light rounded">Site Header</h4>
</div> </div>
</div> </div>
<div class="row my-3"> <div class="row my-3">
<div class="col"> <div class="col">
<siteHeader imageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3" /> <siteHeader
imageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3"
/>
</div> </div>
</div> </div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Vehicle Banner</h4> <h4 class="m-0 p-2 bg-light rounded">Vehicle Banner</h4>
</div> </div>
</div> </div>
<div class="row my-3"> <div class="row my-3">
<div class="col"> <div class="col">
<vehicleBanner vehicleImageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3" /> <vehicleBanner
vehicleImageSrc="https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import buttonPrimary from "@/ux-components/button-primary/button-primary"; import buttonPrimary from "@/ux-components/button-primary/button-primary";
import buttonSecondary from "@/ux-components/button-secondary/button-secondary"; import buttonSecondary from "@/ux-components/button-secondary/button-secondary";
import radioCard from "@/ux-components/radio-card/radio-card"; import radioCard from "@/ux-components/radio-card/radio-card";
import listButton from "@/ux-components/list-button/list-button"; import listButton from "@/ux-components/list-button/list-button";
import radio from "@/ux-components/radio/radio"; import radio from "@/ux-components/radio/radio";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import siteHeader from "@/common-components/site-header/site-header"; import siteHeader from "@/common-components/site-header/site-header";
import radioHorizontal from "@/ux-components/radio-horizontal/radio-horizontal"; import radioHorizontal from "@/ux-components/radio-horizontal/radio-horizontal";
export default { export default {
name: "App", name: "App",
components: { components: {
buttonPrimary, buttonPrimary,
buttonSecondary, buttonSecondary,
radioCard, radioCard,
listButton, listButton,
radio, radio,
alert, alert,
vehicleBanner, vehicleBanner,
radioHorizontal, radioHorizontal,
siteHeader, siteHeader,
}, },
data() { data() {
return { return {
years: [2023, 2022, 2021, 2020], years: [2023, 2022, 2021, 2020],
}; };
} },
}; };
</script> </script>

View file

@ -7,11 +7,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col my-3 d-flex align-items-center"> <div class="col my-3 d-flex align-items-center">
<loader <loader sizeInRem="10" loaderColor="blue" loaderPosition="center" />
sizeInRem="10"
loaderColor="blue"
loaderPosition="center"
/>
</div> </div>
</div> </div>
</div> </div>
@ -22,7 +18,7 @@ import loader from "@/ux-components/loader/loader";
export default { export default {
name: "App", name: "App",
components: { components: {
loader loader,
} },
}; };
</script> </script>

View file

@ -2,16 +2,15 @@ import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue"; import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue";
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-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(),
})); }));
describe("vehicle-year.vue", () => { describe("vehicle-year.vue", () => {
test("vehicle-year.vue should render data from CMS", async () => { test("vehicle-year.vue should render data from CMS", async () => {
// Arrange // Arrange
// Our mock data for our call to settleAllPromises // Our mock data for our call to settleAllPromises
@ -19,31 +18,43 @@ describe("vehicle-year.vue", () => {
getPageContent: { getPageContent: {
PageHeaderWidget: [{ HeaderText: "Select a year to get started" }], PageHeaderWidget: [{ HeaderText: "Select a year to get started" }],
RadioQuestionWidget: [{ QuestionText: "What year is your vehicle?" }], RadioQuestionWidget: [{ QuestionText: "What year is your vehicle?" }],
VehicleBannerWidget: [{ GenericVehicleImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3" }], VehicleBannerWidget: [
SiteHeaderWidget: [{ "LogoImage": "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3" }], {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
],
SiteHeaderWidget: [
{
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
],
isCmsContentReady: true, isCmsContentReady: true,
}, },
getVehicleYear: [2023, 2022, 2021], getVehicleYear: [2023, 2022, 2021],
store: { store: {
commit: jest.fn(), commit: jest.fn(),
year: null year: null,
}, },
router: { router: {
push: jest.fn() push: jest.fn(),
} },
} };
// our router information needed. // our router information needed.
const to = { const to = {
query: { query: {
fmgPage: 'vehicle-year' fmgPage: "vehicle-year",
} },
}; };
const mountOptions = getMountOptions(mockData); const mountOptions = getMountOptions(mockData);
// our mock implementation of settleAllPromises // our mock implementation of settleAllPromises
settleAllPromises.mockImplementation(() => { return Promise.resolve(mockData);}); settleAllPromises.mockImplementation(() => {
return Promise.resolve(mockData);
});
// Act // Act
const wrapper = shallowMount(vehicleYear, mountOptions); const wrapper = shallowMount(vehicleYear, mountOptions);
@ -52,7 +63,9 @@ describe("vehicle-year.vue", () => {
// Call our beforeRouteEnter on the component. // Call our beforeRouteEnter on the component.
// This passes (c) => c(wrapper.vm) so that next can be called and our // This passes (c) => c(wrapper.vm) so that next can be called and our
// data can be set. // data can be set.
vehicleYear.beforeRouteEnter.call(wrapper.vm, to, undefined, (c) => c(wrapper.vm)); vehicleYear.beforeRouteEnter.call(wrapper.vm, to, undefined, (c) =>
c(wrapper.vm)
);
await nextTick(); // Wait for the DOM to update. await nextTick(); // Wait for the DOM to update.
@ -60,7 +73,9 @@ describe("vehicle-year.vue", () => {
const header = await wrapper.find(".Header"); const header = await wrapper.find(".Header");
expect(header.attributes("text")).toEqual("Select a year to get started"); expect(header.attributes("text")).toEqual("Select a year to get started");
const yearQuestion = wrapper.findComponent({ name: 'year-question' }); const yearQuestion = wrapper.findComponent({ name: "year-question" });
expect(yearQuestion.attributes("questiontext")).toEqual("What year is your vehicle?"); expect(yearQuestion.attributes("questiontext")).toEqual(
"What year is your vehicle?"
);
}); });
}); });

View file

@ -1,10 +1,16 @@
<template v-if="isCmsContentReady"> <template>
<siteHeader :imageSrc="siteHeaderWidget.LogoImage" /> <siteHeader :imageSrc="siteHeaderWidget.LogoImage" />
<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 :vehicleImageSrc="vehicleBannerWidget.GenericVehicleImage" /> <vehicleBanner
:vehicleImageSrc="vehicleBannerWidget.GenericVehicleImage"
/>
<pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" /> <pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" />
<yearQuestion :questionText="radioQuestionWidgets.QuestionText" :years="vehicleYears" v-model="selectedYear" /> <yearQuestion
:questionText="radioQuestionWidgets.QuestionText"
:years="vehicleYears"
v-model="selectedYear"
/>
</div> </div>
</div> </div>
</template> </template>
@ -29,16 +35,18 @@ export default {
vehicleYears: [], vehicleYears: [],
siteHeaderWidget: {}, siteHeaderWidget: {},
vehicleBannerWidget: {}, vehicleBannerWidget: {},
selectedYear: null selectedYear: null,
}; };
}, },
computed: {}, computed: {},
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const contentPromise = fetchCmsContentForPage(to.query.fmgPage); const contentPromise = fetchCmsContentForPage(to.query.fmgPage);
const getVehicleYearPromise = store.dispatch(storeActions.GET_VEHICLE_YEARS, {}); const getVehicleYearPromise = store.dispatch(
storeActions.GET_VEHICLE_YEARS,
{}
);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
@ -52,13 +60,14 @@ export default {
}, },
]; ];
settleAllPromises(promiseResultMap).then((resultMap) => { settleAllPromises(promiseResultMap).then((resultMap) => {
// 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.pageHeaderWidgets = resultMap.getPageContent.PageHeaderWidget[0]; vm.pageHeaderWidgets = resultMap.getPageContent.PageHeaderWidget[0];
vm.siteHeaderWidget = resultMap.getPageContent.SiteHeaderWidget[0]; vm.siteHeaderWidget = resultMap.getPageContent.SiteHeaderWidget[0];
vm.radioQuestionWidgets = resultMap.getPageContent.RadioQuestionWidget[0]; vm.radioQuestionWidgets =
vm.vehicleBannerWidget = resultMap.getPageContent.VehicleBannerWidget[0]; resultMap.getPageContent.RadioQuestionWidget[0];
vm.vehicleBannerWidget =
resultMap.getPageContent.VehicleBannerWidget[0];
vm.vehicleYears = resultMap.getVehicleYear; vm.vehicleYears = resultMap.getVehicleYear;
}); });
}); });
@ -66,9 +75,9 @@ export default {
watch: { watch: {
selectedYear(year) { selectedYear(year) {
this.$store.commit(this.storeMutations.UPDATE_YEAR, year); this.$store.commit(this.storeMutations.UPDATE_YEAR, year);
this.$router.push('?fmgPage=vehicle-make'); this.$router.push("?fmgPage=vehicle-make");
} },
}, },
components: { components: {
@ -81,15 +90,15 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.select-car { .select-car {
height: calc(100vh - 56px); height: calc(100vh - 56px);
padding: 0 1.5rem; padding: 0 1.5rem;
.car_list { .car_list {
// Height will be determined by overall height of content above list // Height will be determined by overall height of content above list
height: calc(100% - 300px); height: calc(100% - 300px);
padding: 0 1.5rem; padding: 0 1.5rem;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
} }
</style> </style>

View file

@ -1,21 +1,25 @@
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question"; import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
describe('year-question.vue', () => { describe("year-question.vue", () => {
test('year-question should take a prop for years and questionText, and trigger a selectYear function when an option is clicked.', async () => { test("year-question should take a prop for years and questionText, and trigger a selectYear function when an option is clicked.", async () => {
// Act // Act
const wrapper = shallowMount(yearQuestion); const wrapper = shallowMount(yearQuestion);
await wrapper.setProps({ await wrapper.setProps({
questionText: 'What year is your vehicle?', questionText: "What year is your vehicle?",
years: ['2023', '2022', '2021'], years: ["2023", "2022", "2021"],
modelValue: '2020' modelValue: "2020",
});
wrapper.vm.$options.watch.selectedYear.call(wrapper.vm);
// Assert
const radioQuestion = await wrapper.findComponent({name: 'radioQuestion'});
expect(radioQuestion.attributes('questiontext')).toBe("What year is your vehicle?");
expect(radioQuestion.attributes('answers')).toBe("2023,2022,2021");
expect(wrapper.componentVM.modelValue).toBe("2020");
}); });
wrapper.vm.$options.watch.selectedYear.call(wrapper.vm);
// Assert
const radioQuestion = await wrapper.findComponent({
name: "radioQuestion",
});
expect(radioQuestion.attributes("questiontext")).toBe(
"What year is your vehicle?"
);
expect(radioQuestion.attributes("answers")).toBe("2023,2022,2021");
expect(wrapper.componentVM.modelValue).toBe("2020");
});
}); });

View file

@ -1,33 +1,34 @@
<template> <template>
<radioQuestion class="radioQuestion" <radioQuestion
class="radioQuestion"
:questionText="questionText" :questionText="questionText"
:answers="years" :answers="years"
v-model="selectedYear" v-model="selectedYear"
/> />
</template> </template>
<script> <script>
import radioQuestion from "@/common-components/radio-question/radio-question"; import radioQuestion from "@/common-components/radio-question/radio-question";
export default { export default {
name: "year-question", name: "year-question",
data(){ data() {
return { return {
selectedYear: null selectedYear: null,
} };
},
props: {
questionText: String,
years: Array,
modelValue: String,
},
components: {
radioQuestion,
},
watch: {
selectedYear(val) {
this.$emit("update:modelValue", val);
}, },
props: { },
questionText: String,
years: Array,
modelValue: String
},
components: {
radioQuestion,
},
watch: {
selectedYear(val) {
this.$emit('update:modelValue', val)
}
}
}; };
</script> </script>

View file

@ -3,10 +3,9 @@ import { storeMutations } from "@/constants/store-mutations.js";
import { widgetNames } from "@/constants/widget-names.js"; import { widgetNames } from "@/constants/widget-names.js";
export default { export default {
data(){ data() {
return { return {
isCmsContentReady: false };
}
}, },
methods: { methods: {
// dispatchBlockingStoreAction(type, payload) { // dispatchBlockingStoreAction(type, payload) {

View file

@ -1,54 +1,53 @@
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";
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);
expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload); expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
}); });
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(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload); expect(mixIn.methods.$store.dispatch).toBeCalledWith(type, payload);
}); });
}) });
function getMixInInstance({ isDispatchSuccess = true }) { function getMixInInstance({ isDispatchSuccess = true }) {
// Mock Store
const store = {
dispatch: jest.fn(),
};
// Mock Store if (isDispatchSuccess) {
const store = { store.dispatch.mockReturnValue(Promise.resolve());
dispatch: jest.fn() } else {
} store.dispatch.mockReturnValue(Promise.reject());
}
if (isDispatchSuccess) { // Mock Route
store.dispatch.mockReturnValue(Promise.resolve()) const route = {
} else { query: {
store.dispatch.mockReturnValue(Promise.reject()) fmgPage: "test-page",
} },
};
// Mock Route // Attach mocks to mixin
const route = { const baseMixIn = baseMixin;
query: { baseMixIn.methods.$route = route;
fmgPage: 'test-page' baseMixIn.methods.$store = store;
} baseMixIn.methods.storeActions = storeActions;
}; baseMixIn.methods.widgetNames = widgetNames;
// Attach mocks to mixin return baseMixIn;
const baseMixIn = baseMixin;
baseMixIn.methods.$route = route;
baseMixIn.methods.$store = store;
baseMixIn.methods.storeActions = storeActions;
baseMixIn.methods.widgetNames = widgetNames;
return baseMixIn;
} }

View file

@ -1,6 +1,7 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { routingTable } from "@/router/router-constants/routing-table.js";
import ComponentTest from "@/layouts/component-test/component-test.vue"; import ComponentTest from "@/layouts/component-test/component-test.vue";
import LoaderDemo from "@/layouts/loader-demo/loader-demo.vue"; import LoaderDemo from "@/layouts/loader-demo/loader-demo.vue";
import NotFound from "@/layouts/not-found/not-found.vue"; import NotFound from "@/layouts/not-found/not-found.vue";
@ -70,6 +71,59 @@ const router = createRouter({
routes, routes,
}); });
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
// Navigate to the next route, depending on the scenario.
router.navigate = (
scenario,
currentRoute,
optionalQuery = {},
optionalParams = {}
) => {
if (!scenario) {
console.error("No scenario provided. Please review the routing table.");
return;
}
// Match our maps up and navigate if we have a destination.
const matchingScenarioMap = router.getNavigationMap(scenario, currentRoute);
if (matchingScenarioMap.destinationFmgPageValue !== undefined) {
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one.
router.push({
path: "/",
query: Object.assign(optionalQuery, {
fmgPage: matchingScenarioMap.destinationFmgPageValue,
}),
params: optionalParams,
});
} else if (matchingScenarioMap.destinationUrl !== undefined) {
navigateToUrl(matchingScenarioMap.destinationUrl);
}
};
// Get navigation map depeding on the scenario and the current 'page' you're on.
router.getNavigationMap = (scenario, currentRoute) => {
const fmgPageValue = currentRoute.query.fmgPage;
const matchedQueryValue = routingTable
.filter(
(item) =>
item.fmgPageValue === fmgPageValue &&
item.maps.filter((map) => map.scenario === scenario).length > 0
)
.map((m) => m.maps.filter((map) => map.scenario === scenario));
return matchedQueryValue[0][0];
};
//---------------------------------------------------------- Private Functions ----------------------------------------------------------
// Navigate to an external url.
function navigateToUrl(url) {
// possibly show some loading screen in the future here.
window.location.assign(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.
function GetRouteInfoFromPageName(pageName) { function GetRouteInfoFromPageName(pageName) {

View file

@ -0,0 +1,6 @@
const fmgPageValues = {
VEHICLE_YEAR: "vehicle-year",
VEHICLE_MAKE: "vehicle-make",
};
export { fmgPageValues };

View file

@ -0,0 +1,5 @@
const navigationScenarios = {
SELECTED_YEAR: "SELECTED_YEAR",
};
export { navigationScenarios };

View file

@ -0,0 +1,16 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
const routingTable = [
{
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
maps: [
{
scenario: navigationScenarios.SELECTED_YEAR,
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
},
],
},
];
export { routingTable };

View file

@ -32,7 +32,7 @@ export default createStore({
zipCode: null, zipCode: null,
firstName: null, firstName: null,
lastName: null, lastName: null,
licensePlate: null licensePlate: null,
}, },
damage: { damage: {
isRepair: null, isRepair: null,
@ -52,16 +52,16 @@ export default createStore({
isCash: null, isCash: null,
}, },
referralSeqNum: null, referralSeqNum: null,
} },
}, },
applicationUser: { applicationUser: {
experiments: null, experiments: null,
} },
}, },
// See IMPORTANT note at top of "state" declaration. // See IMPORTANT note at top of "state" declaration.
mutations: { mutations: {
updateYear(state, year){ updateYear(state, year) {
state.order.vehicle.year = year; state.order.vehicle.year = year;
}, },
}, },
@ -86,7 +86,7 @@ export default createStore({
method: endpoints.LookupVehicleByVin.method, method: endpoints.LookupVehicleByVin.method,
endpoint: endpoints.LookupVehicleByVin.url, endpoint: endpoints.LookupVehicleByVin.url,
payload: { payload: {
"vin": vin // EX "1J4GW58S4XC541166" vin: vin, // EX "1J4GW58S4XC541166"
}, },
}); });
}, },
@ -135,6 +135,6 @@ export default createStore({
endpoint: relativeUrl, endpoint: relativeUrl,
payload: {}, payload: {},
}); });
} },
}, },
}); });

View file

@ -1,121 +1,131 @@
import store from './index' import store from "./index";
import globalMethods from '@/global-methods' import globalMethods from "@/global-methods";
describe("Actions", () => { describe("Actions", () => {
it("Should return list of years retrieved", async () => { it("Should return list of years retrieved", async () => {
// Arrange // Arrange
let years = []; let years = [];
// Act // Act
globalMethods.callHttpClient = jest.fn(); globalMethods.callHttpClient = jest.fn();
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: [2023,2022,2021]}); return Promise.resolve({ data: [2023, 2022, 2021] });
}); });
await store.dispatch('getVehicleYears') await store.dispatch("getVehicleYears").then((response) => {
.then( (response) => { years = response.data;
years = response.data;
});
// Assert
expect(years[0]).toBe(2023);
}); });
it("Should return list of makes retrieved", async () => { // Assert
// Arrange expect(years[0]).toBe(2023);
let makes = []; });
// Act it("Should return list of makes retrieved", async () => {
globalMethods.callHttpClient.mockImplementation(() => { // Arrange
return Promise.resolve({ data: ['Baic','Honda','Ford']}); let makes = [];
});
await store.dispatch('getVehicleMakes', {year: 2023})
.then( (response) => {
makes = response.data;
});
// Assert // Act
expect(makes[0]).toBe('Baic'); globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["Baic", "Honda", "Ford"] });
});
await store.dispatch("getVehicleMakes", { year: 2023 }).then((response) => {
makes = response.data;
}); });
it("Should return list of models retrieved", async () => { // Assert
// Arrange expect(makes[0]).toBe("Baic");
let models = []; });
// Act it("Should return list of models retrieved", async () => {
globalMethods.callHttpClient.mockImplementation(() => { // Arrange
return Promise.resolve({ data: ['BJ40 (MEX)','Civic','Accord']}); let models = [];
});
await store.dispatch('getVehicleModels', {year: 2023, make: 'Baic'})
.then( (response) => {
models = response.data;
});
// Assert // Act
expect(models[0]).toBe('BJ40 (MEX)'); globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["BJ40 (MEX)", "Civic", "Accord"] });
}); });
await store
.dispatch("getVehicleModels", { year: 2023, make: "Baic" })
.then((response) => {
models = response.data;
});
it("Should return list of styles retrieved", async () => { // Assert
// Arrange expect(models[0]).toBe("BJ40 (MEX)");
let styles = []; });
// Act it("Should return list of styles retrieved", async () => {
globalMethods.callHttpClient.mockImplementation(() => { // Arrange
return Promise.resolve({ data: ['4 DOOR UTILITY','2 DOOR']}); let styles = [];
});
await store.dispatch('getVehicleStyles', {year: 2023, make: 'Baic', model: 'BJ40 (MEX)'})
.then( (response) => {
styles = response.data;
});
// Assert // Act
expect(styles[0]).toBe('4 DOOR UTILITY'); globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["4 DOOR UTILITY", "2 DOOR"] });
}); });
await store
.dispatch("getVehicleStyles", {
year: 2023,
make: "Baic",
model: "BJ40 (MEX)",
})
.then((response) => {
styles = response.data;
});
it("Should return data from url retrieved", async () => { // Assert
// Arrange expect(styles[0]).toBe("4 DOOR UTILITY");
let routeInfo = []; });
// Act it("Should return data from url retrieved", async () => {
globalMethods.callHttpClient.mockImplementation(() => { // Arrange
return Promise.resolve({ data: { let routeInfo = [];
Result: 'Route Info Data'
}});
});
await store.dispatch('getRouteInfo', { pageName: 'vehicle-year' })
.then( (response) => {
routeInfo = response.data.Result;
});
// Assert // Act
expect(routeInfo).toBe('Route Info Data'); globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "Route Info Data",
},
});
}); });
await store
.dispatch("getRouteInfo", { pageName: "vehicle-year" })
.then((response) => {
routeInfo = response.data.Result;
});
it("Should return page data from url retrieved", async () => { // Assert
// Arrange expect(routeInfo).toBe("Route Info Data");
let pageData = []; });
// Act it("Should return page data from url retrieved", async () => {
globalMethods.callHttpClient.mockImplementation(() => { // Arrange
return Promise.resolve({ data: { let pageData = [];
Result: 'Page Info Data'
}});
});
await store.dispatch('getPageData', { pageName: 'vehicle-year' })
.then( (response) => {
pageData = response.data.Result;
});
// Assert // Act
expect(pageData).toBe('Page Info Data'); globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({
data: {
Result: "Page Info Data",
},
});
}); });
}) await store
.dispatch("getPageData", { pageName: "vehicle-year" })
.then((response) => {
pageData = response.data.Result;
});
// Assert
expect(pageData).toBe("Page Info Data");
});
});
describe("Mutations", () => { describe("Mutations", () => {
it("Should update the year property in the store", () => { it("Should update the year property in the store", () => {
// Act // Act
store.commit('updateYear', 2020); store.commit("updateYear", 2020);
// Assert // Assert
expect(store.state.order.vehicle.year).toBe(2020); expect(store.state.order.vehicle.year).toBe(2020);
}); });
}); });

View file

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

View file

@ -1,12 +1,25 @@
<template> <template>
<div class="alert fade show text-center mb-0 py-2 px-3" role="alert" <div
:class="[ isDismissible ? 'alert-dismissible' : '', this.alertClass ]" 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 p-2"
data-bs-dismiss="alert"
aria-label="Close"
> >
<p class="m-0 fw-bold small alert-heading">{{alertHeadline}}</p> <svg
<p class="m-0 text-body small">{{alertCopy}}</p> xmlns="http://www.w3.org/2000/svg"
<button type="button" class="btn-close p-2" data-bs-dismiss="alert" aria-label="Close"> viewBox="0 0 23.7 23.7"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.7 23.7" xml:space="preserve"> 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"/> >
<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> </svg>
</button> </button>
</div> </div>
@ -26,8 +39,8 @@ export default {
alert-warning (yellow) alert-warning (yellow)
alert-info (blue) alert-info (blue)
*/ */
alertClass: String alertClass: String,
} },
}; };
</script> </script>
@ -39,8 +52,8 @@ export default {
.btn-close { .btn-close {
background: none; background: none;
opacity: 1; opacity: 1;
width: .75rem; width: 0.75rem;
height: .75rem; height: 0.75rem;
} }
&.alert-dismissible { &.alert-dismissible {
button { button {

View file

@ -3,14 +3,14 @@
:disabled="isDisabled" :disabled="isDisabled"
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
class="btn btn-primary d-flex align-items-center py-3 px-4" class="btn btn-primary d-flex align-items-center py-3 px-4"
@click='displayComponent' @click="displayComponent"
> >
<span class="m-0">{{ this.buttonText }}</span> <span class="m-0">{{ this.buttonText }}</span>
<loader <loader
class="ms-2" class="ms-2"
v-if="display" v-if="display"
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]" v-bind:class="[this.loaderColor, this.loaderPosition]"
/> />
</button> </button>
</template> </template>
@ -24,7 +24,7 @@ export default {
isDisabled: Boolean, isDisabled: Boolean,
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number,String] sizeInRem: [Number, String],
}, },
data() { data() {
return { return {

View file

@ -3,14 +3,14 @@
:disabled="isDisabled" :disabled="isDisabled"
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
class="btn btn-secondary d-flex align-items-center py-3 px-4" class="btn btn-secondary d-flex align-items-center py-3 px-4"
@click='displayComponent' @click="displayComponent"
> >
<span class="m-0">{{ this.buttonText }}</span> <span class="m-0">{{ this.buttonText }}</span>
<loader <loader
class="ms-2" class="ms-2"
v-if="display" v-if="display"
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]" v-bind:class="[this.loaderColor, this.loaderPosition]"
/> />
</button> </button>
</template> </template>
@ -24,7 +24,7 @@ export default {
isDisabled: Boolean, isDisabled: Boolean,
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number,String] sizeInRem: [Number, String],
}, },
data() { data() {
return { return {

View file

@ -1,7 +1,9 @@
<template> <template>
<div class="current_car_info-text"> <div class="current_car_info-text">
<div class="d-flex align-items-center justify-content-center"> <div class="d-flex align-items-center justify-content-center">
<h2 class="text-center text-dark fs-5 d-block fw-normal mb-0">{{ text }}</h2> <h2 class="text-center text-dark fs-5 d-block fw-normal mb-0">
{{ text }}
</h2>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,21 +1,27 @@
<template> <template>
<div class="d-flex flex-column w-100"> <div class="d-flex flex-column w-100">
<button <button
class="btn list-button d-flex align-items-center justify-content-between py-3 px-4" class="
@click='displayComponent' btn
v-bind:class="[ list-button
this.isError ? 'error' : '', d-flex
]" align-items-center
justify-content-between
py-3
px-4
"
@click="displayComponent"
v-bind:class="[this.isError ? 'error' : '']"
> >
<span class="m-0">{{ this.buttonText }}</span> <span class="m-0">{{ this.buttonText }}</span>
<loader <loader
v-if="display" v-if="display"
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" v-bind:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
v-bind:class="[this.loaderColor, this.loaderPosition]" v-bind:class="[this.loaderColor, this.loaderPosition]"
/> />
</button> </button>
<label class="small mt-1">{{ this.errorText }}</label> <label class="small mt-1">{{ this.errorText }}</label>
</div> </div>
</template> </template>
<script> <script>
@ -27,12 +33,12 @@ export default {
errorText: String, errorText: String,
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number,String] sizeInRem: [Number, String],
}, },
data() { data() {
return { return {
display: false, display: false,
isError: false isError: false,
}; };
}, },
methods: { methods: {

View file

@ -1,7 +1,8 @@
<template> <template>
<div class="loader" <div
v-bind:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" class="loader"
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>
@ -12,18 +13,18 @@ export default {
props: { props: {
sizeInRem: { sizeInRem: {
type: Number, type: Number,
default: 1 default: 1,
}, },
/* Color options: red, green, blue, white, black */ /* Color options: red, green, blue, white, black */
loaderColor: { loaderColor: {
type: String type: String,
}, },
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */ /* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
loaderPosition: { loaderPosition: {
type: String type: String,
} },
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">
@ -31,7 +32,7 @@ export default {
display: flex; display: flex;
//Open an overlay to prevent page interaction //Open an overlay to prevent page interaction
&:before { &:before {
content: ''; content: "";
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -43,7 +44,7 @@ export default {
} }
//Spinner basics //Spinner basics
&:after { &:after {
content: ''; content: "";
mask: url(../../assets/img/icons/spinner.svg); mask: url(../../assets/img/icons/spinner.svg);
mask-size: cover; mask-size: cover;
position: relative; position: relative;
@ -52,7 +53,7 @@ export default {
animation: rotation 1s infinite linear; animation: rotation 1s infinite linear;
@keyframes rotation { @keyframes rotation {
100% { 100% {
transform:rotate(360deg); transform: rotate(360deg);
} }
} }
} }
@ -70,7 +71,8 @@ export default {
left: 1rem; left: 1rem;
} }
//Spinner color //Spinner color
&:after { //Default spinner color (blue) if no other color is specified from the options below &:after {
//Default spinner color (blue) if no other color is specified from the options below
background-color: $blue; background-color: $blue;
} }
&.red:after { &.red:after {

View file

@ -11,7 +11,7 @@ describe("radio.vue", () => {
loaderColor: "blue", loaderColor: "blue",
loaderPosition: "right", loaderPosition: "right",
sizeInRem: "1.5", sizeInRem: "1.5",
errorMessage: 'null' errorMessage: "null",
}, },
}); });
@ -20,14 +20,14 @@ describe("radio.vue", () => {
const label = wrapper.find("label"); const label = wrapper.find("label");
const paragraph = wrapper.find("span"); const paragraph = wrapper.find("span");
await label.trigger('click'); await label.trigger("click");
expect(input.attributes()).toEqual({ expect(input.attributes()).toEqual({
id: "2023", id: "2023",
type: "radio", type: "radio",
value: "2023", value: "2023",
name: "TestGroup", name: "TestGroup",
"aria-required": "true" "aria-required": "true",
}); });
expect(label.attributes()).toEqual({ expect(label.attributes()).toEqual({
@ -35,14 +35,13 @@ describe("radio.vue", () => {
tabindex: "-1", tabindex: "-1",
for: "2023", for: "2023",
class: "d-flex flex-column justify-content-center py-3 px-4 last-item", class: "d-flex flex-column justify-content-center py-3 px-4 last-item",
"aria-checked": "false" "aria-checked": "false",
}); });
expect(label.text()).toEqual('2023'); expect(label.text()).toEqual("2023");
expect(paragraph.text()).toEqual('2023'); expect(paragraph.text()).toEqual("2023");
expect(wrapper.vm.display).toBe(true); expect(wrapper.vm.display).toBe(true);
}); });
}); });

View file

@ -8,13 +8,34 @@
<template> <template>
<!-- IMPORTANT: Refrain from using more than 4 horizontal radio buttons on desktop, 3 on mobile. --> <!-- IMPORTANT: Refrain from using more than 4 horizontal radio buttons on desktop, 3 on mobile. -->
<div class="col radiogroup radio-horizontal d-flex flex-column mb-2"> <div class="col radiogroup radio-horizontal d-flex flex-column mb-2">
<input type="radio" :id="radioID" :name="groupName" :value="radioID" aria-required="true" @keyup.space="displayComponent"/> <input
<label role="radio" tabindex="-1" aria-checked="false" :for="radioID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton" @click='displayComponent'> type="radio"
<span class="m-0" :class="[this.textPosition]">{{radioID}}</span> :id="radioID"
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span> :name="groupName"
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" /> :value="radioID"
aria-required="true"
@keyup.space="displayComponent"
/>
<label
role="radio"
tabindex="-1"
aria-checked="false"
:for="radioID"
class="d-flex flex-column justify-content-center py-3 px-4"
:class="isFirstOrLastButton"
@click="displayComponent"
>
<span class="m-0" :class="[this.textPosition]">{{ radioID }}</span>
<span class="m-0 small" :class="[this.textPosition]">{{
radioLabelSubCopy
}}</span>
<loader
v-if="display"
:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
:class="[this.loaderColor, this.loaderPosition]"
/>
</label> </label>
<p class="small">{{errorMessage}}</p> <p class="small">{{ errorMessage }}</p>
</div> </div>
</template> </template>
@ -23,42 +44,52 @@ import loader from "@/ux-components/loader/loader";
export default { export default {
name: "radioHorizontal", name: "radioHorizontal",
props: { props: {
groupName: String, /* Required, unique for each radio button GROUP */ groupName: String /* Required, unique for each radio button GROUP */,
radioID: String, /* Required, unique for each radio button. Used for button id, label and <label for> */ radioID:
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */ String /* Required, unique for each radio button. Used for button id, label and <label for> */,
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */ radioLabelSubCopy: String /* Optional, used for multi-line radio buttons */,
errorMessage: String, /* Optional, if there is an error message to be displayed */ textPosition:
loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */ String /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */,
loaderPosition: String, /* Specify horizontal position of loader/spinner. Options are center, right, left */ errorMessage:
sizeInRem: [Number,String], /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */ String /* Optional, if there is an error message to be displayed */,
totalInGroup: Number, /* Required, total number of radio buttons in group. Used to tell first and last in group to apply border radius. */ loaderColor:
positionInGroup: Number /* Rquired, position of radio button in group. Example, 1,2,3 */ String /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */,
loaderPosition:
String /* Specify horizontal position of loader/spinner. Options are center, right, left */,
sizeInRem: [
Number,
String,
] /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */,
totalInGroup:
Number /* Required, total number of radio buttons in group. Used to tell first and last in group to apply border radius. */,
positionInGroup:
Number /* Rquired, position of radio button in group. Example, 1,2,3 */,
}, },
data() { data() {
return { return {
isError: false, isError: false,
display: false display: false,
}; };
}, },
methods: { methods: {
displayComponent() { displayComponent() {
this.display = true; this.display = true;
} },
}, },
components: { components: {
loader, loader,
}, },
computed: { computed: {
isFirstOrLastButton() { isFirstOrLastButton() {
let className = ''; let className = "";
if(this.positionInGroup == this.totalInGroup) { if (this.positionInGroup == this.totalInGroup) {
className = 'last-item' className = "last-item";
} else if(this.positionInGroup == 1) { } else if (this.positionInGroup == 1) {
className = 'first-item' className = "first-item";
} }
return className; return className;
} },
} },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@ -103,12 +134,12 @@ export default {
} }
} }
&.first-item { &.first-item {
border-bottom-left-radius: .5rem; border-bottom-left-radius: 0.5rem;
border-top-left-radius: .5rem; border-top-left-radius: 0.5rem;
} }
&.last-item { &.last-item {
border-bottom-right-radius: .5rem; border-bottom-right-radius: 0.5rem;
border-top-right-radius: .5rem; border-top-right-radius: 0.5rem;
} }
} }
} }

View file

@ -11,7 +11,7 @@ describe("radio.vue", () => {
loaderColor: "blue", loaderColor: "blue",
loaderPosition: "right", loaderPosition: "right",
sizeInRem: "1.5", sizeInRem: "1.5",
errorMessage: 'null' errorMessage: "null",
}, },
}); });
@ -20,14 +20,14 @@ describe("radio.vue", () => {
const label = wrapper.find("label"); const label = wrapper.find("label");
const paragraph = wrapper.find("span"); const paragraph = wrapper.find("span");
await label.trigger('click'); await label.trigger("click");
expect(input.attributes()).toEqual({ expect(input.attributes()).toEqual({
id: "2023", id: "2023",
type: "radio", type: "radio",
value: "2023", value: "2023",
name: "TestGroup", name: "TestGroup",
"aria-required": "true" "aria-required": "true",
}); });
expect(label.attributes()).toEqual({ expect(label.attributes()).toEqual({
@ -35,14 +35,13 @@ describe("radio.vue", () => {
tabindex: "-1", tabindex: "-1",
for: "2023", for: "2023",
class: "d-flex flex-column justify-content-center py-3 px-4", class: "d-flex flex-column justify-content-center py-3 px-4",
"aria-checked": "false" "aria-checked": "false",
}); });
expect(label.text()).toEqual('2023'); expect(label.text()).toEqual("2023");
expect(paragraph.text()).toEqual('2023'); expect(paragraph.text()).toEqual("2023");
expect(wrapper.vm.display).toBe(true); expect(wrapper.vm.display).toBe(true);
}); });
}); });

View file

@ -7,14 +7,34 @@
<!-- Example --> <!-- Example -->
<!-- <h3 class="visually-hidden" id="demo-radio-group">Select Vehicle Year</h3> --> <!-- <h3 class="visually-hidden" id="demo-radio-group">Select Vehicle Year</h3> -->
<div class="radiogroup radio-list-button d-flex flex-column w-100 mb-2"> <div class="radiogroup radio-list-button d-flex flex-column w-100 mb-2">
<input type="radio" :id="radioID" :name="groupName" :value="radioID" aria-required="true" @keyup.space="displayComponent"> <input
<label role="radio" tabindex="-1" aria-checked="false" :for="radioID" class="d-flex flex-column justify-content-center py-3 px-4" @click='displayComponent'> type="radio"
<span class="m-0" :class="[this.textPosition]">{{radioID}}</span> :id="radioID"
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span> :name="groupName"
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" /> :value="radioID"
</label> aria-required="true"
<p class="small">{{errorMessage}}</p> @keyup.space="displayComponent"
</div> />
<label
role="radio"
tabindex="-1"
aria-checked="false"
:for="radioID"
class="d-flex flex-column justify-content-center py-3 px-4"
@click="displayComponent"
>
<span class="m-0" :class="[this.textPosition]">{{ radioID }}</span>
<span class="m-0 small" :class="[this.textPosition]">{{
radioLabelSubCopy
}}</span>
<loader
v-if="display"
:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
:class="[this.loaderColor, this.loaderPosition]"
/>
</label>
<p class="small">{{ errorMessage }}</p>
</div>
</template> </template>
<script> <script>
@ -22,19 +42,27 @@ import loader from "@/ux-components/loader/loader";
export default { export default {
name: "radioList", name: "radioList",
props: { props: {
groupName: String, /* Required, unique for each radio button GROUP */ groupName: String /* Required, unique for each radio button GROUP */,
radioID: String, /* Required, unique for each radio button. Used for button id, label and <label for> */ radioID:
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */ String /* Required, unique for each radio button. Used for button id, label and <label for> */,
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */ radioLabelSubCopy: String /* Optional, used for multi-line radio buttons */,
errorMessage: String, /* Optional, if there is an error message to be displayed */ textPosition:
loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */ String /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */,
loaderPosition: String, /* Specify horizontal position of loader/spinner. Options are center, right, left */ errorMessage:
sizeInRem: [Number,String], /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */ String /* Optional, if there is an error message to be displayed */,
loaderColor:
String /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */,
loaderPosition:
String /* Specify horizontal position of loader/spinner. Options are center, right, left */,
sizeInRem: [
Number,
String,
] /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */,
}, },
data() { data() {
return { return {
isError: false, isError: false,
display: false display: false,
}; };
}, },
methods: { methods: {