From 42a5b4d2cf3ca15b05a0274d86fca9ba34692517 Mon Sep 17 00:00:00 2001 From: Severin Beauvais Date: Fri, 21 Oct 2022 17:55:42 -0700 Subject: [PATCH] - used sub-component in Documents Delivery + simplify logic - added new Verified Email component - misc cleanup - added new unit test WIP --- src/components/common/DocumentsDelivery.vue | 66 ++------ src/components/common/VerifiedEmail.vue | 87 ++++++++++ .../YourCompany/BusinessContactInfo.vue | 7 +- src/components/common/index.ts | 1 + ...rvice.ts => email-verification-service.ts} | 2 +- src/services/index.ts | 2 +- src/views/Alteration.vue | 2 - src/views/Change.vue | 2 - src/views/SpecialResolution.vue | 2 - tests/unit/BusinessContactInfo.spec.ts | 6 +- tests/unit/DocumentDelivery.spec.ts | 6 +- tests/unit/VerifiedEmail.spec.ts | 160 ++++++++++++++++++ ....ts => email-verification-service.spec.ts} | 22 +-- 13 files changed, 281 insertions(+), 84 deletions(-) create mode 100644 src/components/common/VerifiedEmail.vue rename src/services/{million-verifier-service.ts => email-verification-service.ts} (97%) create mode 100644 tests/unit/VerifiedEmail.spec.ts rename tests/unit/{million-verifier-service.spec.ts => email-verification-service.spec.ts} (69%) diff --git a/src/components/common/DocumentsDelivery.vue b/src/components/common/DocumentsDelivery.vue index 4add4f8f9..dabd4686c 100644 --- a/src/components/common/DocumentsDelivery.vue +++ b/src/components/common/DocumentsDelivery.vue @@ -23,16 +23,12 @@ - @@ -57,10 +53,13 @@ import { CommonMixin } from '@/mixins/' import { FilingNames } from '@/enums/' import { ActionBindingIF, FlagsReviewCertifyIF } from '@/interfaces/' import { ContactPointIF } from '@bcrs-shared-components/interfaces/' +import { VerifiedEmail } from '@/components/common/' // FUTURE: update this component so it doesn't set changes flag initially -@Component({}) +@Component({ + components: { VerifiedEmail } +}) export default class DocumentsDelivery extends Mixins(CommonMixin) { // Global getters @Getter getUserEmail!: string @@ -80,61 +79,16 @@ export default class DocumentsDelivery extends Mixins(CommonMixin) { /** Whether to perform validation. */ @Prop({ default: false }) readonly validate!: boolean - // Local properties - private optionalEmail = '' - - private entityEmailRules = [ - (v: string) => !/^\s/g.test(v) || 'Invalid spaces', // leading spaces - (v: string) => !/\s$/g.test(v) || 'Invalid spaces', // trailing spaces - (v: string) => this.validateEmailFormat(v) || 'Enter valid email address' - ] - - /** Called when component is mounted. */ - protected mounted (): void { - this.optionalEmail = this.getDocumentOptionalEmail - } - - private validateEmailFormat (value: string): boolean { - // allow empty as the email is optional - if (!value) { - return true - } else { - const VALID_FORMAT = new RegExp(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) - return VALID_FORMAT.test(value) - } - } - - /** True if invalid class should be set for certify container. */ + /** True if invalid class should be set for document delivery container. */ get documentDeliveryInvalid (): boolean { return (this.validate && !this.getFlagsReviewCertify.isValidDocumentOptionalEmail) } - - @Watch('optionalEmail') - onOptionalEmailChanged (val: string): void { - if (this.validateEmailFormat(val)) { - this.setDocumentOptionalEmail(val) - this.setDocumentOptionalEmailValidity(true) - } else { - this.setDocumentOptionalEmailValidity(false) - } - } - - @Emit('valid') - private async emitValid (): Promise { - // wait for form to update itself before checking validity - await this.$nextTick() - return (this.validateEmailFormat(this.optionalEmail)) - } } diff --git a/src/components/common/YourCompany/BusinessContactInfo.vue b/src/components/common/YourCompany/BusinessContactInfo.vue index ca52f8daa..f4f5e052a 100644 --- a/src/components/common/YourCompany/BusinessContactInfo.vue +++ b/src/components/common/YourCompany/BusinessContactInfo.vue @@ -18,7 +18,7 @@ import { Component, Mixins, Prop, Watch } from 'vue-property-decorator' import { Action, Getter } from 'vuex-class' import { ContactInfo as ContactInfoShared } from '@bcrs-shared-components/contact-info/' -import { AuthServices, MillionVerifierService } from '@/services/' +import { AuthServices, EmailVerificationService } from '@/services/' import { CommonMixin } from '@/mixins/' import { ActionBindingIF, ResourceIF, EntitySnapshotIF } from '@/interfaces/' import { ContactPointIF } from '@bcrs-shared-components/interfaces/' @@ -69,9 +69,10 @@ export default class BusinessContactInfo extends Mixins(CommonMixin) { this.isCorrectionFiling || this.isSpecialResolutionFiling ) { - const valid = await MillionVerifierService.isValidEmail(contactInfo.email) + const valid = await EmailVerificationService.isValidEmail(contactInfo.email) + .catch(() => true) // if error, assume email is valid if (!valid) { - alert('Invalid email address') + this.$root.$emit('update-error-event', 'Invalid email address') return } await AuthServices.updateContactInfo(contactInfo, this.getBusinessId) diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 9f44f23d5..51a794dda 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -11,6 +11,7 @@ export { default as ErrorContact } from './ErrorContact.vue' export { default as StaffPayment } from './StaffPayment.vue' export { default as CourtOrderPoa } from './CourtOrderPoa.vue' export { default as TransactionalFolioNumber } from './TransactionalFolioNumber.vue' +export { default as VerifiedEmail } from './VerifiedEmail.vue' // these must come last as they depend on the above export * from './PeopleAndRoles' diff --git a/src/services/million-verifier-service.ts b/src/services/email-verification-service.ts similarity index 97% rename from src/services/million-verifier-service.ts rename to src/services/email-verification-service.ts index 75a1bbab2..f822f29ea 100644 --- a/src/services/million-verifier-service.ts +++ b/src/services/email-verification-service.ts @@ -14,7 +14,7 @@ enum ResultCodes { * Class that provides integration with the Million Verifier API. * Ref: https://developer.millionverifier.com/ */ -export default class MillionVerifierService { +export default class EmailVerificationService { /** * Verifies an email address in real time. * @param email the email address to verify diff --git a/src/services/index.ts b/src/services/index.ts index 65507e4e7..cd5cdf2ec 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,5 @@ export { default as AuthServices } from './auth-services' export { default as BusinessLookupServices } from './business-lookup-services' +export { default as EmailVerificationService } from './email-verification-service' export { default as LegalServices } from './legal-services' -export { default as MillionVerifierService } from './million-verifier-service' export { default as NaicsServices } from './naics-services' diff --git a/src/views/Alteration.vue b/src/views/Alteration.vue index a518e5d96..5fc5018a9 100644 --- a/src/views/Alteration.vue +++ b/src/views/Alteration.vue @@ -47,7 +47,6 @@ class="mt-10" sectionNumber="1." :validate="getAppValidate" - @valid="setDocumentOptionalEmailValidity($event)" /> { expect((wrapper.vm as any).getUserEmail).toBe('currentuser@mail.com') }) - it('validates a valid email', async () => { + xit('validates a valid email', async () => { const wrapper: Wrapper = createComponent() const vm: any = wrapper.vm @@ -57,7 +57,7 @@ describe('Document Delivery component', () => { expect(vm.validateEmailFormat).toBeTruthy() }) - it('validates an invalid email', async () => { + xit('validates an invalid email', async () => { const wrapper: Wrapper = createComponent() const vm: any = wrapper.vm diff --git a/tests/unit/VerifiedEmail.spec.ts b/tests/unit/VerifiedEmail.spec.ts new file mode 100644 index 000000000..9b35a49d8 --- /dev/null +++ b/tests/unit/VerifiedEmail.spec.ts @@ -0,0 +1,160 @@ +import Vue from 'vue' +import Vuetify from 'vuetify' +import { mount } from '@vue/test-utils' +import VerifiedEmail from '@/components/common/VerifiedEmail.vue' +import EmailVerificationService from '@/services/email-verification-service' + +Vue.use(Vuetify) +const vuetify = new Vuetify({}) + +describe('Verified Email component', () => { + it('displays correctly with no props', () => { + const wrapper = mount(VerifiedEmail, { vuetify }) + + // verify misc elements + expect(wrapper.find('#verified-email').exists()).toBe(true) + expect(wrapper.find('.v-text-field').exists()).toBe(true) + expect(wrapper.find('.v-label').text()).toBe('Email Address') + expect(wrapper.find('.v-messages__message').text()).toBe('Example: name@email.com') + + // verify initial events + expect(wrapper.emitted('newEmail')).toBeUndefined() // not updated + expect(wrapper.emitted('valid').pop()).toEqual([false]) // initially invalid + + wrapper.destroy() + }) + + it('displays label prop correctly', () => { + const wrapper = mount(VerifiedEmail, { + vuetify, + propsData: { label: 'My Label' } + }) + + expect(wrapper.find('.v-label').text()).toBe('My Label') + + wrapper.destroy() + }) + + it('displays hint prop correctly', () => { + const wrapper = mount(VerifiedEmail, { + vuetify, + propsData: { hint: 'My Hint' } + }) + + expect(wrapper.find('.v-messages__message').text()).toBe('My Hint') + + wrapper.destroy() + }) + + it('is valid when optional and no email is set', async () => { + // mock email verification service function + const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(true) + + const wrapper = mount(VerifiedEmail, { vuetify }) + + // set input value + const input = wrapper.find('input') + await input.setValue('') + await input.trigger('blur') + + // verify mock was not called + expect(mock).not.toHaveBeenCalled() + + // verify events + expect(wrapper.emitted('newEmail')).toBeUndefined() // not updated + expect(wrapper.emitted('valid').pop()).toEqual([true]) // now valid + + wrapper.destroy() + }) + + it('is invalid when required and no email is set', async () => { + // mock email verification service function + const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(true) + + const wrapper = mount(VerifiedEmail, { + vuetify, + propsData: { required: true } + }) + + // set input value + const input = wrapper.find('input') + await input.setValue('') + await input.trigger('blur') + + // verify mock was not called + expect(mock).not.toHaveBeenCalled() + + // verify events + expect(wrapper.emitted('newEmail')).toBeUndefined() // not updated + expect(wrapper.emitted('valid').pop()).toEqual([false]) // still invalid + + wrapper.destroy() + }) + + xit('is valid with initial valid email', async () => { + // mock email verification service function + const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(true) + + const wrapper = mount(VerifiedEmail, { + vuetify, + propsData: { email: 'email@gmail.com' } + }) + + // verify mock was called + expect(mock).toHaveBeenCalledWith('email@gmail.com') + + // verify events + expect(wrapper.emitted('newEmail').pop()).toBe(['email@gmail.com']) + expect(wrapper.emitted('valid').pop()).toEqual([true]) // now valid + + wrapper.destroy() + }) + + xit('is invalid with initial invalid email', async () => { + // mock email verification service function + const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(false) + + const wrapper = mount(VerifiedEmail, { + vuetify, + propsData: { email: 'email@example.com' } + }) + + // verify mock was called + expect(mock).not.toHaveBeenCalledWith('email@example.com') + + // verify events + expect(wrapper.emitted('newEmail').pop()).toBeUndefined() // not updated + expect(wrapper.emitted('valid').pop()).toEqual([false]) // still invalid + + wrapper.destroy() + }) + + // it('validates a valid email', async () => { + // const wrapper: Wrapper = createComponent() + // const vm: any = wrapper.vm + + // // Set Input field value + // const input = wrapper.find(optionalEmailInput) + // await input.setValue('optional@mail.com') + // await input.trigger('change') + + // // verify email is valid + // expect(vm.validateEmailFormat).toBeTruthy() + // }) + + // it('validates an invalid email', async () => { + // const wrapper: Wrapper = createComponent() + // const vm: any = wrapper.vm + + // // Set Input field values + // vm.$el.querySelector(optionalEmailInput).textContent = '1212' + // await wrapper.find('.email-input-field').trigger('focus') + // await wrapper.find('.email-input-field').find('input').setValue('1212') + // await wrapper.find('.email-input-field').trigger('blur') + + // expect(wrapper.find(optionalEmailInput).text()).toEqual('1212') + + // // verify there is an error. Vue doesn't like the blur event with testing + // expect(wrapper.findAll('.v-messages__message').length).toBe(1) + // }) +}) diff --git a/tests/unit/million-verifier-service.spec.ts b/tests/unit/email-verification-service.spec.ts similarity index 69% rename from tests/unit/million-verifier-service.spec.ts rename to tests/unit/email-verification-service.spec.ts index 082ab868b..30340c0c3 100644 --- a/tests/unit/million-verifier-service.spec.ts +++ b/tests/unit/email-verification-service.spec.ts @@ -1,8 +1,8 @@ import sinon from 'sinon' import axios from 'axios' -import MillionVerifierService from '@/services/million-verifier-service' +import EmailVerificationService from '@/services/email-verification-service' -describe('Million Verifier Service', () => { +describe('Email Verification Service', () => { let get: any beforeEach(() => { @@ -15,25 +15,25 @@ describe('Million Verifier Service', () => { it('throws an error when there is no email address', async () => { // test it - await expect(MillionVerifierService.isValidEmail(null, 'https://url', 'KEY', 5)).rejects + await expect(EmailVerificationService.isValidEmail(null, 'https://url', 'KEY', 5)).rejects .toThrow('Email address is required') }) it('throws an error when there is no API URL', async () => { // test it - await expect(MillionVerifierService.isValidEmail('valid@example.com', null, 'KEY', 5)) + await expect(EmailVerificationService.isValidEmail('valid@example.com', null, 'KEY', 5)) .rejects.toThrow('API URL is required') }) it('throws an error when there is no timeout', async () => { // test it - await expect(MillionVerifierService.isValidEmail('valid@example.com', 'https://url', 'KEY', null)) + await expect(EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', null)) .rejects.toThrow('Timeout is required') }) it('returns True when there is no API key', async () => { // test it - expect(await MillionVerifierService.isValidEmail('valid@example.com', 'https://url', null, 5)) + expect(await EmailVerificationService.isValidEmail('valid@example.com', 'https://url', null, 5)) .toBe(true) }) @@ -43,7 +43,7 @@ describe('Million Verifier Service', () => { .returns(Promise.resolve({ data: { result: 'ok' } })) // test it - expect(await MillionVerifierService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5)) + expect(await EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5)) .toBe(true) }) @@ -53,7 +53,7 @@ describe('Million Verifier Service', () => { .returns(new Promise(resolve => resolve({ data: { result: 'unknown' } }))) // test it - expect(await MillionVerifierService.isValidEmail('invalid@example.com', 'https://url', 'KEY', 5)) + expect(await EmailVerificationService.isValidEmail('invalid@example.com', 'https://url', 'KEY', 5)) .toBe(true) }) @@ -63,7 +63,7 @@ describe('Million Verifier Service', () => { .returns(new Promise(resolve => resolve({ data: { result: 'invalid' } }))) // test it - expect(await MillionVerifierService.isValidEmail('invalid@example.com', 'https://url', 'KEY', 5)) + expect(await EmailVerificationService.isValidEmail('invalid@example.com', 'https://url', 'KEY', 5)) .toBe(false) }) @@ -73,7 +73,7 @@ describe('Million Verifier Service', () => { .returns(Promise.resolve({})) // test it - await expect(MillionVerifierService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5)) + await expect(EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5)) .rejects.toThrow('Invalid API response') }) @@ -83,7 +83,7 @@ describe('Million Verifier Service', () => { .returns(Promise.reject(new Error('Network error'))) // test it - await expect(MillionVerifierService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5)) + await expect(EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5)) .rejects.toThrow('Network error') }) })