Merge pull request #763 from Safelite/feature/CSR-762-GA

DO NOT MERGE - For viewing purposes
This commit is contained in:
katieoh-safelite 2022-10-12 13:54:16 -04:00 committed by GitHub
commit 39d922fd4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1803 additions and 1103 deletions

View file

@ -1,4 +1,4 @@
{ {
"tabWidth": 2, "tabWidth": 4,
"bracketSameLine": true "bracketSameLine": true
} }

View file

@ -28,8 +28,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
// TODO KO statements: 85,
statements: 55,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
}, },
}, },

View file

@ -1,16 +1,65 @@
<template> <template>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition :duration="{ enter: 200, leave: 200 }" name="route-fade" mode="out-in"> <transition
<!-- The above durations should be kept in sync with the global css class "fade-on-route-transition" --> :duration="{ enter: 200, leave: 200 }"
<component :is="Component" /> name="route-fade"
</transition> mode="out-in">
</router-view> <!-- The above durations should be kept in sync with the global css class "fade-on-route-transition" -->
<component :is="Component" @focusin="handleChildFocus" />
</transition>
</router-view>
</template> </template>
<script>
// TODO KO glass-part-question has nested button questions
// TODO KO selectedValues - make consistent for checkbox and radio if possible
export default {
name: "app",
data() {
return {
lastFocusedInputGroupName: "",
onFocusCallback: null,
};
},
methods: {
handleChildFocus(e) {
const targetType = e.target.type;
if (targetType !== "radio" && targetType !== "checkbox") {
this.handleInputFocus({
groupName: null,
});
}
},
handleInputFocus(e) {
if (
e &&
this.lastFocusedInputGroupName !== e.groupName &&
this.onFocusCallback
) {
this.onFocusCallback();
}
},
handleInputBlur(e) {
if (e) {
this.lastFocusedInputGroupName = e.groupName;
this.onFocusCallback = e.onFocusCallback;
}
},
},
watch: {
$route: {
handler() {
this.lastFocusedInputGroup = null;
},
},
},
};
</script>
<style lang="scss"> <style lang="scss">
@import "./node_modules/bootstrap/scss/bootstrap"; @import "./node_modules/bootstrap/scss/bootstrap";
@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"; @import "@/styles/common-animations.scss";
</style> </style>

View file

@ -0,0 +1,539 @@
import { shallowMount, mount } from "@vue/test-utils";
import baseInputButton from "./base-input-button";
describe("baseInputButton.vue", () => {
describe("general", () => {
describe("checkbox", () => {
test("isMultiSelect => baseInputButton is a checkbox", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(inputElement.attributes().type).toEqual("checkbox");
});
});
describe("radio", () => {
test("!isMultiSelect => baseInputButton is a radio button", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(inputElement.attributes().type).toEqual("radio");
});
});
// tests in here should be test.each
describe("shared", () => {
const isMultiSelectOptions = [true, false];
test.each(isMultiSelectOptions)("groupName", (isMultiSelect) => {
const { wrapper } = setupMocks({
mockData: {
propsData: {
groupName: "boogly",
isMultiSelect: isMultiSelect,
},
},
});
// Assert
const inputElement = wrapper.find("input");
const inputElementAttributes = inputElement.attributes();
expect(inputElementAttributes.name).toEqual("boogly");
});
});
});
describe("mouse clicks", () => {
describe("checkbox", () => {
test("clicked => correct event and value are emitted", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
value: "X",
},
},
});
// Act
await wrapper.trigger("mousedown.left");
await wrapper.trigger("click");
// Assert
expect(wrapper.emitted()["inputButtonClicked"][0][0]).toEqual([
"X",
]);
});
});
describe("radio", () => {
test("clicked => correct event and value are emitted", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
value: "X",
},
},
});
// Act
await wrapper.trigger("mousedown.left");
await wrapper.trigger("click");
// Assert
expect(wrapper.emitted()["inputButtonClicked"][0][0]).toEqual(
"X"
);
});
});
});
describe("keyboard navigation and events", () => {
describe("checkbox", () => {
test.todo(
"focus on a checkbox => inputButtonClicked is not emitted"
);
test.todo(
"blur from a checkbox => inputButtonClicked is not emitted"
);
test("change event fired from checkbox => inputButtonClicked is emitted with correct value", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
value: "Hi",
},
},
});
const input = wrapper.find("input");
// Act
await input.trigger("change");
// Assert
expect(wrapper.emitted()["inputButtonClicked"][0][0]).toEqual([
"Hi",
]);
});
test("focus and click space on a checkbox => inputButtonClicked is not emitted", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
value: "Hi",
},
},
});
const input = wrapper.find("input");
// Act
await input.trigger("keypress", { key: "space" });
// Assert
expect(wrapper.emitted()).not.toHaveProperty(
"inputButtonClicked"
);
});
test("focus and click enter on a checkbox => inputButtonClicked is emitted with correct value", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
value: "Hi",
},
},
});
const input = wrapper.find("input");
// Act
await input.trigger("keypress", { key: "enter" });
// Assert
expect(wrapper.emitted()["inputButtonClicked"][0][0]).toEqual([
"Hi",
]);
});
});
describe("radio", () => {
test.todo(
"focus on a radio button => inputButtonClicked is not emitted"
);
test.todo(
"blur from a radio button => inputButtonClicked is not emitted"
);
test("focus and click space on a radio button => inputButtonClicked is emitted with correct value", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
value: "Hi",
},
},
});
const input = wrapper.find("input");
// Act
await input.trigger("keypress", { key: "space" });
// Assert
expect(wrapper.emitted()).toHaveProperty("inputButtonClicked");
expect(wrapper.emitted()["inputButtonClicked"][0][0]).toEqual(
"Hi"
);
});
test("focus and click enter on a radio button => inputButtonClicked is emitted with correct value", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
value: "Hi",
},
},
});
const input = wrapper.find("input");
// Act
await input.trigger("keypress", { key: "enter" });
// Assert
expect(wrapper.emitted()).toHaveProperty("inputButtonClicked");
expect(wrapper.emitted()["inputButtonClicked"][0][0]).toEqual(
"Hi"
);
});
});
});
describe("methods", () => {
describe("handleEventAction", () => {});
describe("handleSelectionChange", () => {
test.todo("handleChange is called with valueToEmit");
describe("checkbox", () => {
test.todo("modelValue is null => valueToEmit is correct value");
test.todo(
"modelValue is undefined => valueToEmit is correct value"
);
test.todo(
"modelValue is empty => valueToEmit is correct value"
);
test.todo(
"modelValue is not empty and does not contain this button's value => valueToEmit is correct value"
);
test.todo(
"modelValue is not empty and does contain this button's value => valueToEmit is correct value"
);
});
describe("radio", () => {});
});
describe("handleClick", () => {
test.todo("handleSelectChange is also called");
test.todo("inputButtonClicked is emitted with valueToEmit");
});
});
describe("computed", () => {
describe("isChecked", () => {
describe("checkbox", () => {
const falsyModelValues = [[], null, undefined];
test.each(falsyModelValues)(
"modelValue is falsy/empty => checkbox isn't checked",
(modelValue) => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
modelValue,
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.isChecked).toEqual(false);
expect(inputElement.element.checked).toBe(false);
}
);
test("modelValue doesn't contain this button's value => checkbox isn't checked", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
modelValue: ["Aaa", "Bbb", "Ccc"],
value: "Ddd",
},
},
});
await wrapper.vm.$nextTick();
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.isChecked).toEqual(false);
expect(inputElement.element.checked).toBe(false);
});
test("modelValue contains this button's value => checkbox is checked", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
modelValue: ["Aaa", "Bbb", "Ddd", "Ccc"],
value: "Ddd",
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.isChecked).toEqual(true);
expect(inputElement.element.checked).toBe(true);
});
});
describe("radio", () => {
const falsyModelValues = ["", null, undefined, []];
test.each(falsyModelValues)(
"modelValue is falsy/empty => radio button isn't checked",
(modelValue) => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
modelValue,
value: "Aaa",
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.isChecked).toEqual(false);
expect(inputElement.element.checked).toBe(false);
}
);
test("modelValue equals this button's value => radio button is checked", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
modelValue: "Ddd",
value: "Ddd",
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.isChecked).toEqual(true);
expect(inputElement.element.checked).toBe(true);
});
test("modelValue doesn't equal this button's value => radio button isn't checked", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
modelValue: "Aaa",
value: "Ddd",
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.isChecked).toEqual(false);
expect(inputElement.element.checked).toBe(false);
});
});
});
describe("buttonId", () => {
test("groupName and value combo yield correct id for input button with string value", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
groupName: "my test-name",
value: "Aaa-BBB CcC",
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.buttonId).toBe("my-test-name-Aaa-BBB-CcC");
expect(inputElement.attributes().id).toBe(
"my-test-name-Aaa-BBB-CcC"
);
});
test("groupName and value combo yield correct id for input button with number value", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
groupName: "my test-name",
value: 2,
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.buttonId).toBe("my-test-name-2");
expect(inputElement.attributes().id).toBe("my-test-name-2");
});
});
describe("inputType", () => {
test("isMultiSelect is true => inputType is 'checkbox'", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.inputType).toEqual("checkbox");
expect(inputElement.attributes().type).toBe("checkbox");
});
test("isMultiSelect is false => inputType is 'radio'", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
},
},
});
// Assert
const inputElement = wrapper.find("input");
expect(wrapper.vm.inputType).toEqual("radio");
expect(inputElement.attributes().type).toBe("radio");
});
});
});
});
// TODO KO look at how I tested groups of these in SFA (making sure selecting one radio changes the value, etc, that this acts like a regular input aside from a different emitted event)
function setupMocks({ mockData = {}, shouldShallowMount = true }) {
const baseInputButtonWrapper = {
components: { baseInputButton },
template: '<baseInputButton v-model="myValue" />',
data() {
return {
myValue: "",
};
},
};
const mountMockData = {
...mockData,
propsData: {
// to get rid of some annoying warnings
groupName: "groupName",
modelValue: mockData.propsData?.isMultiSelect ? [] : "",
value: "5",
setLastValuePushedToGa: () => {},
// should override the above if they exist
...mockData.propsData,
},
};
const wrapper = shouldShallowMount
? shallowMount(baseInputButton, mountMockData)
: mount(baseInputButton, mountMockData);
wrapper.vm.pushEventToGA = jest.fn();
wrapper.vm.$route = {
query: {},
};
wrapper.vm.GaActions = {}
return { wrapper };
}
function setupBaseInputButtonWrapper({ mockData = {} }) {
const baseInputButtonWrapper = {
components: { baseInputButton },
template:
'<div><baseInputButton v-model="myValue" groupName="myGroupName" value="X" :isMultiSelect="isMultiSelect" /></div>',
data() {
return {
myValue: "",
isMultiSelect: mockData.isMultiSelect,
};
},
// const parentComponent = mount({
// data() {
// return {
// value: "value1",
// }
// },
// template: '<div><baseInputButton v-model="value" value="value1" /><baseInputButton v-model="value" value="value2" /></div>',
// components: { baseInputButton }
// })
};
const wrapper = mount(baseInputButtonWrapper, {});
return { wrapper };
}

View file

@ -2,8 +2,9 @@
<label <label
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0 }]" :class="[buttonWrapperClasses, { 'has-error': errors.length > 0 }]"
:for="buttonId" :for="buttonId"
@mousedown.left="handleEventAction('click', $event)" @focusin="handleFocus"
@keyup="handleKeyboardNavigation"> @focusout="handleBlur"
@mousedown.left="handleEventAction('click', $event)">
<input <input
:type="inputType" :type="inputType"
:id="buttonId" :id="buttonId"
@ -13,7 +14,6 @@
:aria-required="isRequired" :aria-required="isRequired"
:value="value" :value="value"
:checked="isChecked" :checked="isChecked"
@blur="handleBlur"
@keypress.space="handleEventAction('space', $event)" @keypress.space="handleEventAction('space', $event)"
@keypress.enter="handleEventAction('enter', $event)" @keypress.enter="handleEventAction('enter', $event)"
@change="handleEventAction('change', $event)" /> @change="handleEventAction('change', $event)" />
@ -23,18 +23,20 @@
</template> </template>
<script> <script>
import { queryStrings } from "@/constants/query-strings";
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import { toRef } from "vue"; import { toRef } from "vue";
import { queryStrings } from "@/constants/query-strings";
import inputButtonWrapperMixin from "../../mixins/input-button-wrapper-mixin";
export default { export default {
name: "base-input-button", name: "base-input-button",
emits: ["change"], emits: ["change", "inputButtonClicked"],
model: { model: {
prop: "modelValue", prop: "modelValue",
event: "change", event: "change",
}, },
props: { props: {
// ...inputButtonWrapperMixin.props
value: { value: {
type: [String, Number], type: [String, Number],
required: true, required: true,
@ -60,6 +62,9 @@ export default {
default: true, default: true,
}, },
isRequired: Boolean, isRequired: Boolean,
lastValuePushedToGa: [String, Number],
setLastValuePushedToGa: Function,
shouldPushClickEventToGAOnMount: Boolean,
}, },
data() { data() {
return { return {
@ -68,36 +73,37 @@ export default {
}, },
mounted() { mounted() {
if (this.isChecked) { if (this.isChecked) {
this.handleChange(this.modelValue); if (this.shouldPushClickEventToGAOnMount) {
this.handleEventAction(this.eventTypes.MOUNT)
} else {
this.handleChange(this.modelValue);
}
} }
}, },
methods: { methods: {
handleEventAction(eventType, e) { handleEventAction(eventType, e) {
const eventTypes = {
CHANGE: "change",
ENTER: "enter",
SPACE: "space",
CLICK: "click",
};
if (this.isMultiSelect) { if (this.isMultiSelect) {
switch (eventType) { switch (eventType) {
case eventTypes.ENTER: case this.eventTypes.ENTER:
case eventTypes.CHANGE: case this.eventTypes.CHANGE:
this.handleClick(e); this.handleClick(e);
window.lastFocusedInputGroup = this.groupName; this.handlePushClickEventToGACheck(
this.pushClickEventToGA(); this.eventTypes.CLICK
);
break; break;
} }
} else { } else {
switch (eventType) { switch (eventType) {
case eventTypes.CLICK: case this.eventTypes.CLICK:
case eventTypes.ENTER: case this.eventTypes.ENTER:
case eventTypes.SPACE: case this.eventTypes.SPACE:
case this.eventTypes.MOUNT:
this.handleClick(e); this.handleClick(e);
this.test(); this.handlePushClickEventToGACheck(
this.eventTypes.CLICK
);
break; break;
case eventTypes.CHANGE: case this.eventTypes.CHANGE:
this.selectOnKeypress this.selectOnKeypress
? this.handleClick(e) ? this.handleClick(e)
: this.handleSelectionChange(e); : this.handleSelectionChange(e);
@ -122,21 +128,37 @@ export default {
this.valueToEmit = this.value; this.valueToEmit = this.value;
} }
window.currentlySelectedValues =
window.currentlySelectedValues ?? {};
window.currentlySelectedValues[this.groupName] = this.value;
this.handleChange(this.valueToEmit); this.handleChange(this.valueToEmit);
}, },
handleClick(e) { handleClick(e) {
this.handleSelectionChange(e); this.handleSelectionChange(e);
this.$emit("buttonClicked", this.valueToEmit); this.$emit("inputButtonClicked", this.valueToEmit);
},
handleFocus() {
this.$root.handleInputFocus({
groupName: this.groupName,
});
},
handleBlur() {
this.$root.handleInputBlur({
groupName: this.groupName,
onFocusCallback: this.handlePushClickEventToGACheck,
});
},
handlePushClickEventToGACheck(source) {
if (source === this.eventTypes.CLICK) {
this.pushClickEventToGA();
} else {
if (
!this.isValueSelectedOnClick &&
this.isChecked &&
this.lastValuePushedToGa != this.value
) {
this.pushClickEventToGA();
}
}
}, },
pushClickEventToGA(value) { pushClickEventToGA(value) {
window.firedGaClickEventValues =
window.firedGaClickEventValues ?? {};
window.firedGaClickEventValues[window.lastFocusedInputGroup] =
value ?? this.value;
this.pushEventToGA( this.pushEventToGA(
this.$route.query[queryStrings.FMG_PAGE], this.$route.query[queryStrings.FMG_PAGE],
this.GaActions.CLICKED, this.GaActions.CLICKED,
@ -144,88 +166,39 @@ export default {
true, true,
this.valueToLogType this.valueToLogType
); );
},
handleBlur() {
window.lastFocusedInputGroup = this.groupName;
window.wasLastFocusedInputMultiselect = this.isMultiSelect;
},
handleKeyboardNavigation(key) {
this.test(key);
},
test(event) {
window.firedGaClickEventValues =
window.firedGaClickEventValues ?? {};
window.currentlySelectedValues =
window.currentlySelectedValues ?? {};
const keyCode = event?.code; this.setLastValuePushedToGa(this.value);
// Keyboard navigation
if (keyCode) {
if (
window.currentlySelectedValues &&
window.firedGaClickEventValues &&
window.lastFocusedInputGroup &&
!window.wasLastFocusedInputMultiselect &&
window.currentlySelectedValues[
window.lastFocusedInputGroup
] &&
window.firedGaClickEventValues[
window.lastFocusedInputGroup
] !=
window.currentlySelectedValues[
window.lastFocusedInputGroup
]
) {
if (keyCode === "Tab") {
this.pushClickEventToGA(
window.currentlySelectedValues[
window.lastFocusedInputGroup
]
);
} else if (keyCode?.includes("Arrow")) {
if (window.lastFocusedInputGroup != this.groupName) {
this.pushClickEventToGA(
window.currentlySelectedValues[
window.lastFocusedInputGroup
]
);
}
}
}
} else {
// Radio click
window.lastFocusedInputGroup = this.groupName;
if (
window.firedGaClickEventValues[
window.lastFocusedInputGroup
] !=
window.currentlySelectedValues[window.lastFocusedInputGroup]
) {
this.pushClickEventToGA(
window.currentlySelectedValues[
window.lastFocusedInputGroup
]
);
}
}
}, },
}, },
computed: { computed: {
isChecked() { isChecked() {
if (this.isMultiSelect && this.modelValue instanceof Array) { if (this.isMultiSelect && this.modelValue instanceof Array) {
return this.modelValue.includes(this.value); return this.modelValue.includes(this.value);
} else if (!this.isMultiSelect) {
return this.modelValue == this.value;
} else {
return false;
} }
return this.modelValue == this.value;
}, },
inputType() { inputType() {
return this.isMultiSelect ? "checkbox" : "radio"; return this.isMultiSelect ? "checkbox" : "radio";
}, },
buttonId() { buttonId() {
return `${this.groupName}-${JSON.stringify(this.value)?.replace( return `${this.groupName?.replace(" ", "-")}-${this.value
" ", ?.toString()
"-" ?.replace(" ", "-")}`;
)}`; },
isValueSelectedOnClick() {
return this.isMultiSelect || this.selectingInitiatesLoad;
},
eventTypes() {
return {
CHANGE: "change",
ENTER: "enter",
SPACE: "space",
CLICK: "click",
MOUNT: "mount"
};
}, },
}, },
setup(props) { setup(props) {

View file

@ -2,189 +2,170 @@ import { shallowMount } from "@vue/test-utils";
import buttonQuestion from "@/common-components/button-question/button-question"; import buttonQuestion from "@/common-components/button-question/button-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
jest.mock("@/store", () => { return {}; }, { virtual: true }); jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
// TODO KO describe("buttonQuestion.vue", () => {
describe.skip("buttonQuestion.vue", () => {
it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => { it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => {
// Act // Act
const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
propsData: { propsData: {
isOverflowScrollable: true, isOverflowScrollable: true,
groupName: "group-name" groupName: "group-name",
} },
}); });
// Assert // Assert
const fieldSet = wrapper.find('fieldset'); const fieldSet = wrapper.find("fieldset");
expect(fieldSet.classes()).toContain("overflow-scroll"); expect(fieldSet.classes()).toContain("overflow-scroll");
}); });
}); });
// describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
// it("Fieldset classes should contain row if button type is listCard", () => { it("Fieldset classes should contain row if button type is listCard", () => {
// // Act // Act
// const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
// propsData: { propsData: {
// buttonType: "listCard", buttonType: "listCard",
// groupName: "group-name" groupName: "group-name",
// } },
// }); });
// // Assert // Assert
// const Div = wrapper.find('fieldset div'); const Div = wrapper.find("fieldset div");
// expect(Div.classes()).toContain("row"); expect(Div.classes()).toContain("row");
// }); });
// }); });
// describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
// it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", () => { it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", () => {
// // Act // Act
// const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
// propsData: { propsData: {
// buttonType: "listButtonHorizontal", buttonType: "listButtonHorizontal",
// groupName: "group-name" groupName: "group-name",
// } },
// }); });
// // Assert // Assert
// const Div = wrapper.find('fieldset div'); const Div = wrapper.find("fieldset div");
// expect(Div.classes()).toContain("d-flex"); expect(Div.classes()).toContain("d-flex");
// }); });
// }); });
// describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
// it("Fieldset classes should contain ui-radio if button type is radio", () => { it("Fieldset classes should contain ui-radio if button type is radio", () => {
// // Act // Act
// const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
// propsData: { propsData: {
// buttonType: "radio", buttonType: "radio",
// groupName: "group-name" groupName: "group-name",
// } },
// }); });
// // Assert // Assert
// const Div = wrapper.find('fieldset div'); const Div = wrapper.find("fieldset div");
// expect(Div.classes()).toContain("ui-radio"); expect(Div.classes()).toContain("ui-radio");
// }); });
// }); });
// testing a computed property
describe("buttonQuestion.vue", () => {
it("getColLength should return '12' if prop isWide is set to true", () => {
// Act
const localThis = { isWide: true };
// // testing a computed property expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("12");
// describe("buttonQuestion.vue", () => { });
// it("getColLength should return '12' if prop isWide is set to true", () => { });
// // Act
// const localThis = { isWide: true }
// expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("12"); describe("buttonQuestion.vue", () => {
// }); it("getColLength should return '' if prop isWide is set to false", () => {
// }); // Act
const localThis = {
isWide: false,
answers: ["a", "b"],
groupName: "group-name",
};
// describe("buttonQuestion.vue", () => { expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
// it("getColLength should return '' if prop isWide is set to false", () => { });
// // Act });
// const localThis = {
// isWide: false,
// answers: ['a', 'b'],
// groupName: "group-name"
// }
// expect(buttonQuestion.computed.getColLength.call(localThis)).toBe(""); describe("buttonQuestion.vue", () => {
// }); it("Should return answer.Text if prop useTextForValue is true", async () => {
// }); // Act
const localThis = { useTextForValue: true };
const answer = { Name: "testName", Text: "testText" };
// describe("buttonQuestion.vue", () => { // Assert
// it("Should return answer.Text if prop useTextForValue is true", async () => { expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe(
// // Act "testText"
// const localThis = { useTextForValue: true }; );
// const answer = { 'Name': 'testName', 'Text': 'testText' }; });
});
// // Assert describe("buttonQuestion.vue", () => {
// expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe('testText'); it("Should return answer.Name if prop useTextForValue is false and there's not buttonLabel and answer.Name exists", async () => {
// }); // Act
// }); const localThis = { useTextForValue: false };
const answer = { Name: "testName", Text: "testText" };
// describe("buttonQuestion.vue", () => { // Assert
// it("Should return answer.Name if prop useTextForValue is false and answer.Name exists", async () => { expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe(
// // Act "testName"
// const localThis = { useTextForValue: false }; );
// const answer = { 'Name': 'testName', 'Text': 'testText' }; });
});
// // Assert describe("buttonQuestion.vue", () => {
// expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe('testName'); describe("handleAnswerChange", () => {
// }); test("is radio => should emit captured value", async () => {
// }); // Act
const wrapper = shallowMount(
buttonQuestion,
setupMocks({ propsData: { groupName: "group-name" } })
);
await wrapper.setProps({
answers: ["2022", "2021", "2020"],
isMultiSelect: false,
modelValue: "",
});
const val = "2021";
wrapper.vm.handleAnswerChange(val);
// describe("buttonQuestion.vue", () => { // Assert
// it("Should trigger event modelValue change to new value on when radio button selected", async () => { expect(wrapper.emitted()["update:modelValue"][0]).toEqual(["2021"]);
// // Act });
// const wrapper = shallowMount(buttonQuestion, setupMocks({propsData: {groupName: "group-name"}}));
// await wrapper.setProps({
// answers: ["2022", "2021", "2020"],
// isMultiSelect: false,
// modelValue: []
// });
// const val = { checkValue: true, value: "2021", }
// wrapper.vm.handleCheckedChanged(val);
// // Assert
// expect(wrapper.emitted()["update:modelValue"][0]).toEqual([["2021"]]);
// });
// });
// describe("buttonQuestion.vue", () => { test("is checkbox => should emit captured value", async () => {
// it("Should add values to array on checkbox click", () => { const wrapper = shallowMount(
// // Act buttonQuestion,
// const wrapper = shallowMount(buttonQuestion, setupMocks({ setupMocks({ propsData: { groupName: "group-name" } })
// propsData: { );
// modelValue: ["2022", "2021", "2020"], await wrapper.setProps({
// isMultiSelect: true, answers: ["2022", "2021", "2020"],
// groupName: "group-name" isMultiSelect: false,
// } modelValue: "",
// })); });
// const val = { checkValue: true, value: "2019", } const val = ["2022", "2021", , "2019", "2020"];
// wrapper.vm.handleCheckedChanged(val); wrapper.vm.handleAnswerChange(val);
// // Assert
// expect(wrapper.emitted()["update:modelValue"][0]).toEqual([["2022", "2021", "2020", "2019"]]);
// });
// });
// describe("buttonQuestion.vue", () => { // Assert
// it("Should add a value to this.selectedValues if prop isMultiSelect is true, checkValue is true and this.selectedValues already exists", () => { expect(wrapper.emitted()["update:modelValue"][0]).toEqual([
// // Act ["2022", "2021", , "2019", "2020"],
// const wrapper = shallowMount(buttonQuestion, setupMocks({ ]);
// propsData: { });
// isMultiSelect: true, });
// modelValue: ['a', 'b'], });
// groupName: "group-name"
// }
// }));
// const val = { checkValue: true, value: "2021", }
// wrapper.vm.handleCheckedChanged(val);
// // Assert
// expect(wrapper.vm.selectedValues).toEqual(["a", "b", "2021"]);
// });
// });
// describe("buttonQuestion.vue", () => {
// it("Should remove a value to this.selectedValues if prop isMultiSelect is true, checkValue is false and this.selectedValues already exists", () => {
// // Act
// const wrapper = shallowMount(buttonQuestion, setupMocks({
// propsData: {
// isMultiSelect: true,
// modelValue: ['a', 'b'],
// groupName: "group-name"
// }
// }));
// const val = { checkValue: false, value: "a", }
// wrapper.vm.handleCheckedChanged(val);
// // Assert
// expect(wrapper.vm.selectedValues).toEqual(["b"]);
// });
// });
function setupMocks(mountOptionsMockData = {}) { function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } }; const defaultMountOptions = { route: { query: { fmgPage: "page-name" } } };
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData)); const baseMountOptions = getMountOptions(
Object.assign(defaultMountOptions, mountOptionsMockData)
);
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions); const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
return allMountOptions; return allMountOptions;

View file

@ -13,7 +13,6 @@
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset <fieldset
class="w-100" class="w-100"
:aria-required="isRequired" :aria-required="isRequired"
@ -52,13 +51,16 @@
:validationRules="validationRules" :validationRules="validationRules"
:textPosition="textPosition" :textPosition="textPosition"
:selectOnKeypress="selectOnKeypress" :selectOnKeypress="selectOnKeypress"
@buttonClicked="handleAnswerChange" /> :lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
:shouldPushClickEventToGAOnMount="shouldPushClickEventToGAOnMount"
@inputButtonClicked="handleAnswerChange" />
<!-- For nested questions --> <!-- For nested questions -->
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div <div
v-if=" v-if="
typeof primaryValue == 'string' && typeof selectedValues == 'string' &&
primaryValue == answer.value selectedValues == answer.value
"> ">
<slot></slot> <slot></slot>
</div> </div>
@ -79,8 +81,8 @@
import listButton from "@/ux-components/list-button/list-button"; import listButton from "@/ux-components/list-button/list-button";
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal"; import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
import listCard from "@/ux-components/list-card/list-card"; import listCard from "@/ux-components/list-card/list-card";
import { ErrorMessage } from "vee-validate";
import radio from "@/ux-components/radio/radio"; import radio from "@/ux-components/radio/radio";
import { ErrorMessage } from "vee-validate";
export default { export default {
name: "buttonQuestion", name: "buttonQuestion",
@ -120,11 +122,11 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
shouldPushClickEventToGAOnMount: Boolean
}, },
data() { data() {
return { return {
primaryValue: "", lastValuePushedToGa: null,
lastFocusedInputGroup: "",
}; };
}, },
computed: { computed: {
@ -196,6 +198,14 @@ export default {
}) })
); );
}, },
selectedValues: {
get() {
return this.modelValue;
},
set(selectedAnswers) {
this.$emit("update:modelValue", selectedAnswers);
}
}
}, },
methods: { methods: {
formatString(str) { formatString(str) {
@ -219,10 +229,13 @@ export default {
: this.formatString(answer.toString()); : this.formatString(answer.toString());
} }
}, },
handleAnswerChange(primaryAnswerValue) { handleAnswerChange(selectedAnswers) {
this.primaryValue = primaryAnswerValue; this.selectedValues = selectedAnswers;
this.$emit("buttonQuestionChange", primaryAnswerValue);
this.$emit("update:modelValue", primaryAnswerValue); this.$emit("buttonQuestionChange", selectedAnswers);
},
setLastValuePushedToGa(lastValuePushedToGa) {
this.lastValuePushedToGa = lastValuePushedToGa;
}, },
}, },
components: { components: {

View file

@ -36,6 +36,7 @@ const storeActions = {
RUN_EXPERIMENTS_FOR_TRIGGER: "runExperimentsForTrigger", RUN_EXPERIMENTS_FOR_TRIGGER: "runExperimentsForTrigger",
CLEAR_VIN: "clearVin", CLEAR_VIN: "clearVin",
RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise", RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies", RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies",
@ -63,12 +64,4 @@ const storeActions = {
SAVE_CAPABILITY_QUESTION_ANSWERS: "saveCapabilityQuestionAnswers", SAVE_CAPABILITY_QUESTION_ANSWERS: "saveCapabilityQuestionAnswers",
}; };
const gaStoreActions = { export { storeActions };
UPDATE_CURRENTLY_SELECTED_VALUES: "updateCurrentlySelectedValues",
UPDATE_FIRED_GA_CLICK_EVENT_VALUES: "updateFiredGaClickEventValues",
UPDATE_LAST_FOCUSED_INPUT_GROUP: "updateLastFocusedInputGroup",
UPDATE_WAS_LAST_FOCUSED_INPUT_MULTISELECT:
"updateWasLastFocusedInputMultiselect",
};
export { storeActions, gaStoreActions };

View file

@ -68,12 +68,4 @@ const storeMutations = {
UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry", UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry",
}; };
const gaStoreMutations = { export { storeMutations };
UPDATE_CURRENTLY_SELECTED_VALUES: "updateCurrentlySelectedValues",
UPDATE_FIRED_GA_CLICK_EVENT_VALUES: "updateFiredGaClickEventValues",
UPDATE_LAST_FOCUSED_INPUT_GROUP: "updateLastFocusedInputGroup",
UPDATE_WAS_LAST_FOCUSED_INPUT_MULTISELECT:
"updateWasLastFocusedInputMultiselect",
};
export { storeMutations, gaStoreMutations };

View file

@ -36,8 +36,7 @@ describe("replace-options-question.vue", () => {
}); });
describe("replace-options-question.vue", () => { describe("replace-options-question.vue", () => {
// TODO KO test("when updateSelectedValues method is called with a single answerToDisplay it will call to update this.selectedReplaceOptions", async () => {
test.skip("when updateSelectedValues method is called with a single answerToDisplay it will call to update this.selectedValues", async () => {
//Arrange //Arrange
const { wrapper, cmsContent, replaceOptions const { wrapper, cmsContent, replaceOptions

View file

@ -57,7 +57,7 @@ export default ({
name: "sideDoorOptions", name: "sideDoorOptions",
props: { props: {
groupName: String, groupName: String,
modelValue: Array, modelValue: [Array, Object], // TODO Does this take an array?
selectedDamageLocations: Array, selectedDamageLocations: Array,
cmsWidgetName: String, cmsWidgetName: String,
}, },

View file

@ -55,8 +55,7 @@ jest.mock("@/store", () => ({
}, },
})); }));
// TODO KO describe("vehicle-damage.vue", () => {
describe.skip("vehicle-damage.vue", () => {
describe("navigation", () => { describe("navigation", () => {
test("BackButtonAction triggers a router.navigateWithoutSaving change", async () => { test("BackButtonAction triggers a router.navigateWithoutSaving change", async () => {
@ -130,20 +129,20 @@ describe.skip("vehicle-damage.vue", () => {
}, },
}); });
wrapper.vm.selectedDamageLocations = ["Windshield", "SideDoor", "RearWindow"]; wrapper.setData({
wrapper.vm.selectedWindshieldOptions = { selectedDamageLocations: ["Windshield", "SideDoor", "RearWindow"],
selectedWindshieldChipCount: null, selectedWindshieldOptions: {
selectedWindshieldReplaceOptions: ["Single"], selectedWindshieldChipCount: null,
selectedWindshieldDamageType: "Replace" selectedWindshieldReplaceOptions: ["Single"],
}; selectedWindshieldDamageType: "Replace"
},
wrapper.vm.sideDoorOptionsData = { sideDoorOptionsData: {
selectedDoorSides: ["DriverSide", "PassengerSide"], selectedDoorSides: ["DriverSide", "PassengerSide"],
selectedDriverSideReplaceOptions: ["Back"], selectedDriverSideReplaceOptions: ["Back"],
selectedPassengerSideReplaceOptions: ["Quarter"] selectedPassengerSideReplaceOptions: ["Quarter"]
}; },
selectedRearReplaceOptions: "Stationary",
wrapper.vm.selectedRearReplaceOptions = ["Stationary"]; })
const expectedGlassToReplace = [{ glassLocation: "Windshield", glassName: "Single" }, { glassLocation: "Driver", glassName: "Back" }, const expectedGlassToReplace = [{ glassLocation: "Windshield", glassName: "Single" }, { glassLocation: "Driver", glassName: "Back" },
{ glassLocation: "Passenger", glassName: "Quarter" }, { glassLocation: "Rear", glassName: "Stationary" }]; { glassLocation: "Passenger", glassName: "Quarter" }, { glassLocation: "Rear", glassName: "Stationary" }];
@ -520,19 +519,19 @@ describe.skip("vehicle-damage.vue", () => {
const storeWindshieldOptions = [[1, false, "Windshield", "Single", { const storeWindshieldOptions = [[1, false, "Windshield", "Single", {
selectedWindshieldDamageType: damageLocationsSelected.REPLACE, selectedWindshieldDamageType: damageLocationsSelected.REPLACE,
selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.SINGLE] selectedWindshieldChipCount: null, selectedWindshieldReplaceOptions: [damageLocationsSelected.SINGLE]
}], }],
[2, false, "Windshield", "Driver", { [2, false, "Windshield", "Driver", {
selectedWindshieldDamageType: damageLocationsSelected.REPLACE, selectedWindshieldDamageType: damageLocationsSelected.REPLACE,
selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.DRIVER] selectedWindshieldChipCount: null, selectedWindshieldReplaceOptions: [damageLocationsSelected.DRIVER]
}], }],
[3, false, "Windshield", "Passenger", { [3, false, "Windshield", "Passenger", {
selectedWindshieldDamageType: damageLocationsSelected.REPLACE, selectedWindshieldDamageType: damageLocationsSelected.REPLACE,
selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.PASSENGER] selectedWindshieldChipCount: null, selectedWindshieldReplaceOptions: [damageLocationsSelected.PASSENGER]
}], }],
[4, true, "", "", { [4, true, "", "", {
selectedWindshieldDamageType: damageLocationsSelected.REPAIR, selectedWindshieldDamageType: damageLocationsSelected.REPAIR,
selectedWindshieldChipCount: [2], selectedWindshieldReplaceOptions: [] selectedWindshieldChipCount: 2, selectedWindshieldReplaceOptions: []
}] }]
]; ];
test.each(storeWindshieldOptions)("getWindshieldOptionsFromStore test #%s", async (testNum, isRepair, damageLocation, damageName, expectedWindshieldOptions) => { test.each(storeWindshieldOptions)("getWindshieldOptionsFromStore test #%s", async (testNum, isRepair, damageLocation, damageName, expectedWindshieldOptions) => {
@ -619,8 +618,8 @@ describe.skip("vehicle-damage.vue", () => {
expect(glassSelections).toEqual(expectedGlass); expect(glassSelections).toEqual(expectedGlass);
}); });
const rearReplaceOptions = [["Rear", "Stationary", [damageLocationsSelected.STATIONARY]], const rearReplaceOptions = [["Rear", "Stationary", damageLocationsSelected.STATIONARY],
["Rear", "Slider", [damageLocationsSelected.SLIDER]] ["Rear", "Slider", damageLocationsSelected.SLIDER]
]; ];
test.each(rearReplaceOptions)("getRearReplaceOptionsFromStore for %s-%s returns expected %s", async (damageLocation, damageName, expectedGlass) => { test.each(rearReplaceOptions)("getRearReplaceOptionsFromStore for %s-%s returns expected %s", async (damageLocation, damageName, expectedGlass) => {

View file

@ -83,7 +83,7 @@ export default ({
}, },
props: { props: {
modelValue: String, modelValue: [Object, String], // TODO Does this take a string?
selectedDamageLocations: Array, selectedDamageLocations: Array,
hasRepairReplaceConflict: Boolean, hasRepairReplaceConflict: Boolean,
hasSplitSingleConflict: Boolean, hasSplitSingleConflict: Boolean,

View file

@ -18,9 +18,7 @@ const featureListData = {
modelValueProp: {} modelValueProp: {}
} }
// TODO KO describe("glass-part-question.vue", () => {
describe.skip("glass-part-question.vue", () => {
test("Part data passed in, should map data for ButtonQuestion (radio type)", async () => { test("Part data passed in, should map data for ButtonQuestion (radio type)", async () => {
//Arrange //Arrange

View file

@ -13,8 +13,7 @@
altText="" altText=""
isRequired isRequired
:groupName="`${glassLocation}-${glassName}`" :groupName="`${glassLocation}-${glassName}`"
:validationRules="tintValidationRules" :validationRules="tintValidationRules">
>
<div class="row my-2" aria-live="polite"> <div class="row my-2" aria-live="polite">
<div class="col"> <div class="col">
<buttonQuestion <buttonQuestion
@ -28,7 +27,9 @@
isRequired isRequired
:groupName="`${glassLocation}-${glassName}-${selectedTint}`" :groupName="`${glassLocation}-${glassName}-${selectedTint}`"
:validationRules="partValidationRules" :validationRules="partValidationRules"
/> :shouldPushClickEventToGAOnMount="
shouldPushClickEventToGAOnMount
" />
</div> </div>
</div> </div>
</buttonQuestion> </buttonQuestion>
@ -56,6 +57,7 @@ export default {
glassColorQuestion: "", glassColorQuestion: "",
glassFeatureQuestion: "", glassFeatureQuestion: "",
selectedTint: "", selectedTint: "",
shouldPushClickEventToGAOnMount: true,
}; };
}, },
props: { props: {
@ -63,7 +65,10 @@ export default {
glassLocation: String, glassLocation: String,
colorAnswers: Array, colorAnswers: Array,
modelValue: Object, modelValue: Object,
alreadyPopulatedPartsData: Array alreadyPopulatedPartsData: {
type: Array,
default: () => [],
},
}, },
mounted() { mounted() {
this.LoadPreselectedValues(); this.LoadPreselectedValues();
@ -74,12 +79,18 @@ export default {
computed: { computed: {
tintValidationRules() { tintValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-tint-required`; const validationRuleName = `${this.glassLocation}-${this.glassName}-tint-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED)); defineRule(
validationRuleName,
required(errorMessages.OPTION_REQUIRED)
);
return validationRuleName; return validationRuleName;
}, },
partValidationRules() { partValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`; const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED)); defineRule(
validationRuleName,
required(errorMessages.OPTION_REQUIRED)
);
return validationRuleName; return validationRuleName;
}, },
colorQuestionText() { colorQuestionText() {
@ -110,16 +121,29 @@ export default {
return this.modelValue?.partNumber; return this.modelValue?.partNumber;
}, },
set(newValue) { set(newValue) {
this.$emit("update:modelValue", this.partsForSelectedTint.filter(part => part.partNumber == newValue)[0]); this.$emit(
"update:modelValue",
this.partsForSelectedTint.filter(
(part) => part.partNumber == newValue
)[0]
);
}, },
}, },
partsForSelectedTint() { partsForSelectedTint() {
const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter(dataForGlassLocationAndName => const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter(
dataForGlassLocationAndName.glassName == this.glassName && (dataForGlassLocationAndName) =>
dataForGlassLocationAndName.glassLocation == this.glassLocation); dataForGlassLocationAndName.glassName == this.glassName &&
const matchingGlassParts = matchingGlass?.length == 1 ? matchingGlass[0].parts : []; dataForGlassLocationAndName.glassLocation ==
return matchingGlassParts.filter(part => part.color == this.selectedTint) ?? []; this.glassLocation
);
const matchingGlassParts =
matchingGlass?.length == 1 ? matchingGlass[0].parts : [];
return (
matchingGlassParts.filter(
(part) => part.color == this.selectedTint
) ?? []
);
}, },
// Creates a map of the feature list data in the correct Name/Value // Creates a map of the feature list data in the correct Name/Value
@ -151,7 +175,9 @@ export default {
}, },
PartDataFromApi() { PartDataFromApi() {
return this.$store.getters.pageData(this.$route.query.fmgPage) ?? {}; return (
this.$store.getters.pageData(this.$route.query.fmgPage) ?? {}
);
}, },
}, },
methods: { methods: {
@ -188,7 +214,8 @@ export default {
// Check if only a single part is present for the tint and set the v-model if it is. // Check if only a single part is present for the tint and set the v-model if it is.
AutoSelectIfSinglePart() { AutoSelectIfSinglePart() {
if (this.partsForSelectedTint?.length == 1) { if (this.partsForSelectedTint?.length == 1) {
this.selectedPartNumber = this.partsForSelectedTint[0].partNumber; this.selectedPartNumber =
this.partsForSelectedTint[0].partNumber;
} }
}, },
@ -197,7 +224,8 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if (this.modelValue !== undefined) { if (this.modelValue !== undefined) {
// Populate button-question model-value if parts data already exists in VueX // Populate button-question model-value if parts data already exists in VueX
this.selectedTint = this.alreadyPopulatedPartsData.filter(part => part.partNumber === this.selectedPartNumber)[0]?.color; this.shouldPushClickEventToGAOnMount = false;
this.selectedTint = this.modelValue?.color
} }
}); });
}, },
@ -205,8 +233,8 @@ export default {
watch: { watch: {
selectedTint() { selectedTint() {
this.AutoSelectIfSinglePart(); this.AutoSelectIfSinglePart();
} },
} },
}; };
</script> </script>

View file

@ -183,7 +183,16 @@ describe("vehicle-parts.vue", () => {
await nextTick(); await nextTick();
//Assert //Assert
expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } }); expect(wrapper.vm.glassParts).toEqual({
"Rear-Stationary": {
partNumber: "DB12209YPYNOEM",
description: "heated glass, solar, 1 hole",
color: "Gray Tint Privacy",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null
}
});
}); });
test("User had part questions > BackButtonAction triggers a router.navigateWithoutSaving change with correct scenario", async () => { test("User had part questions > BackButtonAction triggers a router.navigateWithoutSaving change with correct scenario", async () => {
@ -509,6 +518,8 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn(); wrapper.vm.$refs.loadingModal.showModal = jest.fn();
// wrapper.vm.$refs.onSubmit = jest.fn();
// wrapper.vm.$refs.onInvalidSubmit = jest.fn();
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -204,9 +204,7 @@ export default {
const partNumber = this.alreadyPopulatedPartsData[key].partNumber; const partNumber = this.alreadyPopulatedPartsData[key].partNumber;
g.parts.forEach((p) => { g.parts.forEach((p) => {
if (p.partNumber === partNumber) { if (p.partNumber === partNumber) {
this.glassParts[g.glassLocation + "-" + g.glassName] = { this.glassParts[g.glassLocation + "-" + g.glassName] = p;
[g.glassLocation]: [partNumber],
};
} }
}); });
}); });

View file

@ -6,12 +6,11 @@ import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { routerParams } from "@/router/router-constants/router-params"; import { routerParams } from "@/router/router-constants/router-params";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings"; import { dynamicStrings } from "@/constants/dynamic-strings";
import { settleAllPromises } from "@/helpers/layout-helper";
export default { export default {
data() { data() {
return { return {
cmsContentByWidget: {} cmsContentByWidget: {},
}; };
}, },
methods: { methods: {
@ -19,7 +18,9 @@ export default {
this.$root.cmsContentByWidget = cmsContent; this.$root.cmsContentByWidget = cmsContent;
}, },
getCmsContent(widgetName, fieldName) { getCmsContent(widgetName, fieldName) {
return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : ''; return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName]
? this.$root.cmsContentByWidget[widgetName][fieldName]
: "";
}, },
dispatchStoreAction(type, payload, encodePayload = true) { dispatchStoreAction(type, payload, encodePayload = true) {
// Encode the payload if required // Encode the payload if required
@ -32,7 +33,7 @@ export default {
savePageDataToStore(page, data) { savePageDataToStore(page, data) {
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data }); store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data });
}, },
onSubmit() { }, // DO NOT REMOVE; needed to prevent default form submit behavior. Cannot use .prevent modifier for vee-validate Form onSubmit() {}, // DO NOT REMOVE; needed to prevent default form submit behavior. Cannot use .prevent modifier for vee-validate Form
onInvalidSubmit({ values, errors, results }) { onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it // identify the first error field and put focus on it
// get error names array // get error names array
@ -49,14 +50,17 @@ export default {
return footerInfoBox ? footerInfoBox.offsetHeight : 0; return footerInfoBox ? footerInfoBox.offsetHeight : 0;
}, },
async getZipCodeData(zipCode) { async getZipCodeData(zipCode) {
const serviceZipValidationResponse = await this.dispatchStoreAction(storeActions.VALIDATE_ZIP, { zip: zipCode }); const serviceZipValidationResponse = await this.dispatchStoreAction(
storeActions.VALIDATE_ZIP,
{ zip: zipCode }
);
return { return {
isValid: serviceZipValidationResponse.data.isValid, isValid: serviceZipValidationResponse.data.isValid,
isServiceable: serviceZipValidationResponse.data.isServiceable, isServiceable: serviceZipValidationResponse.data.isServiceable,
state: serviceZipValidationResponse.data.state state: serviceZipValidationResponse.data.state,
}; };
} },
}, },
computed: { computed: {
storeActions() { storeActions() {
@ -74,13 +78,13 @@ export default {
routerParams() { routerParams() {
return routerParams; return routerParams;
}, },
queryStrings(){ queryStrings() {
return queryStrings; return queryStrings;
}, },
dynamicStrings(){ dynamicStrings() {
return dynamicStrings; return dynamicStrings;
}, },
cssClassNameForCmsWidget(){ cssClassNameForCmsWidget() {
return "widget-name-" + this.cmsWidgetName; return "widget-name-" + this.cmsWidgetName;
}, },
}, },

View file

@ -9,7 +9,7 @@ export default {
buttonImage: String, buttonImage: String,
altText: { altText: {
type: String, type: String,
default: "" default: "",
}, },
textPosition: String, textPosition: String,
screenReaderOnlyText: String, screenReaderOnlyText: String,
@ -18,29 +18,19 @@ export default {
isWide: Boolean, isWide: Boolean,
isRequired: { isRequired: {
type: Boolean, type: Boolean,
default: true default: true,
} },
}, lastValuePushedToGa: [String, Number],
data() { setLastValuePushedToGa: Function,
return { shouldPushClickEventToGAOnMount: Boolean
selectedValue: null,
};
},
mounted() {
this.selectedValue = this.modelValue;
}, },
methods: { methods: {
handleAnswerChange(e) { handleAnswerChange(e) {
if (this.preHandleAnswerChange) { if (this.preHandleAnswerChange) {
this.preHandleAnswerChange(e) this.preHandleAnswerChange(e);
} }
this.$emit("change", e); this.$emit("change", e);
}, },
}, },
watch: {
selectedValue(selectedValue) {
this.$emit("update:modelValue", selectedValue);
},
},
}; };

View file

@ -0,0 +1,81 @@
describe("input-button-wrapper-mixin", () => {
describe("mouse clicks", () => {
describe("checkbox", () => {
test.todo("clicking once checks the baseInputButton");
test.todo("clicking twice unchecks the baseInputButton");
test.todo(
"is initially checked => checking unchecks the baseInputButton"
);
test.todo("clicked => correct event and value are emitted");
});
describe("radio", () => {
test.todo("clicking once selects the baseInputButton");
test.todo("clicking twice keeps the baseInputButton selected");
test.todo(
"is initially selected => click keeps the baseInputButton selected"
);
test.todo("clicked => correct event and value are emitted");
});
});
describe("keyboard navigation and", () => {
describe("checkbox", () => {
test.todo(
"focus on a checkbox => inputButtonClicked is not emitted"
);
test.todo(
"blur from a checkbox => inputButtonClicked is not emitted"
);
test.todo(
"focus and click space on a checkbox => inputButtonClicked is emitted with correct value"
);
test.todo(
"focus and click space on a checkbox that is already checked => inputButtonClicked is emitted with correct value"
);
test.todo(
"focus and click enter on a checkbox => inputButtonClicked is emitted with correct value"
);
test.todo(
"focus and click enter on a checkbox that is already checked => inputButtonClicked is emitted with correct value"
);
});
describe("radio", () => {
test.todo(
"focus on a radio button => inputButtonClicked is not emitted"
);
test.todo(
"blur from a radio button => inputButtonClicked is not emitted"
);
test.todo(
"focus and click space on a radio button => inputButtonClicked is emitted with correct value"
);
test.todo(
"focus and click space on a radio button that is already selected => inputButtonClicked is emitted with correct value"
);
test.todo(
"focus and click enter on a radio button => inputButtonClicked is emitted with correct value"
);
test.todo(
"focus and click enter on a radio button that is already selected => inputButtonClicked is emitted with correct value"
);
});
});
});

View file

@ -1,90 +0,0 @@
import { createStore } from "vuex";
import { endpoints } from "@/constants/endpoints.js";
import { gaStoreMutations } from "@/constants/store-mutations";
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
import createPersistedState from "vuex-persistedstate";
import globalMethods from "@/global-methods";
import { gaStoreActions } from "@/constants/store-actions";
import { applicationConfig } from "@/constants/application-config";
import { experimentTriggers } from "@/constants/experiments";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
// Export State
const getDefaultState = () => {
return {
gaClickInformation: {
currentlySelectedValues: {},
firedGaClickEventValues: {},
lastFocusedInputGroup: "",
wasLastFocusedInputMultiselect: undefined,
},
};
};
export const gaState = getDefaultState();
// Export Mutations
export const gaMutations = {
updateCurrentlySelectedValues(state, groupName, value) {
state.gaClickInformation.currentlySelectedValues[groupName] = value;
},
updateFiredGaClickEventValues(state, groupName, value) {
state.gaClickInformation.firedGaClickEventValues[groupName] = value;
},
updateLastFocusedInputGroup(state, groupName) {
state.gaClickInformation.lastFocusedInputGroup = groupName;
},
updateWasLastFocusedInputMultiselect(
state,
wasLastFocusedInputMultiselect
) {
state.gaClickInformation.wasLastFocusedInputMultiselect =
wasLastFocusedInputMultiselect;
},
};
// Export Getters
export const getters = {
gaClickInformation: (state) => state.gaClickInformation,
};
// Export Actions
export const gaActions = {
updateCurrentlySelectedValues(context, groupName, value) {
context.commit(
gaStoreMutations.UPDATE_CURRENTLY_SELECTED_VALUES,
groupName,
value
);
},
updateFiredGaClickEventValues(context, groupName, value) {
context.commit(
gaStoreMutations.UPDATE_FIRED_GA_CLICK_EVENT_VALUES,
groupName,
value
);
},
updateLastFocusedInputGroup(context, groupName) {
context.commit(
gaStoreMutations.UPDATE_LAST_FOCUSED_INPUT_GROUP,
groupName
);
},
updateWasLastFocusedInputMultiselect(
context,
wasLastFocusedInputMultiselect
) {
context.commit(
gaStoreMutations.UPDATE_WAS_LAST_FOCUSED_INPUT_MULTISELECT,
wasLastFocusedInputMultiselect
);
},
};
export default createStore({
gaState,
gaMutations,
getters,
gaActions,
});

View file

@ -1,4 +1,4 @@
import { createStore } from "vuex"; import { createStore, Store } from "vuex";
import { endpoints } from "@/constants/endpoints.js"; import { endpoints } from "@/constants/endpoints.js";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper"; import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
@ -50,22 +50,22 @@ const getDefaultState = () => {
glassToReplace: null, glassToReplace: null,
partQuestionAnswers: null, partQuestionAnswers: null,
moldingQuestionAnswers: null, moldingQuestionAnswers: null,
capabilityQuestionAnswers: null capabilityQuestionAnswers: null,
}, },
lineItems: { lineItems: {
glassParts: null glassParts: null,
}, },
payment: { payment: {
isInsurance: null, isInsurance: null,
insuranceCoverage: { insuranceCoverage: {
isVerified: null isVerified: null,
} },
}, },
referralNumber: null, referralNumber: null,
referralDate: null, referralDate: null,
referralCorrelationId: null, referralCorrelationId: null,
accountNumber: 0, accountNumber: 0,
eon: null eon: null,
}, },
applicationUser: { applicationUser: {
eventBus: [], eventBus: [],
@ -76,14 +76,15 @@ const getDefaultState = () => {
crmCustomerId: null, crmCustomerId: null,
lastPageVisited: null, lastPageVisited: null,
experiments: [], experiments: [],
triggeredSiteEntry: false triggeredSiteEntry: false,
}, },
gaInformation: { // gaClickInformation: {
currentlySelectedValues: {}, // currentlySelectedValues: {},
firedGaClickEventValues: {}, // firedGaClickEventValues: {},
lastFocusedInputGroup: "" // lastFocusedInputGroup: "",
} // wasLastFocusedInputMultiselect: undefined,
} // },
};
}; };
export const state = getDefaultState(); export const state = getDefaultState();
@ -200,7 +201,6 @@ export const mutations = {
state.order.customer.emailAddress = customerEmailAddress; state.order.customer.emailAddress = customerEmailAddress;
}, },
updateVehicle(state, vehicleInfo) { updateVehicle(state, vehicleInfo) {
state.order.vehicle.year = vehicleInfo.year; state.order.vehicle.year = vehicleInfo.year;
state.order.vehicle.make = vehicleInfo.make; state.order.vehicle.make = vehicleInfo.make;
state.order.vehicle.model = vehicleInfo.model; state.order.vehicle.model = vehicleInfo.model;
@ -214,7 +214,8 @@ export const mutations = {
state.order.vehicle.imageColor = vehicleInfo.imageVifColor; state.order.vehicle.imageColor = vehicleInfo.imageVifColor;
}, },
updateRegistration(state, registrationInfo) { updateRegistration(state, registrationInfo) {
state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate; state.order.vehicle.registration.licensePlate =
registrationInfo?.licensePlate;
state.order.vehicle.registration.address = registrationInfo?.address; state.order.vehicle.registration.address = registrationInfo?.address;
state.order.vehicle.registration.city = registrationInfo?.city; state.order.vehicle.registration.city = registrationInfo?.city;
state.order.vehicle.registration.state = registrationInfo?.state; state.order.vehicle.registration.state = registrationInfo?.state;
@ -240,7 +241,7 @@ export const mutations = {
state.applicationUser.crmCustomerId = crmCustomerId; state.applicationUser.crmCustomerId = crmCustomerId;
}, },
updateLastPageVisited(state, lastPageVisited) { updateLastPageVisited(state, lastPageVisited) {
state.applicationUser.lastPageVisited = lastPageVisited state.applicationUser.lastPageVisited = lastPageVisited;
}, },
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
addEventToBus(state, event) { addEventToBus(state, event) {
@ -249,8 +250,7 @@ export const mutations = {
removeEventFromBus(state, eventData) { removeEventFromBus(state, eventData) {
const matchedEvent = state.applicationUser.eventBus.find( const matchedEvent = state.applicationUser.eventBus.find(
({ category, subCategory }) => ({ category, subCategory }) =>
category === eventData.category && category === eventData.category && subCategory === eventData.subCategory
subCategory === eventData.subCategory
); );
const itemIndex = state.applicationUser.eventBus.indexOf(matchedEvent); const itemIndex = state.applicationUser.eventBus.indexOf(matchedEvent);
@ -336,7 +336,7 @@ export const mutations = {
state: orderInformation.vehicle.registration.state, state: orderInformation.vehicle.registration.state,
zipCode: orderInformation.vehicle.registration.zipCode, zipCode: orderInformation.vehicle.registration.zipCode,
licensePlate: orderInformation.vehicle.registration.licensePlateNumber, licensePlate: orderInformation.vehicle.registration.licensePlateNumber,
} },
}); });
state.order.damage.glassToReplace = orderInformation.damage.glassToReplace; state.order.damage.glassToReplace = orderInformation.damage.glassToReplace;
@ -345,13 +345,18 @@ export const mutations = {
state.order.lineItems.glassParts = orderInformation.parts; state.order.lineItems.glassParts = orderInformation.parts;
state.order.accountNumber = orderInformation.accountNumber; state.order.accountNumber = orderInformation.accountNumber;
state.order.serviceLocation.address = orderInformation.serviceLocation.streetAddress, (state.order.serviceLocation.address =
state.order.serviceLocation.city = orderInformation.serviceLocation.city, orderInformation.serviceLocation.streetAddress),
state.order.serviceLocation.state = orderInformation.serviceLocation.state, (state.order.serviceLocation.city =
state.order.serviceLocation.zipCode = orderInformation.serviceLocation.zipCode; orderInformation.serviceLocation.city),
(state.order.serviceLocation.state =
orderInformation.serviceLocation.state),
(state.order.serviceLocation.zipCode =
orderInformation.serviceLocation.zipCode);
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder; state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified; state.order.payment.insuranceCoverage.isVerified =
orderInformation?.insuranceInfo.coverageVerified;
state.order.customer.emailAddress = orderInformation.customer.emailAddress; state.order.customer.emailAddress = orderInformation.customer.emailAddress;
state.applicationUser.experiments = orderInformation.experiments; state.applicationUser.experiments = orderInformation.experiments;
@ -361,8 +366,23 @@ export const mutations = {
}, },
updateTriggeredSiteEntry(state, wasSiteEntryTriggered) { updateTriggeredSiteEntry(state, wasSiteEntryTriggered) {
state.applicationUser.triggeredSiteEntry = wasSiteEntryTriggered; state.applicationUser.triggeredSiteEntry = wasSiteEntryTriggered;
} },
} // START GA click event mutations
// updateCurrentlySelectedValues(state, groupName, value) {
// state.gaClickInformation.currentlySelectedValues[groupName] = value;
// },
// updateFiredGaClickEventValues(state, groupName, value) {
// state.gaClickInformation.firedGaClickEventValues[groupName] = value;
// },
// updateLastFocusedInputGroup(state, groupName) {
// state.gaClickInformation.lastFocusedInputGroup = groupName;
// },
// updateWasLastFocusedInputMultiselect(state, wasLastFocusedInputMultiselect) {
// state.gaClickInformation.wasLastFocusedInputMultiselect =
// wasLastFocusedInputMultiselect;
// },
// END GA click event mutations
};
// Export Getters // Export Getters
export const getters = { export const getters = {
@ -378,7 +398,9 @@ export const getters = {
eventBus: (state) => state.applicationUser.eventBus, eventBus: (state) => state.applicationUser.eventBus,
damage: (state) => state.order.damage, damage: (state) => state.order.damage,
lineItems: (state) => state.order.lineItems, lineItems: (state) => state.order.lineItems,
pageData: (state) => (page) => { return state.applicationUser.pageData[page]; }, pageData: (state) => (page) => {
return state.applicationUser.pageData[page];
},
applicationUser: (state) => state.applicationUser, applicationUser: (state) => state.applicationUser,
order: (state) => state.order, order: (state) => state.order,
payment: (state) => state.order.payment, payment: (state) => state.order.payment,
@ -395,29 +417,33 @@ export const getters = {
funnelServiceState: state.order.serviceLocation.state, funnelServiceState: state.order.serviceLocation.state,
funnelServiceZipCode: state.order.serviceLocation.zipCode, funnelServiceZipCode: state.order.serviceLocation.zipCode,
funnelParentAccountNumber: state.order.accountNumber, funnelParentAccountNumber: state.order.accountNumber,
funnelIsCoverageVerified: state.order.payment.insuranceCoverage.isVerified, funnelIsCoverageVerified:
state.order.payment.insuranceCoverage.isVerified,
funnelHasRecalibrationPart: getHasRecalibrationPart(state), funnelHasRecalibrationPart: getHasRecalibrationPart(state),
funnelSelectedMultiGlass: state.order.damage.glassToReplace?.length > 1, funnelSelectedMultiGlass: state.order.damage.glassToReplace?.length > 1,
funnelSelectedWindshieldGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.WINDSHIELD), funnelSelectedWindshieldGlass: getNonFalseValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.WINDSHIELD),
funnelSelectedBackGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.REAR), funnelSelectedBackGlass: getNonFalseValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.REAR),
funnelSelectedDriverSideGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.DRIVER), funnelSelectedDriverSideGlass: getNonFalseValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.DRIVER),
funnelSelectedPassengerSideGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.PASSENGER), funnelSelectedPassengerSideGlass: getNonFalseValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.PASSENGER),
funnelOrderPartNumbers: [...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "partNumber"), ...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.otherParts, "partNumber")], funnelOrderPartNumbers: [...getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "partNumber"), ...getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.otherParts, "partNumber")],
funnelOrderPartTypes: [...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType"), ...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.otherParts, "recalibrationType")], funnelOrderPartTypes: [...getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType"), ...getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.otherParts, "recalibrationType")],
} }
}, },
experimentSettings: (state) => state.applicationUser.experiments.map(x => x.settings).reduce((r, c) => Object.assign(r, c), {}) ?? {} experimentSettings: (state) =>
} state.applicationUser.experiments
.map((x) => x.settings)
.reduce((r, c) => Object.assign(r, c), {}) ?? {},
// gaClickInformation: (state) => state.gaClickInformation,
};
function getAllValuesOfPropertyInArrayOfObjects(array, propertyName) { function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
return (array ?? []).map(x => x[propertyName]).filter(x => x); return (array ?? []).map(x => x[propertyName]).filter(x => x);
} }
// Export Actions // Export Actions
export const actions = { export const actions = {
// Vehicle API Actions // Vehicle API Actions
getVehicleYears(context) { getVehicleYears(context) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -448,11 +474,14 @@ export const actions = {
endpoint: endpoints.LookupVinByPlate.url, endpoint: endpoints.LookupVinByPlate.url,
payload: { payload: {
licensePlate: licensePlate, licensePlate: licensePlate,
licenseState: licenseState licenseState: licenseState,
}, },
}); });
}, },
lookupVinByAddress(context, { licenseLastName, licenseStreetAddress, licenseZip, licenseState }) { lookupVinByAddress(
context,
{ licenseLastName, licenseStreetAddress, licenseZip, licenseState }
) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LookupVinByAddress.method, method: endpoints.LookupVinByAddress.method,
endpoint: endpoints.LookupVinByAddress.url, endpoint: endpoints.LookupVinByAddress.url,
@ -460,7 +489,7 @@ export const actions = {
licenseLastName: licenseLastName, licenseLastName: licenseLastName,
licenseStreetAddress: licenseStreetAddress, licenseStreetAddress: licenseStreetAddress,
licenseZip: licenseZip, licenseZip: licenseZip,
licenseState: licenseState licenseState: licenseState,
}, },
}); });
}, },
@ -494,10 +523,22 @@ export const actions = {
}) })
.then((response) => { .then((response) => {
context.commit(storeMutations.UPDATE_CAR_ID, response.data.carId); context.commit(storeMutations.UPDATE_CAR_ID, response.data.carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, response.data.category); context.commit(
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, response.data.imageUrl); storeMutations.UPDATE_VEHICLE_CATEGORY,
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, response.data.imageVifNumber); response.data.category
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, response.data.imageVifColor); );
context.commit(
storeMutations.UPDATE_VEHICLE_IMAGE_URL,
response.data.imageUrl
);
context.commit(
storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER,
response.data.imageVifNumber
);
context.commit(
storeMutations.UPDATE_VEHICLE_IMAGE_COLOR,
response.data.imageVifColor
);
return response; return response;
}); });
}, },
@ -511,8 +552,8 @@ export const actions = {
validateZip(context, { zip }) { validateZip(context, { zip }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.ValidateZip.method, methods: endpoints.ValidateZip.method,
endpoint: `${endpoints.ValidateZip.url}/${zip}` endpoint: `${endpoints.ValidateZip.url}/${zip}`,
}) });
}, },
// Dependency Actions // Dependency Actions
@ -527,7 +568,7 @@ export const actions = {
}, },
resetRegistrationAndDependencies(context) { resetRegistrationAndDependencies(context) {
context.commit(storeMutations.RESET_REGISTRATION_STATE); context.commit(storeMutations.RESET_REGISTRATION_STATE);
context.commit(storeMutations.RESET_GLASS_PARTS_STATE) context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
}, },
resetPartsAndDependencies(context) { resetPartsAndDependencies(context) {
context.commit(storeMutations.RESET_GLASS_PARTS_STATE); context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
@ -583,8 +624,8 @@ export const actions = {
assignmentId: experiment.assignmentId, assignmentId: experiment.assignmentId,
sessionKey: sessionKey, sessionKey: sessionKey,
pageName: pageName, pageName: pageName,
} },
} },
}); });
}, },
@ -592,13 +633,28 @@ export const actions = {
updateStoreWithSaveSessionResponse(context, { referralNumber, referralDate, referralCorrelationId, eon, accountNumber, savedSessionId, crmCustomerId }) { updateStoreWithSaveSessionResponse(context, { referralNumber, referralDate, referralCorrelationId, eon, accountNumber, savedSessionId, crmCustomerId }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); context.commit(
storeMutations.UPDATE_REFERRAL_CORRELATION_ID,
referralCorrelationId
);
context.commit(storeMutations.UPDATE_EON, eon); context.commit(storeMutations.UPDATE_EON, eon);
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, accountNumber); context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, accountNumber);
context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId); context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId); context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
}, },
logPageView(context, { userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId, experimentsForUser }) { logPageView(
context,
{
userId,
sessionKey,
pageName,
sessionId,
action,
event,
shouldUseSessionId,
experimentsForUser,
}
) {
var payload = { var payload = {
userId: userId, userId: userId,
sessionKey: sessionKey, sessionKey: sessionKey,
@ -608,17 +664,31 @@ export const actions = {
action: action, action: action,
event: event, event: event,
shouldUseSessionId: shouldUseSessionId, shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser experimentsForUser: experimentsForUser,
}; };
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LogPageView.method, method: endpoints.LogPageView.method,
endpoint: endpoints.LogPageView.url, endpoint: endpoints.LogPageView.url,
payload: payload, payload: payload,
logApiCall: false logApiCall: false,
}); });
}, },
logCustomEvent(context, { userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser }) { logCustomEvent(
context,
{
userId,
sessionKey,
pageName,
sessionId,
category,
action,
label,
value,
shouldUseSessionId,
experimentsForUser,
}
) {
var payload = { var payload = {
userId: userId, userId: userId,
sessionKey: sessionKey, sessionKey: sessionKey,
@ -630,14 +700,14 @@ export const actions = {
label: label, label: label,
value: value, value: value,
shouldUseSessionId: shouldUseSessionId, shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser experimentsForUser: experimentsForUser,
}; };
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LogCustomEvent.method, method: endpoints.LogCustomEvent.method,
endpoint: endpoints.LogCustomEvent.url, endpoint: endpoints.LogCustomEvent.url,
payload: payload, payload: payload,
logApiCall: false logApiCall: false,
}); });
}, },
initializeSession(context, { userId, sessionId, userAgent, referrer }) { initializeSession(context, { userId, sessionId, userAgent, referrer }) {
@ -649,22 +719,28 @@ export const actions = {
userAgent: userAgent, userAgent: userAgent,
operatorId: "WEB", operatorId: "WEB",
userName: "SafeliteConceptFunnel", userName: "SafeliteConceptFunnel",
referrer: referrer referrer: referrer,
}; };
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.InitializeSession.method, method: endpoints.InitializeSession.method,
endpoint: endpoints.InitializeSession.url, endpoint: endpoints.InitializeSession.url,
payload: payload, payload: payload,
logApiCall: false logApiCall: false,
}); });
}, },
// Misc Actions // Misc Actions
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId, eon }) { setReferralInformation(
context,
{ referralNumber, referralDate, referralCorrelationId, eon }
) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); context.commit(
storeMutations.UPDATE_REFERRAL_CORRELATION_ID,
referralCorrelationId
);
context.commit(storeMutations.UPDATE_EON, eon); context.commit(storeMutations.UPDATE_EON, eon);
}, },
@ -672,11 +748,14 @@ export const actions = {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetExperimentsByUser.method, method: endpoints.GetExperimentsByUser.method,
endpoint: `${endpoints.GetExperimentsByUser.url}/${userId}`, endpoint: `${endpoints.GetExperimentsByUser.url}/${userId}`,
payload: {} payload: {},
}); });
}, },
async runExperimentsForTrigger(context, { userId, triggerEvent, triggerValue }) { async runExperimentsForTrigger(
context,
{ userId, triggerEvent, triggerValue }
) {
if (triggerEvent == experimentTriggers.SITE_ENTRY) { if (triggerEvent == experimentTriggers.SITE_ENTRY) {
context.commit(storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, true); context.commit(storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, true);
} }
@ -686,7 +765,7 @@ export const actions = {
userId: userId, userId: userId,
triggerEvent: triggerEvent, triggerEvent: triggerEvent,
triggerValue: triggerValue, triggerValue: triggerValue,
experimentOrder: context.getters.experimentOrder experimentOrder: context.getters.experimentOrder,
}; };
const response = await globalMethods.callHttpClient({ const response = await globalMethods.callHttpClient({
@ -695,7 +774,10 @@ export const actions = {
payload: payload, payload: payload,
}); });
context.commit(storeMutations.UPDATE_EXPERIMENTS, response.data.experiments); context.commit(
storeMutations.UPDATE_EXPERIMENTS,
response.data.experiments
);
}, },
getEvoxImage(context, { relativeUrl }) { getEvoxImage(context, { relativeUrl }) {
@ -724,7 +806,7 @@ export const actions = {
carId: carId, carId: carId,
glass: glassArray ?? [], glass: glassArray ?? [],
zip: zipCode, zip: zipCode,
vin: vin vin: vin,
}, },
}); });
}, },
@ -749,7 +831,7 @@ export const actions = {
glass: glassArray, glass: glassArray,
answerResults: resultsArray, answerResults: resultsArray,
zip: zipCode, zip: zipCode,
vin: vin vin: vin,
}, },
}); });
}, },
@ -758,24 +840,31 @@ export const actions = {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetCapabilityQuestions.method, method: endpoints.GetCapabilityQuestions.method,
endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`, endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`,
}) });
}, },
getPartFromCapabilityQuestionAnswer(context, glassLocation) { getPartFromCapabilityQuestionAnswer(context, glassLocation) {
const pageData = context.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS); const pageData = context.getters.pageData(
fmgPageValues.CAPABILITY_QUESTIONS
);
const part = pageData.partsOrQuestions.find(x => x.glassLocation === glassLocation).parts[0]; const part = pageData.partsOrQuestions.find(
const capabilityQuestionAnswers = context.getters.damage.capabilityQuestionAnswers; (x) => x.glassLocation === glassLocation
const capabilityQuestionAnswersForPart = capabilityQuestionAnswers.find(x => x.glassLocation === glassLocation); ).parts[0];
const capabilityQuestionAnswers =
context.getters.damage.capabilityQuestionAnswers;
const capabilityQuestionAnswersForPart = capabilityQuestionAnswers.find(
(x) => x.glassLocation === glassLocation
);
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetPartFromCapabilityAnswer.method, method: endpoints.GetPartFromCapabilityAnswer.method,
endpoint: endpoints.GetPartFromCapabilityAnswer.url, endpoint: endpoints.GetPartFromCapabilityAnswer.url,
payload: { payload: {
part, part,
capabilityAnswerResults: capabilityQuestionAnswersForPart capabilityAnswerResults: capabilityQuestionAnswersForPart,
} },
}) });
}, },
// Session API Actions // Session API Actions
@ -810,21 +899,21 @@ export const actions = {
damage: { damage: {
numberOfChips: damage.numberOfChips, numberOfChips: damage.numberOfChips,
glassToReplace: damage.glassToReplace, glassToReplace: damage.glassToReplace,
isRepair: damage.isRepair isRepair: damage.isRepair,
}, },
customer: { customer: {
emailAddress: order.customer.emailAddress, emailAddress: order.customer.emailAddress,
}, },
lineItems: { lineItems: {
glassParts: lineItems.glassParts glassParts: lineItems.glassParts,
}, },
serviceLocation: { serviceLocation: {
streetAddress: order.serviceLocation.address, streetAddress: order.serviceLocation.address,
city: order.serviceLocation.city, city: order.serviceLocation.city,
state: order.serviceLocation.state, state: order.serviceLocation.state,
zipCode: order.serviceLocation.zipCode zipCode: order.serviceLocation.zipCode,
}, },
referralNumber: order.referralNumber?.toString(), // TODO It'd be nice to save these as strings in the first place referralNumber: order.referralNumber?.toString(), // TODO It'd be nice to save these as strings in the first place
referralDate: order.referralDate, referralDate: order.referralDate,
accountNumber: order.accountNumber?.toString(), accountNumber: order.accountNumber?.toString(),
existingPromoCode: null, existingPromoCode: null,
@ -859,7 +948,6 @@ export const actions = {
// Vehicle // Vehicle
saveVehicleYear(context, year) { saveVehicleYear(context, year) {
//Reset dependent state when changing //Reset dependent state when changing
if (context.state.order.vehicle.year !== year) { if (context.state.order.vehicle.year !== year) {
context.commit(storeMutations.UPDATE_MAKE, null); context.commit(storeMutations.UPDATE_MAKE, null);
@ -881,7 +969,6 @@ export const actions = {
} }
}, },
saveVehicleMake(context, make) { saveVehicleMake(context, make) {
//Reset dependent state when changing //Reset dependent state when changing
if (context.state.order.vehicle.make !== make) { if (context.state.order.vehicle.make !== make) {
context.commit(storeMutations.UPDATE_MODEL, null); context.commit(storeMutations.UPDATE_MODEL, null);
@ -902,7 +989,6 @@ export const actions = {
} }
}, },
saveVehicleModel(context, model) { saveVehicleModel(context, model) {
//Reset dependent state when changing //Reset dependent state when changing
if (context.state.order.vehicle.model !== model) { if (context.state.order.vehicle.model !== model) {
context.commit(storeMutations.UPDATE_STYLE, null); context.commit(storeMutations.UPDATE_STYLE, null);
@ -939,20 +1025,35 @@ export const actions = {
context.commit(storeMutations.UPDATE_STYLE, style); context.commit(storeMutations.UPDATE_STYLE, style);
} }
}, },
saveVehicleDamage(context, { isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }) { saveVehicleDamage(
context,
{ isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }
) {
const selectedGlassPassedInSorted = selectedGlassToReplace.slice().sort(); const selectedGlassPassedInSorted = selectedGlassToReplace.slice().sort();
const isGlassToReplaceTheSame = (context.state.order.damage.glassToReplace?.length === selectedGlassToReplace.length) const isGlassToReplaceTheSame =
&& context.state.order.damage.glassToReplace context.state.order.damage.glassToReplace?.length ===
selectedGlassToReplace.length &&
context.state.order.damage.glassToReplace
.slice() .slice()
.sort() .sort()
.every((obj, index) => obj.glassLocation === selectedGlassPassedInSorted[index].glassLocation && obj.glassName === selectedGlassPassedInSorted[index].glassName); .every(
const isWindshieldRepairTheSame = isWindshieldRepair === context.state.order.damage.isRepair; (obj, index) =>
obj.glassLocation ===
selectedGlassPassedInSorted[index].glassLocation &&
obj.glassName === selectedGlassPassedInSorted[index].glassName
);
const isWindshieldRepairTheSame =
isWindshieldRepair === context.state.order.damage.isRepair;
const isChipCountTheSame = Array.isArray(selectedWindshieldChipCount) //TODO: fix the underlying components so this is never an array const isChipCountTheSame = Array.isArray(selectedWindshieldChipCount) //TODO: fix the underlying components so this is never an array
? selectedWindshieldChipCount[0] === context.state.order.damage.numberOfChips ? selectedWindshieldChipCount[0] ===
: selectedWindshieldChipCount === context.state.order.damage.numberOfChips; context.state.order.damage.numberOfChips
: selectedWindshieldChipCount ===
context.state.order.damage.numberOfChips;
const isDamageChanging = !isGlassToReplaceTheSame || !isWindshieldRepairTheSame || (isWindshieldRepair && !isChipCountTheSame); const isDamageChanging =
!isGlassToReplaceTheSame ||
!isWindshieldRepairTheSame ||
(isWindshieldRepair && !isChipCountTheSame);
if (isDamageChanging) { if (isDamageChanging) {
//Reset dependent state when changing //Reset dependent state when changing
@ -960,13 +1061,22 @@ export const actions = {
// Save new values // Save new values
context.commit(storeMutations.UPDATE_IS_REPAIR, isWindshieldRepair); context.commit(storeMutations.UPDATE_IS_REPAIR, isWindshieldRepair);
context.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, isWindshieldRepair ? parseInt(selectedWindshieldChipCount) : null); context.commit(
context.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, selectedGlassToReplace); storeMutations.UPDATE_NUMBER_OF_CHIPS,
isWindshieldRepair ? parseInt(selectedWindshieldChipCount) : null
);
context.commit(
storeMutations.UPDATE_GLASS_TO_REPLACE,
selectedGlassToReplace
);
} }
}, },
// Vin lookup // Vin lookup
saveVinLookup(context, { isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) { saveVinLookup(
context,
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
) {
//Reset dependent state when changing //Reset dependent state when changing
if (vehicleInfo.vin !== context.state.order.vehicle.vin) { if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
@ -981,10 +1091,15 @@ export const actions = {
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo); context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
} }
}, },
saveRegistrationLicensePlateLookup(context, { isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) { saveRegistrationLicensePlateLookup(
context,
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
) {
//Reset dependent state when changing //Reset dependent state when changing
if (registrationInfo?.licensePlate !== context.state.order.vehicle.registration?.licensePlate) { if (
registrationInfo?.licensePlate !==
context.state.order.vehicle.registration?.licensePlate
) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
@ -997,10 +1112,25 @@ export const actions = {
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo); context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
} }
}, },
saveRegistrationAddressLookup(context, { isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) { saveRegistrationAddressLookup(
context,
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
) {
//Reset dependent state when changing //Reset dependent state when changing
if (registrationInfo?.address !== context.state.order.vehicle.registration?.address || registrationInfo?.city !== context.state.order.vehicle.registration?.city || registrationInfo?.state !== context.state.order.vehicle.registration?.state || registrationInfo?.zipCode !== context.state.order.vehicle.registration?.zipCode || registrationInfo?.firstName !== context.state.order.vehicle.registration?.firstName || registrationInfo?.lastName !== context.state.order.vehicle.registration?.lastName) { if (
registrationInfo?.address !==
context.state.order.vehicle.registration?.address ||
registrationInfo?.city !==
context.state.order.vehicle.registration?.city ||
registrationInfo?.state !==
context.state.order.vehicle.registration?.state ||
registrationInfo?.zipCode !==
context.state.order.vehicle.registration?.zipCode ||
registrationInfo?.firstName !==
context.state.order.vehicle.registration?.firstName ||
registrationInfo?.lastName !==
context.state.order.vehicle.registration?.lastName
) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
@ -1015,72 +1145,141 @@ export const actions = {
}, },
savePartQuestionAnswers(context, partQuestionAnswersArray) { savePartQuestionAnswers(context, partQuestionAnswersArray) {
// if part question answers have changed, reset subsequent question answers // if part question answers have changed, reset subsequent question answers
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(context.getters.damage.partQuestionAnswers, "result"); const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
const sortedPartQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(partQuestionAnswersArray, "result") context.getters.damage.partQuestionAnswers,
const havePartQuestionAnswersChanged = sortedPreviousResultsArray?.length !== sortedPartQuestionAnswersArray.length || "result"
!sortedPreviousResultsArray?.every((x, i) => x.result === sortedPartQuestionAnswersArray[i].result); );
const sortedPartQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(
partQuestionAnswersArray,
"result"
);
const havePartQuestionAnswersChanged =
sortedPreviousResultsArray?.length !==
sortedPartQuestionAnswersArray.length ||
!sortedPreviousResultsArray?.every(
(x, i) => x.result === sortedPartQuestionAnswersArray[i].result
);
if (havePartQuestionAnswersChanged) { if (havePartQuestionAnswersChanged) {
context.commit(storeMutations.UPDATE_GLASS_PARTS, null); context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, null); context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, null);
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null); context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null);
context.commit(storeMutations.UPDATE_PAGE_DATA, { page: fmgPageValues.VEHICLE_PARTS, data: null }); context.commit(storeMutations.UPDATE_PAGE_DATA, {
context.commit(storeMutations.UPDATE_PAGE_DATA, { page: fmgPageValues.MOLDING_QUESTIONS, data: null }); page: fmgPageValues.VEHICLE_PARTS,
context.commit(storeMutations.UPDATE_PAGE_DATA, { page: fmgPageValues.CAPABILITY_QUESTIONS, data: null }); data: null,
});
context.commit(storeMutations.UPDATE_PAGE_DATA, {
page: fmgPageValues.MOLDING_QUESTIONS,
data: null,
});
context.commit(storeMutations.UPDATE_PAGE_DATA, {
page: fmgPageValues.CAPABILITY_QUESTIONS,
data: null,
});
} }
//Save new values //Save new values
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray); context.commit(
storeMutations.UPDATE_PART_QUESTION_ANSWERS,
partQuestionAnswersArray
);
}, },
resetMoldingAndCapabilityQuestionAnswersIfNeeded(context, matchedParts) { resetMoldingAndCapabilityQuestionAnswersIfNeeded(context, matchedParts) {
const partsOrQuestionsDataToCompareWith = context.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)?.partsOrQuestions ?? context.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS)?.partsOrQuestions ?? []; const partsOrQuestionsDataToCompareWith =
context.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)
?.partsOrQuestions ??
context.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS)
?.partsOrQuestions ??
[];
function getAllPartNumbers(partsOrQuestions) { function getAllPartNumbers(partsOrQuestions) {
return partsOrQuestions[0]?.parts return partsOrQuestions[0]?.parts
? [...partsOrQuestions].map(glass => glass.parts).flat().map(part => part.partNumber).filter(partNumber => !partNumber.toUpperCase().includes("FEE")).sort().join(",") ? [...partsOrQuestions]
: [] .map((glass) => glass.parts)
.flat()
.map((part) => part.partNumber)
.filter((partNumber) => !partNumber.toUpperCase().includes("FEE"))
.sort()
.join(",")
: [];
} }
const previouslySelectedPartNumbers = getAllPartNumbers(partsOrQuestionsDataToCompareWith); const previouslySelectedPartNumbers = getAllPartNumbers(
partsOrQuestionsDataToCompareWith
);
const currentlySelectedPartNumbers = getAllPartNumbers(matchedParts); const currentlySelectedPartNumbers = getAllPartNumbers(matchedParts);
const haveSelectedVehiclePartsChanged = previouslySelectedPartNumbers !== currentlySelectedPartNumbers; const haveSelectedVehiclePartsChanged =
previouslySelectedPartNumbers !== currentlySelectedPartNumbers;
if (haveSelectedVehiclePartsChanged) { if (haveSelectedVehiclePartsChanged) {
context.commit(storeMutations.UPDATE_GLASS_PARTS, null); context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, null); context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, null);
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null); context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null);
context.commit(storeMutations.UPDATE_PAGE_DATA, { page: fmgPageValues.MOLDING_QUESTIONS, data: null }); context.commit(storeMutations.UPDATE_PAGE_DATA, {
context.commit(storeMutations.UPDATE_PAGE_DATA, { page: fmgPageValues.CAPABILITY_QUESTIONS, data: null }); page: fmgPageValues.MOLDING_QUESTIONS,
data: null,
});
context.commit(storeMutations.UPDATE_PAGE_DATA, {
page: fmgPageValues.CAPABILITY_QUESTIONS,
data: null,
});
} }
}, },
saveMoldingQuestionAnswers(context, moldingQuestionAnswers) { saveMoldingQuestionAnswers(context, moldingQuestionAnswers) {
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(context.getters.damage.moldingQuestionAnswers, "partNum"); const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
const sortedMoldingQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(moldingQuestionAnswers, "partNum"); context.getters.damage.moldingQuestionAnswers,
const haveMoldingQuestionAnswersChanged = sortedPreviousResultsArray?.length !== sortedMoldingQuestionAnswersArray.length || "partNum"
!sortedPreviousResultsArray?.every((x, i) => x.partNum === sortedMoldingQuestionAnswersArray[i].partNum); );
const sortedMoldingQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(
moldingQuestionAnswers,
"partNum"
);
const haveMoldingQuestionAnswersChanged =
sortedPreviousResultsArray?.length !==
sortedMoldingQuestionAnswersArray.length ||
!sortedPreviousResultsArray?.every(
(x, i) => x.partNum === sortedMoldingQuestionAnswersArray[i].partNum
);
if (haveMoldingQuestionAnswersChanged) { if (haveMoldingQuestionAnswersChanged) {
context.commit(storeMutations.UPDATE_GLASS_PARTS, null); context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null); context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null);
context.commit(storeMutations.UPDATE_PAGE_DATA, { page: fmgPageValues.CAPABILITY_QUESTIONS, data: null }); context.commit(storeMutations.UPDATE_PAGE_DATA, {
page: fmgPageValues.CAPABILITY_QUESTIONS,
data: null,
});
} }
//Save new values //Save new values
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, moldingQuestionAnswers); context.commit(
storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS,
moldingQuestionAnswers
);
}, },
saveCapabilityQuestionAnswers(context, capabilityQuestionAnswers) { saveCapabilityQuestionAnswers(context, capabilityQuestionAnswers) {
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(context.getters.damage.capabilityQuestionAnswers, "result"); const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
const sortedCapabilityQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(capabilityQuestionAnswers, "result"); context.getters.damage.capabilityQuestionAnswers,
const haveCapabilityQuestionAnswersChanged = sortedPreviousResultsArray?.length !== sortedCapabilityQuestionAnswersArray.length || "result"
!sortedPreviousResultsArray?.every((x, i) => x.result === sortedCapabilityQuestionAnswersArray[i].result); );
const sortedCapabilityQuestionAnswersArray =
sortArrayOfObjectsByPropertyValue(capabilityQuestionAnswers, "result");
const haveCapabilityQuestionAnswersChanged =
sortedPreviousResultsArray?.length !==
sortedCapabilityQuestionAnswersArray.length ||
!sortedPreviousResultsArray?.every(
(x, i) => x.result === sortedCapabilityQuestionAnswersArray[i].result
);
if (haveCapabilityQuestionAnswersChanged) { if (haveCapabilityQuestionAnswersChanged) {
context.commit(storeMutations.UPDATE_GLASS_PARTS, null); context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
} }
//Save new values //Save new values
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, capabilityQuestionAnswers); context.commit(
storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS,
capabilityQuestionAnswers
);
}, },
// Misc order actions // Misc order actions
saveServiceLocation(context, serviceLocationInfo) { saveServiceLocation(context, serviceLocationInfo) {
@ -1092,7 +1291,6 @@ export const actions = {
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) { saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
//Reset dependent state when changing //Reset dependent state when changing
if (vehicleInfo.vin !== context.state.order.vehicle.vin) { if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
@ -1107,12 +1305,11 @@ export const actions = {
}, },
clearVin(context) { clearVin(context) {
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null); context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
} },
} };
export default createStore({ export default createStore({
plugins: [createPersistedState()], plugins: [createPersistedState()],
// IMPORTANT: Be VERY careful when modifying these fields for at least a few reasons: // IMPORTANT: Be VERY careful when modifying these fields for at least a few reasons:
// * The CMS can reference the fields by name // * The CMS can reference the fields by name
// * Return users may have a previous "version" of the model, and we don't want // * Return users may have a previous "version" of the model, and we don't want
@ -1126,30 +1323,27 @@ export default createStore({
// Private Functions // Private Functions
function getHasRecalibrationPart(state) { function getHasRecalibrationPart(state) {
var hasRequiresRecalibration = getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "requiresRecalibration")?.length > 0; var hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "requiresRecalibration")?.length > 0;
var hasRecalibrationType = getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType")?.length > 0; var hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType")?.length > 0;
if (hasRequiresRecalibration) { if (hasRequiresRecalibration) {
if (hasRecalibrationType) { // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType' if (hasRecalibrationType) { // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType'
return getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType")[0].toLowerCase() != "unknown"; return getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType")[0].toLowerCase() != "unknown";
} else { // Has 'requiresRecalibration' but no 'recalibrationType' at all } else { // Has 'requiresRecalibration' but no 'recalibrationType' at all
return true; return true;
} }
} else { // Does not have 'requiresRecalibration' } else {
// Does not have 'requiresRecalibration'
return false; return false;
} }
} }
function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) { function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) {
if (!arrayOfObjects) return null; if (!arrayOfObjects) return null;
return arrayOfObjects.sort((a, b) => { return arrayOfObjects.sort((a, b) => {
if (a[propertyName] < b[propertyName]) if (a[propertyName] < b[propertyName]) return -1;
return -1; else if (a[propertyName] > b[propertyName]) return 1;
else if (a[propertyName] > b[propertyName]) else return 0;
return 1; });
else
return 0;
})
} }

View file

@ -2,15 +2,17 @@
<button <button
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
class="btn d-flex align-items-center py-3 px-4 delay" class="btn d-flex align-items-center py-3 px-4 delay"
:class="[isPrimary ? 'btn-primary' : 'btn-secondary',isFloat ? 'float-end' : '', isLoaderDisplayed ? 'has-loader' : '']" :class="[
@click="clicked" isPrimary ? 'btn-primary' : 'btn-secondary',
> isFloat ? 'float-end' : '',
isLoaderDisplayed ? 'has-loader' : '',
]"
@click="clicked">
<span class="m-0">{{ this.buttonText }}</span> <span class="m-0">{{ this.buttonText }}</span>
<loader <loader
class="ms-2" class="ms-2"
v-if="isLoaderDisplayed" v-if="isLoaderDisplayed"
v-bind:class="[this.loaderColor, this.loaderPosition]" v-bind:class="[this.loaderColor, this.loaderPosition]" />
/>
</button> </button>
</template> </template>
@ -33,11 +35,16 @@ export default {
}; };
}, },
methods: { methods: {
removeLoader(){ removeLoader() {
this.isLoaderDisplayed = false; this.isLoaderDisplayed = false;
}, },
clicked() { clicked() {
this.pushEventToGA(this.$route.query[this.queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.buttonText, true); this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.CLICKED,
this.buttonText,
true
);
if (!this.isDisabled) { if (!this.isDisabled) {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
this.$emit("click-event"); this.$emit("click-event");
@ -98,7 +105,8 @@ export default {
background: $blue-700; background: $blue-700;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700; box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
} }
&.delay {// fixes flicker while transitioning between states &.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out; transition: background 0s 0s ease-in-out;
} }
} }
@ -137,7 +145,8 @@ export default {
color: $white; color: $white;
@include blue-gradient; @include blue-gradient;
} }
&.delay {// fixes flicker while transitioning between states &.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out; transition: background 0s 0s ease-in-out;
} }
} }

View file

@ -1,272 +1,201 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount, mount } from "@vue/test-utils";
import listButtonHorizontal from "./list-button-horizontal"; import listButtonHorizontal from "./list-button-horizontal";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { GaActions } from "@/constants/analytics"; import { GaActions } from "@/constants/analytics";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
// TODO KO describe("list-button-horizontal.vue", () => {
describe.skip("list-button-horizontal.vue", () => { // TODO KO Have this moved out to somewhere shared
it("Should return input type checkbox if isMultiSelect is true", async () => { describe("Shared baseInputButton checks", () => {
// Act it("Should return input type checkbox if isMultiSelect is true", async () => {
const wrapper = shallowMount(listButtonHorizontal, { // Act
propsData: { const { wrapper } = setupMocks({
isMultiSelect: true, mockData: {
}, propsData: {
isMultiSelect: true,
},
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
}); });
// Assert it("Should return input type radio if isMultiSelect is false or not specified", async () => {
const input = wrapper.find("input"); // Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
},
},
});
expect(input.attributes().type).toEqual("checkbox"); // Assert
}); const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isMultiSelect: false,
},
}); });
// Assert it("Should emit button value on click", async () => {
const input = wrapper.find("input"); // Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
value: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
buttonID: "list-card-id",
},
},
});
expect(input.attributes().type).toEqual("radio"); // Act
wrapper.vm.handleAnswerChange("test");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted()["change"][0]).toEqual(["test"]);
});
}); });
it("Should return primary label text (buttonID)", async () => { describe("styling/UI", () => {
// Act it("Should return screen reader text", async () => {
const wrapper = shallowMount(listButtonHorizontal, { // Act
propsData: { const { wrapper } = setupMocks({
buttonID: "List Card Checkbox", mockData: {
}, propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
},
});
// Assert
const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text");
}); });
// Assert it("Should return text alignment class", async () => {
const label = wrapper.find("label"); // Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
textPosition: "text-center",
},
},
});
expect(label.attributes().for).toEqual("List Card Checkbox"); // Assert
}); const paragraph = wrapper.find("span.m-0");
it("Should return screen reader text", async () => { expect(paragraph.attributes("class")).toContain("text-center");
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
}); });
// Assert it("Should return aria-required state", async () => {
const paragraph = wrapper.find("span.sr-only"); // Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isRequired: true,
},
},
});
expect(paragraph.text()).toEqual("Screen Reader Only Text"); // Assert
}); const input = wrapper.find("input");
it("Should return text alignment class", async () => { expect(input.attributes()["aria-required"]).toEqual("true");
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
textPosition: "text-center",
},
}); });
// Assert it("is cash or insurance button => has 'radio-fancy' class", () => {
const paragraph = wrapper.find("span.m-0"); // Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isCashOrInsurance: true,
},
},
});
expect(paragraph.attributes("class")).toContain("text-center"); // Assert
}); const label = wrapper.find("label");
it("Should return aria-required state", async () => { expect(label.classes()).toContain("radio-fancy");
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isRequired: true,
},
}); });
// Assert test("has buttonLabel => displays buttonLabel", () => {
const input = wrapper.find("input"); // Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
},
},
});
expect(input.attributes()["aria-required"]).toEqual("true"); // Assert
}); const content = wrapper.find(".list-button-horizontal-content");
expect(content.isVisible()).toBe(true);
it("Should return loader enabled true", async () => { expect(content.text()).toContain("Surprise!");
// Act
const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
selectingInitiatesLoad: true,
},
}); });
// Assert test("has buttonLabelSubCopy => displays buttonLabelSubCopy", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
},
},
});
const label = wrapper.find("label"); // Assert
const content = wrapper.find(".list-button-horizontal-content");
wrapper.vm.handleCheckChange = jest.fn(); expect(content.isVisible()).toBe(true);
wrapper.vm.triggerButton(); expect(content.text()).toContain("Super duper surprise :)");
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.exists()).toBe(true);
});
it("Should return loader color", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
loaderColor: "blue",
selectingInitiatesLoad: true,
},
}); });
// Assert test("has screenReaderOnlyText => displays screenReaderOnlyText", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
screenReaderOnlyText: "Tests are fun!"
},
},
});
const label = wrapper.find("label"); // Assert
const content = wrapper.find(".list-button-horizontal-content");
wrapper.vm.handleCheckChange = jest.fn(); const screenReaderOnlyText = wrapper.find(".sr-only");
wrapper.vm.triggerButton(); expect(content.isVisible()).toBe(true);
expect(screenReaderOnlyText.exists()).toBe(true);
await nextTick(); expect(screenReaderOnlyText.text()).toContain("Tests are fun!");
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("blue");
});
it("Should return loader position", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
loaderPosition: "right",
selectingInitiatesLoad: true,
},
}); });
// Assert
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.triggerButton();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("right");
}); });
it("Should emit button value on click", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
value: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
},
});
wrapper.vm.handleCheckChange();
// Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean, buttonId: undefined, checkValue: false}]);
});
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
isMultiSelect: false,
value: "Car-Front",
selectedValues: ["Car-Front"]
},
});
// Assert
expect(wrapper.vm.checkValue).toEqual(true);
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
selectingInitiatesLoad: false,
},
});
// Assert
wrapper.vm.handleInputChange();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isMultiSelect: true,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
selectingInitiatesLoad: false,
isMultiSelect: false,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
}); });
function setupMocks({ mockData }) {
const wrapper = mount(listButtonHorizontal, {
...mockData,
propsData: {
...mockData.propsData,
groupName: "my-group",
modelValue: mockData.propsData?.isMultiSelect ? ["5"] : "5",
value: mockData.propsData?.isMultiSelect ? ["4"] : "4"
},
mixins: [inputButtonWrapperMixin],
});
return { wrapper };
}

View file

@ -5,7 +5,7 @@
'list-group list-button-horizontal d-flex flex-column w-100', 'list-group list-button-horizontal d-flex flex-column w-100',
{ 'radio-fancy': isCashOrInsurance }, { 'radio-fancy': isCashOrInsurance },
]" ]"
@buttonClicked="handleAnswerChange"> @inputButtonClicked="handleAnswerChange">
<div <div
class="list-button-horizontal-content d-flex flex-column justify-content-center p-3"> class="list-button-horizontal-content d-flex flex-column justify-content-center p-3">
<span class="m-0" :class="textPosition"> <span class="m-0" :class="textPosition">

View file

@ -1,264 +1,261 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount, mount } from "@vue/test-utils";
import listButton from "./list-button"; import listButton from "./list-button";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { GaActions } from "@/constants/analytics"; import { GaActions } from "@/constants/analytics";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
// TODO KO describe("list-button.vue", () => {
describe.skip("list-button.vue", () => { describe("loader", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("selectingInitiatesLoad is true and answer is changed => show the loader", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const { wrapper } = setupMocks({
propsData: { mockData: {
isMultiSelect: true, global: {
}, mocks: {
$route: { query: { fmgPage: "page-name" } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
},
},
propsData: {
selectingInitiatesLoad: true,
},
},
});
// Act
wrapper.vm.handleAnswerChange("something");
await wrapper.vm.$nextTick();
// Assert
const loader = wrapper.findComponent({ name: "loader" });
expect(loader.exists()).toBe(true);
}); });
// Assert it("Should return loader color", async () => {
const input = wrapper.find("input"); // Act
const { wrapper } = setupMocks({
mockData: {
global: {
mocks: {
$route: { query: { fmgPage: "page-name" } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
},
},
propsData: {
loaderColor: "blue",
selectingInitiatesLoad: true,
},
},
});
expect(input.attributes().type).toEqual("checkbox"); // Act
}); wrapper.vm.handleAnswerChange("something");
await nextTick();
it("Should return input type radio if isMultiSelect is false or not specified", async () => { // Assert
// Act const loader = wrapper.findComponent({ name: "loader" });
const wrapper = shallowMount(listButton, { expect(loader.attributes("class")).toContain("blue");
propsData: {
isMultiSelect: false,
},
}); });
// Assert it("Should return loader position", async () => {
const input = wrapper.find("input"); // Act
const { wrapper } = setupMocks({
mockData: {
global: {
mocks: {
$route: { query: { fmgPage: "page-name" } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
},
},
propsData: {
loaderPosition: "right",
selectingInitiatesLoad: true,
},
},
});
expect(input.attributes().type).toEqual("radio"); // Act
wrapper.vm.handleAnswerChange("test");
await nextTick();
// Assert
const loader = wrapper.findComponent({ name: "loader" });
expect(loader.attributes("class")).toContain("right");
});
}); });
it("Should return primary label text (buttonID)", async () => { describe("baseInputButton checks", () => {
// Act it("Should return input type checkbox if isMultiSelect is true", async () => {
const wrapper = shallowMount(listButton, { // Act
propsData: { const { wrapper } = setupMocks({
buttonID: "List Card Checkbox", mockData: {
}, propsData: {
isMultiSelect: true,
},
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
}); });
// Assert it("Should return input type radio if isMultiSelect is false or not specified", async () => {
const label = wrapper.find("label"); // Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
},
},
});
expect(label.attributes().for).toEqual("List Card Checkbox"); // Assert
}); const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
it("Should return screen reader text", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
}); });
// Assert it("Should emit button value on click", async () => {
const paragraph = wrapper.find("span.sr-only"); // Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
value: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
buttonID: "list-card-id",
},
},
});
expect(paragraph.text()).toEqual("Screen Reader Only Text"); // Act
wrapper.vm.handleAnswerChange("test");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted()["change"][0]).toEqual(["test"]);
});
}); });
it("Should return text alignment class", async () => { describe("styling/UI", () => {
// Act test("has buttonLabel => displays buttonLabel", () => {
const wrapper = shallowMount(listButton, { // Arrange/Act
propsData: { const { wrapper } = setupMocks({
textPosition: "text-center", mockData: {
}, propsData: {
buttonLabel: "Surprise!",
},
},
});
// Assert
const content = wrapper.find(".list-button-content");
expect(content.isVisible()).toBe(true);
expect(content.text()).toContain("Surprise!");
}); });
// Assert test("has buttonLabelSubCopy => displays buttonLabelSubCopy", () => {
const paragraph = wrapper.find("span.m-0"); // Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
},
},
});
expect(paragraph.attributes("class")).toContain("text-center"); // Assert
}); const content = wrapper.find(".list-button-content");
expect(content.isVisible()).toBe(true);
it("Should return aria-required state", async () => { expect(content.text()).toContain("Super duper surprise :)");
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isRequired: true,
},
}); });
// Assert test("has screenReaderOnlyText => displays screenReaderOnlyText", () => {
const input = wrapper.find("input"); // Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
screenReaderOnlyText: "Tests are fun!",
},
},
});
expect(input.attributes()["aria-required"]).toEqual("true"); // Assert
}); const content = wrapper.find(".list-button-content");
const screenReaderOnlyText = wrapper.find(".sr-only");
it("Should return loader enabled true", async () => { expect(content.isVisible()).toBe(true);
// Act expect(screenReaderOnlyText.exists()).toBe(true);
const wrapper = shallowMount(listButton, { expect(screenReaderOnlyText.text()).toContain("Tests are fun!");
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
selectingInitiatesLoad: true,
},
}); });
// Assert it("Should return screen reader text", async () => {
wrapper.vm.handleCheckChange = jest.fn(); // Act
wrapper.vm.triggerButton(); const { wrapper } = setupMocks({
mockData: {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
},
});
await nextTick(); // Assert
const paragraph = wrapper.find("span.sr-only");
const loader = wrapper.find("loader-stub"); expect(paragraph.text()).toEqual("Screen Reader Only Text");
expect(loader.exists()).toBe(true);
});
it("Should return loader color", async () => {
// Act
const wrapper = shallowMount(listButton, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
loaderColor: "blue",
selectingInitiatesLoad: true,
},
}); });
// Assert it("Should return text alignment class", async () => {
wrapper.vm.handleCheckChange = jest.fn(); // Act
wrapper.vm.triggerButton(); const { wrapper } = setupMocks({
await nextTick(); mockData: {
propsData: {
textPosition: "text-center",
},
},
});
const loader = wrapper.find("loader-stub"); // Assert
const paragraph = wrapper.find("span.m-0");
expect(loader.attributes("class")).toContain("blue"); expect(paragraph.attributes("class")).toContain("text-center");
});
it("Should return loader position", async () => {
// Act
const wrapper = shallowMount(listButton, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
loaderPosition: "right",
selectingInitiatesLoad: true,
},
}); });
// Assert it("Should return aria-required state", async () => {
wrapper.vm.handleCheckChange = jest.fn(); // Act
wrapper.vm.triggerButton(); const { wrapper } = setupMocks({
mockData: {
propsData: {
isRequired: true,
},
},
});
await nextTick(); // Assert
const input = wrapper.find("input");
const loader = wrapper.find("loader-stub"); expect(input.attributes()["aria-required"]).toEqual("true");
expect(loader.attributes("class")).toContain("right");
});
it("Should emit button value on click", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
value: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
buttonID: 'list-card-id'
},
}); });
wrapper.vm.handleCheckChange();
// Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: false, buttonId: 'list-card-id'}]);
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isRadioHorizontal: true,
buttonLabel: "Windshield",
buttonID: "List Card Checkbox",
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
selectedValues: "Car-Front"
},
});
// Assert
expect(wrapper.componentVM.checkValue).toEqual(false);
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
selectingInitiatesLoad: false,
},
});
// Assert
wrapper.vm.handleInputChange();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isMultiSelect: true,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
});
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
selectingInitiatesLoad: false,
isMultiSelect: false,
},
});
// Assert
wrapper.vm.handleKeyupArrow();
await nextTick();
expect(wrapper.vm.handleCheckChange).toBeCalled;
});
}); });
function setupMocks({ mockData }) {
const wrapper = mount(listButton, {
...mockData,
mixins: [inputButtonWrapperMixin],
});
return { wrapper };
}

View file

@ -2,7 +2,7 @@
<baseInputButton <baseInputButton
v-bind="$props" v-bind="$props"
buttonWrapperClasses="list-group list-button rounded-3 d-flex flex-column w-100 mb-2" buttonWrapperClasses="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
@buttonClicked="handleAnswerChange"> @inputButtonClicked="handleAnswerChange">
<div <div
:aria-label="buttonLabel" :aria-label="buttonLabel"
class="list-button-content d-flex flex-column justify-content-center py-3 px-4"> class="list-button-content d-flex flex-column justify-content-center py-3 px-4">

View file

@ -14,6 +14,7 @@ describe("list-card.vue", () => {
groupID: "checkbox-demo-1", groupID: "checkbox-demo-1",
groupName: "Checkbox 1", groupName: "Checkbox 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
value: "test value",
}, },
}); });
@ -32,6 +33,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
value: "test value",
}, },
}); });
@ -51,6 +53,7 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test", buttonLabelSubCopy: "Test",
value: "test value",
}, },
}); });
@ -70,6 +73,7 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test", buttonLabelSubCopy: "Test",
value: "test value",
}, },
}); });
@ -89,6 +93,7 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
value: "test value",
}, },
}); });
@ -110,6 +115,7 @@ describe("list-card.vue", () => {
isRequired: true, isRequired: true,
isWide: true, isWide: true,
buttonLabelSubCopy: "", buttonLabelSubCopy: "",
value: "test value",
}, },
}); });
@ -134,6 +140,7 @@ describe("list-card.vue", () => {
isRequired: true, isRequired: true,
isWide: true, isWide: true,
buttonLabelSubCopy: "Button Subcopy", buttonLabelSubCopy: "Button Subcopy",
value: "test value",
}, },
}); });
@ -159,6 +166,7 @@ describe("list-card.vue", () => {
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
isWide: false, isWide: false,
value: "test value",
}, },
}); });

View file

@ -5,7 +5,7 @@
'list-card w-100 rounded-3 d-flex align-items-center h-100', 'list-card w-100 rounded-3 d-flex align-items-center h-100',
{ horizontal: isWide }, { horizontal: isWide },
]" ]"
@buttonClicked="handleAnswerChange"> @inputButtonClicked="handleAnswerChange">
<div <div
class="d-flex w-100 align-items-center px-2 h-100 list-card-content" class="d-flex w-100 align-items-center px-2 h-100 list-card-content"
:class="labelClasses"> :class="labelClasses">

View file

@ -6,12 +6,16 @@ import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
describe("radio.vue", () => { describe("radio.vue", () => {
it("Should have correct group name", async () => { it("Should have correct group name", async () => {
// Arrange // Arrange
let { wrapper } = setupMocks({}); let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: {
groupName: "radio-button-test",
value: "test value",
},
},
});
// Act // Act
await wrapper.setProps({
groupName: "radio-button-test",
});
const input = wrapper.find("input"); const input = wrapper.find("input");
// Assert // Assert
@ -20,12 +24,16 @@ describe("radio.vue", () => {
it("Should have correct label text", async () => { it("Should have correct label text", async () => {
// Act // Act
let { wrapper } = setupMocks({}); let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: {
buttonLabel: "label text",
value: "test value",
},
},
});
// Arrange // Arrange
await wrapper.setProps({
buttonLabel: "label text",
});
const paragraph = wrapper.find("p"); const paragraph = wrapper.find("p");
// Assert // Assert
@ -39,6 +47,7 @@ describe("radio.vue", () => {
// Arrange // Arrange
await wrapper.setProps({ await wrapper.setProps({
screenReaderOnlyText: "screenreader text", screenReaderOnlyText: "screenreader text",
value: "test value",
}); });
const paragraph = wrapper.find(".sr-only"); const paragraph = wrapper.find(".sr-only");
@ -48,13 +57,10 @@ describe("radio.vue", () => {
}); });
function setupMocks({ mountOptionsMockData = {} }) { function setupMocks({ mountOptionsMockData = {} }) {
const wrapper = mount( const wrapper = mount(radio, {
radio, ...mountOptionsMockData,
getMountOptions({ mixins: [inputButtonWrapperMixin],
...mountOptionsMockData, });
mixins: [inputButtonWrapperMixin],
})
);
return { wrapper }; return { wrapper };
} }

View file

@ -3,7 +3,7 @@
v-bind="$props" v-bind="$props"
buttonWrapperClasses="ui-radio form-check" buttonWrapperClasses="ui-radio form-check"
inputClasses="form-check-input" inputClasses="form-check-input"
@buttonClicked="handleAnswerChange"> @inputButtonClicked="handleAnswerChange">
<div class="d-flex align-items-start form-check-label"> <div class="d-flex align-items-start form-check-label">
<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">{{