Merge branch 'develop' into feature/SSR-233

This commit is contained in:
Kulbhushan Kaushik 2023-01-25 13:04:44 -05:00
commit 4341ed913b
13 changed files with 549 additions and 452 deletions

View file

@ -1,76 +1,79 @@
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
<template>
<div
:class="
isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'
">
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<span class="fw-bold w-100">{{ questionText }}</span>
</div>
<div
:class="
isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'
">
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<span class="fw-bold w-100">{{ questionText }}</span>
</div>
<div class="w-100 d-flex justify-content-center">
<fieldset
class="w-100"
:aria-required="isRequired"
:class="getFieldSetClasses"
:role="isMultiSelect ? 'group' : 'radiogroup'"
:aria-labelledby="formatString(groupName)">
<legend
class="sr-only"
:data-focus-target="formatString(groupName)"
tabindex="-1"
:id="formatString(groupName)">
{{ questionText }}
{{
isMultiSelect && answers && answers.length > 1
? "Select one or more options below."
: "Select an option below."
}}
</legend>
<div :class="getComponentLoopWrapperClasses">
<div
:class="getComponentWrapperClasses"
v-for="answer in buttonsInfo"
:key="answer.value ? answer.value : answer">
<component
:is="buttonTypeString"
:buttonLabel="answer.buttonLabel"
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
:buttonBodyCopy="answer.buttonBodyCopy"
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
:buttonFooterCopy="answer.buttonFooterCopy"
:buttonImage="answer.buttonImage"
:buttonImageId="answer.buttonImageId"
:groupName="answer.groupName"
:isMultiSelect="isMultiSelect"
:value="answer.value"
:selectingInitiatesLoad="selectingInitiatesLoad"
:isWide="isWide"
:validationRules="validationRules"
:textPosition="textPosition"
:additionalButtonStyling="additionalButtonStyling"
:lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
:suppressError="suppressError"
v-model="selectedValues" />
<!-- For nested questions -->
<transition name="fade" mode="out-in">
<div
v-if="
typeof selectedValues == 'string' &&
selectedValues == answer.value
">
<slot></slot>
</div>
</transition>
</div>
</div>
</fieldset>
</div>
<div class="row form-test-error mt-1">
<error-message :name="formatString(groupName)" v-if="!suppressError"></error-message>
</div>
</div>
<div class="w-100 d-flex justify-content-center">
<fieldset
class="w-100"
:aria-required="isRequired"
:class="getFieldSetClasses"
:role="isMultiSelect ? 'group' : 'radiogroup'"
:aria-labelledby="formatString(groupName)">
<legend
class="sr-only"
:data-focus-target="formatString(groupName)"
tabindex="-1"
:id="formatString(groupName)">
{{ questionText }}
{{
isMultiSelect && answers && answers.length > 1
? "Select one or more options below."
: "Select an option below."
}}
</legend>
<div :class="getComponentLoopWrapperClasses">
<div
:class="getComponentWrapperClasses"
v-for="answer in buttonsInfo"
:key="answer.value ? answer.value : answer">
<component
:is="buttonTypeString"
:buttonLabel="answer.buttonLabel"
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
:buttonBodyCopy="answer.buttonBodyCopy"
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
:buttonFooterCopy="answer.buttonFooterCopy"
:buttonImage="answer.buttonImage"
:buttonImageId="answer.buttonImageId"
:groupName="answer.groupName"
:isMultiSelect="isMultiSelect"
:value="answer.value"
:selectingInitiatesLoad="selectingInitiatesLoad"
:isWide="isWide"
:validationRules="validationRules"
:textPosition="textPosition"
:additionalButtonStyling="additionalButtonStyling"
:lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
:suppressError="suppressError"
v-model="selectedValues" />
<!-- For nested questions -->
<transition name="fade" mode="out-in">
<div
v-if="
typeof selectedValues == 'string' &&
selectedValues == answer.value
">
<slot></slot>
</div>
</transition>
</div>
</div>
</fieldset>
</div>
<div class="row form-test-error mt-1">
<error-message
class="small"
:name="formatString(groupName)"
v-if="!suppressError"></error-message>
</div>
</div>
</template>
<script>
@ -81,195 +84,193 @@ import radio from "@/ux-components/radio/radio";
import { ErrorMessage } from "vee-validate";
export default {
name: "buttonQuestion",
props: {
buttonTypeString: {
type: String,
default: "listButton",
},
buttonTypeObject: {
type: Object,
default: null,
},
isMultiSelect: Boolean,
groupName: String,
questionText: String,
answers: Array,
textPosition: {
type: String,
default: "text-center",
},
selectingInitiatesLoad: Boolean,
loaderColor: {
type: String,
default: "blue",
},
loaderPosition: {
type: String,
default: "right",
},
isRequired: Boolean,
isOverflowScrollable: Boolean,
isWide: Boolean,
isCashOrInsurance: Boolean,
modelValue: [Array, Number, String],
value: [Number, String],
validationRules: String,
suppressError: Boolean,
useTextForValue: Boolean,
valueToLogType: String,
additionalButtonStyling: String,
},
beforeMount() {
if (this.buttonTypeObject) {
this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
}
},
data() {
return {
lastValuePushedToGa: null,
};
},
computed: {
getFieldSetClasses() {
if (this.isOverflowScrollable) {
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
name: "buttonQuestion",
props: {
buttonTypeString: {
type: String,
default: "listButton",
},
buttonTypeObject: {
type: Object,
default: null,
},
isMultiSelect: Boolean,
groupName: String,
questionText: String,
answers: Array,
textPosition: {
type: String,
default: "text-center",
},
selectingInitiatesLoad: Boolean,
loaderColor: {
type: String,
default: "blue",
},
loaderPosition: {
type: String,
default: "right",
},
isRequired: Boolean,
isOverflowScrollable: Boolean,
isWide: Boolean,
isCashOrInsurance: Boolean,
modelValue: [Array, Number, String],
value: [Number, String],
validationRules: String,
suppressError: Boolean,
useTextForValue: Boolean,
valueToLogType: String,
additionalButtonStyling: String,
},
beforeMount() {
if (this.buttonTypeObject) {
this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
}
else if (this.buttonTypeString === "listCard") {
return "w-100";
}
else {
return "";
}
},
getComponentLoopWrapperClasses() {
let classes;
switch (this.buttonTypeString) {
case "listButton":
classes = "w-100";
break;
case "listButtonHorizontal":
classes = "d-flex flex-row p-0";
break;
case "listCard":
classes = "row g-2 justify-content-center";
if (this.isWide) {
classes += " flex-column";
}
break;
case "radio":
classes = "ui-radio d-flex";
break;
case "servicePackageRadio":
classes = "package-main";
break;
}
return classes;
},
getComponentWrapperClasses() {
let classes = "";
},
data() {
return {
lastValuePushedToGa: null,
};
},
computed: {
getFieldSetClasses() {
if (this.isOverflowScrollable) {
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
} else if (this.buttonTypeString == "listCard") {
return "w-100";
} else {
return "";
}
},
getComponentLoopWrapperClasses() {
let classes;
switch (this.buttonTypeString) {
case "listButton":
classes = "w-100";
break;
case "listButtonHorizontal":
classes = "d-flex flex-row p-0";
break;
case "listCard":
classes = "row g-2 justify-content-center";
if (this.isWide) {
classes += " flex-column";
}
break;
case "radio":
classes = "ui-radio d-flex";
break;
case "servicePackageRadio":
classes = "package-main";
break;
}
return classes;
},
getComponentWrapperClasses() {
let classes = "";
classes += this.isWide ? "col-12" : "col";
classes += this.isWide ? "col-12" : "col";
if (this.buttonTypeString == "radio") {
classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper";
}
if (this.buttonTypeString == "radio") {
classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper";
}
return classes;
},
buttonsInfo() {
return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
altText: answer.altText ?? (answer.Name ? answer.Name : answer),
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId,
groupName: this.formatString(this.groupName),
value:
answer.value ??
(this.useTextForValue && answer.Text ? answer.Text : answer.Name) ??
(typeof answer !== "object" ? answer : null),
}));
},
selectedValues: {
get() {
return this.modelValue;
},
set(selectedAnswers) {
this.$emit("update:modelValue", selectedAnswers);
},
},
},
methods: {
formatString(str) {
return String(str).replaceAll(" ", "-");
},
setLastValuePushedToGa(lastValuePushedToGa) {
this.lastValuePushedToGa = lastValuePushedToGa;
},
},
components: {
listButton,
listButtonHorizontal,
listCard,
ErrorMessage,
radio,
},
return classes;
},
buttonsInfo() {
return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
altText: answer.altText ?? (answer.Name ? answer.Name : answer),
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId,
groupName: this.formatString(this.groupName),
value:
answer.value ??
(this.useTextForValue && answer.Text ? answer.Text : answer.Name) ??
(typeof answer !== "object" ? answer : null),
}));
},
selectedValues: {
get() {
return this.modelValue;
},
set(selectedAnswers) {
this.$emit("update:modelValue", selectedAnswers);
},
},
},
methods: {
formatString(str) {
return String(str).replaceAll(" ", "-");
},
setLastValuePushedToGa(lastValuePushedToGa) {
this.lastValuePushedToGa = lastValuePushedToGa;
},
},
components: {
listButton,
listButtonHorizontal,
listCard,
ErrorMessage,
radio,
},
};
</script>
<style lang="scss">
.button-question-overflow {
height: calc(100vh - 274px);
height: calc(100vh - 274px);
.overflow-scroll {
// Height will be determined by overall height of content above list
height: calc(100% - 314px);
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
}
.overflow-scroll {
// Height will be determined by overall height of content above list
height: calc(100% - 314px);
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
}
}
.button-question {
color: $black;
color: $black;
.radio-button-container {
&:not(:last-child) {
padding-bottom: map-get($spacers, 2);
}
}
.radio-button-container {
&:not(:last-child) {
padding-bottom: map-get($spacers, 2);
}
}
}
.question-text {
margin-top: 1.5rem;
margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
margin-top: 1.5rem;
margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
& > span {
text-align: center;
}
& > span {
text-align: center;
}
}
.vehicle-parts {
.question-text {
span {
font-size: 0.875rem;
text-align: left;
margin: 0 0 0.5rem 0;
}
}
.question-text {
margin: 0;
}
fieldset {
.ui-radio {
margin: 0;
}
}
.question-text {
span {
font-size: 0.875rem;
text-align: left;
margin: 0 0 0.5rem 0;
}
}
.question-text {
margin: 0;
}
fieldset {
.ui-radio {
margin: 0;
}
}
}
.welcome {

View file

@ -1,159 +1,164 @@
<template>
<div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<select v-model="selectedOption"
class="form-select"
:id="inputId"
:name="inputId"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:validationRules="validationRules"
:placeHolderText="placeHolderText"
>
<option v-if="placeHolderText" value="" selected>{{ placeHolderText }}</option>
<option v-for="(value, name, index) in options" :value="name" :key="index">
{{ value }}
</option>
</select>
<div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
</div>
<div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label
:for="inputId"
:aria-label="questionText"
class="form-label"
v-html="labelText"></label>
<select
v-model="selectedOption"
class="form-select"
:id="inputId"
:name="inputId"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:validationRules="validationRules"
:placeHolderText="placeHolderText">
<option v-if="placeHolderText" value="" selected>{{ placeHolderText }}</option>
<option v-for="(value, name, index) in options" :value="name" :key="index">
{{ value }}
</option>
</select>
<div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
</div>
</template>
<script>
import { useField } from "vee-validate";
export default {
name: "dropdown-question",
props: {
modelValue: String,
inputId: String,
options: {
type: Object,
required: true
},
isDisabled: Boolean,
isRequired: Boolean,
disableAutoFill: Boolean,
validationRules: String,
cmsWidgetName: String,
hasError: Boolean,
errors: Array,
placeHolderText: String
},
setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
name: "dropdown-question",
props: {
modelValue: String,
inputId: String,
options: {
type: Object,
required: true,
},
isDisabled: Boolean,
isRequired: Boolean,
disableAutoFill: Boolean,
validationRules: String,
cmsWidgetName: String,
hasError: Boolean,
errors: Array,
placeHolderText: 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;
}
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = (modelValue && modelValue.length > 0) ? modelValue : "";
break;
}
const fieldOptions = {
type: "select",
value: props.modelValue,
initialValue: initialValue,
};
const fieldOptions = {
type: "select",
value: props.modelValue,
initialValue: initialValue,
};
const {
errorMessage,
handleBlur,
handleChange,
meta,
errors
} = useField(props.inputId, props.validationRules, fieldOptions);
const {errorMessage, handleBlur, handleChange, meta, errors } = useField(
props.inputId,
props.validationRules,
fieldOptions
);
return {
errorMessage,
handleBlur,
handleChange,
meta,
errors
};
},
computed: {
questionText(){
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
selectedOption: {
get: function() {
return !this.modelValue ? "" : 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 {
errorMessage,
handleBlur,
handleChange,
meta,
errors
};
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
selectedOption: {
get: function () {
return !this.modelValue ? "" : 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} `;
});
return questionText;
}
}
},
watch: {
selectedOption(newValue) {
this.handleChange(newValue);
}
}
questionText = questionText.trimEnd();
} else {
questionText = this.questionText.toString();
}
return questionText;
},
},
},
watch: {
selectedOption(newValue) {
this.handleChange(newValue);
},
},
};
</script>
<style lang="scss">
.dropdown-question {
label {
color: $black;
font-weight: 500;
}
.form-label {
margin-bottom: .25rem;
}
.form-select {
color: $gray-600;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e");
border: 1px solid $gray-500;
border-radius: .5rem;
min-height: 3rem;
&:focus,
&:focus-visible {
box-shadow: 0 0 0 2.5px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
filter: grayscale(100%);
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid $gray-500;
box-shadow: 0 0 0 4px $blue-300;
}
}
label {
color: $black;
font-weight: 500;
}
.form-label {
margin-bottom: 0.25rem;
}
.form-select {
color: $gray-600;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e");
border: 1px solid $gray-500;
border-radius: 0.5rem;
min-height: 3rem;
&:focus,
&:focus-visible {
box-shadow: 0 0 0 2.5px $blue;
}
&:disabled,
&.disabled {
background-color: $gray-100;
filter: grayscale(100%);
&:hover {
box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500;
}
}
&:hover {
border: 1px solid $gray-500;
box-shadow: 0 0 0 4px $blue-300;
}
}
}
</style>

View file

@ -3,7 +3,7 @@
<div
class="modal fade modal-component"
v-on="{ 'hidden.bs.modal': resetButtonStyle }"
:id="this.cmsWidgetName"
:id="cmsWidgetName"
tabindex="-1"
aria-labelledby="ModalComponentLabel"
aria-hidden="true">
@ -17,13 +17,18 @@
aria-label="Close"></button>
</div>
<div class="modal-body ps-4 pe-4 pt-5 pb-4">
<img :src="this.ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<h5 class="mb-4" v-html="this.ModalHeadline"></h5>
<p class="fw-bold mb-2 subheader-text" v-html="this.ModalSubheadertext"></p>
<p class="mb-0" v-html="this.ModalBodyText"></p>
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<h5 class="mb-4" v-html="ModalHeadline"></h5>
<p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
<p class="mb-0" v-html="ModalBodyText"></p>
<p
class="my-4 caption modal-sub-body"
v-if="ModalSubBodyText"
v-html="ModalSubBodyText"></p>
</div>
<div class="modal-footer px-5 py-4">
<buttonMain
isPrimary
class="w-100"
ref="buttonMain"
suppressLoader
@ -54,6 +59,9 @@ export default {
ModalBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
ModalSubBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
},
ModalImage() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
@ -97,6 +105,9 @@ export default {
box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25);
border-radius: 1.5rem 1.5rem 0 0;
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
@ -130,4 +141,4 @@ body {
}
}
}
</style>
</style>

View file

@ -1,6 +1,6 @@
<template>
<div
class="text-block d-flex w-100 mt-2"
class="text-block w-100 mt-2"
:class="[justifyText, typeStyle, fontWeight]"
v-html="this.TextBlockCopy"></div>
</template>

View file

@ -44,8 +44,8 @@
<button v-if="includeSelectIcon" type="submit" data-bs-toggle="modal"
:data-bs-target="'#' + this.cmsWidgetName" aria-label="Select button" />
</div>
<div v-show="errorMessage" class="row my-2 form-test-error mb-0">
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
<div v-show="errorMessage" class="row my-1 form-test-error">
<span class="d-inline-flex small mt-0" role="alert">{{ errorMessage }}</span>
</div>
</div>
</template>

View file

@ -15,7 +15,7 @@ describe("address-questions.vue", () => {
});
describe("initial state", () => {
test("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => {
test("Should hide addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => {
// Arrange
const { wrapper } = setupMocks({});
@ -24,17 +24,16 @@ describe("address-questions.vue", () => {
const city = wrapper.findComponent({ ref: "city" });
const state = wrapper.findComponent({ ref: "state" });
const zipCode = wrapper.findComponent({ ref: "zipCode" });
const streetAddress2 = wrapper.findComponent({ ref: "streetAddress2"});
// Assert
expect(streetAddress.exists()).toBe(true);
expect(city.exists()).toBe(true);
expect(state.exists()).toBe(true);
expect(zipCode.exists()).toBe(true);
expect(city.exists()).toBe(false);
expect(state.exists()).toBe(false);
expect(zipCode.exists()).toBe(false);
expect(streetAddress2.exists()).toBe(false);
});
});
describe("happy paths", () => {
test("full street address is passed in => address fields are displayed", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
@ -45,6 +44,7 @@ describe("address-questions.vue", () => {
state: "OH",
zipCode: "12312",
},
includeStreetAddress2: true,
},
});
@ -54,14 +54,40 @@ describe("address-questions.vue", () => {
const cityField = wrapper.findComponent({ ref: "city" });
const stateField = wrapper.findComponent({ ref: "state" });
const zipField = wrapper.findComponent({ ref: "zipCode" });
const streetAddress2 = wrapper.findComponent({ ref: "streetAddress2"});
expect(cityField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy();
expect(stateField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy();
expect(zipField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy();
expect(streetAddress2.isVisible()).toBeTruthy();
});
test("address2 is hidden if includeStreetAddress2 is false", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
props: {
modelValue: {
streetAddress: "12345 Test Road",
city: "Tests",
state: "OH",
zipCode: "12312",
},
includeStreetAddress2: false,
},
});
await wrapper.vm.$nextTick();
// Assert
const streetAddress2 = wrapper.findComponent({ ref: "streetAddress2"});
expect(streetAddress2.exists()).toBe(false);
});
});
describe("happy paths", () => {
test("street address is entered, user chooses good result from autocomplete results => other fields are filled in", async () => {
// Arrange
const { wrapper } = setupMocks({});

View file

@ -14,7 +14,6 @@
cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning"
v-bind:isDismissible="false" />
<div class="row mt-2 mb-4">
<div class="col">
<textboxQuestion
@ -31,55 +30,55 @@
@keydown.enter.prevent />
</div>
</div>
<div class="row mt-2 mb-4" v-if="includeStreetAddress2">
<div class="col">
<textboxQuestion
cmsWidgetName="StreetAddress2QuestionWidget"
v-model="addressModel.streetAddress2"
aria-haspopup=""
inputId="streetAddress2Field"
disableAutoFill
/>
</div>
</div>
<transition name="fade" mode="out-in">
<div class="row mb-4" aria-live="polite">
<div class="col">
<textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required" />
<div v-if="showAllFields">
<div class="row mt-2 mb-4" v-if="includeStreetAddress2">
<div class="col">
<textboxQuestion
ref="streetAddress2"
cmsWidgetName="StreetAddress2QuestionWidget"
v-model="addressModel.streetAddress2"
aria-haspopup=""
inputId="streetAddress2Field"
disableAutoFill
/>
</div>
</div>
<div class="row mb-4" aria-live="polite">
<div class="col">
<textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required" />
</div>
</div>
<div class="row mb-4" aria-live="polite">
<div class="col">
<dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required" />
</div>
<div class="col">
<textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format" />
</div>
</div>
</div>
</transition>
<transition name="fade" mode="out-in">
<div class="row mb-4" aria-live="polite">
<div class="col">
<dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required" />
</div>
<div class="col">
<textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format" />
</div>
</div>
</transition>
</div>
</template>
@ -130,6 +129,7 @@ export default {
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
enterPressed: false,
isAddressWatchActive: false, // Only deep watch the address model when a match was not found
showAllFields: false,
};
},
computed: {
@ -210,6 +210,8 @@ export default {
// If a match has been previously found then do nothing
// OR
// If the user pressed "Enter" then do nothing
this.showAllFields = true;
if (self.matchFound || self.enterPressed) {
return;
}
@ -307,6 +309,7 @@ export default {
},
mounted() {
this.setupAddressLookup();
this.showAllFields = !!this.addressModel.streetAddress;
},
watch: {
matchFound: {

View file

@ -31,6 +31,7 @@
v-model="licensePlate"
isRequired
disableAutoFill
id="license-plate-question-wrapper"
inputId="license-plate-question"
validationRules="license-plate-required" />
<dropdownQuestion
@ -272,3 +273,12 @@ export default {
},
};
</script>
<style>
#license-plate-question-wrapper .form-test-error {
/**
Override extra margin-bottom in the error message in TextboxQuestion
*/
margin-bottom: 0 !important;
}
</style>

View file

@ -1,6 +1,9 @@
/* eslint-env jest */
import { mount } from '@vue/test-utils';
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import VehicleLookup from './vehicle-lookup.vue';
const vinLookupMethodsMockData = {
@ -55,6 +58,12 @@ function setupMocks() {
mixins: [
{
computed: {
GaActions() {
return GaActions;
},
queryStrings() {
return queryStrings;
},
navigationScenarios() {
return navigationScenarios;
},
@ -80,6 +89,7 @@ function setupMocks() {
}),
getFooterInfoBoxHeight: jest.fn(() => 80),
getPageNameByQueryString: jest.fn(() => "vehicle-lookup"),
pushEventToGA: jest.fn(),
},
},
],

View file

@ -2,12 +2,23 @@
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import VinLocationInformationComponent from './vin-location-information.vue';
const mockText = Object.freeze({
HEADER: 'Mock Header',
BODY: 'Mock Body',
});
const mockRoute = {
query: {
issPage: issPageValues.VIN_LOOKUP,
},
};
const mockRouter = {
navigate: jest.fn(),
};
const mountOptions = {
global: {
@ -27,9 +38,22 @@ const mountOptions = {
return '';
}),
pushEventToGA: jest.fn()
},
computed: {
GaActions() {
return GaActions;
},
queryStrings() {
return queryStrings;
},
}
},
],
mocks: {
$route: mockRoute,
$router: mockRouter,
},
},
};

View file

@ -6,6 +6,8 @@ import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { errorMessages } from '@/constants/error-messages';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
import { routerParams } from '@/router/router-params';
import { useMainStore } from '@/store';
@ -134,11 +136,18 @@ const mountOptions = {
getFooterInfoBoxHeight: jest.fn(() => 80),
cssClassNameForCmsWidget: jest.fn(() => 'widget-name-mock-class'),
getPageNameByQueryString: jest.fn(() => ''),
pushEventToGA: jest.fn(),
},
computed: {
GaActions() {
return GaActions;
},
navigationScenarios() {
return navigationScenarios;
},
queryStrings() {
return queryStrings;
},
},
},
],

View file

@ -7,7 +7,7 @@
&.list-card {
&:not(.selected) {
position: relative;
z-index: 4;
z-index: 5;
@include box-shadow-hover($blue-300);
}
}

View file

@ -11,10 +11,6 @@
{{text}}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'dashedUnderline'" @click="handleClick" class="dashed-underline" :href="href">
{{text}}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">
{{text}}
<slot name="after-text"></slot>
@ -33,6 +29,12 @@
},
methods: {
handleClick(event) {
this.pushEventToGA(
this.$route.query[this.queryStrings.ISS_PAGE],
this.GaActions.CLICKED,
this.text,
true
);
this.$emit("click-event");
},
},
@ -41,10 +43,9 @@
<style lang="scss">
a {
display: inline-flex !important;
color: $blue;
text-underline-offset: 0.5em;
line-height: 26px;
text-underline-offset: 5px;
line-height: 2;
padding: 0 0 4px 0;
font-weight: 500;
max-width: fit-content;
@ -75,9 +76,6 @@
text-decoration: underline;
}
}
&.dashed-underline {
text-decoration: underline dashed 1px;
}
}
</style>