Merge pull request #13 from Safelite/feature/SSR-76

Feature/ssr 76
This commit is contained in:
Kulbhushan Kaushik 2022-10-21 19:51:02 -04:00 committed by GitHub
commit 5769d83257
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 537 additions and 15192 deletions

15240
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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"
},

View 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

View 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;
});
});

View 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 = "&NoBreak;";
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>

View file

@ -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 };

View 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;
}
}

View 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);
});
});

View file

@ -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>

View file

@ -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");