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:
Matt Sykes 2026-06-25 12:47:02 -04:00
parent 5b80824f17
commit 84d31fa55c
5 changed files with 53 additions and 23 deletions

View file

@ -13,6 +13,22 @@ const mockMixin = {
const maska = jest.fn();
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 () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {

View file

@ -25,7 +25,7 @@
<input
class="form-control"
v-model.trim="value"
v-maska="mask"
v-maska="effectiveMask"
:type="type"
:ref="inputId"
:id="inputId"
@ -90,6 +90,19 @@ import loader from "@/ux-components/loader/loader.vue";
import { ref } from "vue";
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 {
name: "textbox-question",
props: {
@ -145,18 +158,8 @@ export default {
const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
let isImageProcessing = ref(false);
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = modelValue && modelValue.length > 0 ? modelValue : "";
break;
}
const modelValue = coerceTextboxValue(propsClone.modelValue);
const initialValue = modelValue;
const fieldOptions = {
type: "text",
@ -164,6 +167,8 @@ export default {
initialValue: initialValue,
};
let isImageProcessing = ref(false);
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
inputId,
props.validationRules,
@ -223,18 +228,25 @@ export default {
},
value: {
get: function () {
return this.modelValue;
return coerceTextboxValue(this.modelValue);
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
effectiveMask() {
return this.mask ?? "";
},
},
mounted() {
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
},
watch: {
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
if (result.valid) {
this.handleChange(newValue); // trigger full validation on this field only

View file

@ -34,7 +34,7 @@
class="keys-message" />
<mobileAddressQuestions
ref="addressQuestions"
v-model="this.addressQuestions"
v-model="addressQuestions"
captureApartmentNumberOrBusinessName="true"
preserveCityAndStateOnReset="true"
labelBold="true"
@ -42,7 +42,7 @@
isZipCodeDisabled="true" />
<vehicleProtectedQuestion
ref="vehicleProtectedQuestion"
v-model="this.isVehicleProtected"
v-model="isVehicleProtected"
cmsWidgetName="VehicleProtectedQuestionWidget"
labelBold="true" />
<textBlock
@ -84,11 +84,11 @@ export default {
data() {
return {
addressQuestions: {
streetAddress: this.getServiceAddressFromStore(),
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
streetAddress: String(this.getServiceAddressFromStore() ?? ""),
apartmentNumberOrBusinessName: String(this.getServiceAddress2FromStore() ?? ""),
city: String(this.getServiceCityFromStore() ?? ""),
state: String(this.getServiceStateFromStore() ?? ""),
zipCode: String(this.getServiceZipCodeFromStore() ?? ""),
},
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
};

View file

@ -25,7 +25,8 @@ export default {
name: "vehicle-protected-question",
props: {
modelValue: {
isVehicleProtected: Boolean,
type: Boolean,
default: null,
},
cmsWidgetName: String,
labelBold: {

View file

@ -29,7 +29,8 @@ export default {
name: "service-zip-question",
props: {
modelValue: {
serviceZipCode: String,
type: String,
default: "",
},
cmsWidgetName: String,
isRequired: {