Merge pull request #3245 from Safelite/feature/CASH-2952

Feature/cash 2952
This commit is contained in:
matthew-sykes 2026-06-29 12:43:20 -04:00 committed by GitHub
commit 4c504b54ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 23 deletions

View file

@ -87,9 +87,22 @@ window.onerror = (msg, url, line, col, error) => {
return suppressErrorAlert; return suppressErrorAlert;
}; };
function isThirdPartyUnhandledRejection(reason) {
const stack = reason instanceof Error ? reason.stack : "";
if (typeof stack !== "string") {
return false;
}
return stack.includes("chrome-extension://") || stack.includes("quantummetric.com");
}
// Log Promise rejections that are never handled (.catch / await try/catch), e.g. fire-and-forget async. // Log Promise rejections that are never handled (.catch / await try/catch), e.g. fire-and-forget async.
window.addEventListener("unhandledrejection", (event) => { window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason; const reason = event.reason;
if (isThirdPartyUnhandledRejection(reason)) {
return;
}
const message = reason instanceof Error ? reason.message : String(reason); const message = reason instanceof Error ? reason.message : String(reason);
global.$logger.logError(`unhandledrejection: ${message}`, { global.$logger.logError(`unhandledrejection: ${message}`, {
stack: reason instanceof Error ? reason.stack : undefined, stack: reason instanceof Error ? reason.stack : undefined,

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