Fix textbox-question render failures on mobile-details and schedule
Harden the shared textbox-question component and its mobile flow parents to prevent runtime-1 "undefined is not a function" errors seen in CloudWatch (chunk 5615 / textbox-question.vue). - Coerce modelValue to a string in textbox-question setup and value getter - Ensure v-maska always receives a string mask - Guard the value watcher when handleChange is unavailable - Fix mobile-details v-model bindings (remove invalid this. prefix) - Coerce address fields from store to strings before binding - Correct malformed modelValue prop definitions on vehicle-protected-question and service-zip-question - Add unit test for null modelValue coercion
This commit is contained in:
parent
5b80824f17
commit
84d31fa55c
5 changed files with 53 additions and 23 deletions
|
|
@ -13,6 +13,22 @@ const mockMixin = {
|
||||||
const maska = jest.fn();
|
const maska = jest.fn();
|
||||||
|
|
||||||
describe("textboxQuestion.vue", () => {
|
describe("textboxQuestion.vue", () => {
|
||||||
|
it("Should coerce non-string modelValue to empty string for v-model.trim", async () => {
|
||||||
|
const wrapper = shallowMount(textboxQuestion, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
maska: maska,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
modelValue: null,
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.vm.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("Should render a text input", async () => {
|
it("Should render a text input", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(textboxQuestion, {
|
const wrapper = shallowMount(textboxQuestion, {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<input
|
<input
|
||||||
class="form-control"
|
class="form-control"
|
||||||
v-model.trim="value"
|
v-model.trim="value"
|
||||||
v-maska="mask"
|
v-maska="effectiveMask"
|
||||||
:type="type"
|
:type="type"
|
||||||
:ref="inputId"
|
:ref="inputId"
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
|
|
@ -90,6 +90,19 @@ import loader from "@/ux-components/loader/loader.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
function coerceTextboxValue(modelValue) {
|
||||||
|
if (modelValue == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof modelValue === "number") {
|
||||||
|
return String(modelValue);
|
||||||
|
}
|
||||||
|
if (typeof modelValue === "string") {
|
||||||
|
return modelValue;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "textbox-question",
|
name: "textbox-question",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -145,18 +158,8 @@ export default {
|
||||||
const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
|
const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
|
||||||
|
|
||||||
const propsClone = Object.assign({}, props);
|
const propsClone = Object.assign({}, props);
|
||||||
const modelValue = propsClone.modelValue;
|
const modelValue = coerceTextboxValue(propsClone.modelValue);
|
||||||
let initialValue;
|
const initialValue = modelValue;
|
||||||
let isImageProcessing = ref(false);
|
|
||||||
|
|
||||||
switch (typeof modelValue) {
|
|
||||||
case "number":
|
|
||||||
initialValue = modelValue;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
initialValue = modelValue && modelValue.length > 0 ? modelValue : "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldOptions = {
|
const fieldOptions = {
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -164,6 +167,8 @@ export default {
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let isImageProcessing = ref(false);
|
||||||
|
|
||||||
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
|
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
|
||||||
inputId,
|
inputId,
|
||||||
props.validationRules,
|
props.validationRules,
|
||||||
|
|
@ -223,18 +228,25 @@ export default {
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
get: function () {
|
get: function () {
|
||||||
return this.modelValue;
|
return coerceTextboxValue(this.modelValue);
|
||||||
},
|
},
|
||||||
set: function (newValue) {
|
set: function (newValue) {
|
||||||
this.$emit("update:modelValue", newValue);
|
this.$emit("update:modelValue", newValue);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
effectiveMask() {
|
||||||
|
return this.mask ?? "";
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
|
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async value(newValue) {
|
async value(newValue) {
|
||||||
|
if (typeof this.handleChange !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
this.handleChange(newValue); // trigger full validation on this field only
|
this.handleChange(newValue); // trigger full validation on this field only
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
class="keys-message" />
|
class="keys-message" />
|
||||||
<mobileAddressQuestions
|
<mobileAddressQuestions
|
||||||
ref="addressQuestions"
|
ref="addressQuestions"
|
||||||
v-model="this.addressQuestions"
|
v-model="addressQuestions"
|
||||||
captureApartmentNumberOrBusinessName="true"
|
captureApartmentNumberOrBusinessName="true"
|
||||||
preserveCityAndStateOnReset="true"
|
preserveCityAndStateOnReset="true"
|
||||||
labelBold="true"
|
labelBold="true"
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
isZipCodeDisabled="true" />
|
isZipCodeDisabled="true" />
|
||||||
<vehicleProtectedQuestion
|
<vehicleProtectedQuestion
|
||||||
ref="vehicleProtectedQuestion"
|
ref="vehicleProtectedQuestion"
|
||||||
v-model="this.isVehicleProtected"
|
v-model="isVehicleProtected"
|
||||||
cmsWidgetName="VehicleProtectedQuestionWidget"
|
cmsWidgetName="VehicleProtectedQuestionWidget"
|
||||||
labelBold="true" />
|
labelBold="true" />
|
||||||
<textBlock
|
<textBlock
|
||||||
|
|
@ -84,11 +84,11 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addressQuestions: {
|
addressQuestions: {
|
||||||
streetAddress: this.getServiceAddressFromStore(),
|
streetAddress: String(this.getServiceAddressFromStore() ?? ""),
|
||||||
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
apartmentNumberOrBusinessName: String(this.getServiceAddress2FromStore() ?? ""),
|
||||||
city: this.getServiceCityFromStore(),
|
city: String(this.getServiceCityFromStore() ?? ""),
|
||||||
state: this.getServiceStateFromStore(),
|
state: String(this.getServiceStateFromStore() ?? ""),
|
||||||
zipCode: this.getServiceZipCodeFromStore(),
|
zipCode: String(this.getServiceZipCodeFromStore() ?? ""),
|
||||||
},
|
},
|
||||||
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ export default {
|
||||||
name: "vehicle-protected-question",
|
name: "vehicle-protected-question",
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
isVehicleProtected: Boolean,
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
labelBold: {
|
labelBold: {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ export default {
|
||||||
name: "service-zip-question",
|
name: "service-zip-question",
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
serviceZipCode: String,
|
type: String,
|
||||||
|
default: "",
|
||||||
},
|
},
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
isRequired: {
|
isRequired: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue