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
}

View file

@ -28,8 +28,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
// TODO KO
statements: 55,
statements: 85,
// 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>
<router-view v-slot="{ Component }">
<transition :duration="{ enter: 200, leave: 200 }" name="route-fade" mode="out-in">
<!-- The above durations should be kept in sync with the global css class "fade-on-route-transition" -->
<component :is="Component" />
</transition>
</router-view>
<router-view v-slot="{ Component }">
<transition
:duration="{ enter: 200, leave: 200 }"
name="route-fade"
mode="out-in">
<!-- 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>
<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">
@import "./node_modules/bootstrap/scss/bootstrap";
@import "@/styles/common-styles.scss";
@import "@/styles/common-typography-styles.scss";
@import "@/styles/common-error-styles.scss";
@import "@/styles/common-animations.scss";
@import "./node_modules/bootstrap/scss/bootstrap";
@import "@/styles/common-styles.scss";
@import "@/styles/common-typography-styles.scss";
@import "@/styles/common-error-styles.scss";
@import "@/styles/common-animations.scss";
</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
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0 }]"
:for="buttonId"
@mousedown.left="handleEventAction('click', $event)"
@keyup="handleKeyboardNavigation">
@focusin="handleFocus"
@focusout="handleBlur"
@mousedown.left="handleEventAction('click', $event)">
<input
:type="inputType"
:id="buttonId"
@ -13,7 +14,6 @@
:aria-required="isRequired"
:value="value"
:checked="isChecked"
@blur="handleBlur"
@keypress.space="handleEventAction('space', $event)"
@keypress.enter="handleEventAction('enter', $event)"
@change="handleEventAction('change', $event)" />
@ -23,18 +23,20 @@
</template>
<script>
import { queryStrings } from "@/constants/query-strings";
import { useField } from "vee-validate";
import { toRef } from "vue";
import { queryStrings } from "@/constants/query-strings";
import inputButtonWrapperMixin from "../../mixins/input-button-wrapper-mixin";
export default {
name: "base-input-button",
emits: ["change"],
emits: ["change", "inputButtonClicked"],
model: {
prop: "modelValue",
event: "change",
},
props: {
// ...inputButtonWrapperMixin.props
value: {
type: [String, Number],
required: true,
@ -60,6 +62,9 @@ export default {
default: true,
},
isRequired: Boolean,
lastValuePushedToGa: [String, Number],
setLastValuePushedToGa: Function,
shouldPushClickEventToGAOnMount: Boolean,
},
data() {
return {
@ -68,36 +73,37 @@ export default {
},
mounted() {
if (this.isChecked) {
this.handleChange(this.modelValue);
if (this.shouldPushClickEventToGAOnMount) {
this.handleEventAction(this.eventTypes.MOUNT)
} else {
this.handleChange(this.modelValue);
}
}
},
methods: {
handleEventAction(eventType, e) {
const eventTypes = {
CHANGE: "change",
ENTER: "enter",
SPACE: "space",
CLICK: "click",
};
if (this.isMultiSelect) {
switch (eventType) {
case eventTypes.ENTER:
case eventTypes.CHANGE:
case this.eventTypes.ENTER:
case this.eventTypes.CHANGE:
this.handleClick(e);
window.lastFocusedInputGroup = this.groupName;
this.pushClickEventToGA();
this.handlePushClickEventToGACheck(
this.eventTypes.CLICK
);
break;
}
} else {
switch (eventType) {
case eventTypes.CLICK:
case eventTypes.ENTER:
case eventTypes.SPACE:
case this.eventTypes.CLICK:
case this.eventTypes.ENTER:
case this.eventTypes.SPACE:
case this.eventTypes.MOUNT:
this.handleClick(e);
this.test();
this.handlePushClickEventToGACheck(
this.eventTypes.CLICK
);
break;
case eventTypes.CHANGE:
case this.eventTypes.CHANGE:
this.selectOnKeypress
? this.handleClick(e)
: this.handleSelectionChange(e);
@ -122,21 +128,37 @@ export default {
this.valueToEmit = this.value;
}
window.currentlySelectedValues =
window.currentlySelectedValues ?? {};
window.currentlySelectedValues[this.groupName] = this.value;
this.handleChange(this.valueToEmit);
},
handleClick(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) {
window.firedGaClickEventValues =
window.firedGaClickEventValues ?? {};
window.firedGaClickEventValues[window.lastFocusedInputGroup] =
value ?? this.value;
this.pushEventToGA(
this.$route.query[queryStrings.FMG_PAGE],
this.GaActions.CLICKED,
@ -144,88 +166,39 @@ export default {
true,
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;
// 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
]
);
}
}
this.setLastValuePushedToGa(this.value);
},
},
computed: {
isChecked() {
if (this.isMultiSelect && this.modelValue instanceof Array) {
return this.modelValue.includes(this.value);
} else if (!this.isMultiSelect) {
return this.modelValue == this.value;
} else {
return false;
}
return this.modelValue == this.value;
},
inputType() {
return this.isMultiSelect ? "checkbox" : "radio";
},
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) {

View file

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

View file

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

View file

@ -36,6 +36,7 @@ const storeActions = {
RUN_EXPERIMENTS_FOR_TRIGGER: "runExperimentsForTrigger",
CLEAR_VIN: "clearVin",
RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise",
// DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies",
@ -63,12 +64,4 @@ const storeActions = {
SAVE_CAPABILITY_QUESTION_ANSWERS: "saveCapabilityQuestionAnswers",
};
const gaStoreActions = {
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 };
export { storeActions };

View file

@ -68,12 +68,4 @@ const storeMutations = {
UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry",
};
const gaStoreMutations = {
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 };
export { storeMutations };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,8 +13,7 @@
altText=""
isRequired
:groupName="`${glassLocation}-${glassName}`"
:validationRules="tintValidationRules"
>
:validationRules="tintValidationRules">
<div class="row my-2" aria-live="polite">
<div class="col">
<buttonQuestion
@ -28,7 +27,9 @@
isRequired
:groupName="`${glassLocation}-${glassName}-${selectedTint}`"
:validationRules="partValidationRules"
/>
:shouldPushClickEventToGAOnMount="
shouldPushClickEventToGAOnMount
" />
</div>
</div>
</buttonQuestion>
@ -56,6 +57,7 @@ export default {
glassColorQuestion: "",
glassFeatureQuestion: "",
selectedTint: "",
shouldPushClickEventToGAOnMount: true,
};
},
props: {
@ -63,7 +65,10 @@ export default {
glassLocation: String,
colorAnswers: Array,
modelValue: Object,
alreadyPopulatedPartsData: Array
alreadyPopulatedPartsData: {
type: Array,
default: () => [],
},
},
mounted() {
this.LoadPreselectedValues();
@ -74,12 +79,18 @@ export default {
computed: {
tintValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-tint-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
defineRule(
validationRuleName,
required(errorMessages.OPTION_REQUIRED)
);
return validationRuleName;
},
partValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
defineRule(
validationRuleName,
required(errorMessages.OPTION_REQUIRED)
);
return validationRuleName;
},
colorQuestionText() {
@ -110,16 +121,29 @@ export default {
return this.modelValue?.partNumber;
},
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() {
const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter(dataForGlassLocationAndName =>
dataForGlassLocationAndName.glassName == this.glassName &&
dataForGlassLocationAndName.glassLocation == this.glassLocation);
const matchingGlassParts = matchingGlass?.length == 1 ? matchingGlass[0].parts : [];
return matchingGlassParts.filter(part => part.color == this.selectedTint) ?? [];
const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter(
(dataForGlassLocationAndName) =>
dataForGlassLocationAndName.glassName == this.glassName &&
dataForGlassLocationAndName.glassLocation ==
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
@ -151,7 +175,9 @@ export default {
},
PartDataFromApi() {
return this.$store.getters.pageData(this.$route.query.fmgPage) ?? {};
return (
this.$store.getters.pageData(this.$route.query.fmgPage) ?? {}
);
},
},
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.
AutoSelectIfSinglePart() {
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(() => {
if (this.modelValue !== undefined) {
// 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: {
selectedTint() {
this.AutoSelectIfSinglePart();
}
}
},
},
};
</script>

View file

@ -183,7 +183,16 @@ describe("vehicle-parts.vue", () => {
await nextTick();
//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 () => {
@ -509,6 +518,8 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
// wrapper.vm.$refs.onSubmit = jest.fn();
// wrapper.vm.$refs.onInvalidSubmit = jest.fn();
return { wrapper, apiPromise };
}

View file

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

View file

@ -6,12 +6,11 @@ import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { routerParams } from "@/router/router-constants/router-params";
import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings";
import { settleAllPromises } from "@/helpers/layout-helper";
export default {
data() {
return {
cmsContentByWidget: {}
cmsContentByWidget: {},
};
},
methods: {
@ -19,7 +18,9 @@ export default {
this.$root.cmsContentByWidget = cmsContent;
},
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) {
// Encode the payload if required
@ -32,7 +33,7 @@ export default {
savePageDataToStore(page, 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 }) {
// identify the first error field and put focus on it
// get error names array
@ -49,14 +50,17 @@ export default {
return footerInfoBox ? footerInfoBox.offsetHeight : 0;
},
async getZipCodeData(zipCode) {
const serviceZipValidationResponse = await this.dispatchStoreAction(storeActions.VALIDATE_ZIP, { zip: zipCode });
return {
isValid: serviceZipValidationResponse.data.isValid,
const serviceZipValidationResponse = await this.dispatchStoreAction(
storeActions.VALIDATE_ZIP,
{ zip: zipCode }
);
return {
isValid: serviceZipValidationResponse.data.isValid,
isServiceable: serviceZipValidationResponse.data.isServiceable,
state: serviceZipValidationResponse.data.state
state: serviceZipValidationResponse.data.state,
};
}
},
},
computed: {
storeActions() {
@ -74,13 +78,13 @@ export default {
routerParams() {
return routerParams;
},
queryStrings(){
queryStrings() {
return queryStrings;
},
dynamicStrings(){
dynamicStrings() {
return dynamicStrings;
},
cssClassNameForCmsWidget(){
cssClassNameForCmsWidget() {
return "widget-name-" + this.cmsWidgetName;
},
},

View file

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

View file

@ -2,15 +2,17 @@
<button
:aria-disabled="isDisabled"
class="btn d-flex align-items-center py-3 px-4 delay"
:class="[isPrimary ? 'btn-primary' : 'btn-secondary',isFloat ? 'float-end' : '', isLoaderDisplayed ? 'has-loader' : '']"
@click="clicked"
>
:class="[
isPrimary ? 'btn-primary' : 'btn-secondary',
isFloat ? 'float-end' : '',
isLoaderDisplayed ? 'has-loader' : '',
]"
@click="clicked">
<span class="m-0">{{ this.buttonText }}</span>
<loader
class="ms-2"
v-if="isLoaderDisplayed"
v-bind:class="[this.loaderColor, this.loaderPosition]"
/>
v-bind:class="[this.loaderColor, this.loaderPosition]" />
</button>
</template>
@ -33,11 +35,16 @@ export default {
};
},
methods: {
removeLoader(){
removeLoader() {
this.isLoaderDisplayed = false;
},
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) {
this.isLoaderDisplayed = true;
this.$emit("click-event");
@ -98,7 +105,8 @@ export default {
background: $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;
}
}
@ -137,7 +145,8 @@ export default {
color: $white;
@include blue-gradient;
}
&.delay {// fixes flicker while transitioning between states
&.delay {
// fixes flicker while transitioning between states
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 { nextTick } from "vue";
import { GaActions } from "@/constants/analytics";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
// TODO KO
describe.skip("list-button-horizontal.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isMultiSelect: true,
},
describe("list-button-horizontal.vue", () => {
// TODO KO Have this moved out to somewhere shared
describe("Shared baseInputButton checks", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
},
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
});
// Assert
const input = wrapper.find("input");
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
},
},
});
expect(input.attributes().type).toEqual("checkbox");
});
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isMultiSelect: false,
},
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
});
// Assert
const input = wrapper.find("input");
it("Should emit button value on click", async () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
buttonID: "List Card Checkbox",
},
describe("styling/UI", () => {
it("Should return screen reader text", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
},
});
// Assert
const paragraph = wrapper.find("span.sr-only");
expect(paragraph.text()).toEqual("Screen Reader Only Text");
});
// Assert
const label = wrapper.find("label");
it("Should return text alignment class", async () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
expect(paragraph.attributes("class")).toContain("text-center");
});
// Assert
const paragraph = wrapper.find("span.sr-only");
it("Should return aria-required state", async () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
textPosition: "text-center",
},
expect(input.attributes()["aria-required"]).toEqual("true");
});
// Assert
const paragraph = wrapper.find("span.m-0");
it("is cash or insurance button => has 'radio-fancy' class", () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
propsData: {
isRequired: true,
},
expect(label.classes()).toContain("radio-fancy");
});
// Assert
const input = wrapper.find("input");
test("has buttonLabel => displays buttonLabel", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
},
},
});
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("Should return loader enabled true", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
selectingInitiatesLoad: true,
},
// Assert
const content = wrapper.find(".list-button-horizontal-content");
expect(content.isVisible()).toBe(true);
expect(content.text()).toContain("Surprise!");
});
// Assert
test("has buttonLabelSubCopy => displays buttonLabelSubCopy", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
},
},
});
const label = wrapper.find("label");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.triggerButton();
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
const content = wrapper.find(".list-button-horizontal-content");
expect(content.isVisible()).toBe(true);
expect(content.text()).toContain("Super duper surprise :)");
});
// 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");
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.triggerButton();
await nextTick();
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("blue");
});
it("Should return loader position", async () => {
// Act
const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
loaderPosition: "right",
selectingInitiatesLoad: true,
},
// Assert
const content = wrapper.find(".list-button-horizontal-content");
const screenReaderOnlyText = wrapper.find(".sr-only");
expect(content.isVisible()).toBe(true);
expect(screenReaderOnlyText.exists()).toBe(true);
expect(screenReaderOnlyText.text()).toContain("Tests are fun!");
});
// 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',
{ 'radio-fancy': isCashOrInsurance },
]"
@buttonClicked="handleAnswerChange">
@inputButtonClicked="handleAnswerChange">
<div
class="list-button-horizontal-content d-flex flex-column justify-content-center p-3">
<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 { nextTick } from "vue";
import { GaActions } from "@/constants/analytics";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
// TODO KO
describe.skip("list-button.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isMultiSelect: true,
},
describe("list-button.vue", () => {
describe("loader", () => {
it("selectingInitiatesLoad is true and answer is changed => show the loader", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
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
const input = wrapper.find("input");
it("Should return loader color", async () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isMultiSelect: false,
},
// Assert
const loader = wrapper.findComponent({ name: "loader" });
expect(loader.attributes("class")).toContain("blue");
});
// Assert
const input = wrapper.find("input");
it("Should return loader position", async () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
buttonID: "List Card Checkbox",
},
describe("baseInputButton checks", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: true,
},
},
});
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("checkbox");
});
// Assert
const label = wrapper.find("label");
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isMultiSelect: false,
},
},
});
expect(label.attributes().for).toEqual("List Card Checkbox");
});
it("Should return screen reader text", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
screenReaderOnlyText: "Screen Reader Only Text",
},
// Assert
const input = wrapper.find("input");
expect(input.attributes().type).toEqual("radio");
});
// Assert
const paragraph = wrapper.find("span.sr-only");
it("Should emit button value on click", async () => {
// 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 () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
textPosition: "text-center",
},
describe("styling/UI", () => {
test("has buttonLabel => displays buttonLabel", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
},
},
});
// Assert
const content = wrapper.find(".list-button-content");
expect(content.isVisible()).toBe(true);
expect(content.text()).toContain("Surprise!");
});
// Assert
const paragraph = wrapper.find("span.m-0");
test("has buttonLabelSubCopy => displays buttonLabelSubCopy", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
},
},
});
expect(paragraph.attributes("class")).toContain("text-center");
});
it("Should return aria-required state", async () => {
// Act
const wrapper = shallowMount(listButton, {
propsData: {
isRequired: true,
},
// Assert
const content = wrapper.find(".list-button-content");
expect(content.isVisible()).toBe(true);
expect(content.text()).toContain("Super duper surprise :)");
});
// Assert
const input = wrapper.find("input");
test("has screenReaderOnlyText => displays screenReaderOnlyText", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
buttonLabel: "Surprise!",
buttonLabelSubCopy: "Super duper surprise :)",
screenReaderOnlyText: "Tests are fun!",
},
},
});
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("Should return loader enabled true", async () => {
// Act
const wrapper = shallowMount(listButton, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: {
selectingInitiatesLoad: true,
},
// Assert
const content = wrapper.find(".list-button-content");
const screenReaderOnlyText = wrapper.find(".sr-only");
expect(content.isVisible()).toBe(true);
expect(screenReaderOnlyText.exists()).toBe(true);
expect(screenReaderOnlyText.text()).toContain("Tests are fun!");
});
// Assert
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.triggerButton();
it("Should return screen reader text", async () => {
// Act
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(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,
},
expect(paragraph.text()).toEqual("Screen Reader Only Text");
});
// Assert
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.triggerButton();
await nextTick();
it("Should return text alignment class", async () => {
// Act
const { wrapper } = setupMocks({
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");
});
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,
},
expect(paragraph.attributes("class")).toContain("text-center");
});
// Assert
wrapper.vm.handleCheckChange = jest.fn();
wrapper.vm.triggerButton();
it("Should return aria-required state", async () => {
// Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isRequired: true,
},
},
});
await nextTick();
// Assert
const input = wrapper.find("input");
const loader = wrapper.find("loader-stub");
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'
},
expect(input.attributes()["aria-required"]).toEqual("true");
});
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
v-bind="$props"
buttonWrapperClasses="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
@buttonClicked="handleAnswerChange">
@inputButtonClicked="handleAnswerChange">
<div
:aria-label="buttonLabel"
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",
groupName: "Checkbox 1",
buttonImage: "windshield-damage.svg",
value: "test value",
},
});
@ -32,6 +33,7 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1",
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
value: "test value",
},
});
@ -51,6 +53,7 @@ describe("list-card.vue", () => {
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test",
value: "test value",
},
});
@ -70,6 +73,7 @@ describe("list-card.vue", () => {
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test",
value: "test value",
},
});
@ -89,6 +93,7 @@ describe("list-card.vue", () => {
groupName: "radio 1",
buttonImage: "windshield-damage.svg",
isRequired: true,
value: "test value",
},
});
@ -110,6 +115,7 @@ describe("list-card.vue", () => {
isRequired: true,
isWide: true,
buttonLabelSubCopy: "",
value: "test value",
},
});
@ -134,6 +140,7 @@ describe("list-card.vue", () => {
isRequired: true,
isWide: true,
buttonLabelSubCopy: "Button Subcopy",
value: "test value",
},
});
@ -159,6 +166,7 @@ describe("list-card.vue", () => {
buttonImage: "windshield-damage.svg",
isRequired: true,
isWide: false,
value: "test value",
},
});

View file

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

View file

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

View file

@ -3,7 +3,7 @@
v-bind="$props"
buttonWrapperClasses="ui-radio form-check"
inputClasses="form-check-input"
@buttonClicked="handleAnswerChange">
@inputButtonClicked="handleAnswerChange">
<div class="d-flex align-items-start form-check-label">
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{