Merge remote-tracking branch 'origin/develop' into archived/apiRefactoring-encapsulated

This commit is contained in:
Mark Harris 2021-12-13 14:09:01 -05:00
commit 904df04ac7
41 changed files with 860 additions and 616 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

@ -1,13 +1,13 @@
<template> <template>
<div class="radio_question"> <div class="radio_question">
<div class="needed_car_info mt-5 mb-2 d-flex"> <div class="needed_car_info mt-6 mb-4 d-flex">
<span class="text-center fs-6 fw-bold w-100 needed_car_info-text">{{ <span class="text-center fs-6 fw-bold w-100 needed_car_info-text">{{
questionText questionText
}}</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"> <fieldset class="car_list overflow-scroll position-absolute container-fluid w-100 pt-1 px-5 py-0" role="radiogroup">
<h3 class="visually-hidden" id="select-year-radio-group">*</h3> <legend class="sr-only">{{groupName}}</legend>
<radio v-for="answer in answers" :key="answer" <radio v-for="answer in answers" :key="answer"
:radioID="answer" :radioID="answer"
@click="chooseAnswer(answer)" @click="chooseAnswer(answer)"
@ -15,11 +15,13 @@
loaderPosition="right" loaderPosition="right"
sizeInRem="1.5" sizeInRem="1.5"
data-test="radio" data-test="radio"
groupName="radio-list" :groupName="groupName"
textPosition="text-start" textPosition="text-start"
isRequired=true
:value="modelValue" :value="modelValue"
screenReaderOnlyText="(opens new window)"
/> />
</div> </fieldset>
</div> </div>
</div> </div>
</template> </template>
@ -31,12 +33,13 @@ export default {
props: { props: {
questionText: String, questionText: String,
answers: Array, answers: Array,
modelValue: String modelValue: String,
groupName: 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

@ -1,5 +1,5 @@
<template> <template>
<div class="vehicle_banner mb-3 text-center"> <div class="vehicle_banner mb-4 text-center">
<img <img
class="vehicle-image img-fluid blurrycar" class="vehicle-image img-fluid blurrycar"
:src="vehicleImageSrc" :src="vehicleImageSrc"
@ -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 {
const promiseName = promiseNames[i][1].resultKey; resultMap[promiseName] = results[i].value.data;
}
// 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) { return resultMap;
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,35 +2,35 @@
<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">
@ -38,84 +38,106 @@
<buttonPrimary <buttonPrimary
buttonText="Primary" buttonText="Primary"
loaderColor="white" loaderColor="white"
sizeInRem="1" 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">
<div class="col my-3 d-flex align-items-center">
<buttonBack/>
</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="sr-only" 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"
isRequired=true
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
<radio <radio
groupName="demo-1" groupName="demo-1"
ariaLabelBy="vehicle-year" ariaLabelBy="vehicle-year"
radioID="2020" radioID="2020"
isRequired=true
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
<radio <radio
groupName="demo-1" groupName="demo-1"
ariaLabelBy="vehicle-year" ariaLabelBy="vehicle-year"
radioID="2019" radioID="2019"
isRequired=true
textPosition="text-start" textPosition="text-start"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
</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="sr-only" id="demo-2-radio-group">
Select Vehicle Year
</h3>
<radio <radio
groupName="demo-2" groupName="demo-2"
ariaLabelBy="vehicle-make" ariaLabelBy="vehicle-make"
@ -125,6 +147,7 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
<radio <radio
groupName="demo-2" groupName="demo-2"
@ -135,6 +158,7 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
<radio <radio
groupName="demo-2" groupName="demo-2"
@ -145,19 +169,26 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
</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="sr-only" id="demo-3-radio-group">
Multi-Line Centered
</h3>
<radio <radio
groupName="demo-3" groupName="demo-3"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
@ -167,6 +198,7 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
<radio <radio
groupName="demo-3" groupName="demo-3"
@ -177,6 +209,7 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
<radio <radio
groupName="demo-3" groupName="demo-3"
@ -187,19 +220,26 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText="(opens new window)"
/> />
</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="sr-only" id="demo-4-radio-group">
Select Vehicle Year
</h3>
<radioHorizontal <radioHorizontal
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
@ -211,6 +251,7 @@
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" v-bind:totalInGroup="3"
v-bind:positionInGroup="1" v-bind:positionInGroup="1"
screenReaderOnlyText="(opens new window)"
/> />
<radioHorizontal <radioHorizontal
groupName="demo-4" groupName="demo-4"
@ -223,6 +264,7 @@
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" v-bind:totalInGroup="3"
v-bind:positionInGroup="2" v-bind:positionInGroup="2"
screenReaderOnlyText="(opens new window)"
/> />
<radioHorizontal <radioHorizontal
groupName="demo-4" groupName="demo-4"
@ -235,12 +277,13 @@
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" v-bind:totalInGroup="3"
v-bind:positionInGroup="3" v-bind:positionInGroup="3"
screenReaderOnlyText="(opens new window)"
/> />
</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 +293,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 +344,125 @@
</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 buttonBack from "@/ux-components/button-back/button-back";
import listButton from "@/ux-components/list-button/list-button"; import radioCard from "@/ux-components/radio-card/radio-card";
import radio from "@/ux-components/radio/radio"; import listButton from "@/ux-components/list-button/list-button";
import alert from "@/ux-components/alert/alert"; import radio from "@/ux-components/radio/radio";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import alert from "@/ux-components/alert/alert";
import siteHeader from "@/common-components/site-header/site-header"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import radioHorizontal from "@/ux-components/radio-horizontal/radio-horizontal"; import siteHeader from "@/common-components/site-header/site-header";
export default { import radioHorizontal from "@/ux-components/radio-horizontal/radio-horizontal";
name: "App", export default {
components: { name: "App",
buttonPrimary, components: {
buttonSecondary, buttonPrimary,
radioCard, buttonSecondary,
listButton, buttonBack,
radio, radioCard,
alert, listButton,
vehicleBanner, radio,
radioHorizontal, alert,
siteHeader, vehicleBanner,
}, radioHorizontal,
data() { siteHeader,
return { },
years: [2023, 2022, 2021, 2020], data() {
}; return {
} 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,12 @@
<template v-if="isCmsContentReady"> <template v-if="isCmsContentReady">
<siteHeader :imageSrc="siteHeaderWidget.LogoImage" /> <div class="container-fluid shadow rounded-3 p-0">
<div class="select-car"> <siteHeader :imageSrc="siteHeaderWidget.LogoImage" />
<div class="select-car-form rounded text-center"> <div class="select-car">
<vehicleBanner :vehicleImageSrc="vehicleBannerWidget.GenericVehicleImage" /> <div class="select-car-form rounded text-center">
<pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" /> <vehicleBanner :vehicleImageSrc="vehicleBannerWidget.GenericVehicleImage" />
<yearQuestion v-model="selectedYear" ref="yearQuestion" /> <pageHeader :text="pageHeaderWidgets.HeaderText" class="Header" />
<yearQuestion v-model="selectedYear" ref="yearQuestion" />
</div>
</div> </div>
</div> </div>
</template> </template>
@ -25,13 +27,13 @@ export default {
pageHeaderWidgets: {}, pageHeaderWidgets: {},
siteHeaderWidget: {}, siteHeaderWidget: {},
vehicleBannerWidget: {}, vehicleBannerWidget: {},
selectedYear: null selectedYear: null,
}; };
}, },
computed: {}, computed: {},
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData(); const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
@ -61,8 +63,8 @@ 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');
} }
}, },
@ -76,15 +78,13 @@ 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;
.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; -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,44 +1,47 @@
<template> <template>
<radioQuestion class="radioQuestion" <radioQuestion class="radioQuestion"
:questionText="questionText" :questionText="questionText"
:answers="years" :answers="years"
groupName="Choose Vehicle Year"
v-model="selectedYear" v-model="selectedYear"
/> />
</template> </template>
<script> <script>
// Components
import radioQuestion from "@/common-components/radio-question/radio-question"; import radioQuestion from "@/common-components/radio-question/radio-question";
// Supporting files
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
export default { export default {
name: "year-question", name: "year-question",
data(){ data() {
return { return {
questionText: null, questionText: null,
selectedYear: null, selectedYear: null,
years: Array, years: Array,
}
},
props: {
modelValue: String
},
components: {
radioQuestion,
},
methods: {
loadInitialData() {
return store.dispatch(storeActions.GET_VEHICLE_YEARS, {});
},
initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText;
this.years = initialData;
}
},
watch: {
selectedYear(val) {
this.$emit('update:modelValue', val)
}
} }
},
props: {
modelValue: String,
},
components: {
radioQuestion,
},
methods: {
loadInitialData() {
return store.dispatch(storeActions.GET_VEHICLE_YEARS, {});
},
initializeComponent(cmsContent, initialData) {
this.questionText = cmsContent.QuestionText;
this.years = initialData;
}
},
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

@ -25,5 +25,14 @@ body {
.container-fluid { .container-fluid {
overflow-x: hidden; overflow-x: hidden;
} }
.sr-only {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
} }
// Hide horizontal scrollbar // Hide horizontal scrollbar

View file

@ -54,6 +54,7 @@ $gray-200: #E3E4E4;// Used in theme
$gray-300: #D2D4D4; $gray-300: #D2D4D4;
$gray: #B0B3B3;// Default Gray $gray: #B0B3B3;// Default Gray
$gray-500: #8E9292;// Used in theme $gray-500: #8E9292;// Used in theme
$gray-550: #727676;// Used in theme
$gray-600: #4D5151;// Used in theme $gray-600: #4D5151;// Used in theme
$gray-700: #303333;// Used in theme $gray-700: #303333;// Used in theme
$gray-800: #222424;// Used in theme $gray-800: #222424;// Used in theme
@ -130,6 +131,7 @@ $h5-font-size: $font-size-base * 1.25;
$h6-font-size: $font-size-base * .875; $h6-font-size: $font-size-base * .875;
//Border Radius //Border Radius
// Helper classes are rounded, rounded-1, rounded-2, rounded-3
$border-radius: .25rem; $border-radius: .25rem;
$border-radius-sm: .2rem; $border-radius-sm: .2rem;
$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course. $border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course.

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

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

View file

@ -0,0 +1,65 @@
<template>
<button class="back-button-wrapper d-flex p-0">
<div class="button-back">
<div class="arrow-left"></div>
<div class="box"></div>
</div>
</button>
</template>
<script>
export default {
name: "buttonBack"
};
</script>
<style lang="scss">
.back-button-wrapper {
border: none;
background: transparent;
.button-back {
display: inline-flex;
position: relative;
height: 18px;
width: 23px;
overflow: hidden;
.arrow-left {
position: absolute;
width: 18px;
height: 18px;
left: 4px;
background-color: $gray-550;
transform: rotate(45deg);
border-radius: 4px;
}
.box {
content: "";
position: absolute;
background: $gray-550;
height: 100%;
width: 11px;
display: flex;
justify-content: center;
align-items: center;
left: 12px;
border-radius: 5px;
&:before, &:after {
position: absolute;
left: 1px;
top: 3px;
content: " ";
height: 12px;
width: 2px;
background-color: $white;
border-radius: 2px;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
}
}
}
</style>

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,7 @@
<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 fs-5 d-block fw-normal mb-0">{{ text }}</h2>
</div> </div>
</div> </div>
</template> </template>
@ -14,3 +14,9 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss">
h2 {
color: $gray-550;
}
</style>

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,5 +1,7 @@
<template> <template>
<div class="loader" <div class="loader"
role="alert"
aria-label="Loading new page"
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]"
></div> ></div>
@ -12,18 +14,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 +33,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 +45,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 +54,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 +72,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

@ -1,10 +1,4 @@
<!-- See the component-test.vue page for example implementation --> <!-- See the component-test.vue page for example implementation -->
<!-- role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
<!-- Example: -->
<!-- <div role="radiogroup" aria-labelledby="demo-radio-group" class="col my-3 d-flex align-items-center flex-column"> -->
<!-- An h3 with id must be included just before the opening radio button group. ***The id must match the aria-labelledby of the parent div.*** -->
<!-- Example -->
<!-- <h3 class="visually-hidden" id="demo-radio-group">Select Vehicle Year</h3> -->
<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">
@ -12,9 +6,10 @@
<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'> <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" :class="[this.textPosition]">{{radioID}}</span>
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span> <span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" /> <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>
@ -26,6 +21,7 @@ export default {
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: String, /* Required, unique for each radio button. Used for button id, label and <label for> */
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */ radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */
screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */ textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */
errorMessage: String, /* Optional, if there is an error message to be displayed */ errorMessage: 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 */ loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */
@ -37,28 +33,28 @@ export default {
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 +99,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": "false",
}); });
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

@ -1,16 +1,11 @@
<template> <template>
<!-- See the component-test.vue page for example implementation --> <!-- See the component-test.vue page for example implementation -->
<!-- role="radiogroup" and aria-labelledby must be included in the parent component for the radio group -->
<!-- Example: -->
<!-- <div role="radiogroup" aria-labelledby="demo-radio-group" class="col my-3 d-flex align-items-center flex-column"> -->
<!-- An h3 with id must be included just before the opening radio button group. ***The id must match the aria-labelledby of the parent div.*** -->
<!-- Example -->
<!-- <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 type="radio" :id="radioID" :name="groupName" :value="radioID" :aria-required="isRequired" @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" @click='displayComponent'> <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" :class="[this.textPosition]">{{radioID}}</span>
<span class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span> <span v-if="radioLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{radioLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
<loader v-if="display" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" /> <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>
@ -24,6 +19,8 @@ export default {
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: String, /* Required, unique for each radio button. Used for button id, label and <label for> */
isRequired: Boolean, /* Optional, default is false */
screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */
radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */ radioLabelSubCopy: String, /* Optional, used for multi-line radio buttons */
textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */ textPosition: String, /* Optional, use Bootstrap classes: text-start, text-center, text-end. Default (empty) is text-start */
errorMessage: String, /* Optional, if there is an error message to be displayed */ errorMessage: String, /* Optional, if there is an error message to be displayed */
@ -34,7 +31,7 @@ export default {
data() { data() {
return { return {
isError: false, isError: false,
display: false display: false,
}; };
}, },
methods: { methods: {
@ -44,6 +41,6 @@ export default {
}, },
components: { components: {
loader, loader,
}, }
}; };
</script> </script>