Merged from develop

This commit is contained in:
Leah Schumann 2022-02-24 13:24:02 -05:00
commit ed8cd94ac0
40 changed files with 2548 additions and 1463 deletions

View file

@ -16,6 +16,7 @@ module.exports = {
"!src/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question.vue", "!src/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question.vue",
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue", "!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/address-poc/address-poc.vue", "!src/layouts/address-poc/address-poc.vue",
"!src/layouts/nested-radio-poc/nested-radio.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

@ -7,4 +7,5 @@
@import "@/styles/common-styles.scss"; @import "@/styles/common-styles.scss";
@import "@/styles/common-typography-styles.scss"; @import "@/styles/common-typography-styles.scss";
@import "@/styles/common-error-styles.scss"; @import "@/styles/common-error-styles.scss";
@import "@/styles/common-animations.scss";
</style> </style>

View file

@ -3,12 +3,14 @@ import buttonQuestion from "@/common-components/button-question/button-question"
import { nextTick } from "vue"; import { nextTick } from "vue";
describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
it("Should show overflow classes on fieldset if isOverflowScrollable is true", async () => { it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => {
// Act // Act
const wrapper = shallowMount(buttonQuestion); const wrapper = shallowMount(buttonQuestion, {
await wrapper.setProps({ propsData: {
isOverflowScrollable: true, isOverflowScrollable: true,
}
}); });
// Assert // Assert
const fieldSet = wrapper.find('fieldset'); const fieldSet = wrapper.find('fieldset');
expect(fieldSet.classes()).toContain("overflow-scroll"); expect(fieldSet.classes()).toContain("overflow-scroll");
@ -16,11 +18,12 @@ describe("buttonQuestion.vue", () => {
}); });
describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
it("Fieldset classes should contain row if button type is listCard", async () => { it("Fieldset classes should contain row if button type is listCard", () => {
// Act // Act
const wrapper = shallowMount(buttonQuestion); const wrapper = shallowMount(buttonQuestion, {
await wrapper.setProps({ propsData: {
buttonType: "listCard", buttonType: "listCard",
}
}); });
// Assert // Assert
const Div = wrapper.find('fieldset div'); const Div = wrapper.find('fieldset div');
@ -29,11 +32,12 @@ describe("buttonQuestion.vue", () => {
}); });
describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", async () => { it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", () => {
// Act // Act
const wrapper = shallowMount(buttonQuestion); const wrapper = shallowMount(buttonQuestion, {
await wrapper.setProps({ propsData: {
buttonType: "listButtonHorizontal", buttonType: "listButtonHorizontal",
}
}); });
// Assert // Assert
const Div = wrapper.find('fieldset div'); const Div = wrapper.find('fieldset div');
@ -49,7 +53,7 @@ describe("buttonQuestion.vue", () => {
answers: ["2022", "2021", "2020"], answers: ["2022", "2021", "2020"],
isMultiSelect: false isMultiSelect: false
}); });
const val = {isChecked: true, buttonId: "2021", } const val = {checkValue: true, value: "2021", }
wrapper.vm.handleCheckedChanged(val); wrapper.vm.handleCheckedChanged(val);
// Assert // Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([["2021"]]); expect(wrapper.emitted()["update:modelValue"][0]).toEqual([["2021"]]);
@ -57,14 +61,15 @@ describe("buttonQuestion.vue", () => {
}); });
describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
it("Should add values to array on checkbox click", async () => { it("Should add values to array on checkbox click", () => {
// Act // Act
const wrapper = shallowMount(buttonQuestion); const wrapper = shallowMount(buttonQuestion, {
await wrapper.setProps({ propsData: {
modelValue: ["2022", "2021", "2020"], modelValue: ["2022", "2021", "2020"],
isMultiSelect: true isMultiSelect: true,
}
}); });
const val = {isChecked: true, buttonId: "2019", } const val = {checkValue: true, value: "2019", }
wrapper.vm.handleCheckedChanged(val); wrapper.vm.handleCheckedChanged(val);
// Assert // Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([["2022", "2021", "2020", "2019"]]); expect(wrapper.emitted()["update:modelValue"][0]).toEqual([["2022", "2021", "2020", "2019"]]);

View file

@ -4,7 +4,7 @@
<span class="text-center fs-6 fw-bold w-100">{{ questionText }}</span> <span class="text-center fs-6 fw-bold w-100">{{ questionText }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :class="getFieldSetClasses" role="radiogroup" :aria-labelledby="groupName ? groupName + '-radio-group' : ''"> <fieldset class="w-100" :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''">
<legend class="sr-only">{{groupName}}</legend> <legend class="sr-only">{{groupName}}</legend>
<div :class="getComponentWrapperClasses"> <div :class="getComponentWrapperClasses">
<component <component
@ -12,7 +12,7 @@
v-for="answer in answers" v-for="answer in answers"
:key="answer.Name ? answer.Name : answer" :key="answer.Name ? answer.Name : answer"
@isCheckedChanged="handleCheckedChanged" @isCheckedChanged="handleCheckedChanged"
:buttonID="answer.Name ? answer.Name : answer" :buttonID="answer.Name ? groupName + '-' + answer.Name : groupName + '-' + answer"
:value="answer.Name ? answer.Name : answer" :value="answer.Name ? answer.Name : answer"
:buttonLabel="answer.Text ? answer.Text : answer" :buttonLabel="answer.Text ? answer.Text : answer"
:buttonLabelSubCopy="answer.SubText" :buttonLabelSubCopy="answer.SubText"
@ -22,15 +22,14 @@
:selectingInitiatesLoad="selectingInitiatesLoad" :selectingInitiatesLoad="selectingInitiatesLoad"
:loaderColor="loaderColor" :loaderColor="loaderColor"
:loaderPosition="loaderPosition" :loaderPosition="loaderPosition"
:sizeInRem="sizeInRem"
:isWide="isWide" :isWide="isWide"
:isRequired="isRequired" :isRequired="isRequired"
:buttonImage="answer.AnswerImageUrl" :buttonImage="answer.AnswerImageUrl"
:buttonImageId="answer.ImageId" :buttonImageId="answer.ImageId"
:altText="answer.Name ? answer.Name : answer" :altText="answer.Name ? answer.Name : answer"
screenReaderOnlyText="(opens new window)" screenReaderOnlyText="(opens new window)"
:colLength="this.answers.length < 3 ? '' : '-4'" :colLength="getColLength"
:selectedButtonIDs="selectedValues" :selectedValues="selectedValues"
data-test="button" data-test="button"
:validationRules="validationRules" :validationRules="validationRules"
/> />
@ -74,17 +73,11 @@ export default {
type: String, type: String,
default: "right", default: "right",
}, },
sizeInRem: {
type: [String, Number],
default: 1.5,
},
isRequired: Boolean, isRequired: Boolean,
isOverflowScrollable: Boolean, isOverflowScrollable: Boolean,
isWide: Boolean, isWide: Boolean,
modelValue: Array, modelValue: Array,
validationRules: String, validationRules: String,
name: String,
value: String,
suppressError: Boolean, suppressError: Boolean,
}, },
computed: { computed: {
@ -111,6 +104,13 @@ export default {
} }
return classes; return classes;
}, },
getColLength(){
if(this.isWide) {
return "12"
} else {
return this.answers.length < 3 ? '' : '-4';
}
},
selectedValues: { selectedValues: {
get: function() { get: function() {
return this.modelValue; return this.modelValue;
@ -120,25 +120,17 @@ export default {
} }
}, },
}, },
mounted(){
if(Array.isArray(this.answers) && this.answers.length === 1) {
const newSelectedValues = this.selectedValues;
newSelectedValues.push(typeof(this.answers[0]) === 'object' ? this.answers[0].Name : this.answers[0]);
this.selectedValues = newSelectedValues;
}
},
methods: { methods: {
chooseAnswer(answer) {
this.$emit("update:modelValue", answer);
},
handleCheckedChanged(val) { handleCheckedChanged(val) {
if(this.isMultiSelect) { if(this.isMultiSelect && this.selectedValues) {
// Add or remove item to array of data to emit // Add or remove item to array of data to emit
const newSelectedValues = this.selectedValues; const newSelectedValues = this.selectedValues;
val.isChecked ? newSelectedValues.push(val.buttonId) : newSelectedValues.splice(newSelectedValues.indexOf(val.buttonId), 1); if(Array.isArray(this.selectedValues)) {
this.selectedValues = newSelectedValues; val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
this.selectedValues = newSelectedValues;
}
} else { } else {
this.selectedValues = [val.buttonId]; this.selectedValues = [val.value];
} }
}, },
}, },

View file

@ -0,0 +1,81 @@
import { shallowMount } from "@vue/test-utils";
import dropdownQuestion from "./dropdown-question";
import { nextTick } from "vue";
describe("dropdownQuestion.vue", () => {
it("Should return aria-disabled state", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
isDisabled: true,
},
});
// Assert
const input = wrapper.find("select");
// Expect
expect(input.attributes()["aria-disabled"]).toEqual("true");
});
it("Should render a text input", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
name: "test",
label: "unit test label",
},
});
// Assert
const input = wrapper.find("select");
expect(input.exists()).toBe(true);
});
it("Should return input id", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
inputId: "input ID",
},
});
// Assert
const input = wrapper.find("select");
// Expect
expect(input.attributes().id).toEqual("input ID");
});
it("Should return label text", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
labelText: "label text",
},
});
// Assert
const label = wrapper.find("label");
expect(label.text()).toEqual("label text");
});
it("Should return input id", async () => {
// Act
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
inputId: "input ID",
},
});
// Assert
const input = wrapper.find("select");
// Expect
expect(input.attributes().id).toEqual("input ID");
});
});

View file

@ -1,13 +1,19 @@
<template> <template>
<label :for="inputId">{{ labelText }}</label> <div class="dropdown-question">
<label :for="inputId" class="form-label">{{labelText}}</label>
<select v-model="selectedOption" <select v-model="selectedOption"
:ref="inputId" class="form-select"
:id="inputId" aria-label="Default select example"
class="form-control"> :id="inputId"
<option v-for="(value, name, index) in options" :value="name" :key="index"> :aria-disabled="isDisabled"
{{ value }} :disabled="isDisabled"
</option> :aria-required="isRequired">
<option v-for="(value, name, index) in options" :value="name" :key="index">
{{ value }}
</option>
</select> </select>
<p class="mt-1 mb-0">There has been an error!</p>
</div>
</template> </template>
<script> <script>
@ -39,3 +45,41 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss">
.dropdown-question {
.form-label {
margin-bottom: .25rem;
}
.form-select {
color: $gray-500;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e");
border: 1px solid $gray-500;
border-radius: .5rem;
min-height: 3rem;
option[selected] {
color: $gray-500;
background-color: red;
}
&:focus,
&:focus-visible {
box-shadow: 0 0 0 2px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
filter: grayscale(100%);
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid transparent;
box-shadow: 0 0 0 4px $blue-300;
}
}
}
</style>

View file

@ -21,7 +21,6 @@
isPrimary isPrimary
:buttonText="buttonText" :buttonText="buttonText"
loaderColor="white" loaderColor="white"
sizeInRem="1"
:class="isDisabled && 'form-test-invalid'" :class="isDisabled && 'form-test-invalid'"
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
:isDisabled="isDisabled" :isDisabled="isDisabled"
@ -97,6 +96,10 @@ export default {
.btn-primary { .btn-primary {
width: 100%; width: 100%;
} }
a {
display: flex;
justify-content: center;
}
@media only screen and (min-width: 340px) { @media only screen and (min-width: 340px) {
.button-col, .button-col,
.link-col { .link-col {
@ -105,6 +108,10 @@ export default {
.btn-primary { .btn-primary {
width: auto; width: auto;
} }
a {
display: flex;
justify-content: flex-start;
}
} }
} }
</style> </style>

View file

@ -60,7 +60,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.funnel-header { .funnel-header {
height: 56px; padding: 0.97rem 0;
} }
.logo-image { .logo-image {

View file

@ -0,0 +1,81 @@
import { shallowMount } from "@vue/test-utils";
import textboxQuestion from "./textbox-question";
import { nextTick } from "vue";
describe("textboxQuestion.vue", () => {
it("Should return aria-disabled state", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
propsData: {
isDisabled: true,
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes()["aria-disabled"]).toEqual("true");
});
it("Should render a text input", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
propsData: {
name: "test",
label: "unit test label",
},
});
// Assert
const input = wrapper.find("input");
expect(input.exists()).toBe(true);
});
it("Should return input id", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
propsData: {
inputId: "input ID",
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().id).toEqual("input ID");
});
it("Should return label text", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
propsData: {
labelText: "label text",
},
});
// Assert
const label = wrapper.find("label");
expect(label.text()).toEqual("label text");
});
it("Should return input id", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
propsData: {
inputId: "input ID",
},
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().id).toEqual("input ID");
});
});

View file

@ -0,0 +1,52 @@
<template>
<div class="textbox-question">
<label :for="inputId" class="form-label">{{labelText}}</label>
<input type="text" class="form-control" :id="inputId" :placeholder="placeholderText" :aria-disabled="isDisabled" :disabled="isDisabled" :aria-required="isRequired">
<p class="mt-1 mb-0">There has been an error!</p>
</div>
</template>
<script>
export default {
name: "textboxQuestion",
props: {
isDisabled: Boolean,
modelValue: String,
labelText: String,
placeholderText: String,
inputId: String,
isRequired: Boolean,
}
};
</script>
<style lang="scss">
.textbox-question {
.form-label {
margin-bottom: .25rem;
}
.form-control {
border: 1px solid $gray-500;
border-radius: .5rem;
min-height: 3rem;
&::placeholder {
color: $gray-500;
}
&:focus {
box-shadow: 0 0 0 2px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid transparent;
box-shadow: 0 0 0 4px $blue-300;
}
}
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
<template>
<div class="container-fluid nested-radio">
<div class="row mb-1">
<div class="col">
<listCard
groupName="listcard1"
isWide="true"
buttonImage="https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3"
buttonLabel="Green Tint, Blue Shade"
buttonID="radio1"
/>
</div>
</div>
<div class="row mb-1">
<div class="col">
<p class="mb-0 fw-bold">Ok, select the features</p>
<buttonQuestion
buttonType="radio"
groupName="Demo Group"
buttonID="button1"
isRequired
value="test button"
screenReaderOnlyText="rain sensor, solar, 3rd visor band"
:answers="demoArray"
/>
</div>
</div>
<div class="row mb-2">
<div class="col">
<listCard
groupName="listcard1"
isWide="true"
buttonImage="https://digitalconsumercms-dev.safelite.com/images/default-source/damage-options/car.svg?sfvrsn=1468d7f1_3"
buttonLabel="Green Tint"
buttonID="radio2"
/>
</div>
</div>
</div>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import listCard from "@/ux-components/list-card/list-card";
export default {
name: "nestedRadio",
components: {
buttonQuestion,
listCard,
},
data() {
return {
demoArray: [
"rain sensor, solar, 3rd visor band",
"rain sensor, heated glass, solar, 3rd visor band",
]
}
}
};
</script>
<style lang="scss">
.nested-radio {
.ui-radio {
flex-direction: column;
margin: .25rem 0;
}
p {
font-size: .875rem;
}
}
</style>

View file

@ -41,7 +41,7 @@ describe("damage-location-question.vue", () => {
damageLocationQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, damageOptions, "car-group"); damageLocationQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, damageOptions, "car-group");
//Assert //Assert
expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'car-Windshield' }, { Name: 'car-SideDoor' } ]) expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'Windshield' }, { Name: 'SideDoor' } ])
}); });
}); });

View file

@ -1,7 +1,6 @@
<template> <template>
<div class="damage-location-question"> <div class="damage-location-question">
<buttonQuestion <buttonQuestion
v-if="answersToDisplay.length > 1"
:questionText="questionText" :questionText="questionText"
isMultiSelect isMultiSelect
:answers="answersToDisplay" :answers="answersToDisplay"
@ -68,13 +67,18 @@ export default ({
} }
}, },
answersToDisplay(){ answersToDisplay(){
return Array.isArray(this.answersFromCms) const filteredAnswers = Array.isArray(this.answersFromCms)
? this.answersFromCms.filter(ans => ? this.answersFromCms.filter(ans =>
{ {
const name = ans.Name.split('-'); const name = ans.Name.split('-');
return name[0].toUpperCase() === store.getters.vehicle.category && this.damageOptionsMap[name[1]]; return name[0].toUpperCase() === store.getters.vehicle.category && this.damageOptionsMap[name[1]];
}) })
: []; : [];
return filteredAnswers.map(ans => {
const newName = ans.Name.includes('-') ? ans.Name.split('-')[1] : ans.Name;
ans.Name = newName;
return ans;
});
}, },
}, },
components: { components: {

View file

@ -1,76 +1,124 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import store from "@/store"; import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true}); jest.mock("@/store", () => { return {}; }, {virtual: true});
describe("replace-options-question.vue", () => { describe("replace-options-question.vue", () => {
test("Selected damage option is emitted upon selection.", async () => { test("Selected damage option is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: ["Windshield"] });
const damageToSelect = ["Backseat"];
//Act
wrapper.setValue({ modelValue: damageToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{modelValue: ["Backseat"]}]);
});
});
describe("replace-options-question.vue", () => { //Arrange
test("Answers to display filtered by data from api.", async () => { const { wrapper } = setupMocks({ modelValueProp: ["Windshield"] });
const damageToSelect = ["Backseat"];
//Arrange
const { wrapper, cmsContent, replaceOptions } = setupMocks({ dataFromStoreApi: ["Windshield", "FrontDoor"]});
//Act
replaceOptionsQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, replaceOptions, "car-group");
//Assert
expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'car-Windshield' }, { Name: 'car-FrontDoor' } ])
});
});
function setupMocks({ //Act
modelValueProp = ["Windshield"], wrapper.setValue({ modelValue: damageToSelect });
isAvailale = true, await wrapper.vm.$nextTick();
isMultiSelect = false,
filterByVehicleCategory = false, //Assert
groupName = "damageQuestion", expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{modelValue: ["Backseat"]}]);
cmsQuestionText = "CMS text goes here", });
cmsAnswers = [{Name: "car-Windshield"}, {Name: "car-BackDoor"}, {Name: "car-FrontDoor"}], });
dataFromStoreApi = [],
}) { describe("replace-options-question.vue", () => {
test("Answers to display filtered by data from api.", async () => {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi); //Arrange
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} }; const { wrapper, cmsContent, replaceOptions } = setupMocks({ dataFromStoreApi: ["Windshield", "FrontDoor"], filterByVehicleCategory: true});
const mountOptions = getMountOptions({
store: { //Act
dispatch: store.dispatch, replaceOptionsQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, replaceOptions, "car-group");
getters: store.getters,
//Assert
expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'Windshield' }, { Name: 'FrontDoor' } ])
});
});
describe("replace-options-question.vue", () => {
test("when updateSelectedValues method is called with a single answerToDisplay it will call to update this.selectedValues", async () => {
//Arrange
const { wrapper, cmsContent, replaceOptions
} = setupMocks({
modelValueProp: [],
});
wrapper.setData({answersFromCms: [
{
"Name": "Stationary",
}, },
}); {
"Name": "Slider",
}
]});
wrapper.setData({replaceOptions: ["Stationary"]});
//Act
wrapper.vm.$options.methods.updateSelectedValues.call(wrapper.vm);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([['Stationary']]);
});
});
describe("replace-options-question.vue", () => {
test("when isAvailable is true, will run updateSelectedValues method", async () => {
//Arrange
const { wrapper, cmsContent, replaceOptions } = setupMocks({ isAvailable: false, methodsToMock: ["updateSelectedValues"] });
//Act
wrapper.vm.$options.methods.initializeComponent.call(wrapper.vm, cmsContent, replaceOptions, "car-group");
wrapper.vm.$options.watch.isAvailable.call(wrapper.vm, true);
//Assert
expect(replaceOptionsQuestion.methods.updateSelectedValues).toHaveBeenCalled();
wrapper.unmount();
});
});
function setupMocks({
modelValueProp = ["Windshield"],
isAvailable = true,
isMultiSelect = false,
filterByVehicleCategory = false,
groupName = "damageQuestion",
cmsQuestionText = "CMS text goes here",
cmsAnswers = [{Name: "car-Windshield"}, {Name: "car-BackDoor"}, {Name: "car-FrontDoor"}],
dataFromStoreApi = [],
methodsToMock = [],
}) {
//Mock props //Mock store
mountOptions.propsData = { store.dispatch = jest.fn(() => dataFromStoreApi);
modelValue: modelValueProp, store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
isAvailable: isAvailale, const mountOptions = getMountOptions({
isMultiSelect: isMultiSelect, store: {
filterByVehicleCategory: filterByVehicleCategory dispatch: store.dispatch,
}; getters: store.getters,
},
const wrapper = shallowMount(replaceOptionsQuestion, mountOptions); });
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp,
isAvailable: isAvailable,
isMultiSelect: isMultiSelect,
filterByVehicleCategory: filterByVehicleCategory
};
//Mock methods
methodsToMock.forEach((methodName) => {
replaceOptionsQuestion.methods[methodName] = jest.fn();
});
//Mock CMS content const wrapper = shallowMount(replaceOptionsQuestion, mountOptions);
const cmsContent = {
groupName: groupName, //Mock CMS content
QuestionText: cmsQuestionText, const cmsContent = {
Answers: cmsAnswers, groupName: groupName,
}; QuestionText: cmsQuestionText,
const replaceOptions = dataFromStoreApi; Answers: cmsAnswers,
return { wrapper, cmsContent, replaceOptions }; };
} const replaceOptions = dataFromStoreApi;
return { wrapper, cmsContent, replaceOptions };
}

View file

@ -1,10 +1,10 @@
<template> <template>
<transition name="fade"> <transition name="fade" mode="out-in">
<div class="replace-options-question"> <div v-if="isAvailable && this.answersToDisplay.length > 0" class="replace-options-question" :class="this.answersToDisplay.length < 2 ? 'd-none' : ''" aria-live="polite">
<buttonQuestion <buttonQuestion
v-if="isAvailable && answersToDisplay.length > 1" isWide
:questionText="questionText" :questionText="questionText"
isMultiSelect :isMultiSelect="isMultiSelect"
:answers="answersToDisplay" :answers="answersToDisplay"
:groupName="groupName" :groupName="groupName"
buttonType="listCard" buttonType="listCard"
@ -42,6 +42,7 @@ export default ({
filterByVehicleCategory: Boolean, filterByVehicleCategory: Boolean,
groupName: String, groupName: String,
modelValue: Array, modelValue: Array,
isMultiSelect: Boolean,
}, },
methods: { methods: {
initializeComponent(cmsContent, replaceOptions){ initializeComponent(cmsContent, replaceOptions){
@ -49,6 +50,12 @@ export default ({
this.answersFromCms = cmsContent.Answers; this.answersFromCms = cmsContent.Answers;
this.replaceOptions = replaceOptions; this.replaceOptions = replaceOptions;
}, },
updateSelectedValues() {
// UPDATE SELECTEDVALUES IF ONLY ONE ANSWER
if(Array.isArray(this.answersToDisplay) && this.answersToDisplay.length === 1 && this.selectedValues) {
this.selectedValues = [this.answersToDisplay[0].Name];
}
},
}, },
computed: { computed: {
selectedValues: { selectedValues: {
@ -60,29 +67,29 @@ export default ({
} }
}, },
answersToDisplay(){ answersToDisplay(){
return Array.isArray(this.answersFromCms) const filteredAnswers = Array.isArray(this.answersFromCms)
? this.answersFromCms.filter(ans => ? this.answersFromCms.filter(ans =>
{ {
const name = ans.Name.split('-'); const name = ans.Name.split('-');
return this.filterByVehicleCategory ? name[0].toUpperCase() === store.getters.vehicle.category : name[0].toUpperCase() === store.getters.vehicle.category && this.replaceOptions.includes(name[1]) return this.filterByVehicleCategory ? name[0].toUpperCase() === store.getters.vehicle.category && this.replaceOptions.includes(name[1]) : this.replaceOptions.includes(ans.Name);
}) })
: []; : [];
return filteredAnswers.map(ans => {
const newName = ans.Name.includes('-') ? ans.Name.split('-')[1] : ans.Name;
ans.Name = newName;
return ans;
});
}, },
}, },
watch: {
isAvailable(val) {
// CHECK TO UPDATE SELECTED VALUES WHEN ISAVAILABLE IS TRUE
val && this.updateSelectedValues();
}
},
components: { components: {
buttonQuestion, buttonQuestion,
} }
}) })
</script> </script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View file

@ -0,0 +1,126 @@
import { shallowMount } from "@vue/test-utils";
import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
describe("replace-options-question.vue", () => {
test("Selected side door option is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: ["Windshield"] });
const sideDoorOptions = ["Backseat"];
//Act
wrapper.setValue({ modelValue: sideDoorOptions });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{modelValue: ["Backseat"]}]);
});
});
describe("replace-options-question.vue", () => {
test("Selected door side option is updated when selection made.", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.selectedDoorSidesValues = ["DriverSide"]
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.getSideDoorReplacementOptions()).toHaveBeenCalled;
});
});
describe("replace-options-question.vue", () => {
test("Selected driver side option is updated when selection made.", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.selectedDriverSideReplaceOptionsValues = ["FrontDoor"]
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.getSideDoorReplacementOptions()).toHaveBeenCalled;
});
});
describe("replace-options-question.vue", () => {
test("Selected passenger side option is updated when selection made.", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.selectedPassengerSideReplaceOptionsValues = ["BacktDoor"]
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.getSideDoorReplacementOptions()).toHaveBeenCalled;
});
});
describe("replace-options-question.vue", () => {
test("Answers to display filtered by data from api.", async () => {
//Arrange
const { wrapper, cmsContent, driverSideReplaceOptions, passengerSideReplaceOptions, driverSideOptions, passengerSideOptions } = setupMocks({});
//Act
sideDoorOptions.methods.initializeComponent.call(wrapper.vm, cmsContent, driverSideReplaceOptions, passengerSideReplaceOptions, driverSideOptions, passengerSideOptions, "car-group");
//Assert
expect(wrapper.vm.answersToDisplay).toStrictEqual([ { Name: 'DriverSide' }, { Name: 'PassengerSide' } ])
});
});
function setupMocks({
modelValueProp = ["DriverSide"],
groupName = "sideDoorOptions",
cmsQuestionText = "CMS text goes here",
cmsAnswers = [{Name: "Car-DriverSide"}, {Name: "Car-PassengerSide"}],
driverSideReplaceOptions = ["FrontDoor", "BackDoor"],
passengerSideReplaceOptions = ["FrontDoor", "BackDoor"],
driverSideOptions = ["Car-FrontDoor", "Car-BackDoor"],
passengerSideOptions = ["Car-FrontDoor", "Car-BackDoor"],
selectedDamageLocations = ["SideDoor"]
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp,
groupName: groupName,
selectedDamageLocations: selectedDamageLocations,
};
const wrapper = shallowMount(sideDoorOptions, mountOptions);
const OptionsWrapper = wrapper.findAllComponents({name: "replaceOptionsQuestion"});
OptionsWrapper[0].vm.initializeComponent = replaceOptionsQuestion.methods.initializeComponent;
OptionsWrapper[1].vm.initializeComponent = replaceOptionsQuestion.methods.initializeComponent;
//Mock CMS content
const cmsContent = {
groupName: groupName,
QuestionText: cmsQuestionText,
Answers: cmsAnswers,
};
return { wrapper, cmsContent, driverSideReplaceOptions, passengerSideReplaceOptions, driverSideOptions, passengerSideOptions };
}

View file

@ -0,0 +1,116 @@
<template>
<div class="side-door-options">
<transition name="fade" mode="out-in">
<div class="side-doors" v-if="selectedDamageLocations.includes('SideDoor')" aria-live="polite">
<buttonQuestion
:questionText="questionText"
isMultiSelect
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
v-model="selectedDoorSidesValues"
/>
</div>
</transition>
<replaceOptionsQuestion ref="driverSideOptions" :isAvailable="isDriverSideReplaceOptionsQuestionAvailable" groupName="driverSideOptions" filterByVehicleCategory isMultiSelect v-model="selectedDriverSideReplaceOptionsValues" />
<replaceOptionsQuestion ref="passengerSideOptions" :isAvailable="isPassengerSideReplaceOptionsQuestionAvailable" groupName="passengerSideOptions" filterByVehicleCategory isMultiSelect v-model="selectedPassengerSideReplaceOptionsValues" />
</div>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import store from "@/store";
export default ({
name: "sideDoorOptions",
data(){
return {
questionText: String,
answersFromCms: Array,
}
},
props: {
groupName: String,
modelValue: Array,
selectedDamageLocations: Array,
},
methods: {
initializeComponent(cmsContent, driverSideReplaceOptions, passengerSideReplaceOptions, driverSideOptions, passengerSideOptions){
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
// Child Components intialize
this.$refs.driverSideOptions.initializeComponent(driverSideReplaceOptions, driverSideOptions);
this.$refs.passengerSideOptions.initializeComponent(passengerSideReplaceOptions, passengerSideOptions);
},
getSideDoorReplacementOptions(selectedDoorSides, selectedDriverSideReplaceOptions, selectedPassengerSideReplaceOptions){
return {
selectedDoorSides: selectedDoorSides,
selectedDriverSideReplaceOptions: selectedDriverSideReplaceOptions,
selectedPassengerSideReplaceOptions: selectedPassengerSideReplaceOptions
}
},
},
computed: {
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
selectedDoorSidesValues: {
get: function() {
return this.selectedValues.selectedDoorSides;
},
set: function(newValue) {
this.selectedValues = this.getSideDoorReplacementOptions(newValue, this.selectedValues.selectedDriverSideReplaceOptions, this.selectedValues.selectedPassengerSideReplaceOptions);
}
},
selectedDriverSideReplaceOptionsValues: {
get: function() {
return this.selectedValues.selectedDriverSideReplaceOptions;
},
set: function(newValue) {
this.selectedValues = this.getSideDoorReplacementOptions(this.selectedValues.selectedDoorSides, newValue, this.selectedValues.selectedPassengerSideReplaceOptions);
}
},
selectedPassengerSideReplaceOptionsValues: {
get: function() {
return this.selectedValues.selectedPassengerSideReplaceOptions;
},
set: function(newValue) {
this.selectedValues = this.getSideDoorReplacementOptions(this.selectedValues.selectedDoorSides, this.selectedValues.selectedDriverSideReplaceOptions, newValue);
}
},
answersToDisplay(){
const filteredAnswers = Array.isArray(this.answersFromCms)
? this.answersFromCms.filter(ans =>
{
const name = ans.Name.split('-');
return name[0].toUpperCase() === store.getters.vehicle.category;
})
: [];
return filteredAnswers.map(ans => {
const newName = ans.Name.includes('-') ? ans.Name.split('-')[1] : ans.Name;
ans.Name = newName;
return ans;
});
},
isDriverSideReplaceOptionsQuestionAvailable(){
return (Array.isArray(this.selectedDoorSidesValues) && this.selectedDoorSidesValues.includes("DriverSide") && (Array.isArray(this.selectedDamageLocations) && this.selectedDamageLocations.includes("SideDoor")));
},
isPassengerSideReplaceOptionsQuestionAvailable(){
return (Array.isArray(this.selectedDoorSidesValues) && this.selectedDoorSidesValues.includes("PassengerSide") && (Array.isArray(this.selectedDamageLocations) && this.selectedDamageLocations.includes("SideDoor")));
},
},
components: {
buttonQuestion,
replaceOptionsQuestion,
}
})
</script>

View file

@ -4,8 +4,10 @@ import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question"; import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
// Supporting Files // Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
@ -165,19 +167,60 @@ describe("vehicle-damage.vue", () => {
}); });
}); });
describe("vehicle-damage.vue", () => {
test("BackButtonAction triggers a router.navigate change", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
});
describe("vehicle-damage.vue", () => {
test("isRearWindowDamageLocation is true if Rear Window damage is selected", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.selectedDamageLocations = ["RearWindow"];
//Assert
expect(wrapper.vm.isRearWindowDamageLocation).toEqual(true);
});
});
function setupMocks({ function setupMocks({
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {
router: {
navigate: jest.fn(),
},
},
}) { }) {
//Mock api responses //Mock api responses
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn(); baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
baseMixin.methods.dispatchNonBlockingStoreAction.mockImplementation(() => {
return Promise.resolve({
driverSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
}
});
});
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
@ -193,7 +236,16 @@ function setupMocks({
damageOptions: { damageOptions: {
driverSideOptions: { driverSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"], availableReplacementOptions: ["Front", "Back", "Side"],
} },
passengerSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
},
windshieldOptions: {
availableReplacementOptions: ["Single", "Driver", "Passenger"],
},
backGlassOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
},
}, },
}; };
@ -215,12 +267,19 @@ function setupMocks({
initializeComponent: jest.fn(), initializeComponent: jest.fn(),
}; };
const driverSideOptions = replaceOptionsQuestion damageLocationQuestion.methods = {
driverSideOptions.methods = {
initializeComponent: jest.fn(), initializeComponent: jest.fn(),
}; };
damageLocationQuestion.methods = { sideDoorOptions.methods = {
initializeComponent: jest.fn(),
};
windshieldOptions.methods = {
initializeComponent: jest.fn(),
};
replaceOptionsQuestion.methods = {
initializeComponent: jest.fn(), initializeComponent: jest.fn(),
}; };
@ -239,16 +298,27 @@ function setupMocks({
vehicleBannerWrapper.vm.initializeComponent = vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent; vehicleBanner.methods.initializeComponent;
const sideDoorOptionsWrapper = wrapper.findComponent({ name: "sideDoorOptions" });
sideDoorOptionsWrapper.vm.initializeComponent =
sideDoorOptions.methods.initializeComponent;
const windshieldOptionsWrapper = wrapper.findComponent({
name: "windshieldOptions",
});
windshieldOptionsWrapper.vm.initializeComponent =
windshieldOptions.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({ const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader", name: "funnelSubHeader",
}); });
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent; funnelSubHeader.methods.initializeComponent;
const driverSideOptionsWrapper = wrapper.findComponent({ const backGlassOptionsWrapper = wrapper.findComponent({
name: "replaceOptionsQuestion", name: "replaceOptionsQuestion",
}); });
driverSideOptionsWrapper.vm.initializeComponent = backGlassOptionsWrapper.vm.initializeComponent =
replaceOptionsQuestion.methods.initializeComponent; replaceOptionsQuestion.methods.initializeComponent;
const damageLocationQuestionWrapper = wrapper.findComponent({ const damageLocationQuestionWrapper = wrapper.findComponent({

View file

@ -3,8 +3,28 @@
<funnelHeader ref="funnelHeader" /> <funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false /> <vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader ref="funnelSubHeader" /> <funnelSubHeader ref="funnelSubHeader" />
<damageLocationQuestion ref="damageLocation" v-model="selectedDamageLocations" groupName="DamageLocationQuestion" /> <damageLocationQuestion
<replaceOptionsQuestion ref="driverSideOptions" isAvailable v-model="driverSideOptionsData" groupName="DriverSideReplaceOptionsQuestion" /> ref="damageLocation"
v-model="selectedDamageLocations"
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:selectedDamageLocations="selectedDamageLocations"
/>
<sideDoorOptions
ref="sideDoorOptions"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
:selectedDamageLocations="selectedDamageLocations"
/>
<replaceOptionsQuestion
ref="backGlassOptions"
:isAvailable="isRearWindowDamageLocation"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
/>
<funnelFooter ref="funnelFooter" @back-clicked="backButtonAction" /> <funnelFooter ref="funnelFooter" @back-clicked="backButtonAction" />
</div> </div>
</template> </template>
@ -15,8 +35,10 @@ import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question"; import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -62,21 +84,42 @@ export default {
vm.$refs.funnelSubHeader.initializeComponent( vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget resultMap.cmsContent.FunnelSubHeaderWidget
); );
vm.$refs.driverSideOptions.initializeComponent(
resultMap.cmsContent.DriverSideReplaceOptionsQuestion, resultMap.damageOptions.driverSideOptions.availableReplacementOptions
);
vm.$refs.damageLocation.initializeComponent( vm.$refs.damageLocation.initializeComponent(
resultMap.cmsContent.DamageLocationQuestion, resultMap.damageOptions resultMap.cmsContent.DamageLocationQuestion, resultMap.damageOptions
); );
vm.$refs.sideDoorOptions.initializeComponent(
resultMap.cmsContent.SideDoorSideQuestion, resultMap.cmsContent.DriverSideReplaceOptionsQuestion, resultMap.cmsContent.PassengerSideReplaceOptionsQuestion, resultMap.damageOptions.driverSideOptions.availableReplacementOptions, resultMap.damageOptions.passengerSideOptions.availableReplacementOptions
);
vm.$refs.windshieldOptions.initializeComponent(
resultMap.cmsContent.WindshieldDamageTypeQuestion, resultMap.cmsContent.WindshieldChipCountQuestion,
resultMap.cmsContent.WindshieldReplaceOptionsQuestion, resultMap.damageOptions.windshieldOptions.availableReplacementOptions
);
vm.$refs.backGlassOptions.initializeComponent(
resultMap.cmsContent.RearReplaceOptionsQuestion, resultMap.damageOptions.backGlassOptions.availableReplacementOptions
);
vm.$refs.funnelFooter.initializeComponent( vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget resultMap.cmsContent.FunnelFooterWidget
); );
}); });
}, },
computed: {
isRearWindowDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
{
return selectedDamages.toUpperCase() === "REARWINDOW";
});
},
},
data(){ data(){
return { return {
selectedDamageLocations: [], selectedDamageLocations: [],
driverSideOptionsData: [], sideDoorOptionsData: {
selectedDoorSides: [],
selectedDriverSideReplaceOptions: [],
selectedPassengerSideReplaceOptions: []
},
selectedWindshieldOptions: [],
selectedRearReplaceOptions: [],
} }
}, },
methods: { methods: {
@ -100,8 +143,10 @@ export default {
funnelFooter, funnelFooter,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
replaceOptionsQuestion, sideDoorOptions,
damageLocationQuestion, damageLocationQuestion,
windshieldOptions,
replaceOptionsQuestion,
}, },
}; };
</script> </script>

View file

@ -1,99 +0,0 @@
<template>
<div class="windshield-damage-type-question">
<buttonQuestion
v-if="isAvailable && answersToDisplay.length > 1"
:questionText="questionText"
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
v-model="selectedValues"
validationRules="windshield-damage-required|checkForRepairAndReplace:@DamageLocationQuestion"
:suppressError="suppressError"
/>
</div>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import store from "@/store";
import { defineRule } from "vee-validate";
// DEFINE VALIDATION RULES
defineRule("windshield-damage-required", (value) => {
if (!value || value.length < 1) {
return "Please select windshield damage";
}
return true;
});
defineRule("checkForRepairAndReplace", (value, [other]) => {
if (value === "Windshield-Chip" && other.toString().includes("Windshield") && other.length > 1) {
return "checkForRepairAndReplace error"
}
return true;
});
export default ({
name: "windshieldDamageTypeQuestion",
data(){
return {
questionText: String,
answersFromCms: Array,
damageOptions: Object,
}
},
props: {
isMultiSelect: Boolean,
modelValue: Array,
isAvailable: Boolean,
name: String,
groupName: String,
suppressError: Boolean,
},
methods: {
initializeComponent(cmsContent, damageOptions){
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
this.damageOptions = damageOptions;
},
},
computed: {
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
damageOptionsMap(){
return {
Windshield: true,
SideDoor: this.damageOptions.driverSideOptions.availableReplacementOptions || this.damageOptions.passengerSideOptions.availableReplacementOption ? true : false,
RearWindow: this.damageOptions.backGlassOptions.availableReplacementOptions ? true : false,
}
},
answersToDisplay(){
// HARD CODED MOCK DATA FOR NOW
return [
{
"Name": "Windshield-Crack",
"Text": "Crack",
"SubText": "My damage is larger than six inches.",
"ImageId": "2cf13fbb-22d8-4b61-bd3d-3d413ea58c87",
"AnswerImageUrl": "https://fixmyglass.safelite.com/Shared/images/icon-replace-desktop-ds.png"
},
{
"Name": "Windshield-Chip",
"Text": "Chip(s)",
"SubText": "I have three or fewer chips smaller than six inches.",
"ImageId": "faaa14d9-3650-4949-a687-7665962337b8",
"AnswerImageUrl": "https://fixmyglass.safelite.com/Shared/images/icon-repair-desktop-ds.png"
}
]
},
},
components: {
buttonQuestion,
},
})
</script>

View file

@ -0,0 +1,71 @@
import { shallowMount } from "@vue/test-utils";
import windshieldChipCountQuestion from "@/layouts/vehicle-damage/windshield-options/windshield-chip-count-question/windshield-chip-count-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
describe("windshield-chip-count-question.vue", () => {
test("Selected chip count is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({modelValueProp: ["One"]});
//Act
wrapper.setValue({ modelValue: ["Two"] });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.selectedChipCountValues).toEqual(["One"]);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ modelValue: ["Two"] }]);
});
});
describe("Windshield-chip-count-question.vue", () => {
test("Should display question and answers from api.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({modelValueProp: ["One"]});
//Act
windshieldChipCountQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent);
//Assert
expect(wrapper.vm.questionText).toStrictEqual("How many chips are we repairing?");
expect(wrapper.vm.answersFromCms).toStrictEqual([ { Name: 'One' }, { Name: 'Two' }, { Name: 'Three'} ]);
});
});
function setupMocks({
modelValueProp = ["Two"],
groupName = "WindshieldChipCountQuestion",
cmsQuestionText = "How many chips are we repairing?",
cmsAnswers = [{Name: "One"}, {Name: "Two"}, {Name: "Three"}],
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp
};
const wrapper = shallowMount(windshieldChipCountQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
groupName: groupName,
QuestionText: cmsQuestionText,
Answers: cmsAnswers,
};
const damageOptions = dataFromStoreApi;
return { wrapper, cmsContent, damageOptions };
}

View file

@ -0,0 +1,51 @@
<template>
<transition name="fade" mode="out-in">
<div class="windshield-chip-count-question" v-if="isAvailable" aria-live="polite">
<buttonQuestion
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonType="listButtonHorizontal"
v-model="selectedChipCountValues"
/>
</div>
</transition>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
export default ({
name: "windshieldOptions",
data(){
return {
questionText: String,
answersFromCms: Array
}
},
props: {
modelValue: Array,
groupName: String,
isAvailable: Boolean,
},
methods: {
initializeComponent(cmsContent){
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
}
},
computed: {
selectedChipCountValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
},
components: {
buttonQuestion,
}
})
</script>

View file

@ -0,0 +1,71 @@
import { shallowMount } from "@vue/test-utils";
import windshieldDamageTypeQuestion from"@/layouts/vehicle-damage/windshield-options/windshield-damage-type-question/windshield-damage-type-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock("@/store", () => { return {}; }, {virtual: true});
describe("windshield-damage-type-question.vue", () => {
test("Selected chip count is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({modelValueProp: ["Repair"]});
//Act
wrapper.setValue({ modelValue: ["Replace"] });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.selectedValues).toEqual(["Repair"]);
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ modelValue: ["Replace"] }]);
});
});
describe("windshield-damage-type-question.vue", () => {
test("Should display question and answers from api.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({modelValueProp: ["Repair"]});
//Act
windshieldDamageTypeQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent);
//Assert
expect(wrapper.vm.questionText).toStrictEqual("What's your windshield damage?");
expect(wrapper.vm.answersFromCms).toStrictEqual([ { Name: 'Repair' }, { Name: 'Replace' } ]);
});
});
function setupMocks({
modelValueProp = ["Two"],
groupName = "WindshieldDamageTypeQuestion",
cmsQuestionText = "What's your windshield damage?",
cmsAnswers = [{Name: "Repair"}, {Name: "Replace"}],
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp
};
const wrapper = shallowMount(windshieldDamageTypeQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
groupName: groupName,
QuestionText: cmsQuestionText,
Answers: cmsAnswers,
};
const damageOptions = dataFromStoreApi;
return { wrapper, cmsContent, damageOptions };
}

View file

@ -0,0 +1,70 @@
<template>
<transition name="fade" mode="out-in">
<div class="windshield-damage-type-question" v-if="isAvailable" aria-live="polite">
<buttonQuestion
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonType="listCard"
v-model="selectedValues"
/>
</div>
</transition>
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
//import { defineRule } from "vee-validate";
//DEFINE VALIDATION RULES
// add these to the button component above:
// validationRules="windshield-damage-required|checkForRepairAndReplace:@DamageLocationQuestion"
// :suppressError="suppressError"
// defineRule("windshield-damage-required", (value) => {
// if (!value || value.length < 1) {
// return "Please select windshield damage";
// }
// return true;
// });
// defineRule("checkForRepairAndReplace", (value, [other]) => {
// if (value === "Windshield-Chip" && other.toString().includes("Windshield") && other.length > 1) {
// return "checkForRepairAndReplace error"
// }
// return true;
// });
export default ({
name: "windshieldDamageTypeQuestion",
data(){
return {
questionText: String,
answersFromCms: Array
}
},
props: {
modelValue: Array,
groupName: String,
isAvailable: Boolean,
suppressError: Boolean,
},
methods: {
initializeComponent(cmsContent){
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
}
},
computed: {
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
},
components: {
buttonQuestion,
}
})
</script>

View file

@ -1,26 +1,110 @@
<template> <template>
<div class="windshield-options"> <div class="windshield-options">
<windshieldDamageTypeQuestion <windshieldDamageTypeQuestion ref="windshieldDamageTypeQuestion"
ref="windshieldDamageType" :isAvailable=isWindshieldDamageLocation
v-model="selectedWindshieldDamageTypes"
groupName="windshieldDamageTypeQuestion"
:isAvailable="showWindshieldDamageTypeQuestion"
:suppressError="suppressError" :suppressError="suppressError"
groupName="WindshieldDamageTypeQuestion"
v-model="selectedWindshieldDamageTypeValues"
/>
<windshieldChipCountQuestion ref="windshieldChipCountQuestion"
:isAvailable=isRepairOptionSelected
groupName="WindshieldChipCountQuestion"
v-model="selectedWindshieldChipCountValues"
/>
<replaceOptionsQuestion ref="replaceOptionsQuestion"
:isAvailable=isReplaceOptionSelected
isMultiSelect
groupName="WindshieldReplaceOptions"
v-model="selectedWindshieldReplaceOptionsValues"
/> />
</div> </div>
</template> </template>
<script> <script>
import windshieldDamageTypeQuestion from "@/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question"; import windshieldDamageTypeQuestion from"@/layouts/vehicle-damage/windshield-options/windshield-damage-type-question/windshield-damage-type-question";
import windshieldChipCountQuestion from"@/layouts/vehicle-damage/windshield-options/windshield-chip-count-question/windshield-chip-count-question";
import replaceOptionsQuestion from"@/layouts/vehicle-damage/replace-options-question/replace-options-question";
export default ({ export default ({
name: "windshieldOptions", name: "windshieldOptions",
props: { props: {
showWindshieldDamageTypeQuestion: Boolean, modelValue: Array,
suppressError: Boolean, selectedDamageLocations: Array,
selectedChipCount: Array,
selectedReplaceOptions: Array,
suppressError: Boolean
},
methods: {
initializeComponent(windshieldDamageTypeQuestionFromCms, windshieldChipCountQuestionFromCms,
windshieldReplaceOptionsQuestionFromCms, windshieldAvailableReplacementOptions){
this.$refs.windshieldDamageTypeQuestion.initializeComponent(windshieldDamageTypeQuestionFromCms);
this.$refs.windshieldChipCountQuestion.initializeComponent(windshieldChipCountQuestionFromCms);
this.$refs.replaceOptionsQuestion.initializeComponent(windshieldReplaceOptionsQuestionFromCms, windshieldAvailableReplacementOptions);
},
getWindshieldOptions(selectedWindshieldDamageType, selectedWindshieldChipCount, selectedWindshieldReplaceOptions){
return {
selectedWindshieldDamageType: selectedWindshieldDamageType,
selectedWindshieldChipCount: selectedWindshieldChipCount,
selectedWindshieldReplaceOptions: selectedWindshieldReplaceOptions
}
},
},
computed: {
selectedValues: {
get: function() {
return this.modelValue;
},
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
selectedWindshieldDamageTypeValues:{
get: function() {
return this.selectedValues.selectedWindshieldDamageType;
},
set: function(newValue) {
this.selectedValues = this.getWindshieldOptions(newValue, null, null);
}
},
selectedWindshieldChipCountValues: {
get: function() {
return this.selectedValues.selectedChipCount;
},
set: function(newValue) {
this.selectedValues = this.getWindshieldOptions(this.selectedWindshieldDamageTypeValues, newValue, null);
}
},
selectedWindshieldReplaceOptionsValues: {
get: function() {
return this.selectedValues.selectedWindshieldReplaceOptions;
},
set: function(newValue) {
this.selectedValues = this.getWindshieldOptions(this.selectedWindshieldDamageTypeValues, null, newValue);
}
},
isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
{
return Boolean(selectedDamages.toUpperCase() === "WINDSHIELD");
});
},
isRepairOptionSelected(){
if (!this.selectedWindshieldDamageTypeValues) return false;
return this.selectedWindshieldDamageTypeValues.some(val => val.toUpperCase() === "REPAIR") && this.isWindshieldDamageLocation;
},
isReplaceOptionSelected(){
if (!this.selectedWindshieldDamageTypeValues) return false;
return this.selectedWindshieldDamageTypeValues.some(val => val.toUpperCase() === "REPLACE") && this.isWindshieldDamageLocation;
},
}, },
components: { components: {
windshieldDamageTypeQuestion, windshieldDamageTypeQuestion,
windshieldChipCountQuestion,
replaceOptionsQuestion
}, },
}) })
</script> </script>

View file

@ -11,6 +11,7 @@ import store from "@/store";
import ComponentTest from "@/layouts/component-test/component-test.vue"; import ComponentTest from "@/layouts/component-test/component-test.vue";
import AddressPOC from "@/layouts/address-poc/address-poc.vue"; import AddressPOC from "@/layouts/address-poc/address-poc.vue";
import FormTest from "@/layouts/form-test/form-test.vue"; import FormTest from "@/layouts/form-test/form-test.vue";
import NestedRadio from "@/layouts/nested-radio-poc/nested-radio.vue";
const routes = [ const routes = [
{ {
@ -33,6 +34,11 @@ const routes = [
name: "FormTest", name: "FormTest",
component: FormTest, component: FormTest,
}, },
{
path: "/nested-radio", // This is a temporary route for testing.
name: "NestedRadio",
component: NestedRadio,
},
{ {
path: "/", path: "/",
name: "root", name: "root",

View file

@ -0,0 +1,12 @@
.fade-enter-active{
transition: opacity 0.6s ease;
}
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View file

@ -1,10 +1,10 @@
.has-error { .has-error {
&.list-button, &.list-button,
&.list-card { &.list-card {
border: 1px solid transparent; border: 1px solid $gray-500;
color: $red; color: $red;
label { label {
border: 1px solid $red; box-shadow: 0 0 1px $red;
border-radius: .5rem; border-radius: .5rem;
} }
} }
@ -20,6 +20,31 @@
border: 1px solid $red; border: 1px solid $red;
} }
} }
&.ui-radio,
&.ui-checkbox {
input[type=checkbox],
input[type=radio],
input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
color: $red;
}
input,
select {
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
}
}
}
} }
.form-test-error { .form-test-error {

View file

@ -76,26 +76,4 @@ describe("buttonMain.vue", () => {
expect(loader.attributes("class")).toContain("right"); expect(loader.attributes("class")).toContain("right");
}); });
it("Should return loader size in rem", async () => {
// Act
const wrapper = shallowMount(buttonMain, {
propsData: {
sizeInRem: 1,
loaderEnabled: true,
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.clicked();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("style")).toContain("1rem");
});
}); });

View file

@ -9,7 +9,6 @@
<loader <loader
class="ms-2" class="ms-2"
v-if="isLoaderDisplayed" v-if="isLoaderDisplayed"
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>
@ -25,7 +24,6 @@ export default {
isDisabled: Boolean, isDisabled: Boolean,
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number, String],
isFloat: Boolean, isFloat: Boolean,
}, },
data() { data() {
@ -56,6 +54,7 @@ export default {
border-radius: $border-radius-lg; border-radius: $border-radius-lg;
color: $white; color: $white;
justify-content: center; justify-content: center;
font-weight: 500;
&:hover { &:hover {
background: linear-gradient( background: linear-gradient(
270deg, 270deg,
@ -82,7 +81,8 @@ export default {
$gray-200 0%, $gray-200 0%,
$gray-200 100% $gray-200 100%
) !important; ) !important;
color: $gray-550 !important; color: $gray-600 !important;
font-weight: 400;
height: 48px; height: 48px;
border: none; border: none;
border-radius: $border-radius-lg; border-radius: $border-radius-lg;
@ -103,6 +103,7 @@ export default {
border: 1px solid $blue; border: 1px solid $blue;
border-radius: $border-radius-lg; border-radius: $border-radius-lg;
color: $blue; color: $blue;
font-weight: 500;
transition: all 150ms linear; transition: all 150ms linear;
&:hover { &:hover {
color: $white; color: $white;
@ -118,10 +119,13 @@ export default {
} }
&:disabled { &:disabled {
background: transparent; background: transparent;
color: $gray !important; color: $gray-550 !important;
font-weight: 400;
height: 48px; height: 48px;
border: 1px solid $gray-300; border: 1px solid $gray-550;
border-radius: $border-radius-lg; border-radius: $border-radius-lg;
cursor: pointer;
pointer-events: all;
} }
&.has-loader { &.has-loader {
color: $white; color: $white;

View file

@ -1,15 +1,15 @@
<template> <template>
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag --> <!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
<div class="ui-checkbox d-flex"> <div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
<input <input class="form-check-input"
type="checkbox" type="checkbox"
aria-checked="false" aria-checked="false"
:name="checkboxName" :name="checkboxName"
:id="buttonID" :id="buttonID"
:tabindex="tabIndex" :tabindex="tabIndex"
:aria-required="isRequired" :aria-required="isRequired"
/> />
<label class="d-flex align-items-center" :for="buttonID"> <label class="d-flex align-items-start" :for="buttonID">
<p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p> <p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{ <span v-if="screenReaderOnlyText" class="sr-only">{{
screenReaderOnlyText screenReaderOnlyText
@ -28,65 +28,27 @@ export default {
checkboxLabel: String, checkboxLabel: String,
screenReaderOnlyText: String, screenReaderOnlyText: String,
isRequired: Boolean, isRequired: Boolean,
hasError: Boolean
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.ui-checkbox { .form-check {
position: relative; .form-check-input {
input[type="checkbox"] { border: 1px solid $gray-500;
position: absolute !important; border-radius: 2px;
height: 1px; &:checked {
width: 1px; background-size: 125%;
overflow: hidden; border: 1px solid $blue;
clip: rect(1px, 1px, 1px, 1px);
+ label {
display: block;
position: relative;
&:hover {
cursor: pointer;
}
} }
+ label::before { &:focus {
content: "";
position: relative;
display: inline-block;
margin-right: 8px;
width: 16px;
height: 16px;
background: white;
border: 1px solid $gray-500;
border-radius: 2px;
}
&:checked + label::before {
background: $blue;
}
&:checked + label::after {
content: "";
position: absolute;
top: 8px;
left: 2px;
border-left: 2px solid $white;
border-bottom: 2px solid $white;
height: 6px;
width: 12px;
transform: rotate(-45deg);
}
&:hover + label::before {
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;
} }
&:focus + label::before { }
box-shadow: 0 0 0 2px $blue; &:hover {
} .form-check-input {
&:focus:checked + label::before { box-shadow: 0 0 0 4px $blue-300;
box-shadow: 0 0 0 2px transparent;
}
&:disabled + label {
color: $gray-200;
}
&:disabled + label::before {
background: $gray-200;
} }
} }
} }

View file

@ -155,36 +155,13 @@ describe("list-button-horizontal.vue", () => {
expect(loader.attributes("class")).toContain("right"); expect(loader.attributes("class")).toContain("right");
}); });
it("Should return loader size in rem", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
sizeInRem: 1,
selectingInitiatesLoad: true,
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("style")).toContain("1rem");
});
it("Should emit button value on click", async () => { it("Should emit button value on click", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
propsData: { propsData: {
isRadioHorizontal: true, isRadioHorizontal: true,
buttonLabel: "Windshield", buttonLabel: "Windshield",
buttonID: "List Card Checkbox", value: "List Card Checkbox",
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
@ -195,7 +172,7 @@ describe("list-button-horizontal.vue", () => {
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{buttonId: "List Card Checkbox", isChecked: Boolean}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean}]);
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
@ -212,7 +189,7 @@ describe("list-button-horizontal.vue", () => {
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"], modelValue: ["List Card Checkbox"],
isMultiSelect: false, isMultiSelect: false,
selectedButtonIDs: ["Car-Front"] selectedValues: ["Car-Front"]
}, },
}); });
// Assert // Assert

View file

@ -1,6 +1,7 @@
<template> <template>
<div <div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
:class="hasError ? 'has-error' : ''"
@mouseup="handleClick(value)" @mouseup="handleClick(value)"
@keyup.space="handleClick(value)" @keyup.space="handleClick(value)"
> >
@ -8,11 +9,11 @@
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName" :data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange : ''" @change="!selectingInitiatesLoad ? handleCheckChange() : ''"
/> />
<label <label
tabindex="-1" tabindex="-1"
@ -38,7 +39,6 @@
</span> </span>
<loader <loader
v-if="isLoaderDisplayed && !isMultiSelect" v-if="isLoaderDisplayed && !isMultiSelect"
:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
:class="[loaderColor, loaderPosition]" :class="[loaderColor, loaderPosition]"
/> />
</label> </label>
@ -63,14 +63,14 @@ export default {
selectingInitiatesLoad: Boolean, selectingInitiatesLoad: Boolean,
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number, String],
isRequired: Boolean, isRequired: Boolean,
value: { value: {
// Field initial value // Field initial value
type: String, type: String,
default: "", default: "",
}, },
selectedButtonIDs: [Array, String], selectedValues: [Array, String],
hasError: Boolean,
}, },
data() { data() {
return { return {
@ -79,8 +79,8 @@ export default {
}; };
}, },
created(){ created(){
if(this.selectedButtonIDs){ if(Array.isArray(this.selectedValues)){
this.checkValue = this.isMultiSelect ? this.selectedButtonIDs.includes(this.buttonID) : this.selectedButtonIDs[0]; this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0];
} }
}, },
methods: { methods: {
@ -97,7 +97,7 @@ export default {
handleCheckChange(newValue, oldValue){ handleCheckChange(newValue, oldValue){
const isInitialization = typeof(oldValue) === 'function'; const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) { if (!isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID.toString() }); this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });
} }
} }
}, },
@ -133,13 +133,16 @@ export default {
height: 0; height: 0;
&:focus-visible + label { &:focus-visible + label {
box-shadow: 0 0 0 2px $blue; box-shadow: 0 0 0 2px $blue;
z-index: 2;
} }
&:focus + label { &:focus + label {
box-shadow: 0 0 0 2px $blue; box-shadow: 0 0 0 2px $blue;
z-index: 3;
} }
&:checked + label { &:checked + label {
background: $blue-100; background: $blue-100;
box-shadow: 0 0 0 1px $blue; box-shadow: 0 0 0 1px $blue;
z-index: 2;
} }
&:checked + label p:first-child { &:checked + label p:first-child {
font-weight: 500; font-weight: 500;
@ -156,11 +159,14 @@ export default {
&:hover { &:hover {
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;
cursor: pointer; cursor: pointer;
z-index: 2; z-index: 4 !important;
} }
+ p { + p {
display: none; display: none;
} }
span {
font-size: .875rem;
}
} }
&:first-of-type { &:first-of-type {
label { label {

View file

@ -153,36 +153,13 @@ describe("list-button.vue", () => {
expect(loader.attributes("class")).toContain("right"); expect(loader.attributes("class")).toContain("right");
}); });
it("Should return loader size in rem", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
sizeInRem: 1,
selectingInitiatesLoad: true,
},
});
// Assert
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.handleClick();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("style")).toContain("1rem");
});
it("Should emit button value on click", async () => { it("Should emit button value on click", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
propsData: { propsData: {
isRadioHorizontal: true, isRadioHorizontal: true,
buttonLabel: "Windshield", buttonLabel: "Windshield",
buttonID: "List Card Checkbox", value: "List Card Checkbox",
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
@ -193,7 +170,7 @@ describe("list-button.vue", () => {
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{buttonId: "List Card Checkbox", isChecked: Boolean}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean}]);
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
@ -209,7 +186,7 @@ describe("list-button.vue", () => {
isRequired: true, isRequired: true,
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"], modelValue: ["List Card Checkbox"],
selectedButtonIDs: ["Car-Front"] selectedValues: ["Car-Front"]
}, },
}); });
// Assert // Assert

View file

@ -1,6 +1,7 @@
<template> <template>
<div <div
class="list-group list-button d-flex flex-column w-100 mb-2" class="list-group list-button d-flex flex-column w-100 mb-2"
:class="hasError ? 'has-error' : ''"
@mouseup="handleClick(value)" @mouseup="handleClick(value)"
@keyup.space="handleClick(value)" @keyup.space="handleClick(value)"
> >
@ -8,11 +9,11 @@
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName" :data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange : ''" @change="!selectingInitiatesLoad ? handleCheckChange() : ''"
> >
<label <label
tabindex="-1" tabindex="-1"
@ -41,7 +42,6 @@
</span> </span>
<loader <loader
v-if="isLoaderDisplayed && !isMultiSelect" v-if="isLoaderDisplayed && !isMultiSelect"
:style="{ width: `${sizeInRem}rem`, height: `${sizeInRem}rem` }"
:class="[this.loaderColor, this.loaderPosition]" :class="[this.loaderColor, this.loaderPosition]"
/> />
</label> </label>
@ -66,13 +66,13 @@ export default {
selectingInitiatesLoad: Boolean, selectingInitiatesLoad: Boolean,
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number,String],
value: { value: {
// Field initial value // Field initial value
type: [String, Number], type: [String, Number],
default: "", default: "",
}, },
selectedButtonIDs: [Array, String], selectedValues: [Array, String],
hasError: Boolean,
}, },
data() { data() {
return { return {
@ -81,8 +81,8 @@ export default {
}; };
}, },
created(){ created(){
if(this.selectedButtonIDs){ if(Array.isArray(this.selectedValues)){
this.checkValue = this.isMultiSelect ? this.selectedButtonIDs.includes(this.buttonID) : this.selectedButtonIDs[0]; this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0];
} }
}, },
methods: { methods: {
@ -99,7 +99,7 @@ export default {
handleCheckChange(newValue, oldValue){ handleCheckChange(newValue, oldValue){
const isInitialization = typeof(oldValue) === 'function'; const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) { if (!isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID.toString() }); this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });
} }
}, },
}, },
@ -126,7 +126,7 @@ export default {
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.list-group { .list-group {
&.list-button { &.list-button {
input[type="radio"], input[type="radio"],
@ -147,9 +147,14 @@ export default {
background: $blue-100; background: $blue-100;
box-shadow: 0 0 0 1px $blue; box-shadow: 0 0 0 1px $blue;
} }
&:checked + label p:first-child { &:checked + label p,
&:checked + label span {
font-weight: 500; font-weight: 500;
} }
&:checked + label span:nth-child(2) {
font-weight: 400;
color: $gray-600;
}
} }
} }
label { label {
@ -161,6 +166,13 @@ export default {
border: 1px solid $gray-500; border: 1px solid $gray-500;
width: 100%; width: 100%;
span {
&.small {
font-size: .75rem;
color: $gray-550;
}
}
&:hover { &:hover {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;

View file

@ -197,7 +197,7 @@ describe("list-card.vue", () => {
propsData: { propsData: {
isRadioHorizontal: true, isRadioHorizontal: true,
buttonLabel: "Windshield", buttonLabel: "Windshield",
buttonID: "List Card Checkbox", value: "List Card Checkbox",
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
@ -208,7 +208,7 @@ describe("list-card.vue", () => {
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{buttonId: "List Card Checkbox", isChecked: Boolean}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean}]);
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
@ -217,14 +217,14 @@ describe("list-card.vue", () => {
propsData: { propsData: {
isRadioHorizontal: true, isRadioHorizontal: true,
buttonLabel: "Windshield", buttonLabel: "Windshield",
buttonID: "List Card Checkbox", value: "List Card Checkbox",
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"], modelValue: ["List Card Checkbox"],
selectedButtonIDs: ["Car-Front"] selectedValues: ["Car-Front"]
}, },
}); });
// Assert // Assert

View file

@ -2,18 +2,18 @@
<div :class="'col' + colLength"> <div :class="'col' + colLength">
<div <div
class="list-card w-100 rounded-3 d-flex align-items-center h-100" class="list-card w-100 rounded-3 d-flex align-items-center h-100"
:class="[isWide ? 'horizontal' : '', errors.length > 0 ? 'has-error' : '']" :class="[isWide ? 'horizontal' : '', errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
> >
<input <input
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName" :data-focus-target="groupName"
@click="handleChange(value)" @click="handleChange(value)"
v-model="checkValue" v-model="checkValue"
@change="handleCheckChange" @change="handleCheckChange()"
/> />
<label <label
:for="buttonID" :for="buttonID"
@ -53,10 +53,8 @@ import { useField } from "vee-validate";
export default { export default {
name: "listCard", name: "listCard",
props: { props: {
//Must choose one of the following four options
isMultiSelect: Boolean, //Defines use as checkbox isMultiSelect: Boolean, //Defines use as checkbox
isWide: Boolean, isWide: Boolean,
//end must choose
buttonImage: String, //Required: File name of image buttonImage: String, //Required: File name of image
buttonImageId: String, buttonImageId: String,
buttonLabel: String, //Required: Label text buttonLabel: String, //Required: Label text
@ -72,7 +70,8 @@ export default {
}, },
colLength: String, colLength: String,
validationRules: String, validationRules: String,
selectedButtonIDs: [Array, String], selectedValues: [Array, String],
hasError: Boolean,
}, },
data(){ data(){
return { return {
@ -80,8 +79,8 @@ export default {
} }
}, },
created(){ created(){
if(this.selectedButtonIDs){ if(Array.isArray(this.selectedValues)){
this.checkValue = this.isMultiSelect ? this.selectedButtonIDs.includes(this.buttonID) : this.selectedButtonIDs[0]; this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0];
} }
}, },
computed: { computed: {
@ -101,7 +100,7 @@ export default {
handleCheckChange(newValue, oldValue){ handleCheckChange(newValue, oldValue){
const isInitialization = typeof(oldValue) === 'function'; const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) { if (!isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID.toString() }); this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });
} }
} }
}, },
@ -174,9 +173,11 @@ export default {
&:checked + label { &:checked + label {
p { p {
color: $black; color: $black;
font-weight: 500;
} }
.sub-copy { .sub-copy {
color: $gray-600; color: $gray-600;
font-weight: 400;
} }
} }
+ label::before { + label::before {
@ -247,6 +248,16 @@ export default {
margin: -0.25rem 0 0 0; margin: -0.25rem 0 0 0;
left: 0.875rem; left: 0.875rem;
} }
&:checked + label {
p {
color: $black;
font-weight: 500;
}
.sub-copy {
color: $gray-600;
font-weight: 400;
}
}
+ label { + label {
min-height: 48px; min-height: 48px;
color: $gray-600; color: $gray-600;
@ -254,6 +265,7 @@ export default {
margin-bottom: 0; margin-bottom: 0;
} }
p { p {
color: $gray-600;
text-align: left; text-align: left;
&.sub-copy { &.sub-copy {
color: $gray-550; color: $gray-550;

View file

@ -3,7 +3,6 @@
class="loader" class="loader"
role="alert" role="alert"
aria-label="Loading new page" aria-label="Loading new page"
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>
</template> </template>
@ -13,10 +12,6 @@ export default {
name: "loader", name: "loader",
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */ /* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
props: { props: {
sizeInRem: {
type: Number,
default: 1,
},
/* Color options: red, green, blue, white, black */ /* Color options: red, green, blue, white, black */
loaderColor: { loaderColor: {
type: String, type: String,
@ -50,8 +45,8 @@ export default {
mask: url(../../assets/img/icons/spinner.svg); mask: url(../../assets/img/icons/spinner.svg);
mask-size: cover; mask-size: cover;
position: relative; position: relative;
width: 100%; width: 1rem;
height: 100%; height: 1rem;
animation: rotation 1s infinite linear; animation: rotation 1s infinite linear;
@keyframes rotation { @keyframes rotation {
100% { 100% {

View file

@ -1,17 +1,18 @@
<template> <template>
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex --> <!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex -->
<div class="ui-radio d-flex"> <div class="ui-radio form-check" :class="[hasError ? 'has-error' : '']">
<input <input
type="radio" type="radio"
class="form-check-input"
aria-checked="false" aria-checked="false"
:name="groupName" :name="groupName"
:id="buttonID" :id="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
:value="value" :value="value"
:v-model="checkValue" :v-model="checkValue"
@change="handleCheckChanged" @change="handleCheckChange()"
/> />
<label class="d-flex align-items-center" :for="buttonID"> <label class="d-flex align-items-start form-check-label" :for="buttonID">
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p> <p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{ <span v-if="screenReaderOnlyText" class="sr-only">{{
screenReaderOnlyText screenReaderOnlyText
@ -35,6 +36,7 @@ export default {
default: "", default: "",
}, },
screenReaderOnlyText: String, screenReaderOnlyText: String,
hasError: Boolean,
}, },
data() { data() {
return { return {
@ -53,7 +55,7 @@ export default {
handleCheckChange(newValue, oldValue){ handleCheckChange(newValue, oldValue){
const isInitialization = typeof(oldValue) === 'function'; const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) { if (!isInitialization) {
this.$emit('isCheckedChanged', { isChecked: this.checkValue, buttonId: this.buttonID.toString() }); this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });
} }
} }
}, },
@ -76,62 +78,25 @@ export default {
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.ui-radio { .form-check {
position: relative; .form-check-input {
input[type="radio"] { border: 1px solid $gray-500;
position: absolute !important; border-radius: 100%;
height: 1px; margin-right: .5rem;
width: 1px; &:checked {
overflow: hidden; background-color: $white;
clip: rect(1px, 1px, 1px, 1px); background-size: 71%;
+ label {
display: block;
position: relative;
&:hover {
cursor: pointer;
}
}
+ label::before {
content: "";
position: relative;
display: inline-block;
margin-right: 8px;
width: 16px;
height: 16px;
background: white;
border: 1px solid $gray-500;
border-radius: 50%;
}
&:checked + label::before {
background: $white;
border: 1px solid $blue; border: 1px solid $blue;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' xml:space='preserve'%3e%3ccircle cx='25' cy='25' r='25' fill='%231574A1'/%3e%3c/svg%3e");
} }
&:checked + label::after { &:focus {
content: "";
position: absolute;
top: 8px;
left: 3px;
height: 6px;
width: 6px;
transform: rotate(-45deg);
border: 5px solid $blue;
border-radius: 50%;
}
&:hover + label::before {
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;
} }
&:focus + label::before { }
box-shadow: 0 0 0 2px $blue; &:hover {
} .form-check-input {
&:focus:checked + label::before { box-shadow: 0 0 0 4px $blue-300;
box-shadow: 0 0 0 2px transparent;
}
&:disabled + label {
color: $gray-200;
}
&:disabled + label::before {
background: $gray-200;
} }
} }
} }