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(); 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, {

View file

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

View file

@ -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(),
}; };

View file

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

View file

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