Merge remote-tracking branch 'origin/develop' into feature/CSR-77

This commit is contained in:
Max 2022-01-10 15:39:14 -05:00
commit 21c5316950
23 changed files with 532 additions and 233 deletions

View file

@ -1,8 +1,7 @@
<template> <template>
<button <button
v-if="backButtonAction" @click="handleClick"
@click="backButtonAction" class="back-button-wrapper p-3"
class="back-button-wrapper p-2"
:aria-label="backButtonAccessibleText" :aria-label="backButtonAccessibleText"
> >
<div class="button-back"> <div class="button-back">
@ -20,12 +19,13 @@ export default {
type: String, type: String,
required: true, required: true,
default: "", default: "",
},
backButtonAction: {
type: Function,
default: null,
} }
}, },
methods: {
handleClick() {
this.$emit('click-event');
}
}
}; };
</script> </script>

View file

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

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="button_question"> <div class="button_question">
<div class="needed_car_info mt-6 mb-4 d-flex"> <div class="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">{{
questionText questionText
}}</span> }}</span>
</div> </div>
@ -32,6 +32,7 @@ import listButton from "@/ux-components/list-button/list-button";
export default { export default {
name: "buttonQuestion", name: "buttonQuestion",
props: { props: {
isMultiSelect: Boolean,
questionText: String, questionText: String,
answers: Array, answers: Array,
modelValue: String, modelValue: String,
@ -58,4 +59,4 @@ export default {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
} }
</style> </style>

View file

@ -6,16 +6,17 @@
{{ text }} {{ text }}
</span> </span>
<buttonBack <buttonBack
v-if="hasBackButton" v-if="hasBackButton"
:backButtonAction="backButtonAction" :backButtonAccessibleText="backButtonAccessibleText"
/> @click-event="clickEvent"
</h5> />
</h5>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import buttonBack from "@/ux-components/button-back/button-back"; import buttonBack from "@/common-components/button-back/button-back";
export default { export default {
name: "FunnelSubHeader", name: "FunnelSubHeader",
@ -26,14 +27,15 @@ export default {
}, },
props: { props: {
hasBackButton: Boolean, hasBackButton: Boolean,
backButtonUrl: String,
backButtonAccessibleText: String, backButtonAccessibleText: String,
backButtonAction: Function,
}, },
components: { components: {
buttonBack, buttonBack,
}, },
methods: { methods: {
clickEvent() {
this.$emit('click-event');
},
initializeComponent(cmsContent) { initializeComponent(cmsContent) {
this.text = cmsContent.HeaderText; this.text = cmsContent.HeaderText;
} }

View file

@ -10,16 +10,20 @@ export function fetchCmsContentForPage(fmgPage) {
response.data.Result.forEach((widget) => { response.data.Result.forEach((widget) => {
let widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Type); let widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Name);
if (Object.values(widgetNames).includes(widgetWithReplacements.Type)) { // If we already have this widget, push it on the collection
// If we already have this widget, push it on the collection if (widgetWithReplacements.Name in pageDataFromCms) {
if (widgetWithReplacements.Type in pageDataFromCms) { pageDataFromCms[widgetWithReplacements.Name].push(widgetWithReplacements.Model);
pageDataFromCms[widgetWithReplacements.Type].push(widgetWithReplacements.Model); return;
return; }
}
pageDataFromCms[widgetWithReplacements.Type] = [widgetWithReplacements.Model]; pageDataFromCms[widgetWithReplacements.Name] = [widgetWithReplacements.Model];
});
Object.keys(pageDataFromCms).forEach((key) => {
if (pageDataFromCms[key].length === 1){
pageDataFromCms[key] = pageDataFromCms[key][0];
} }
}); });
@ -66,10 +70,10 @@ function mapStringToState(str) {
// Parent function for processWidgetItemForReplacement. This will loop through the parent // Parent function for processWidgetItemForReplacement. This will loop through the parent
// object and pass any objects that need additional processing to the processWidgetItemForReplacement function. // object and pass any objects that need additional processing to the processWidgetItemForReplacement function.
function findAndReplaceGlobalStateValues(widgetModel, widgetType) { function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
const objWithReplacements = { const objWithReplacements = {
Type: widgetType, Name: widgetName,
Model: {} Model: {}
}; };

View file

@ -15,19 +15,19 @@ describe("cms-content-helper.js", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
{ {
Type: "FunnelSubHeaderWidget", Name: "FunnelHeaderWidget",
Model: { Model: {
HeaderText: "Select a year to get started", HeaderText: "Select a year to get started",
}, },
}, },
{ {
Type: "FunnelSubHeaderWidget", Name: "FunnelSubHeaderWidget",
Model: { Model: {
HeaderText: "Select a model", HeaderText: "Select a model",
}, },
}, },
{ {
Type: "RadioQuestionWidget", Name: "VehicleYearQuestion",
Model: { Model: {
QuestionText: "What year is your vehicle?", QuestionText: "What year is your vehicle?",
}, },
@ -40,7 +40,7 @@ describe("cms-content-helper.js", () => {
// Act // Act
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget[0].HeaderText).toEqual( expect(response.FunnelHeaderWidget.HeaderText).toEqual(
"Select a year to get started" "Select a year to get started"
); );
}); });
@ -53,7 +53,7 @@ describe("cms-content-helper.js", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
{ {
Type: "FunnelSubHeaderWidget", Name: "FunnelSubHeaderWidget",
Model: { Model: {
HeaderText: "{globalState:order.vehicle.year}", HeaderText: "{globalState:order.vehicle.year}",
}, },
@ -65,7 +65,7 @@ describe("cms-content-helper.js", () => {
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget[0].HeaderText).toEqual( expect(response.FunnelSubHeaderWidget.HeaderText).toEqual(
"2019" "2019"
); );
}); });
@ -78,13 +78,13 @@ describe("cms-content-helper.js", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
{ {
Type: "FunnelSubHeaderWidget", Name: "FunnelSubHeaderWidget",
Model: { Model: {
HeaderText: "{globalState:order.vehicle.year}", HeaderText: "{globalState:order.vehicle.year}",
}, },
}, },
{ {
Type: "RadioQuestionWidget", Name: "VehicleYearQuestion",
Model: { Model: {
ExampleText: "My widget value!", ExampleText: "My widget value!",
}, },
@ -96,8 +96,8 @@ describe("cms-content-helper.js", () => {
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget[0].HeaderText).toEqual("2019"); expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019");
expect(response.RadioQuestionWidget[0].ExampleText).toEqual("My widget value!"); expect(response.VehicleYearQuestion.ExampleText).toEqual("My widget value!");
}); });
}); });
@ -109,13 +109,13 @@ describe("cms-content-helper.js", () => {
const cmsMockData = { const cmsMockData = {
Result: [ Result: [
{ {
Type: "FunnelSubHeaderWidget", Name: "FunnelSubHeaderWidget",
Model: { Model: {
HeaderText: "{globalState:order.vehicle.year}", HeaderText: "{globalState:order.vehicle.year}",
}, },
}, },
{ {
Type: "RadioQuestionWidget", Name: "VehicleMakeQuestion",
Model: { Model: {
OtherObjectInside: { OtherObjectInside: {
ExampleText: "{globalState:order.vehicle.make}", ExampleText: "{globalState:order.vehicle.make}",
@ -130,8 +130,8 @@ describe("cms-content-helper.js", () => {
fetchCmsContentForPage("testPage").then((response) => { fetchCmsContentForPage("testPage").then((response) => {
// Assert // Assert
expect(response.FunnelSubHeaderWidget[0].HeaderText).toEqual("2019"); expect(response.FunnelSubHeaderWidget.HeaderText).toEqual("2019");
expect(response.RadioQuestionWidget[0].OtherObjectInside.ExampleText).toEqual("Acura"); expect(response.VehicleMakeQuestion.OtherObjectInside.ExampleText).toEqual("Acura");
}); });
}); });

View file

@ -185,24 +185,6 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div class="row g-2">
<div class="col">
<h6 class="mx-2 my-4">With error</h6>
<fieldset>
<legend class="sr-only">Radio button with error state</legend>
<listCard
isRadioHorizontal
buttonImage="windshield-damage.svg"
buttonLabel="Windshield"
altText=""
buttonID="Horizontal Radio Button Error State"
groupID="radio-demo-5"
buttonLabelSubCopy="Description"
hasError
/>
</fieldset>
</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 - Single-Line</h4> <h4 class="m-0 p-2 bg-light rounded">List Button - Single-Line</h4>
@ -227,8 +209,6 @@
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
screenReaderOnlyText=" opens new window" screenReaderOnlyText=" opens new window"
hasError
errorMessage="Error"
/> />
<listButton <listButton
isMultiSelect isMultiSelect
@ -495,7 +475,6 @@
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
buttonID="1" buttonID="1"
buttonLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
@ -509,7 +488,6 @@
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
buttonID="2" buttonID="2"
buttonLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
@ -523,7 +501,6 @@
groupName="demo-4" groupName="demo-4"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
buttonID="3" buttonID="3"
buttonLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
@ -546,8 +523,8 @@
groupName="demo-4-checkbox" groupName="demo-4-checkbox"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
buttonID="4" buttonID="4"
buttonLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderEnabled=true
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
@ -559,8 +536,8 @@
groupName="demo-4-checkbox" groupName="demo-4-checkbox"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
buttonID="5" buttonID="5"
buttonLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderEnabled=true
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
@ -572,8 +549,8 @@
groupName="demo-4-checkbox" groupName="demo-4-checkbox"
ariaLabelBy="vehicle-model" ariaLabelBy="vehicle-model"
buttonID="6" buttonID="6"
buttonLabelSubCopy=""
textPosition="text-center" textPosition="text-center"
loaderEnabled=true
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
@ -739,7 +716,7 @@
<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 buttonBack from "@/ux-components/button-back/button-back"; import buttonBack from "@/common-components/button-back/button-back";
import listCard from "@/ux-components/list-card/list-card"; import listCard from "@/ux-components/list-card/list-card";
import listButton from "@/ux-components/list-button/list-button"; import listButton from "@/ux-components/list-button/list-button";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";

View file

@ -17,10 +17,10 @@ describe("vehicle-make.vue", () => {
test("Make question component is initized with api data", async (done) => { test("Make question component is initized with api data", async (done) => {
//Arrange //Arrange
const radioQuestionCmsContent = { QuestionText: "What make is your vehicle?" }; const vehicleMakeQuestionCmsContent = { QuestionText: "What make is your vehicle?" };
const makeQuestionInitialData = ["honda", "ford", "dodge"]; const makeQuestionInitialData = ["honda", "ford", "dodge"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks( {
radioQuestionCmsContent: radioQuestionCmsContent, vehicleMakeQuestionCmsContent: vehicleMakeQuestionCmsContent,
makeQuestionInitialData: makeQuestionInitialData, makeQuestionInitialData: makeQuestionInitialData,
} ); } );
@ -29,7 +29,7 @@ describe("vehicle-make.vue", () => {
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, makeQuestionInitialData); expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleMakeQuestionCmsContent, makeQuestionInitialData);
done(); done();
}); });
}); });
@ -122,7 +122,7 @@ describe("vehicle-make.vue", () => {
}); });
function setupMocks({ function setupMocks({
radioQuestionCmsContent = {}, vehicleMakeQuestionCmsContent = {},
makeQuestionInitialData = {}, makeQuestionInitialData = {},
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
@ -131,20 +131,18 @@ function setupMocks({
//Mock api responses //Mock api responses
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: [pageHeaderWidgetHeaderText], FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
RadioQuestionWidget: [radioQuestionCmsContent], VehicleMakeQuestion: vehicleMakeQuestionCmsContent,
VehicleBannerWidget: [ VehicleBannerWidget:
{ {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}, },
], FunnelHeaderWidget:
FunnelHeaderWidget: [
{ {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}, },
],
}, },
makeQuestionInitialData: makeQuestionInitialData, makeQuestionInitialData: makeQuestionInitialData,
}; };

View file

@ -8,8 +8,8 @@
ref="funnelSubHeader" ref="funnelSubHeader"
:hasBackButton="true" :hasBackButton="true"
backButtonAccessibleText="Change Vehicle Year" backButtonAccessibleText="Change Vehicle Year"
:backButtonAction="backButtonAction" @click-event="backButtonAction"
class="Header" class="Header"
/> />
<makeQuestion v-model="selectedMake" ref="makeQuestion" /> <makeQuestion v-model="selectedMake" ref="makeQuestion" />
</div> </div>
@ -55,16 +55,16 @@ 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.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget[0]); vm.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget);
vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget[0]); vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget);
vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget[0]); vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget);
vm.$refs.makeQuestion.initializeComponent(resultMap.cmsContent.RadioQuestionWidget[0], resultMap.makeQuestionInitialData); vm.$refs.makeQuestion.initializeComponent(resultMap.cmsContent.VehicleMakeQuestion, resultMap.makeQuestionInitialData);
}); });
}); });
}, },
methods: { methods: {
backButtonAction: function () { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
} }

View file

@ -16,7 +16,6 @@ import baseMixin from "@/mixins/base-mixin.js";
export default { export default {
name: "model-question", name: "model-question",
mixins: [baseMixin],
data() { data() {
return { return {
questionText: null, questionText: null,

View file

@ -7,44 +7,35 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
// 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-model.vue", () => { describe("vehicle-model.vue", () => {
test("Model question component is initized with api data", async (done) => { test("Model question component is initized with api data", async (done) => {
//Arange //Arange
const radioQuestionCmsContent = { QuestionText: "What model is your vehicle?" }; const buttonQuestionContent = { QuestionText: "What model is your vehicle?" };
const modelQuestionInitialData = ["accord", "civic", "insight"]; const modelQuestionInitialData = ["accord", "civic", "insight"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks( {
radioQuestionCmsContent: radioQuestionCmsContent, buttonQuestionContent: buttonQuestionContent,
modelQuestionInitialData: modelQuestionInitialData, modelQuestionInitialData: modelQuestionInitialData,
} ); } );
//Act //Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, modelQuestionInitialData); expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(buttonQuestionContent, modelQuestionInitialData);
done(); done();
}); });
}); });
}); });
describe("vehicle-model.vue", () => { describe("vehicle-model.vue", () => {
test("Page header is initailized with api data", async (done) => { test("Page header is initailized with api data", async (done) => {
//Arrange //Arrange
const pageHeaderWidgetHeaderText = "Select a model to get started"; const pageHeaderWidgetHeaderText = "Select a model to get started";
const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText }); const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText });
//Act //Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText); expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(pageHeaderWidgetHeaderText);
@ -52,20 +43,16 @@ describe("vehicle-model.vue", () => {
}); });
}); });
}); });
describe("vehicle-model.vue", () => { describe("vehicle-model.vue", () => {
test("Page logo image is initailized with api data", async (done) => { test("Page logo image is initailized with api data", async (done) => {
//Arrange //Arrange
const SiteHeaderWidget = { const SiteHeaderWidget = {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
} }
const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget}); const { wrapper, apiPromise } = setupMocks( { SiteHeaderWidget: SiteHeaderWidget});
//Act //Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget); expect(funnelHeader.methods.initializeComponent).toHaveBeenCalledWith(SiteHeaderWidget);
@ -73,20 +60,16 @@ describe("vehicle-model.vue", () => {
}); });
}); });
}); });
describe("vehicle-model.vue", () => { describe("vehicle-model.vue", () => {
test("Vehicle image is initailized with api data", async (done) => { test("Vehicle image is initailized with api data", async (done) => {
//Arrange //Arrange
const VehicleBannerWidget = { const VehicleBannerWidget = {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
} }
const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget}); const { wrapper, apiPromise } = setupMocks( { VehicleBannerWidget: VehicleBannerWidget});
//Act //Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget); expect(vehicleBanner.methods.initializeComponent).toHaveBeenCalledWith(VehicleBannerWidget);
@ -94,10 +77,8 @@ describe("vehicle-model.vue", () => {
}); });
}); });
}); });
describe("vehicle-model.vue", () => { describe("vehicle-model.vue", () => {
test("BackButtonAction triggers a router.navigate change", async (done) => { test("BackButtonAction triggers a router.navigate change", async (done) => {
//Arrange //Arrange
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks( {
pageHeaderWidgetHeaderText: "Select a model to get started", pageHeaderWidgetHeaderText: "Select a model to get started",
@ -107,12 +88,10 @@ describe("vehicle-model.vue", () => {
}, },
}, },
}); });
//Act //Act
vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm));
wrapper.vm.backButtonAction(); wrapper.vm.backButtonAction();
await nextTick(); await nextTick();
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
@ -120,37 +99,32 @@ describe("vehicle-model.vue", () => {
}); });
}); });
}); });
function setupMocks({ function setupMocks({
radioQuestionCmsContent = {}, buttonQuestionContent = {},
modelQuestionInitialData = {}, modelQuestionInitialData = {},
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
}) { }) {
//Mock api responses //Mock api responses
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: [pageHeaderWidgetHeaderText], FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
RadioQuestionWidget: [radioQuestionCmsContent], VehicleModelQuestion: buttonQuestionContent,
VehicleBannerWidget: [ VehicleBannerWidget:
{ {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}, },
], FunnelHeaderWidget:
FunnelHeaderWidget: [
{ {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}, },
],
}, },
modelQuestionInitialData: modelQuestionInitialData, modelQuestionInitialData: modelQuestionInitialData,
}; };
const apiPromise = Promise.resolve(apiResponses); const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
//Mock model question methods //Mock model question methods
modelQuestion.methods = { modelQuestion.methods = {
loadInitialData: jest.fn(), loadInitialData: jest.fn(),
@ -175,6 +149,5 @@ function setupMocks({
vehicleBannerWrapper.vm.initializeComponent = vehicleBanner.methods.initializeComponent; vehicleBannerWrapper.vm.initializeComponent = vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" }); const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeader.methods.initializeComponent; funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeader.methods.initializeComponent;
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -8,7 +8,7 @@
ref="funnelSubHeader" ref="funnelSubHeader"
:hasBackButton="true" :hasBackButton="true"
backButtonAccessibleText="Change Vehicle Make" backButtonAccessibleText="Change Vehicle Make"
:backButtonAction="backButtonAction" @click-event="backButtonAction"
class="Header" class="Header"
/> />
<modelQuestion v-model="selectedModel" ref="modelQuestion" /> <modelQuestion v-model="selectedModel" ref="modelQuestion" />
@ -55,16 +55,16 @@ 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.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget[0]); vm.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget);
vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget[0]); vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget);
vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget[0]); vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget);
vm.$refs.modelQuestion.initializeComponent(resultMap.cmsContent.RadioQuestionWidget[0], resultMap.modelQuestionInitialData); vm.$refs.modelQuestion.initializeComponent(resultMap.cmsContent.VehicleModelQuestion, resultMap.modelQuestionInitialData);
}); });
}); });
}, },
methods: { methods: {
backButtonAction: function () { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
} }

View file

@ -17,10 +17,10 @@ describe("vehicle-style.vue", () => {
test("Style question component is initized with api data", async (done) => { test("Style question component is initized with api data", async (done) => {
//Arrange //Arrange
const radioQuestionCmsContent = { QuestionText: "What style is your vehicle?" }; const vehicleStyleQuestionCmsContent = { QuestionText: "What style is your vehicle?" };
const styleQuestionInitialData = ["2 Door", "4 Door"]; const styleQuestionInitialData = ["2 Door", "4 Door"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks( {
radioQuestionCmsContent: radioQuestionCmsContent, vehicleStyleQuestionCmsContent: vehicleStyleQuestionCmsContent,
styleQuestionInitialData: styleQuestionInitialData, styleQuestionInitialData: styleQuestionInitialData,
} ); } );
@ -29,7 +29,7 @@ describe("vehicle-style.vue", () => {
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(styleQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, styleQuestionInitialData); expect(styleQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleStyleQuestionCmsContent, styleQuestionInitialData);
done(); done();
}); });
}); });
@ -156,7 +156,7 @@ describe("vehicle-style.vue", () => {
}); });
function setupMocks({ function setupMocks({
radioQuestionCmsContent = {}, vehicleStyleQuestionCmsContent = {},
styleQuestionInitialData = {}, styleQuestionInitialData = {},
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
@ -165,20 +165,18 @@ function setupMocks({
//Mock api responses //Mock api responses
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: [pageHeaderWidgetHeaderText], FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
RadioQuestionWidget: [radioQuestionCmsContent], VehicleStyleQuestion: vehicleStyleQuestionCmsContent,
VehicleBannerWidget: [ VehicleBannerWidget:
{ {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}, },
], FunnelHeaderWidget:
FunnelHeaderWidget: [
{ {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}, },
],
}, },
styleQuestionInitialData: styleQuestionInitialData, styleQuestionInitialData: styleQuestionInitialData,
}; };

View file

@ -8,7 +8,7 @@
ref="funnelSubHeader" ref="funnelSubHeader"
:hasBackButton="true" :hasBackButton="true"
backButtonAccessibleText="Change Vehicle Model" backButtonAccessibleText="Change Vehicle Model"
:backButtonAction="backButtonAction" @click-event="backButtonAction"
class="Header" class="Header"
/> />
<styleQuestion v-model="selectedStyle" ref="styleQuestion" /> <styleQuestion v-model="selectedStyle" ref="styleQuestion" />
@ -55,16 +55,16 @@ 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.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget[0]); vm.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget);
vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget[0]); vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget);
vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget[0]); vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget);
vm.$refs.styleQuestion.initializeComponent(resultMap.cmsContent.RadioQuestionWidget[0], resultMap.styleQuestionInitialData); vm.$refs.styleQuestion.initializeComponent(resultMap.cmsContent.VehicleStyleQuestion, resultMap.styleQuestionInitialData);
}); });
}); });
}, },
methods: { methods: {
backButtonAction: function () { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },

View file

@ -17,10 +17,10 @@ describe("vehicle-year.vue", () => {
test("Year question component is initized with api data", async (done) => { test("Year question component is initized with api data", async (done) => {
//Arrange //Arrange
const radioQuestionCmsContent = { QuestionText: "What year is your vehicle?" }; const vehicleYearQuestionCmsContent = { QuestionText: "What year is your vehicle?" };
const yearQuestionInitialData = ["2023", "2022", "2021"]; const yearQuestionInitialData = ["2023", "2022", "2021"];
const { wrapper, apiPromise } = setupMocks( { const { wrapper, apiPromise } = setupMocks( {
radioQuestionCmsContent: radioQuestionCmsContent, vehicleYearQuestionCmsContent: vehicleYearQuestionCmsContent,
yearQuestionInitialData: yearQuestionInitialData, yearQuestionInitialData: yearQuestionInitialData,
} ); } );
@ -29,7 +29,7 @@ describe("vehicle-year.vue", () => {
//Assert //Assert
apiPromise.finally(() => { apiPromise.finally(() => {
expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, yearQuestionInitialData); expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(vehicleYearQuestionCmsContent, yearQuestionInitialData);
done(); done();
}); });
}); });
@ -78,7 +78,7 @@ describe("vehicle-year.vue", () => {
}); });
function setupMocks({ function setupMocks({
radioQuestionCmsContent = {}, vehicleYearQuestionCmsContent = {},
yearQuestionInitialData = {}, yearQuestionInitialData = {},
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
@ -87,20 +87,18 @@ function setupMocks({
//Mock api responses //Mock api responses
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: [{ HeaderText: pageHeaderWidgetHeaderText }], FunnelSubHeaderWidget: { HeaderText: pageHeaderWidgetHeaderText },
RadioQuestionWidget: [radioQuestionCmsContent], VehicleYearQuestion: vehicleYearQuestionCmsContent,
VehicleBannerWidget: [ VehicleBannerWidget:
{ {
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}, },
], FunnelHeaderWidget:
FunnelHeaderWidget: [
{ {
LogoImage: LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}, },
],
}, },
yearQuestionInitialData: yearQuestionInitialData, yearQuestionInitialData: yearQuestionInitialData,
}; };

View file

@ -53,10 +53,10 @@ 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.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget[0]); vm.$refs.funnelSubHeader.initializeComponent(resultMap.cmsContent.FunnelSubHeaderWidget);
vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget[0]); vm.$refs.funnelHeader.initializeComponent(resultMap.cmsContent.FunnelHeaderWidget);
vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget[0]); vm.$refs.vehicleBanner.initializeComponent(resultMap.cmsContent.VehicleBannerWidget);
vm.$refs.yearQuestion.initializeComponent(resultMap.cmsContent.RadioQuestionWidget[0], resultMap.yearQuestionInitialData); vm.$refs.yearQuestion.initializeComponent(resultMap.cmsContent.VehicleYearQuestion, resultMap.yearQuestionInitialData);
}); });
}); });
}, },

View file

@ -1,46 +1,219 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import listButtonHorizontal from "./list-button-horizontal"; import listButtonHorizontal from "./list-button-horizontal";
import { nextTick } from "vue";
describe("list-button.vue", () => { describe("list-button.vue", () => {
it("Should render the 'buttonID' prop value as the label and id value as well as the button value, groupName as the name value, and fire a click even that sets the display data attribute to true.", async () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
buttonID: "2023", isMultiSelect: true,
groupName: "TestGroup",
loaderColor: "blue",
loaderPosition: "right",
sizeInRem: "1.5",
errorMessage: "null",
}, },
}); });
// Assert // Assert
const input = wrapper.find("input"); const input = wrapper.find("input");
const label = wrapper.find("label");
const paragraph = wrapper.find("span");
await label.trigger("click"); expect(input.attributes().type).toEqual("checkbox");
expect(input.attributes()).toEqual({
id: "2023",
type: "radio",
value: "2023",
name: "TestGroup",
"aria-required": "true",
});
expect(label.attributes()).toEqual({
tabindex: "-1",
for: "2023",
class: "d-flex flex-column justify-content-center py-3 px-4 last-item",
"aria-checked": "false",
});
expect(label.text()).toEqual("2023");
expect(paragraph.text()).toEqual("2023");
expect(wrapper.vm.isLoaderDisplayed).toBe(true);
}); });
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isMultiSelect: false,
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
});
it("Should return primary label text (buttonID)", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
buttonID: "List Card Checkbox"
},
});
// Assert
const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox");
});
it("Should return screen reader text", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text"
},
});
// Assert
const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text");
});
it("Should return text alignment class", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
textPosition: "text-center"
},
});
// Assert
const paragraph = wrapper.find("span.m-0");
expect(paragraph.attributes('class')).toContain("text-center");
});
it("Should return aria-required state", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isRequired: true
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("Should return loader enabled true", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
loaderEnabled: true
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleClick();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.exists()).toBe(true);
});
it("Should return loader color", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
loaderColor: "blue",
loaderEnabled: true
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleClick();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("blue");
});
it("Should return loader position", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
loaderPosition: "right",
loaderEnabled: true
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleClick();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes('class')).toContain("right");
});
it("Should return loader size in rem", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
sizeInRem: 1,
loaderEnabled: true
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleClick();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes('style')).toContain("1rem");
});
it("Should return first item if first item in group", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
positionInGroup: 1
},
});
// Assert
const label = wrapper.find("label");
expect(label.attributes('class')).toContain("first-item");
});
it("Should return last item if last item in group", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
positionInGroup: 3,
totalInGroup: 3
},
});
// Assert
const label = wrapper.find("label");
expect(label.attributes('class')).toContain("last-item");
});
}); });

View file

@ -3,42 +3,39 @@
<!-- IMPORTANT: Refrain from using more than 4 horizontal buttons on desktop, 3 on mobile. --> <!-- IMPORTANT: Refrain from using more than 4 horizontal buttons on desktop, 3 on mobile. -->
<div v-if="isMultiSelect" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"> <div v-if="isMultiSelect" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2">
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired"> <input type="checkbox" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired">
<label tabindex="-1" aria-checked="false" :for="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton"> <label tabindex="-1" aria-checked="false" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton">
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span> <span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span> <span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
</label> </label>
<p class="small">{{errorMessage}}</p>
</div> </div>
<div v-else class="col list-group list-button-horizontal d-flex flex-column mb-2"> <div v-else class="col list-group list-button-horizontal d-flex flex-column mb-2">
<input type="radio" :id="buttonID" :name="groupName" :value="buttonID" aria-required="true" @keyup.space="displayLoader()"/> <input type="radio" :id="buttonID" :name="groupName" :value="buttonID" aria-required="true" @keyup.space="handleClick()" @click="handleClick()" />
<label tabindex="-1" aria-checked="false" :for="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton" @click="displayLoader()"> <label tabindex="-1" aria-checked="false" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton">
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span> <span class="m-0" :class="textPosition">{{buttonID}}</span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span> <span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
<loader v-if="isLoaderDisplayed" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[this.loaderColor, this.loaderPosition]" /> <loader v-if="isLoaderDisplayed" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[loaderColor, loaderPosition]" />
</label> </label>
<p class="small">{{ errorMessage }}</p>
</div> </div>
</template> </template>
<script> <script>
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
export default { export default {
name: "listButtonHorizontal", name: "listButtonHorizontal",
props: { props: {
isMultiSelect: Boolean, isMultiSelect: Boolean, /* Defines use as checkbox */
groupName: String, /* Required, unique for each button GROUP */ groupName: String, /* Required, unique for each button GROUP */
buttonID: String, /* Required, unique for each button. Used for button id, label and <label for> */ buttonID: String, /* Required, unique for each button. Used for button id, label and <label for> */
buttonLabelSubCopy: String, /* Optional, used for multi-line buttons */
screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */ 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 */ loaderEnabled: Boolean, /* Optional, need to include this for loader to be used at all */
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 */
loaderPosition: String, /* Specify horizontal position of loader/spinner. Options are center, right, left */ 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)) */ sizeInRem: Number, /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */
totalInGroup: Number, /* Required, total number of buttons in group. Used to tell first and last in group to apply border radius. */ totalInGroup: Number, /* Required, total number of buttons in group. Used to tell first and last in group to apply border radius. */
positionInGroup: Number /* Rquired, position of button in group. Example, 1,2,3 */ positionInGroup: Number, /* Required, position of button in group. Example, 1,2,3 */
isRequired: Boolean,
}, },
data() { data() {
return { return {
@ -49,6 +46,9 @@ export default {
displayLoader() { displayLoader() {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
handleClick() {
this.loaderEnabled && this.displayLoader();
}
}, },
components: { components: {
loader, loader,
@ -66,6 +66,7 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.list-button-horizontal { .list-button-horizontal {
input[type="radio"], input[type="radio"],
@ -101,13 +102,6 @@ export default {
+ p { + p {
display: none; display: none;
} }
&.error {
border: 1px solid $danger;
+ p {
display: flex;
color: $danger;
}
}
&.first-item { &.first-item {
border-bottom-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem; border-top-left-radius: 0.5rem;

View file

@ -11,7 +11,6 @@ describe("list-button.vue", () => {
loaderColor: "blue", loaderColor: "blue",
loaderPosition: "right", loaderPosition: "right",
sizeInRem: "1.5", sizeInRem: "1.5",
errorMessage: "null",
}, },
}); });

View file

@ -24,14 +24,13 @@ import loader from "@/ux-components/loader/loader";
export default { export default {
name: "listButton", name: "listButton",
props: { props: {
isMultiSelect: Boolean, isMultiSelect: Boolean, /* Defines use as checkbox */
groupName: String, /* Required, unique for each button GROUP */ groupName: String, /* Required, unique for each button GROUP */
buttonID: [Number,String], /* Required, unique for each button. Used for button id, label and <label for> */ buttonID: [Number,String], /* Required, unique for each button. Used for button id, label and <label for> */
isRequired: Boolean, /* Optional, default is false */ isRequired: Boolean, /* Optional, default is false */
screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */ screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */
buttonLabelSubCopy: String, /* Optional, used for multi-line buttons */ buttonLabelSubCopy: String, /* Optional, used for multi-line 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 */
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 */
loaderPosition: String, /* Specify horizontal position of loader/spinner. Options are center, right, left */ 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)) */ sizeInRem: [Number,String], /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */

View file

@ -1 +1,188 @@
test.todo("some test to be written in the future"); import { shallowMount } from "@vue/test-utils";
import listCard from "./list-card";
describe("list-card.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isMultiSelect: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "checkbox-demo-1",
groupName: "Checkbox 1",
buttonImage: "windshield-damage.svg",
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
});
it("Should return input type radio if isRadio is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadio: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
});
it("Should return input type checkbox if isMultiSelectHorizontal is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isMultiSelectHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg"
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
});
it("Should return input type radio if isRadioHorizontal is true", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg"
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
});
it("Should return primary label text", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg"
},
});
// Assert
const paragraph = wrapper.find("p");
expect(paragraph.text()).toEqual("Windshield");
});
it("Should return secondary (sub) label text", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
},
});
// Assert
const paragraph = wrapper.find("p:nth-of-type(2)");
expect(paragraph.text()).toEqual("Test");
});
it("Should return value used for various text settings including the label 'for' and input id", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
},
});
// Assert
const label = wrapper.find("label");
expect(label.attributes().for).toEqual("List Card Checkbox");
});
it("Should return input group name used for radio or checkbox", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test"
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().name).toEqual("radio 1");
});
it("Should return aria-required state", async () => {
// Act
const wrapper = shallowMount(listCard, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes()["aria-required"]).toEqual("true");
});
});

View file

@ -1,38 +1,38 @@
<template> <template>
<!-- Heavily documented below --> <!-- Heavily documented below -->
<div v-if="isMultiSelect" class="list-card w-100 rounded-3 d-flex align-items-center" :class="{'error': hasError}"> <div v-if="isMultiSelect" class="list-card w-100 rounded-3 d-flex align-items-center">
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" /> <input type="checkbox" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" />
<label :for="buttonID" class="d-flex flex-column w-100 align-items-center pt-4 pb-2" tabindex="1"> <label :for="buttonID" class="d-flex flex-column w-100 align-items-center pt-4 pb-2" tabindex="1">
<img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" /> <img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
<p class="small m-0 order-3">{{buttonLabel}}</p> <p class="small m-0 order-3">{{buttonLabel}}</p>
<p class="fs-7 m-0 order-4">{{buttonLabelSubCopy}}</p> <p v-if="buttonLabelSubCopy" class="fs-7 m-0 order-4">{{buttonLabelSubCopy}}</p>
</label> </label>
</div> </div>
<div v-else-if="isRadio" class="list-card w-100 rounded-3 d-flex align-items-center" :class="{'error': hasError}"> <div v-else-if="isRadio" class="list-card w-100 rounded-3 d-flex align-items-center">
<input type="radio" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" /> <input type="radio" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" />
<label :for="buttonID" class="d-flex flex-column w-100 align-items-center pt-4 pb-2" tabindex="1"> <label :for="buttonID" class="d-flex flex-column w-100 align-items-center pt-4 pb-2" tabindex="1">
<img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" /> <img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
<p class="small mt-2 mb-0 order-2">{{buttonLabel}}</p> <p class="small mt-2 mb-0 order-2">{{buttonLabel}}</p>
<p class="fs-7 m-0 order-3">{{buttonLabelSubCopy}}</p> <p v-if="buttonLabelSubCopy" class="fs-7 m-0 order-3">{{buttonLabelSubCopy}}</p>
</label> </label>
</div> </div>
<div v-else-if="isMultiSelectHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center" :class="{'error': hasError}"> <div v-else-if="isMultiSelectHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center">
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" /> <input type="checkbox" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" />
<label :for="buttonID" class="d-flex flex-row w-100 align-items-center py-3 ps-3 pe-8" tabindex="1" :class="{'checkboxTop': buttonLabelSubCopy}"> <label :for="buttonID" class="d-flex flex-row w-100 align-items-center py-3 ps-3 pe-8" tabindex="1" :class="{'checkboxTop': buttonLabelSubCopy}">
<img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" /> <img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
<div class="order-2"> <div class="order-2">
<p class="m-0 small">{{buttonLabel}}</p> <p class="m-0 small">{{buttonLabel}}</p>
<p class="m-0 fs-7">{{buttonLabelSubCopy}}</p> <p v-if="buttonLabelSubCopy" class="m-0 fs-7">{{buttonLabelSubCopy}}</p>
</div> </div>
</label> </label>
</div> </div>
<div v-else-if="isRadioHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center" :class="{'error': hasError}"> <div v-else-if="isRadioHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center">
<input type="radio" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" /> <input type="radio" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" />
<label :for="buttonID" class="d-flex flex-row w-100 align-items-center py-3 ps-3 pe-8" tabindex="1" :class="{'checkboxTop': buttonLabelSubCopy}"> <label :for="buttonID" class="d-flex flex-row w-100 align-items-center py-3 ps-3 pe-8" tabindex="1" :class="{'checkboxTop': buttonLabelSubCopy}">
<img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" /> <img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
<div class="order-2"> <div class="order-2">
<p class="m-0 small">{{buttonLabel}}</p> <p class="m-0 small">{{buttonLabel}}</p>
<p class="m-0 fs-7">{{buttonLabelSubCopy}}</p> <p v-if="buttonLabelSubCopy" class="m-0 fs-7">{{buttonLabelSubCopy}}</p>
</div> </div>
</label> </label>
</div> </div>
@ -43,27 +43,24 @@ export default {
name: "listCard", name: "listCard",
props: { props: {
//Must choose one of the following four options //Must choose one of the following four options
isMultiSelect: Boolean,//Defines use as checkbox isMultiSelect: Boolean, //Defines use as checkbox
isRadio: Boolean,//Defines use as radio button isRadio: Boolean, //Defines use as radio button
isMultiSelectHorizontal: Boolean,//Defines use as horizontal checkbox isMultiSelectHorizontal: Boolean, //Defines use as horizontal checkbox
isRadioHorizontal: Boolean,//Defines use as horizontal radio button isRadioHorizontal: Boolean, //Defines use as horizontal radio button
//end must choose //end must choose
buttonImage: String,//Optional: File name of image buttonImage: String,//Required: File name of image
buttonLabel: String,//Required: Label text buttonLabel: String,//Required: Label text
isRequired: Boolean, //Required: is aria-required required or not?
altText: String,//Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily. altText: String,//Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
buttonID: String,//Required: Unique buttonID: String,//Required: Unique
groupName: String,//Rquired: Unique groupName: String,//Required: Unique
buttonLabelSubCopy: String,//Optional: sub text buttonLabelSubCopy: String,//Optional: sub tex
hasError: Boolean//Used to show error state
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.list-card { .list-card {
&.error {
border: 1px solid $red !important;
}
border: 1px solid $gray-500; border: 1px solid $gray-500;
&.invalid { //Red border if invalid &.invalid { //Red border if invalid
border: 1px solid $red; border: 1px solid $red;