diff --git a/src/mixins/validation-mixin.ts b/src/mixins/validation-mixin.ts index e0fa638d..332b74c1 100644 --- a/src/mixins/validation-mixin.ts +++ b/src/mixins/validation-mixin.ts @@ -40,7 +40,8 @@ export default class ValidationMixin extends Vue { case 'maxLength': obj[prop].push(() => this.maxLengthRule(model, prop)); break case 'isCanada': obj[prop].push(() => this.isCanadaRule(model, prop)); break case 'isBC': obj[prop].push(() => this.isBCRule(model, prop)); break - case 'validatePostalCode': obj[prop].push(() => this.postalCodeRule(model, prop)); break + case 'isValidPostalCode': obj[prop].push(() => this.isValidPostalCodeRule(model, prop)); break + case 'isRequiredPostalCode': obj[prop].push(() => this.isRequiredPostalCodeRule(model, prop)); break // FUTURE: add extra validation functions here default: break } @@ -89,7 +90,11 @@ export default class ValidationMixin extends Vue { return Boolean(this.$v[prop] && this.$v[prop][key].isBC) || 'Address must be in BC' } - protected postalCodeRule (prop: string, key: string): boolean | string { - return Boolean(this.$v[prop] && this.$v[prop][key].validatePostalCode) || 'Enter a valid postal code' + protected isValidPostalCodeRule (prop: string, key: string): boolean | string { + return Boolean(this.$v[prop] && this.$v[prop][key].isValidPostalCode) || 'Enter a valid postal code' + } + + protected isRequiredPostalCodeRule (prop: string, key: string): boolean | string { + return Boolean(this.$v[prop] && this.$v[prop][key].isRequiredPostalCode) || 'This field is required' } } diff --git a/src/validators/index.ts b/src/validators/index.ts index cdcdae5d..3b15f34e 100644 --- a/src/validators/index.ts +++ b/src/validators/index.ts @@ -1 +1,2 @@ -export * from './validate-postal-code' +export * from './is-valid-postal-code' +export * from './is-required-postal-code' diff --git a/src/validators/is-required-postal-code.ts b/src/validators/is-required-postal-code.ts new file mode 100644 index 00000000..5ef87062 --- /dev/null +++ b/src/validators/is-required-postal-code.ts @@ -0,0 +1,25 @@ +/** + * Countries that do not use postal codes. + * Postal code is not required for addresses in these countries. + */ +const NO_POSTAL_CODE_COUNTRY_CODES = new Set([ + 'AO', 'AG', 'AW', 'BS', 'BZ', 'BJ', 'BM', 'BO', 'BQ', 'BW', 'BF', 'BI', + 'CM', 'CF', 'TD', 'KM', 'CG', 'CD', 'CK', 'CI', 'CW', 'DJ', 'DM', 'GQ', + 'ER', 'FJ', 'TF', 'GA', 'GM', 'GH', 'GD', 'GY', 'HM', 'HK', + 'KI', 'KP', 'LY', 'MO', 'MW', 'ML', 'MR', 'NR', + 'AN', 'NU', 'QA', 'RW', 'KN', 'ST', 'SC', 'SL', 'SX', 'SB', 'SO', 'SR', 'SY', + 'TL', 'TG', 'TK', 'TO', 'TT', 'TV', 'UG', 'AE', 'VU', 'YE', 'ZW' +]) + +/** + * Custom validator for postal code required. + * Returns true if postal code has a value, or if the country doesn't use postal codes. + */ +export function isRequiredPostalCode (value: string, parentVm: any): boolean { + // If the country doesn't use postal codes, no value is required + if (NO_POSTAL_CODE_COUNTRY_CODES.has(parentVm.addressCountry)) { + return true + } + // Otherwise, postal code is required + return !!value?.trim() +} diff --git a/src/validators/validate-postal-code.ts b/src/validators/is-valid-postal-code.ts similarity index 87% rename from src/validators/validate-postal-code.ts rename to src/validators/is-valid-postal-code.ts index 347a5754..243b31fd 100644 --- a/src/validators/validate-postal-code.ts +++ b/src/validators/is-valid-postal-code.ts @@ -5,7 +5,7 @@ const CanadaPostalCodeRegex = /^[ABCEGHJ-NPRSTVXY][0-9][ABCEGHJ-NPRSTV-Z][ ]?[0-9][ABCEGHJ-NPRSTV-Z][0-9]$/i /** Custom validator for postal codes. */ -export function validatePostalCode (value: string, parentVm: any): boolean { +export function isValidPostalCode (value: string, parentVm: any): boolean { // if Canada, validate postal code format // empty value is considered valid -- "required" validation is handled separately if (parentVm.addressCountry === 'CA') return !value || CanadaPostalCodeRegex.test(value) diff --git a/tests/unit/is-required-postal-code.spec.ts b/tests/unit/is-required-postal-code.spec.ts new file mode 100644 index 00000000..7d18c834 --- /dev/null +++ b/tests/unit/is-required-postal-code.spec.ts @@ -0,0 +1,55 @@ +import Vue from 'vue' +import { shallowMount } from '@vue/test-utils' +import { isRequiredPostalCode } from '@/validators' + +const Dummy = Vue.component('DummyComponent', { template: '
' }) + +describe('Validate Postal Code Required', () => { + let vm: any + + beforeAll(async () => { + // mount the component and wait for everything to stabilize + // (this can be any component since we are not really using it) + const wrapper = shallowMount(Dummy) + vm = wrapper.vm + await Vue.nextTick() + }) + + it.each([ + 'A1A 1A1', + '12345', + 'some-postal-code', + ' value ' + ])('returns true when postal code "%s" is provided for a country requiring postal codes', (postalCode) => { + expect(isRequiredPostalCode(postalCode, { addressCountry: 'CA' })).toBe(true) + expect(isRequiredPostalCode(postalCode, { addressCountry: 'US' })).toBe(true) + }) + + it.each([ + '', + ' ', + null, + undefined + ])('returns false when postal code is "%s" for a country requiring postal codes', (postalCode) => { + expect(isRequiredPostalCode(postalCode as any, { addressCountry: 'CA' })).toBe(false) + expect(isRequiredPostalCode(postalCode as any, { addressCountry: 'US' })).toBe(false) + }) + + it.each([ + 'AO', 'AG', 'AW', 'BS', 'BZ', 'BJ', 'BM', 'BO', 'BQ', 'BW', 'BF', 'BI', + 'CM', 'CF', 'TD', 'KM', 'CG', 'CD', 'CK', 'CI', 'CW', 'DJ', 'DM', 'GQ', + 'ER', 'FJ', 'TF', 'GA', 'GM', 'GH', 'GD', 'GY', 'HM', 'HK', + 'KI', 'KP', 'LY', 'MO', 'MW', 'ML', 'MR', 'NR', + 'AN', 'NU', 'QA', 'RW', 'KN', 'ST', 'SC', 'SL', 'SX', 'SB', 'SO', 'SR', 'SY', + 'TL', 'TG', 'TK', 'TO', 'TT', 'TV', 'UG', 'AE', 'VU', 'YE', 'ZW' + ])('returns true for country "%s" even when postal code is empty', (countryCode) => { + expect(isRequiredPostalCode('', { addressCountry: countryCode })).toBe(true) + expect(isRequiredPostalCode(' ', { addressCountry: countryCode })).toBe(true) + }) + + it.each([ + 'AO', 'HK', 'AE', 'QA', 'ZW' + ])('returns true for country "%s" when postal code is provided', (countryCode) => { + expect(isRequiredPostalCode('12345', { addressCountry: countryCode })).toBe(true) + }) +}) diff --git a/tests/unit/validate-postal-code.spec.ts b/tests/unit/is-valid-postal-code.spec.ts similarity index 64% rename from tests/unit/validate-postal-code.spec.ts rename to tests/unit/is-valid-postal-code.spec.ts index 4d964186..2cda7c7c 100644 --- a/tests/unit/validate-postal-code.spec.ts +++ b/tests/unit/is-valid-postal-code.spec.ts @@ -1,6 +1,6 @@ import Vue from 'vue' import { shallowMount } from '@vue/test-utils' -import { validatePostalCode } from '@/validators' +import { isValidPostalCode } from '@/validators' const Dummy = Vue.component('DummyComponent', { template: '' }) @@ -22,7 +22,7 @@ describe('Validate Postal Code', () => { 'a1a 1a1', 'a1a1a1' ])('accepts valid postal code "%s" for Canada', (postalCode) => { - expect(validatePostalCode(postalCode, { addressCountry: 'CA' })).toBe(true) + expect(isValidPostalCode(postalCode, { addressCountry: 'CA' })).toBe(true) }) it.each([ @@ -40,7 +40,7 @@ describe('Validate Postal Code', () => { 'W1W 1W1', // W is not allowed in Canadian postal codes 'Z1Z 1Z1' // Z is not allowed in Canadian postal codes ])('rejects invalid postal code "%s" for Canada', (postalCode) => { - expect(validatePostalCode(postalCode, { addressCountry: 'CA' })).toBe(false) + expect(isValidPostalCode(postalCode, { addressCountry: 'CA' })).toBe(false) }) it.each([ @@ -52,6 +52,20 @@ describe('Validate Postal Code', () => { 'W1W 1W1', 'Z1Z 1Z1' ])('accepts any postal code "%s" for non-Canada', (postalCode) => { - expect(validatePostalCode(postalCode, { addressCountry: 'XX' })).toBe(true) + expect(isValidPostalCode(postalCode, { addressCountry: 'XX' })).toBe(true) + }) + + it.each([ + null, + undefined + ])('accepts %s postal code for Canada', (postalCode) => { + expect(isValidPostalCode(postalCode as any, { addressCountry: 'CA' })).toBe(true) + }) + + it.each([ + null, + undefined + ])('accepts %s postal code for non-Canada', (postalCode) => { + expect(isValidPostalCode(postalCode as any, { addressCountry: 'XX' })).toBe(true) }) })