diff --git a/src/components/base-address/BaseAddress.vue b/src/components/base-address/BaseAddress.vue index aaa8948f..76452188 100644 --- a/src/components/base-address/BaseAddress.vue +++ b/src/components/base-address/BaseAddress.vue @@ -454,13 +454,24 @@ export default class BaseAddress extends Mixins(ValidationMixin, CountriesProvin return this.addressLocal } + /** + * Whether the Postal Code model value is valid per the schema. + * NB: Vuelidate validates the model continuously, even while the Vuetify postal code + * rules are disabled (ie, before the field's first blur), so this reflects the actual + * validity of the value rather than whether errors are being displayed. + */ + get isPostalCodeValid (): boolean { + return !this.$v?.addressLocal?.postalCode?.$invalid + } + /** Watches for changes to the address form validity and emits the updated state. */ @Watch('addressFormValid') - @Watch('postalCodeRulesEnabled') + @Watch('isPostalCodeValid') @Emit('valid') onAddressFormValidChanged (): boolean { - // form is valid only if postal code rules are enabled - return (this.addressFormValid && this.postalCodeRulesEnabled) + // NB: addressFormValid alone can be a false positive while the Vuetify postal code + // rules are still disabled, so also check the postal code's Vuelidate state + return (this.addressFormValid && this.isPostalCodeValid) } /** diff --git a/src/components/base-address/package.json b/src/components/base-address/package.json index 63d8fad8..81487c59 100644 --- a/src/components/base-address/package.json +++ b/src/components/base-address/package.json @@ -1,6 +1,6 @@ { "name": "@bcrs-shared-components/base-address", - "version": "2.2.8", + "version": "2.2.9", "publishConfig": { "access": "public" }, diff --git a/tests/unit/BaseAddress.spec.ts b/tests/unit/BaseAddress.spec.ts index ea6437d9..780140e3 100644 --- a/tests/unit/BaseAddress.spec.ts +++ b/tests/unit/BaseAddress.spec.ts @@ -1,7 +1,9 @@ import Vue from 'vue' import Vuetify from 'vuetify' import { mount, Wrapper } from '@vue/test-utils' +import { required, maxLength } from 'vuelidate/lib/validators' import { BaseAddress } from '@/components/base-address' +import { isRequiredPostalCode, isValidPostalCode } from '@bcrs-shared-components/validators' const vuetify = new Vuetify({}) @@ -106,3 +108,89 @@ describe('BaseAddress - isSchemaRequired', () => { expect(wrapper.vm.isSchemaRequired('streetAddress')).toBe(false) }) }) + +describe('BaseAddress - valid event (#34469)', () => { + // schema mirroring the consuming apps' address schemas + const schema = { + streetAddress: { required, maxLength: maxLength(50) }, + streetAddressAdditional: { maxLength: maxLength(105) }, + addressCity: { required, maxLength: maxLength(40) }, + addressCountry: { required }, + addressRegion: { maxLength: maxLength(2) }, + postalCode: { isRequiredPostalCode, maxLength: maxLength(15), isValidPostalCode }, + deliveryInstructions: { maxLength: maxLength(80) } + } + + const address = { + streetAddress: '123 Main St', + streetAddressAdditional: 'Suite 200', + addressCity: 'Victoria', + addressRegion: 'BC', + postalCode: 'V8V 1V1', + addressCountry: 'CA', + deliveryInstructions: '' + } + + let wrapper: Wrapper + + afterEach(() => { + wrapper.destroy() + }) + + it('emits valid=true when an optional field is edited and the postal code was never blurred', async () => { + wrapper = mount(BaseAddress, { + vuetify, + propsData: { schema, editing: true, address } + }) + await Vue.nextTick() + + // clear Additional Street Address without ever touching the postal code field + // (the reported bug: this emitted valid=false and nothing ever re-emitted true) + const line2 = wrapper.find('.street-address-additional textarea') + await line2.setValue('') + await Vue.nextTick() + + const validEvents = wrapper.emitted('valid') + if (validEvents) { + expect(validEvents[validEvents.length - 1][0]).toBe(true) + } + // the model itself must agree + expect(wrapper.vm.isPostalCodeValid).toBe(true) + expect(wrapper.vm.addressFormValid).toBe(true) + }) + + it('emits valid=false when the postal code value is invalid, even before its rules are enabled', async () => { + wrapper = mount(BaseAddress, { + vuetify, + propsData: { schema, editing: true, address: { ...address, postalCode: '' } } + }) + await Vue.nextTick() + + // editing another field must not mask the invalid (empty, required) postal code + const line2 = wrapper.find('.street-address-additional textarea') + await line2.setValue('') + await Vue.nextTick() + + expect(wrapper.vm.isPostalCodeValid).toBe(false) + + const validEvents = wrapper.emitted('valid') + if (validEvents) { + expect(validEvents[validEvents.length - 1][0]).toBe(false) + } + }) + + it('emits valid=true after validate() on a fully valid address', async () => { + wrapper = mount(BaseAddress, { + vuetify, + propsData: { schema, editing: true, address } + }) + await Vue.nextTick() + + await wrapper.vm.validate() + await Vue.nextTick() + + const validEvents = wrapper.emitted('valid') + expect(validEvents).toBeTruthy() + expect(validEvents[validEvents.length - 1][0]).toBe(true) + }) +})