commit
5769d83257
10 changed files with 537 additions and 15192 deletions
15240
package-lock.json
generated
15240
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -13,8 +13,10 @@
|
|||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.1.1",
|
||||
"jest-junit": "^13.0.0",
|
||||
"maska": "^1.5.0",
|
||||
"pinia": "^2.0.22",
|
||||
"pinia-plugin-persistedstate": "^2.2.0",
|
||||
"vee-validate": "^4.7.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3"
|
||||
},
|
||||
|
|
|
|||
3
src/assets/icons/location-pin.svg
Normal file
3
src/assets/icons/location-pin.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.8 16" xml:space="preserve">
|
||||
<path d="M6.4 0c-.85 0-1.69.18-2.47.52-.78.34-1.49.84-2.07 1.47A7.026 7.026 0 0 0 0 6.75c0 1.77.67 3.48 1.86 4.77l4.02 4.26c.07.07.15.13.23.16.1.04.19.06.29.06.1 0 .19-.02.28-.06.09-.04.17-.09.23-.16l4.02-4.26a6.884 6.884 0 0 0 1.87-4.77c.01-1.78-.66-3.49-1.87-4.77A6.166 6.166 0 0 0 8.86.51C8.09.18 7.25 0 6.4 0zm0 9.13c-.47 0-.93-.14-1.32-.41-.39-.27-.7-.64-.88-1.09-.19-.44-.23-.93-.14-1.4.09-.47.32-.9.65-1.24.33-.34.76-.57 1.22-.66.46-.09.94-.05 1.38.14.44.18.81.49 1.07.89s.4.87.4 1.35c0 .64-.25 1.26-.7 1.71-.44.46-1.05.71-1.68.71z" fill="#8e9292"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 650 B |
170
src/common-components/textbox-question/textbox-question.spec.js
Normal file
170
src/common-components/textbox-question/textbox-question.spec.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import textboxQuestion from "./textbox-question";
|
||||
|
||||
// Mock CMS content
|
||||
const questionText = "Question Text";
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn().mockImplementation(()=> {
|
||||
return questionText;
|
||||
})
|
||||
}
|
||||
}
|
||||
const maska = jest.fn();
|
||||
|
||||
describe("textboxQuestion.vue", () => {
|
||||
|
||||
it("Should render a text input", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
wrapper.getCmsContent = jest.fn();
|
||||
|
||||
// Act
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Assert
|
||||
expect(input.exists()).toBe(true);
|
||||
|
||||
});
|
||||
|
||||
it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
// Act
|
||||
const label = wrapper.find("label");
|
||||
|
||||
// Assert
|
||||
expect(label.text()).toContain(questionText);
|
||||
|
||||
});
|
||||
|
||||
it("Should return input id as the id of the input field", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
propsData: {
|
||||
inputId: "input ID",
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
// Act
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Assert
|
||||
expect(input.attributes().id).toEqual("input ID");
|
||||
|
||||
});
|
||||
|
||||
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
|
||||
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
// Act
|
||||
const label = wrapper.find("label");
|
||||
|
||||
// Assert
|
||||
expect(label.attributes("aria-label")).toContain(questionText);
|
||||
|
||||
});
|
||||
|
||||
it("Should return aria-disabled state as disabled", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
propsData: {
|
||||
isDisabled: true,
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
// Assert
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Expect
|
||||
expect(input.attributes("aria-disabled")).toEqual("true");
|
||||
|
||||
});
|
||||
|
||||
it("Should emit new value when modelValue is changed", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
propsData: {
|
||||
modelValue: "val",
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
await wrapper.find("input").setValue("val2");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted()).toHaveProperty('change')
|
||||
|
||||
});
|
||||
|
||||
it("Should call this.handleChange with new value when the value is changed and the new value is valid", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
}
|
||||
},
|
||||
propsData: {
|
||||
options: {},
|
||||
modelValue: "foo",
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
||||
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
|
||||
wrapper.vm.validate = jest.fn().mockImplementation(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.$options.watch.value.call(wrapper.vm, "bar");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.handleChange).toHaveBeenCalled;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
189
src/common-components/textbox-question/textbox-question.vue
Normal file
189
src/common-components/textbox-question/textbox-question.vue
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
|
||||
<input
|
||||
v-model.trim="value"
|
||||
v-maska="mask"
|
||||
:type="type"
|
||||
class="form-control"
|
||||
:ref="inputId"
|
||||
:id="inputId"
|
||||
:name="inputId"
|
||||
:placeholder="placeholderText"
|
||||
:aria-disabled="isDisabled"
|
||||
:disabled="isDisabled"
|
||||
:aria-required="isRequired"
|
||||
:aria-label="questionText"
|
||||
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
|
||||
:validationRules="validationRules"
|
||||
@change="handleChange"
|
||||
@blur="handleChange"
|
||||
:maxlength="maxLength ? maxLength : '999'"
|
||||
@focus="$emit('focus', $event.target.value)"
|
||||
/>
|
||||
<div v-show="errorMessage" class="row my-2 form-test-error">
|
||||
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useField, validate } from "vee-validate"
|
||||
|
||||
export default {
|
||||
name: "textbox-question",
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
placeholderText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
modelValue: String,
|
||||
inputId: String,
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
hasIcon: Boolean, // If input has an icon
|
||||
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
|
||||
hasError: Boolean,
|
||||
mask: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
maxLength: String,
|
||||
},
|
||||
setup(props) {
|
||||
const propsClone = Object.assign({}, props);
|
||||
const modelValue = propsClone.modelValue;
|
||||
let initialValue;
|
||||
|
||||
switch (typeof modelValue) {
|
||||
case "number":
|
||||
initialValue = modelValue;
|
||||
break;
|
||||
default:
|
||||
initialValue = (modelValue && modelValue.length > 0) ? modelValue : "";
|
||||
break;
|
||||
}
|
||||
|
||||
const fieldOptions = {
|
||||
type: "text",
|
||||
value: modelValue,
|
||||
initialValue: initialValue,
|
||||
};
|
||||
|
||||
const { errorMessage, handleBlur, handleChange, meta, validate, errors } =
|
||||
useField(props.inputId, props.validationRules, fieldOptions);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
validate,
|
||||
meta,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
value: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
labelText: {
|
||||
get: function () {
|
||||
const noBreakChar = "⁠";
|
||||
var questionText = "";
|
||||
if (this.disableAutoFill) {
|
||||
var words = this.questionText.toString().split(/[ ]+/);
|
||||
words.forEach(function (word) {
|
||||
const position = 1;
|
||||
word = [
|
||||
word.toString().slice(0, position),
|
||||
noBreakChar,
|
||||
word.toString().slice(position),
|
||||
].join("");
|
||||
questionText += `${word} `;
|
||||
});
|
||||
|
||||
questionText = questionText.trimEnd();
|
||||
} else {
|
||||
questionText = this.questionText.toString();
|
||||
}
|
||||
|
||||
return questionText;
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async value(newValue) {
|
||||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||
if (result.valid) {
|
||||
this.handleChange(newValue); // trigger full validation on this field only
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.textbox-question {
|
||||
label {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
input {
|
||||
&.has-icon {
|
||||
background-image: url(~@/assets/icons/location-pin.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: .75rem 50%;
|
||||
background-size: 1rem auto;
|
||||
padding: 0 0.75rem 0 2.5rem;
|
||||
&.icon-right {
|
||||
background-position: calc(100% - .75rem) 50%;
|
||||
padding: 0 2.5rem 0 0.75rem
|
||||
}
|
||||
}
|
||||
}
|
||||
.form-label {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.form-control {
|
||||
border: 1px solid $gray-500;
|
||||
border-radius: .5rem;
|
||||
min-height: 3rem;
|
||||
padding: 12px 16px;
|
||||
&::placeholder {
|
||||
color: $gray-500;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
background-color: $gray-100;
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 4px transparent;
|
||||
border: 1px solid $gray-500;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border: 1px solid $gray-500;
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
}
|
||||
p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -24,6 +24,12 @@ const errorMessages = {
|
|||
VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
|
||||
OPTION_REQUIRED: "Please select an option",
|
||||
VEHICLE_REQUIRED: "Please select a vehicle",
|
||||
POLICY_NUMBER_REQUIRED: "Please enter policy number",
|
||||
PHONE_NUMBER_REQUIRED: "Please enter phone number",
|
||||
POLICY_ZIP_REQUIRED: "Please enter policy zip",
|
||||
POLICY_ZIP_FORMAT: "Please enter a valid policy ZIP",
|
||||
LOSS_CAUSE_REQUIRED: "Please enter loss cause",
|
||||
LOSS_CITY_REQUIRED: "Please enter loss city"
|
||||
};
|
||||
|
||||
export { errorMessages };
|
||||
25
src/helpers/validation-rules.js
Normal file
25
src/helpers/validation-rules.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export function required(errorMessage) {
|
||||
return (value) => {
|
||||
if (!value || value.length < 1) {
|
||||
return errorMessage;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function regex(expression, errorMessage) {
|
||||
return (value) => {
|
||||
// Field is empty, should pass
|
||||
if (!value || !value.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if email
|
||||
if (!expression.test(value)) {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
65
src/helpers/validation-rules.spec.js
Normal file
65
src/helpers/validation-rules.spec.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { required } from "@/helpers/validation-rules";
|
||||
import { regex } from "@/helpers/validation-rules";
|
||||
|
||||
describe("validation-rules.vue", () => {
|
||||
test("required rules should return error if value missing", () => {
|
||||
|
||||
//Arrange
|
||||
const testFn = required("an error");
|
||||
|
||||
//Act
|
||||
const testResponse = testFn();
|
||||
|
||||
//Assert
|
||||
expect(testResponse).toBe("an error");
|
||||
});
|
||||
|
||||
test("required rules should return true if value present", () => {
|
||||
|
||||
//Arrange
|
||||
const testFn = required("an error");
|
||||
|
||||
//Act
|
||||
const testResponse = testFn('some value');
|
||||
|
||||
//Assert
|
||||
expect(testResponse).toBe(true);
|
||||
});
|
||||
|
||||
test("regex rules should return true if value is not present", () => {
|
||||
|
||||
//Arrange
|
||||
const testFn = regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, "an error"); // Using Zip regex
|
||||
|
||||
//Act
|
||||
const testResponse = testFn();
|
||||
|
||||
//Assert
|
||||
expect(testResponse).toBe(true);
|
||||
});
|
||||
|
||||
test("regex rules should return false if value is present but does not match regular expression", () => {
|
||||
|
||||
//Arrange
|
||||
const testFn = regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, "an error"); // Using Zip regex
|
||||
|
||||
//Act
|
||||
const testResponse = testFn('4321'); // needs to be 5 numbers
|
||||
|
||||
//Assert
|
||||
expect(testResponse).toBe("an error");
|
||||
});
|
||||
|
||||
test("regex rules should return true if value is present and does match regular expression", () => {
|
||||
|
||||
//Arrange
|
||||
const testFn = regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, "an error"); // Using Zip regex
|
||||
|
||||
//Act
|
||||
const testResponse = testFn('43213'); // needs to be 5 numbers
|
||||
|
||||
//Assert
|
||||
expect(testResponse).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
<input type="text" v-model="year"/>
|
||||
<span>{{returnVehicleYear}}</span>
|
||||
<button @click="updateVehicleYear(year)">Add</button>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<textboxQuestion cmsWidgetName="PolicyZipQuestionWidget" v-model="policyZipCode" inputId="policyZipCode" mask="#####" validationRules="policy-zip-required|policy-zip-format" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -14,10 +18,18 @@
|
|||
import navButton from '@/common-components/nav-button/nav-button.vue';
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { regex } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
||||
defineRule("policy-zip-required", required(errorMessages.POLICY_ZIP_REQUIRED));
|
||||
defineRule("policy-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.POLICY_ZIP_FORMAT));
|
||||
|
||||
export default {
|
||||
name: 'vehicle-year',
|
||||
components: { navButton },
|
||||
|
||||
async beforeRouteEnter(to, from, next)
|
||||
{
|
||||
// Call APIs
|
||||
|
|
@ -34,10 +46,17 @@
|
|||
//use resultMap to populate layout content.
|
||||
let resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
});
|
||||
},
|
||||
data()
|
||||
{
|
||||
return {
|
||||
policyZipCode: ""
|
||||
}
|
||||
},
|
||||
methods:
|
||||
{
|
||||
updateVehicleYear(item)
|
||||
|
|
@ -51,6 +70,10 @@
|
|||
{
|
||||
return this.mainStore.order.vehicle.year;
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
navButton,
|
||||
textboxQuestion
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -6,6 +6,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
|||
import { useMainStore } from '@/store';
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { createPinia } from 'pinia';
|
||||
import Maska from "maska";
|
||||
|
||||
const vueApp = createApp(App);
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ useMainStore().populateInitialState();
|
|||
|
||||
// Additional Vue items to setup
|
||||
vueApp.mixin(baseMixin);
|
||||
vueApp.use(Maska);
|
||||
vueApp.use(router);
|
||||
vueApp.mount("#app");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue