From 8c0e23d9617d2ff6dec2f4a90c992711aa94138d Mon Sep 17 00:00:00 2001 From: Dima K Date: Thu, 12 Mar 2026 08:39:14 -0700 Subject: [PATCH 1/3] feat: add tests for examiner store --- strr-examiner-web/tests/mocks/mockedData.ts | 9 +- .../tests/unit/store-examiner.spec.ts | 98 +++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 strr-examiner-web/tests/unit/store-examiner.spec.ts diff --git a/strr-examiner-web/tests/mocks/mockedData.ts b/strr-examiner-web/tests/mocks/mockedData.ts index 74c8bbd89..94c7742ed 100644 --- a/strr-examiner-web/tests/mocks/mockedData.ts +++ b/strr-examiner-web/tests/mocks/mockedData.ts @@ -369,7 +369,8 @@ export const mockHostRegistration: HostRegistrationResp = { listingDetails: [], sbc_account_id: 12345, updatedDate: MOCK_DATES.APPLICATION_DATE, - user_id: 123 + user_id: 123, + nocStatus: null } export const mockPlatformRegistration: PlatformRegistrationResp = { @@ -428,7 +429,8 @@ export const mockPlatformRegistration: PlatformRegistrationResp = { }, sbc_account_id: 12345, updatedDate: MOCK_DATES.APPLICATION_DATE, - user_id: 123 + user_id: 123, + nocStatus: null } export const mockStrataHotelRegistration: StrataHotelRegistrationResp = { @@ -457,7 +459,8 @@ export const mockStrataHotelRegistration: StrataHotelRegistrationResp = { strataHotelRepresentatives: [MOCK_STRATA_REP] as ApiRep[], sbc_account_id: 12345, updatedDate: MOCK_DATES.STRATA_APPLICATION_DATE, - user_id: 123 + user_id: 123, + nocStatus: null } export const mockExpiredRegistration: HostRegistrationResp = { diff --git a/strr-examiner-web/tests/unit/store-examiner.spec.ts b/strr-examiner-web/tests/unit/store-examiner.spec.ts new file mode 100644 index 000000000..473d0da3b --- /dev/null +++ b/strr-examiner-web/tests/unit/store-examiner.spec.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mockNuxtImport } from '@nuxt/test-utils/runtime' +import { ref } from 'vue' +import { setActivePinia, createPinia } from 'pinia' +import { mockHostApplication, mockHostRegistration } from '../mocks/mockedData' +import { ApplicationStatus, RegistrationStatus } from '#imports' + +// mock $strrApi accessed through useNuxtApp() +const mockStrrApi = vi.fn().mockResolvedValue({}) + +mockNuxtImport('useNuxtApp', () => () => ({ + $i18n: { t: (key: string) => key }, + $strrApi: mockStrrApi +})) + +mockNuxtImport('useStrrApi', () => () => ({ + getAccountApplications: vi.fn() +})) + +const mockKcUser = ref({ userName: 'examiner1' }) + +mockNuxtImport('useKeycloak', () => () => ({ kcUser: mockKcUser })) +mockNuxtImport('useStrrModals', () => () => ({ openErrorModal: vi.fn() })) + +vi.mock('@/composables/useExaminerFeatureFlags', () => ({ + useExaminerFeatureFlags: () => ({ isSplitDashboardTableEnabled: ref(false) }) +})) + +describe('Store - Examiner', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + mockStrrApi.mockResolvedValue({}) + mockKcUser.value = { userName: 'examiner1' } + }) + + it('should have correct application and registration records', () => { + const store = useExaminerStore() + + // no activeRecord set → both should be falsy + expect(store.isApplication).toBe(false) + expect(store.activeReg).toBeUndefined() + + // set application + store.activeRecord = mockHostApplication + expect(store.isApplication).toBe(true) + expect(store.activeReg).toEqual(mockHostApplication.registration) + expect(store.activeReg).not.toHaveProperty('header') + + // set registration + store.activeRecord = mockHostRegistration + expect(store.isApplication).toBe(false) + expect(store.activeReg).toEqual(mockHostRegistration) + }) + + it('should have correct active header for registration', () => { + const store = useExaminerStore() + + store.activeRecord = { + ...mockHostRegistration, + header: { + ...mockHostRegistration.header, + applications: [{ + applicationNumber: 'APP-001', + applicationStatus: ApplicationStatus.FULL_REVIEW, + applicationDateTime: '2025-01-01T00:00:00Z' + }] + } + } + + const header = store.activeHeader + + expect(header).not.toHaveProperty('applications') + expect(header).toHaveProperty('applicationStatus', ApplicationStatus.FULL_REVIEW) + expect(header).toHaveProperty('applicationNumber', '12345678901234') + expect(header).toHaveProperty('examinerStatus', mockHostRegistration.header.examinerStatus) + }) + + it('processStatusFilters separates registration and application statuses for API calls', async () => { + const store = useExaminerStore() + + store.tableFilters.status = [RegistrationStatus.ACTIVE] as any + await store.fetchApplications() + + let query = mockStrrApi.mock.calls[0]![1].query + expect(query.status).toEqual([]) + expect(query.registrationStatus).toEqual([RegistrationStatus.ACTIVE]) + + mockStrrApi.mockClear() + + store.tableFilters.status = [ApplicationStatus.FULL_REVIEW] as any + await store.fetchApplications() + + query = mockStrrApi.mock.calls[0]![1].query + expect(query.status).toEqual([ApplicationStatus.FULL_REVIEW]) + expect(query.registrationStatus).toEqual([]) + }) +}) From 66da33f542a6e67a469dec6080348d2a3cbd0a13 Mon Sep 17 00:00:00 2001 From: Dima K Date: Mon, 16 Mar 2026 14:21:44 -0700 Subject: [PATCH 2/3] feat(tests): increase unit test coverage for Examiner app --- .../app/components/ApprovalConditions.vue | 1 + .../layouts/{examiner.vue => dashboard.vue} | 0 strr-examiner-web/app/pages/dashboard.vue | 2 +- .../tests/unit/action-buttons.spec.ts | 314 +++++++++++++++ .../tests/unit/approval-conditions.spec.ts | 337 ++++++++++++++++ .../tests/unit/platform-details-view.spec.ts | 54 +++ .../tests/unit/snapshot-details.spec.ts | 49 +++ .../tests/unit/store-document.spec.ts | 118 ++++++ .../tests/unit/store-examiner.spec.ts | 370 +++++++++++++++++- .../unit/strata-application-details.spec.ts | 81 ++-- .../tests/unit/use-examiner-actions.spec.ts | 78 ++++ 11 files changed, 1338 insertions(+), 66 deletions(-) rename strr-examiner-web/app/layouts/{examiner.vue => dashboard.vue} (100%) create mode 100644 strr-examiner-web/tests/unit/action-buttons.spec.ts create mode 100644 strr-examiner-web/tests/unit/approval-conditions.spec.ts create mode 100644 strr-examiner-web/tests/unit/platform-details-view.spec.ts create mode 100644 strr-examiner-web/tests/unit/snapshot-details.spec.ts create mode 100644 strr-examiner-web/tests/unit/store-document.spec.ts create mode 100644 strr-examiner-web/tests/unit/use-examiner-actions.spec.ts diff --git a/strr-examiner-web/app/components/ApprovalConditions.vue b/strr-examiner-web/app/components/ApprovalConditions.vue index a086df5a7..8ca6b5653 100644 --- a/strr-examiner-web/app/components/ApprovalConditions.vue +++ b/strr-examiner-web/app/components/ApprovalConditions.vue @@ -140,6 +140,7 @@ watch([isCustomConditionOpen, isMinBookingDaysOpen], () => { diff --git a/strr-examiner-web/app/layouts/examiner.vue b/strr-examiner-web/app/layouts/dashboard.vue similarity index 100% rename from strr-examiner-web/app/layouts/examiner.vue rename to strr-examiner-web/app/layouts/dashboard.vue diff --git a/strr-examiner-web/app/pages/dashboard.vue b/strr-examiner-web/app/pages/dashboard.vue index 942ef8e84..24a962d4c 100644 --- a/strr-examiner-web/app/pages/dashboard.vue +++ b/strr-examiner-web/app/pages/dashboard.vue @@ -127,7 +127,7 @@ useHead({ }) definePageMeta({ - layout: 'examiner', + layout: 'dashboard', middleware: ['auth'] }) diff --git a/strr-examiner-web/tests/unit/action-buttons.spec.ts b/strr-examiner-web/tests/unit/action-buttons.spec.ts new file mode 100644 index 000000000..0e87bbd32 --- /dev/null +++ b/strr-examiner-web/tests/unit/action-buttons.spec.ts @@ -0,0 +1,314 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime' +import { flushPromises } from '@vue/test-utils' +import { ref, reactive } from 'vue' +import { enI18n } from '../mocks/i18n' +import ActionButtons from '~/components/ActionButtons.vue' +import { ApplicationActionsE, RegistrationActionsE, RegistrationStatus } from '#imports' + +const mockAssignRegistration = vi.fn().mockResolvedValue(undefined) +const mockUnassignRegistration = vi.fn().mockResolvedValue(undefined) +const mockSetAsideRegistration = vi.fn().mockResolvedValue(undefined) +const mockUpdateRegistrationStatus = vi.fn().mockResolvedValue(undefined) +const mockSendNotice = vi.fn().mockResolvedValue(undefined) +const mockOpenConfirmActionModal = vi.fn() + +const activeHeader = ref({ examinerActions: [], isSetAside: false, assignee: { username: '' } }) +const activeReg = ref({ id: 'reg-123', status: RegistrationStatus.ACTIVE, conditionsOfApproval: null }) +const isAssignedToUser = ref(true) +const decisionIntent = ref(null) +const conditions = ref([]) +const customConditions = ref(null) +const minBookingDays = ref(null) +const decisionEmailContent = ref({ content: '' }) + +vi.mock('@/stores/examiner', () => ({ + useExaminerStore: () => reactive({ + assignRegistration: mockAssignRegistration, + unassignRegistration: mockUnassignRegistration, + setAsideRegistration: mockSetAsideRegistration, + updateRegistrationStatus: mockUpdateRegistrationStatus, + sendNoticeOfConsiderationForRegistration: mockSendNotice, + isAssignedToUser, + activeHeader, + activeReg, + conditions, + customConditions, + minBookingDays, + decisionEmailContent + }) +})) + +vi.mock('@/composables/useExaminerDecision', () => ({ + useExaminerDecision: () => ({ + decisionIntent, + isMainActionDisabled: ref(false), + isDecisionEmailValid: vi.fn().mockResolvedValue(true) + }) +})) + +mockNuxtImport('useStrrModals', () => () => ({ + openConfirmActionModal: mockOpenConfirmActionModal, + close: vi.fn() +})) + +vi.mock('nuxt/app', () => ({ + refreshNuxtData: vi.fn() +})) + +const mount = () => mountSuspended(ActionButtons, { global: { plugins: [enI18n] } }) + +const clickMainButton = (wrapper: any) => + wrapper.find('[data-testid="main-action-button"]').trigger('click') + +// Get confirmHandler action, from openConfirmActionModal (confirmHandler is at position 3) +const getConfirmCallback = () => mockOpenConfirmActionModal.mock.lastCall!.at(3) as () => void + +describe('ActionButtons Component', () => { + beforeEach(() => { + vi.clearAllMocks() + activeHeader.value = { examinerActions: [], isSetAside: false, assignee: { username: '' } } + activeReg.value = { id: 'reg-123', status: RegistrationStatus.ACTIVE, conditionsOfApproval: null } + isAssignedToUser.value = true + decisionIntent.value = null + conditions.value = [] + customConditions.value = null + minBookingDays.value = null + decisionEmailContent.value = { content: '' } + }) + + it('should show assign button when no assignee, and unassign button when assignee exists', async () => { + const wrapper = await mount() + + expect(wrapper.find('[data-testid="action-button-assign"]').exists()).toBe(true) + expect(wrapper.find('[data-testid="action-button-unassign"]').exists()).toBe(false) + + activeHeader.value = { ...activeHeader.value, assignee: { username: 'examiner1' } } + await flushPromises() + + expect(wrapper.find('[data-testid="action-button-assign"]').exists()).toBe(false) + expect(wrapper.find('[data-testid="action-button-unassign"]').exists()).toBe(true) + }) + + it('should correctly show set-aside button', async () => { + const wrapper = await mount() + + expect(wrapper.find('[data-testid="action-button-set-aside"]').exists()).toBe(false) + + activeHeader.value = { ...activeHeader.value, examinerActions: [ApplicationActionsE.SET_ASIDE] } + await flushPromises() + + expect(wrapper.find('[data-testid="action-button-set-aside"]').exists()).toBe(true) + }) + + it('should show main action button when assigned and a decision intent is selected', async () => { + const wrapper = await mount() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(false) + + decisionIntent.value = ApplicationActionsE.REJECT + await flushPromises() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(true) + }) + + it('should show main Approve action button only when conditions have changed', async () => { + activeReg.value = { + ...activeReg.value, + conditionsOfApproval: { predefinedConditions: [], customConditions: null, minBookingDays: null } + } + decisionIntent.value = ApplicationActionsE.APPROVE + + const wrapper = await mount() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(false) + + conditions.value = [{ condition: 'some-condition' }] as any + await flushPromises() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(true) + }) + + it('should show main Approve action button when minBookingDays is updated', async () => { + activeReg.value = { + ...activeReg.value, + conditionsOfApproval: { predefinedConditions: [], customConditions: null, minBookingDays: 5 } + } + decisionIntent.value = ApplicationActionsE.APPROVE + minBookingDays.value = 5 + + const wrapper = await mount() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(false) + + minBookingDays.value = 10 + await flushPromises() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(true) + }) + + it('should show main action button when registration is set aside, ignoring condition changes', async () => { + // isSetAside bypasses the condition-change check + activeHeader.value = { ...activeHeader.value, isSetAside: true } + activeReg.value = { + ...activeReg.value, + conditionsOfApproval: { predefinedConditions: [], customConditions: null, minBookingDays: null } + } + decisionIntent.value = ApplicationActionsE.APPROVE + + const wrapper = await mount() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(true) + }) + + it('should call assignRegistration with the registration id when assign button is clicked', async () => { + const wrapper = await mount() + + await wrapper.find('[data-testid="action-button-assign"]').trigger('click') + await flushPromises() + + expect(mockAssignRegistration).toHaveBeenCalledOnce() + expect(mockAssignRegistration).toHaveBeenCalledWith(activeReg.value.id) + }) + + it('should call setAsideRegistration with the registration id when set-aside button is clicked', async () => { + activeHeader.value = { ...activeHeader.value, examinerActions: [ApplicationActionsE.SET_ASIDE] } + + const wrapper = await mount() + + await wrapper.find('[data-testid="action-button-set-aside"]').trigger('click') + await flushPromises() + + expect(mockSetAsideRegistration).toHaveBeenCalledOnce() + expect(mockSetAsideRegistration).toHaveBeenCalledWith(activeReg.value.id) + }) + + it('should call unassignRegistration directly when the current user is the assignee', async () => { + activeHeader.value = { ...activeHeader.value, assignee: { username: 'examiner1' } } + + const wrapper = await mount() + + await wrapper.find('[data-testid="action-button-unassign"]').trigger('click') + await flushPromises() + + expect(mockUnassignRegistration).toHaveBeenCalledOnce() + expect(mockUnassignRegistration).toHaveBeenCalledWith(activeReg.value.id) + expect(mockOpenConfirmActionModal).not.toHaveBeenCalled() + }) + + it('should open confirm modal when unassign is clicked and the current user is not the assignee', async () => { + isAssignedToUser.value = false + activeHeader.value = { ...activeHeader.value, assignee: { username: 'another-examiner' } } + + const wrapper = await mount() + + await wrapper.find('[data-testid="action-button-unassign"]').trigger('click') + await flushPromises() + + expect(mockOpenConfirmActionModal).toHaveBeenCalledOnce() + expect(mockUnassignRegistration).not.toHaveBeenCalled() + }) + + it('should open confirm modal for APPROVE decision on an active registration', async () => { + activeReg.value = { + ...activeReg.value, + status: RegistrationStatus.ACTIVE, + conditionsOfApproval: { predefinedConditions: [], customConditions: null, minBookingDays: null } + } + decisionIntent.value = ApplicationActionsE.APPROVE + conditions.value = [{ condition: 'some-condition' }] as any + + const wrapper = await mount() + + await clickMainButton(wrapper) + await flushPromises() + + expect(mockOpenConfirmActionModal).toHaveBeenCalledOnce() + }) + + it('should update registration status with CANCELLED when cancel confirm is clicked', async () => { + decisionIntent.value = RegistrationActionsE.CANCEL + decisionEmailContent.value = { content: 'cancellation notice' } + + const wrapper = await mount() + expect(wrapper.find('[data-testid="main-action-button"]').text()).toContain('Cancel') + + await clickMainButton(wrapper) + await flushPromises() + + expect(mockOpenConfirmActionModal).toHaveBeenCalledOnce() + + getConfirmCallback()() + await flushPromises() + + expect(mockUpdateRegistrationStatus).toHaveBeenCalledOnce() + expect(mockUpdateRegistrationStatus).toHaveBeenCalledWith( + 'reg-123', + RegistrationStatus.CANCELLED, + 'cancellation notice' + ) + }) + + it('should update registration status with ACTIVE when approve confirm is clicked', async () => { + activeReg.value = { + ...activeReg.value, + status: RegistrationStatus.CANCELLED, + conditionsOfApproval: { predefinedConditions: [], customConditions: null, minBookingDays: null } + } + decisionIntent.value = ApplicationActionsE.APPROVE + conditions.value = [{ condition: 'some-condition' }] as any + decisionEmailContent.value = { content: 'approval email' } + + const wrapper = await mount() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(true) + + await clickMainButton(wrapper) + await flushPromises() + + expect(mockOpenConfirmActionModal).toHaveBeenCalledWith( + expect.stringContaining('Approve'), + expect.any(String), + expect.stringContaining('Approve'), + expect.any(Function), + expect.any(String) + ) + + getConfirmCallback()() + await flushPromises() + + expect(mockUpdateRegistrationStatus).toHaveBeenCalledOnce() + expect(mockUpdateRegistrationStatus).toHaveBeenCalledWith( + 'reg-123', + RegistrationStatus.ACTIVE, + 'approval email', + { predefinedConditions: conditions.value } + ) + }) + + it('should send notice and clear content when send notice confirm is clicked', async () => { + decisionIntent.value = ApplicationActionsE.SEND_NOC + decisionEmailContent.value = { content: 'notice of consideration body' } + + const wrapper = await mount() + + expect(wrapper.find('[data-testid="main-action-button"]').exists()).toBe(true) + + await clickMainButton(wrapper) + await flushPromises() + + expect(mockOpenConfirmActionModal).toHaveBeenCalledWith( + expect.stringContaining('Notice'), + expect.any(String), + expect.stringContaining('Send'), + expect.any(Function) + ) + + getConfirmCallback()() + await flushPromises() + + expect(mockSendNotice).toHaveBeenCalledOnce() + expect(mockSendNotice).toHaveBeenCalledWith('reg-123', 'notice of consideration body') + expect(decisionEmailContent.value.content).toBe('') + }) +}) diff --git a/strr-examiner-web/tests/unit/approval-conditions.spec.ts b/strr-examiner-web/tests/unit/approval-conditions.spec.ts new file mode 100644 index 000000000..989468eae --- /dev/null +++ b/strr-examiner-web/tests/unit/approval-conditions.spec.ts @@ -0,0 +1,337 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { flushPromises } from '@vue/test-utils' +import { ref } from 'vue' +import { enI18n } from '../mocks/i18n' +import ApprovalConditions from '~/components/ApprovalConditions.vue' + +const mockIsMainActionDisabled = ref(false) + +vi.mock('@/composables/useExaminerDecision', () => ({ + useExaminerDecision: () => ({ + preDefinedConditions: [ + 'principalResidence', + 'validBL', + 'class9FarmLand', + 'partOfStrataHotel', + 'fractionalOwnership' + ], + isMainActionDisabled: mockIsMainActionDisabled + }) +})) + +const defaultProps = () => ({ + conditions: [] as string[], + customCondition: '', + minBookingDays: null as number | null +}) + +describe('Approval Conditions Component', () => { + beforeEach(() => { + vi.clearAllMocks() + mockIsMainActionDisabled.value = false + }) + + it('should render the approval conditions select menu', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + expect(wrapper.find('[data-testid="approval-conditions"]').exists()).toBe(true) + }) + + it('should show open custom condition button and open min booking days button', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + expect(wrapper.find('[data-testid="open-custom-condition-button"]').exists()).toBe(true) + expect(wrapper.find('[data-testid="open-min-book-days-button"]').exists()).toBe(true) + }) + + describe('custom conditions', () => { + it('should open custom condition form when button is clicked', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(false) + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(true) + }) + + it('should disable main action when custom condition form is open', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + + expect(mockIsMainActionDisabled.value).toBe(true) + }) + + it('should show error when adding empty custom condition', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + + await wrapper.find('[data-testid="add-custom-condition-button"]').trigger('click') + await flushPromises() + + // custom condition form should still be open (error state) + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(true) + // no customCondition update emitted + const emitted = wrapper.emitted('update:customCondition') + expect(emitted).toBeUndefined() + }) + + it('should show error when custom condition text exceeds 256 characters', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + + const textarea = wrapper.find('[data-testid="custom-condition-input"]') + await textarea.setValue('a'.repeat(257)) + await flushPromises() + + await wrapper.find('[data-testid="add-custom-condition-button"]').trigger('click') + await flushPromises() + + // form should still be open + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(true) + const emitted = wrapper.emitted('update:customCondition') + expect(emitted).toBeUndefined() + }) + + it('should emit update:customCondition and close form when valid text is added', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + + const textarea = wrapper.find('[data-testid="custom-condition-input"]') + await textarea.setValue('My custom condition text') + await flushPromises() + + await wrapper.find('[data-testid="add-custom-condition-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(false) + + const emitted = wrapper.emitted('update:customCondition') + expect(emitted).toBeDefined() + expect(emitted![0]).toEqual(['My custom condition text']) + }) + + it('should clear customCondition and close form when remove is clicked', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(true) + + await wrapper.find('[data-testid="remove-custom-condition-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="custom-condition"]').exists()).toBe(false) + + const emitted = wrapper.emitted('update:customCondition') + expect(emitted).toBeUndefined() + }) + + it('should re-enable main action when custom condition form is closed', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-custom-condition-button"]').trigger('click') + await flushPromises() + expect(mockIsMainActionDisabled.value).toBe(true) + + await wrapper.find('[data-testid="remove-custom-condition-button"]').trigger('click') + await flushPromises() + expect(mockIsMainActionDisabled.value).toBe(false) + }) + + it('should disable open-custom-condition-button when 3 custom conditions are already selected', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: { + conditions: ['custom1', 'custom2', 'custom3'], + customCondition: '', + minBookingDays: null + } + }) + await flushPromises() + + const btn = wrapper.find('[data-testid="open-custom-condition-button"]') + expect(btn.attributes('disabled')).toBeDefined() + }) + + it('should emit update conditions without predefined condition that were removed', async () => { + const props = { + conditions: ['principalResidence', 'validBL'], + customCondition: '', + minBookingDays: null + } + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props + }) + await flushPromises() + + // click the close icon on the first badge (principalResidence) + await wrapper.findAll('[data-testid="remove-condition-button"]')[0]!.trigger('click') + await flushPromises() + + const emitted = wrapper.emitted('update:conditions') + expect(emitted).toBeUndefined() + expect(props.conditions).not.toContain('principalResidence') + expect(props.conditions).toContain('validBL') + }) + }) + + describe('min booking days', () => { + it('should open min booking days form when button is clicked', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + expect(wrapper.find('[data-testid="min-booking-days"]').exists()).toBe(false) + + await wrapper.find('[data-testid="open-min-book-days-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="min-booking-days"]').exists()).toBe(true) + }) + + it('should disable main action when min booking days form is open', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-min-book-days-button"]').trigger('click') + await flushPromises() + + expect(mockIsMainActionDisabled.value).toBe(true) + }) + + it('should emit updates and close form when valid min booking days is added', async () => { + const props = defaultProps() + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props + }) + await flushPromises() + + await wrapper.find('[data-testid="open-min-book-days-button"]').trigger('click') + await flushPromises() + + // default value is 28 — valid; click add + await wrapper.find('[data-testid="add-min-book-days-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="min-booking-days"]').exists()).toBe(false) + + const minBookingDaysEmit = wrapper.emitted('update:minBookingDays') + expect(minBookingDaysEmit).toBeDefined() + expect(minBookingDaysEmit![0]).toEqual([28]) + + expect(wrapper.emitted('update:conditions')).toBeUndefined() + expect(props.conditions).toContain('minBookingDays') + }) + + it('should close form and reset min booking days when remove is clicked', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: { + conditions: [], + customCondition: '', + minBookingDays: 14 + } + }) + await flushPromises() + + // open the form first + await wrapper.find('[data-testid="open-min-book-days-button"]').trigger('click') + await flushPromises() + + await wrapper.find('[data-testid="remove-min-book-days-button"]').trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="min-booking-days"]').exists()).toBe(false) + + const minBookingDaysEmit = wrapper.emitted('update:minBookingDays') + expect(minBookingDaysEmit).toBeDefined() + // should have been reset to null + const lastEmit = minBookingDaysEmit![minBookingDaysEmit!.length - 1]![0] + expect(lastEmit).toBeNull() + }) + + it('should disable open-min-book-days-button when minBookingDays is already added', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: { + conditions: ['minBookingDays'], + customCondition: '', + minBookingDays: 28 + } + }) + await flushPromises() + + const btn = wrapper.find('[data-testid="open-min-book-days-button"]') + expect(btn.attributes('disabled')).toBeDefined() + }) + + it('should re-enable main action when min booking days form is closed via remove', async () => { + const wrapper = await mountSuspended(ApprovalConditions, { + global: { plugins: [enI18n] }, + props: defaultProps() + }) + await flushPromises() + + await wrapper.find('[data-testid="open-min-book-days-button"]').trigger('click') + await flushPromises() + expect(mockIsMainActionDisabled.value).toBe(true) + + await wrapper.find('[data-testid="remove-min-book-days-button"]').trigger('click') + await flushPromises() + expect(mockIsMainActionDisabled.value).toBe(false) + }) + }) +}) diff --git a/strr-examiner-web/tests/unit/platform-details-view.spec.ts b/strr-examiner-web/tests/unit/platform-details-view.spec.ts new file mode 100644 index 000000000..d14bd2569 --- /dev/null +++ b/strr-examiner-web/tests/unit/platform-details-view.spec.ts @@ -0,0 +1,54 @@ +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { describe, it, expect, vi } from 'vitest' +import { mockPlatformRegistration } from '../mocks/mockedData' +import { enI18n } from '../mocks/i18n' +import PlatformDetailsView from '~/components/PlatformDetailsView.vue' + +const isApplication = ref(false) +const activeReg = ref(mockPlatformRegistration) + +vi.mock('@/stores/examiner', () => ({ + useExaminerStore: () => ({ + isApplication, + activeHeader: ref(mockPlatformRegistration.header), + activeReg + }) +})) + +const mockPlatformApplicationReg = { + ...mockPlatformRegistration, + completingParty: { + firstName: 'Alice', + lastName: 'Johnson', + emailAddress: 'alice@example.com', + phoneNumber: '412345678' + } +} + +describe('Platform Details view', () => { + it('should render platform representative and business details for a registration', async () => { + isApplication.value = false + activeReg.value = mockPlatformRegistration + + const wrapper = await mountSuspended(PlatformDetailsView, { global: { plugins: [enI18n] } }) + + const text = wrapper.text() + expect(text).toContain('Bob Jones') // platform representative name + expect(text).toContain('bob.jones@example.com') // representative email + expect(text).toContain('Test Platform Inc.') // business legal name + expect(wrapper.find('dl').exists()).toBe(true) + expect(text).not.toContain('Completing Party') // hidden for registrations + }) + + it('should show completing party details when viewing an application', async () => { + isApplication.value = true + activeReg.value = mockPlatformApplicationReg + + const wrapper = await mountSuspended(PlatformDetailsView, { global: { plugins: [enI18n] } }) + + const text = wrapper.text() + expect(text).toContain('Completing Party') + expect(text).toContain('Alice Johnson') + expect(text).toContain('alice@example.com') + }) +}) diff --git a/strr-examiner-web/tests/unit/snapshot-details.spec.ts b/strr-examiner-web/tests/unit/snapshot-details.spec.ts new file mode 100644 index 000000000..d30723069 --- /dev/null +++ b/strr-examiner-web/tests/unit/snapshot-details.spec.ts @@ -0,0 +1,49 @@ +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { describe, it, expect, vi, beforeAll } from 'vitest' +import { mockHostRegistration, mockSnapshots } from '../mocks/mockedData' +import { enI18n } from '../mocks/i18n' +import SnapshotDetails from '~/pages/registration/[registrationId]/snapshots/[snapshotId].vue' +import { ApplicationDetailsView, RegistrationInfoHeader } from '#components' + +const mockSnapshotResponse = { + ...mockSnapshots[0], + snapshotData: mockHostRegistration +} + +vi.mock('@/stores/examiner', () => ({ + useExaminerStore: () => ({ + getSnapshotById: vi.fn().mockResolvedValue(mockSnapshotResponse), + activeRecord: ref(mockHostRegistration), + activeReg: ref(mockHostRegistration), + activeHeader: ref(mockHostRegistration.header), + isApplication: ref(false), + snapshotInfo: ref(mockSnapshots[0]), + isFilingHistoryOpen: ref(false), + isEditingRentalUnit: ref(false), + hasUnsavedRentalUnitChanges: ref(false), + startEditRentalUnitAddress: vi.fn(), + resetEditRentalUnitAddress: vi.fn() + }) +})) + +describe('Snapshot Details Page', () => { + let wrapper: any + + beforeAll(async () => { + wrapper = await mountSuspended(SnapshotDetails, { + global: { plugins: [enI18n] } + }) + await nextTick() + }) + + it('should render Snapshot Details page', () => { + expect(wrapper.exists()).toBe(true) + expect(wrapper.findComponent(ApplicationDetailsView).exists()).toBe(true) + expect(wrapper.findComponent(RegistrationInfoHeader).exists()).toBe(true) + }) + + it('should render RegistrationInfoHeader inside ApplicationDetailsView', () => { + const appDetailsView = wrapper.findComponent(ApplicationDetailsView) + expect(appDetailsView.findComponent(RegistrationInfoHeader).exists()).toBe(true) + }) +}) diff --git a/strr-examiner-web/tests/unit/store-document.spec.ts b/strr-examiner-web/tests/unit/store-document.spec.ts new file mode 100644 index 000000000..2ae581518 --- /dev/null +++ b/strr-examiner-web/tests/unit/store-document.spec.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mockNuxtImport } from '@nuxt/test-utils/runtime' +import { setActivePinia, createPinia } from 'pinia' +import { mockHostRegistration, mockDocuments } from '../mocks/mockedData' +import { DocumentUploadType } from '#imports' + +const mockStrrApi = vi.fn().mockResolvedValue({}) +const mockOpenErrorModal = vi.fn() + +mockNuxtImport('useNuxtApp', () => () => ({ + $i18n: { t: (key: string) => key }, + $strrApi: mockStrrApi +})) + +mockNuxtImport('useStrrModals', () => () => ({ openErrorModal: mockOpenErrorModal })) + +describe('Document Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + mockStrrApi.mockResolvedValue({}) + }) + + it('should have correct state when opening PR upload', () => { + const store = useExaminerDocumentStore() + + store.openBlUpload() // set BL state first + store.openPrUpload() + + expect(store.isPrUploadOpen).toBe(true) + expect(store.isBlUploadOpen).toBe(false) + expect(store.uploadSectionType).toBe('PR') + expect(store.selectedDocType).toBeUndefined() + expect(store.docTypeOptions).toHaveLength(17) // all options returned for PR + }) + + it('should reset all upload state when closing upload section', () => { + const store = useExaminerDocumentStore() + + store.openPrUpload() + store.closeUpload() + + expect(store.isPrUploadOpen).toBe(false) + expect(store.isBlUploadOpen).toBe(false) + expect(store.uploadSectionType).toBeUndefined() + expect(store.selectedDocType).toBeUndefined() + }) + + it('should return only Business License for docTypeOptions when upload section is BL', () => { + const store = useExaminerDocumentStore() + + store.openBlUpload() + + expect(store.docTypeOptions).toHaveLength(1) + expect(store.docTypeOptions[0]!.value).toBe(DocumentUploadType.LOCAL_GOVT_BUSINESS_LICENSE) + }) + + it('should show error modal when api fails', async () => { + const exStore = useExaminerStore() + exStore.activeRecord = { ...mockHostRegistration, documents: [...mockDocuments] } + + const docStore = useExaminerDocumentStore() + const apiError = new Error('Upload failed') + mockStrrApi.mockRejectedValueOnce(apiError) + + const uiDoc = { + file: new File(['content'], 'utility-bill.pdf', { type: 'application/pdf' }), + type: DocumentUploadType.UTILITY_BILL, + loading: false + } as UiDocument + + await expect(docStore.addDocumentToRegistration(uiDoc, 12345)).rejects.toThrow('Upload failed') + + expect(mockOpenErrorModal).toHaveBeenCalledWith( + 'error.docUpload.generic.title', + 'error.docUpload.generic.description', + false + ) + expect(uiDoc.loading).toBe(false) + expect(exStore.activeReg!.documents).toHaveLength(mockDocuments.length) // no doc appended + }) + + it('should append document to activeReg when adding document to registration', async () => { + const exStore = useExaminerStore() + exStore.activeRecord = { ...mockHostRegistration, documents: [...mockDocuments] } + + const docStore = useExaminerDocumentStore() + const registrationId = 12345 + + const apiDocResponse = { + documentType: DocumentUploadType.UTILITY_BILL, + fileKey: 'file-key-123', + fileName: 'utility-bill.pdf', + fileType: 'application/pdf', + uploadDate: '2026-03-13', + addedOn: '2026-03-13' + } + mockStrrApi.mockResolvedValueOnce(apiDocResponse) + + const uiDoc = { + file: new File(['content'], 'utility-bill.pdf', { type: 'application/pdf' }), + type: DocumentUploadType.UTILITY_BILL, + loading: false + } as UiDocument + + await docStore.addDocumentToRegistration(uiDoc, registrationId) + + expect(mockStrrApi).toHaveBeenCalledWith( + `/registrations/${registrationId}/documents`, + expect.objectContaining({ method: 'POST' }) + ) + expect(exStore.activeReg!.documents).toHaveLength(mockDocuments.length + 1) + expect(exStore.activeReg!.documents!.at(-1)).toMatchObject({ + fileKey: 'file-key-123', + documentType: DocumentUploadType.UTILITY_BILL + }) + }) +}) diff --git a/strr-examiner-web/tests/unit/store-examiner.spec.ts b/strr-examiner-web/tests/unit/store-examiner.spec.ts index 473d0da3b..de376a525 100644 --- a/strr-examiner-web/tests/unit/store-examiner.spec.ts +++ b/strr-examiner-web/tests/unit/store-examiner.spec.ts @@ -1,8 +1,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { mockNuxtImport } from '@nuxt/test-utils/runtime' -import { ref } from 'vue' +import { ref, nextTick } from 'vue' import { setActivePinia, createPinia } from 'pinia' -import { mockHostApplication, mockHostRegistration } from '../mocks/mockedData' +import { + mockHostApplication, + mockHostApplicationWithReviewer, + mockHostApplicationWithoutReviewer, + mockHostRegistration, + MOCK_UNIT_ADDRESS, + mockSnapshots +} from '../mocks/mockedData' import { ApplicationStatus, RegistrationStatus } from '#imports' // mock $strrApi accessed through useNuxtApp() @@ -17,10 +24,13 @@ mockNuxtImport('useStrrApi', () => () => ({ getAccountApplications: vi.fn() })) -const mockKcUser = ref({ userName: 'examiner1' }) +mockNuxtImport('useKeycloak', () => () => ({ + kcUser: ref({ userName: 'examiner1' }) +})) -mockNuxtImport('useKeycloak', () => () => ({ kcUser: mockKcUser })) -mockNuxtImport('useStrrModals', () => () => ({ openErrorModal: vi.fn() })) +mockNuxtImport('useStrrModals', () => () => ({ + openErrorModal: vi.fn() +})) vi.mock('@/composables/useExaminerFeatureFlags', () => ({ useExaminerFeatureFlags: () => ({ isSplitDashboardTableEnabled: ref(false) }) @@ -31,13 +41,12 @@ describe('Store - Examiner', () => { setActivePinia(createPinia()) vi.clearAllMocks() mockStrrApi.mockResolvedValue({}) - mockKcUser.value = { userName: 'examiner1' } }) it('should have correct application and registration records', () => { const store = useExaminerStore() - // no activeRecord set → both should be falsy + // no activeRecord expect(store.isApplication).toBe(false) expect(store.activeReg).toBeUndefined() @@ -61,7 +70,7 @@ describe('Store - Examiner', () => { header: { ...mockHostRegistration.header, applications: [{ - applicationNumber: 'APP-001', + applicationNumber: '0987654321', applicationStatus: ApplicationStatus.FULL_REVIEW, applicationDateTime: '2025-01-01T00:00:00Z' }] @@ -76,23 +85,354 @@ describe('Store - Examiner', () => { expect(header).toHaveProperty('examinerStatus', mockHostRegistration.header.examinerStatus) }) - it('processStatusFilters separates registration and application statuses for API calls', async () => { + it('should have correct status filter for application and registration in the query', async () => { const store = useExaminerStore() store.tableFilters.status = [RegistrationStatus.ACTIVE] as any await store.fetchApplications() - let query = mockStrrApi.mock.calls[0]![1].query - expect(query.status).toEqual([]) - expect(query.registrationStatus).toEqual([RegistrationStatus.ACTIVE]) + expect(mockStrrApi).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ + query: expect.objectContaining({ + status: [], + registrationStatus: [RegistrationStatus.ACTIVE] + }) + })) + + mockStrrApi.mockClear() + + store.tableFilters.status = [ApplicationStatus.FULL_REVIEW] as any + await store.fetchApplications() + + expect(mockStrrApi).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ + query: expect.objectContaining({ + status: [ApplicationStatus.FULL_REVIEW], + registrationStatus: [] + }) + })) + }) + + it('should correctly show assigned and unassigned examiner for active record', async () => { + const store = useExaminerStore() + + store.activeRecord = mockHostApplicationWithReviewer + await nextTick() + expect(store.isAssignedToUser).toBe(true) + + store.activeRecord = mockHostApplicationWithoutReviewer + await nextTick() + expect(store.isAssignedToUser).toBe(false) + }) + + it('should clear all table filters and reset to defaults', () => { + const store = useExaminerStore() + + store.tableFilters.searchText = 'Victoria' + store.tableFilters.propertyAddress = '123 Main St' + store.tableFilters.registrationNumber = 'H1234567890' + store.tableFilters.adjudicator = 'examiner1' + store.tableFilters.registrationType = [ApplicationType.HOST] as any + store.tableFilters.status = [ApplicationStatus.FULL_REVIEW] as any + store.tableFilters.localGov = 'Victoria' + + store.resetFilters() + + expect(store.tableFilters).toEqual({ + searchText: '', + registrationNumber: '', + registrationType: [], + requirements: [], + applicantName: '', + propertyAddress: '', + status: [], + submissionDate: { start: null, end: null }, + lastModified: { start: null, end: null }, + localGov: '', + adjudicator: '' + }) + }) + + it('should show compose noc email for FULL_REVIEW and PROVISIONAL_REVIEW statuses', () => { + const store = useExaminerStore() + + store.activeRecord = mockHostApplication // status: FULL_REVIEW + expect(store.showComposeNocEmail).toBe(true) + + store.activeRecord = { + ...mockHostApplication, + header: { ...mockHostApplication.header, status: ApplicationStatus.PROVISIONAL_REVIEW } + } + expect(store.showComposeNocEmail).toBe(true) + + store.activeRecord = { + ...mockHostApplication, + header: { ...mockHostApplication.header, status: ApplicationStatus.NOC_PENDING } + } + expect(store.showComposeNocEmail).toBe(false) + }) + + it('should show compose email for NOC_PENDING application and ACTIVE registration', () => { + const store = useExaminerStore() + + store.activeRecord = { + ...mockHostApplication, + header: { ...mockHostApplication.header, status: ApplicationStatus.NOC_PENDING } + } + expect(store.showComposeEmail).toBe(true) + + store.activeRecord = mockHostRegistration // status: ACTIVE, examinerActions: ['APPROVE', 'CANCEL', 'SUSPEND'] + expect(store.showComposeEmail).toBe(true) + + store.activeRecord = mockHostApplication // FULL_REVIEW application + expect(store.showComposeEmail).toBe(false) + }) + + it('should set correct default statuses and reset page', () => { + const store = useExaminerStore() + + store.tableFilters.searchText = 'test' + store.tablePage = 5 + + store.resetFiltersToApplicationsDefault() + expect(store.tableFilters.searchText).toBe('') + expect(store.tableFilters.status).toEqual(store.applicationsOnlyStatuses) + expect(store.tablePage).toBe(1) + + store.tableFilters.searchText = 'test' + store.tablePage = 5 + + store.resetFiltersToRegistrationsDefault() + expect(store.tableFilters.searchText).toBe('') + expect(store.tableFilters.status).toEqual(store.registrationsOnlyStatuses) + expect(store.tablePage).toBe(1) + }) + + it('should call correct endpoint and have correct query when searching by text', async () => { + const store = useExaminerStore() + + await store.fetchRegistrations() + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/search', expect.objectContaining({ + query: expect.objectContaining({ text: undefined }) + })) mockStrrApi.mockClear() + store.tableFilters.searchText = 'Vancouver' + await store.fetchRegistrations() + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/search', expect.objectContaining({ + query: expect.objectContaining({ text: 'Vancouver' }) + })) + }) + + it('should include all active table filters in the search query', async () => { + const store = useExaminerStore() + + store.tableFilters.searchText = 'Victoria' + store.tableFilters.propertyAddress = '123 Main St' + store.tableFilters.registrationNumber = 'H1234567890' + store.tableFilters.adjudicator = 'examiner1' + store.tableFilters.registrationType = [ApplicationType.HOST] as any store.tableFilters.status = [ApplicationStatus.FULL_REVIEW] as any await store.fetchApplications() - query = mockStrrApi.mock.calls[0]![1].query - expect(query.status).toEqual([ApplicationStatus.FULL_REVIEW]) - expect(query.registrationStatus).toEqual([]) + expect(mockStrrApi).toHaveBeenCalledWith('/applications/search', expect.objectContaining({ + query: { + limit: 50, + page: 1, + registrationType: [ApplicationType.HOST], + status: [ApplicationStatus.FULL_REVIEW], + registrationStatus: [], + text: 'Victoria', + sortOrder: 'asc', + sortBy: 'application_date', + address: '123 Main St', + recordNumber: 'H1234567890', + assignee: 'examiner1', + requirement: [] + } + })) + }) + + it('should call correct endpoint, set activeRecord, and return response on getApplicationById', async () => { + const store = useExaminerStore() + mockStrrApi.mockResolvedValueOnce(mockHostApplication) + + const response = await store.getApplicationById('1234567890') + + expect(mockStrrApi).toHaveBeenCalledWith('/applications/1234567890', expect.objectContaining({ + method: 'GET' + })) + expect(store.activeRecord).toEqual(mockHostApplication) + expect(response).toEqual(mockHostApplication) + }) + + it('should call correct endpoint, update activeRecord, and clear edit state on saveRentalUnitAddress', async () => { + const store = useExaminerStore() + const updatedAddress = { ...MOCK_UNIT_ADDRESS, city: 'Kelowna' } + const updatedRegistration = { ...mockHostRegistration, unitAddress: updatedAddress } + mockStrrApi.mockResolvedValueOnce(updatedRegistration) + + store.activeRecord = mockHostRegistration + store.startEditRentalUnitAddress() + store.hasUnsavedRentalUnitChanges = true + + await store.saveRentalUnitAddress(updatedAddress, 99, false) + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/99/str-address', + expect.objectContaining({ + method: 'PATCH', + body: { unitAddress: updatedAddress } + })) + expect(store.activeRecord).toEqual(updatedRegistration) + expect(store.isEditingRentalUnit).toBe(false) + expect(store.rentalUnitAddressToEdit).toEqual({}) + expect(store.hasUnsavedRentalUnitChanges).toBe(false) + + // application path uses applications endpoint + mockStrrApi.mockClear() + mockStrrApi.mockResolvedValueOnce({}) + await store.saveRentalUnitAddress(updatedAddress, '1234567890', true) + expect(mockStrrApi).toHaveBeenCalledWith('/applications/1234567890/str-address', expect.anything()) + }) + + it('should call correct endpoint on updateRegistrationStatus', async () => { + const store = useExaminerStore() + + await store.updateRegistrationStatus(42, RegistrationStatus.SUSPENDED) + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/42/status', + expect.objectContaining({ + method: 'PUT', + body: expect.objectContaining({ status: RegistrationStatus.SUSPENDED }) + })) + + expect(mockStrrApi).not.toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ body: expect.objectContaining({ emailContent: expect.anything() }) }) + ) + + mockStrrApi.mockClear() + + await store.updateRegistrationStatus(42, RegistrationStatus.CANCELLED, 'cancel notice email') + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/42/status', + expect.objectContaining({ + body: expect.objectContaining({ + status: RegistrationStatus.CANCELLED, + emailContent: 'cancel notice email' + }) + })) + }) + + it('should return correct events on success and error for application filing history', async () => { + const store = useExaminerStore() + const mockEvents = [{ id: 1, type: 'SUBMITTED' }, { id: 2, type: 'APPROVED' }] + mockStrrApi.mockResolvedValueOnce(mockEvents) + + const result = await store.getApplicationFilingHistory('1234567890') + expect(mockStrrApi).toHaveBeenCalledWith('/applications/1234567890/events', + expect.objectContaining({ method: 'GET' })) + expect(result).toEqual(mockEvents) + + mockStrrApi.mockClear() + mockStrrApi.mockRejectedValueOnce(new Error('network error')) + expect(await store.getApplicationFilingHistory('1234567890')).toEqual([]) + }) + + it('should return events on success and error for registration filing history', async () => { + const store = useExaminerStore() + const mockEvents = [{ id: 1, type: 'STATUS_CHANGED' }, { id: 2, type: 'SUSPENDED' }] + mockStrrApi.mockResolvedValueOnce(mockEvents) + + const result = await store.getRegistrationFilingHistory(99) + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/99/events', + expect.objectContaining({ method: 'GET' })) + expect(result).toEqual(mockEvents) + + mockStrrApi.mockClear() + mockStrrApi.mockRejectedValueOnce(new Error('network error')) + expect(await store.getRegistrationFilingHistory(99)).toEqual([]) + }) + + it('should call correct endpoint on getRegistrationById', async () => { + const store = useExaminerStore() + mockStrrApi.mockResolvedValueOnce(mockHostRegistration) + + const result = await store.getRegistrationById('99') + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/99', + expect.objectContaining({ method: 'GET' })) + expect(store.activeRecord).toEqual(mockHostRegistration) + expect(result).toEqual(mockHostRegistration) + }) + + it('should update application actions with correct status', async () => { + const store = useExaminerStore() + + await store.approveApplication('1234567890') + expect(mockStrrApi).toHaveBeenCalledWith('/applications/1234567890/status', expect.objectContaining({ + method: 'PUT', + body: expect.objectContaining({ status: ApplicationStatus.FULL_REVIEW_APPROVED }) + })) + + mockStrrApi.mockClear() + + await store.provisionallyApproveApplication('55544433322') + expect(mockStrrApi).toHaveBeenCalledWith('/applications/55544433322/status', expect.objectContaining({ + method: 'PUT', + body: expect.objectContaining({ status: ApplicationStatus.PROVISIONALLY_APPROVED }) + })) + + mockStrrApi.mockClear() + + await store.rejectApplication('9992223342', false) + expect(mockStrrApi).toHaveBeenCalledWith('/applications/9992223342/status', expect.objectContaining({ + method: 'PUT', + body: expect.objectContaining({ status: ApplicationStatus.DECLINED }) + })) + expect(mockStrrApi).not.toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ body: expect.objectContaining({ emailContent: expect.anything() }) }) + ) + + mockStrrApi.mockClear() + + await store.rejectApplication('APP-004', true, 'email body') + expect(mockStrrApi).toHaveBeenCalledWith('/applications/APP-004/status', expect.objectContaining({ + body: expect.objectContaining({ + status: ApplicationStatus.PROVISIONALLY_DECLINED, + emailContent: 'email body' + }) + })) + }) + + it('should have correct response for getSnapshotById', async () => { + const store = useExaminerStore() + mockStrrApi.mockResolvedValueOnce(mockSnapshots[0]) + + const result = await store.getSnapshotById('99', 'snapshot-1') + + expect(mockStrrApi).toHaveBeenCalledWith('/registrations/99/snapshots/snapshot-1', + expect.objectContaining({ method: 'GET' })) + expect(result).toEqual(mockSnapshots[0]) + }) + + it('should correctly edit and reset rental unit address', () => { + const store = useExaminerStore() + store.activeRecord = mockHostRegistration + + store.startEditRentalUnitAddress() + + expect(store.isEditingRentalUnit).toBe(true) + expect(store.rentalUnitAddressToEdit).toEqual(MOCK_UNIT_ADDRESS) + expect(store.hasUnsavedRentalUnitChanges).toBe(false) + + store.hasUnsavedRentalUnitChanges = true + store.resetEditRentalUnitAddress() + + expect(store.isEditingRentalUnit).toBe(false) + expect(store.rentalUnitAddressToEdit).toEqual({}) + expect(store.hasUnsavedRentalUnitChanges).toBe(false) }) }) diff --git a/strr-examiner-web/tests/unit/strata-application-details.spec.ts b/strr-examiner-web/tests/unit/strata-application-details.spec.ts index 85bfef4bc..bbcbce4bd 100644 --- a/strr-examiner-web/tests/unit/strata-application-details.spec.ts +++ b/strr-examiner-web/tests/unit/strata-application-details.spec.ts @@ -40,7 +40,7 @@ vi.mock('@/stores/examiner', () => ({ }) })) -describe('Examiner - Strata Application Details Page', () => { +describe('Strata Application Details Page', () => { let wrapper: any beforeAll(async () => { @@ -50,7 +50,7 @@ describe('Examiner - Strata Application Details Page', () => { await nextTick() }) - it('renders Application Details page and its components', () => { + it('should render Application Details page and its components', () => { expect(wrapper.exists()).toBe(true) expect(wrapper.findComponent(ApplicationInfoHeader).exists()).toBe(true) expect(wrapper.findComponent(HostSubHeader).exists()).toBe(false) @@ -59,7 +59,7 @@ describe('Examiner - Strata Application Details Page', () => { expect(wrapper.findComponent(PlatformSubHeader).exists()).toBe(false) }) - it('renders ApplicationInfoHeader component for Strata application', () => { + it('should render ApplicationInfoHeader component for Strata application', () => { const appHeaderInfo = wrapper.findComponent(ApplicationInfoHeader) expect(appHeaderInfo.exists()).toBe(true) expect(appHeaderInfo.findComponent(UBadge).exists()).toBe(true) @@ -71,62 +71,43 @@ describe('Examiner - Strata Application Details Page', () => { expect(appHeaderInfoText).toContain('Strata Hotel') }) - it('renders Strata SubHeader component for Strata application', () => { + it('should render Strata SubHeader component for Strata application', () => { const strataSubHeader = wrapper.findComponent(StrataSubHeader) expect(strataSubHeader.exists()).toBe(true) const { registration } = mockStrataApplication const strataSubHeaderText = strataSubHeader.text() - if (registration.businessDetails) { - expect(strataSubHeaderText).toContain(registration.businessDetails.legalName) - - if (registration.businessDetails.mailingAddress) { - expect(strataSubHeaderText).toContain(registration.businessDetails.mailingAddress.address) - } - - const attorney = registration.businessDetails.registeredOfficeOrAttorneyForServiceDetails - if (attorney) { - if (attorney.attorneyName) { - expect(strataSubHeaderText).toContain(attorney.attorneyName) - } - - if (attorney.mailingAddress) { - expect(strataSubHeaderText).toContain(attorney.mailingAddress.address) - } - } - } - - if (registration.strataHotelRepresentatives && registration.strataHotelRepresentatives.length > 0) { - const rep = registration.strataHotelRepresentatives[0] - if (rep.firstName) { - expect(strataSubHeaderText).toContain(rep.firstName) - } - if (rep.emailAddress) { - expect(strataSubHeaderText).toContain(rep.emailAddress) - } - } - - if (registration.completingParty) { - if (registration.completingParty.firstName) { - expect(strataSubHeaderText).toContain(registration.completingParty.firstName) - } - if (registration.completingParty.lastName) { - expect(strataSubHeaderText).toContain(registration.completingParty.lastName) - } - } + const { businessDetails, strataHotelRepresentatives, completingParty, strataHotelDetails } = registration + const attorney = businessDetails!.registeredOfficeOrAttorneyForServiceDetails! + const rep = strataHotelRepresentatives![0]! + + expect(strataSubHeaderText).toContain(businessDetails!.legalName) + expect(strataSubHeaderText).toContain(businessDetails!.mailingAddress!.address) + expect(strataSubHeaderText).toContain(attorney.attorneyName) + expect(strataSubHeaderText).toContain(attorney.mailingAddress!.address) + expect(strataSubHeaderText).toContain(rep.firstName) + expect(strataSubHeaderText).toContain(rep.emailAddress) + expect(strataSubHeaderText).toContain(completingParty!.firstName) + expect(strataSubHeaderText).toContain(completingParty!.lastName) + expect(strataSubHeaderText).toContain(strataHotelDetails!.numberOfUnits!.toString()) + expect(strataSubHeaderText).toContain(strataHotelDetails!.category) + }) - if (registration.strataHotelDetails) { - if (registration.strataHotelDetails.numberOfUnits !== undefined) { - expect(strataSubHeaderText).toContain(registration.strataHotelDetails.numberOfUnits.toString()) - } - if (registration.strataHotelDetails.category) { - expect(strataSubHeaderText).toContain(registration.strataHotelDetails.category) - } - } + it('should render Strata SubHeader with correct structure and primary building location', () => { + const strataSubHeader = wrapper.findComponent(StrataSubHeader) + expect(strataSubHeader.findTestId('strata-sub-header').exists()).toBe(true) + expect(strataSubHeader.findTestId('strata-primary-building').exists()).toBe(true) + expect(strataSubHeader.findTestId('strata-business').exists()).toBe(true) + expect(strataSubHeader.findTestId('strata-attorney').exists()).toBe(true) + + const primaryBuilding = strataSubHeader.findTestId('strata-primary-building') + const { location } = mockStrataApplication.registration.strataHotelDetails + expect(primaryBuilding.text()).toContain(location.address) + expect(primaryBuilding.text()).toContain(location.city) }) - it('hides NOC email and disables action buttons when isAssignedToUser is false', async () => { + it('should hide NOC email and disable action buttons when isAssignedToUser is false', async () => { isAssignedToUser.value = false await nextTick() expect(wrapper.findTestId('compose-email').exists()).toBe(false) diff --git a/strr-examiner-web/tests/unit/use-examiner-actions.spec.ts b/strr-examiner-web/tests/unit/use-examiner-actions.spec.ts new file mode 100644 index 000000000..6fd27cc82 --- /dev/null +++ b/strr-examiner-web/tests/unit/use-examiner-actions.spec.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mockNuxtImport } from '@nuxt/test-utils/runtime' +import { ApplicationActionsE, RegistrationActionsE } from '#imports' + +const mockHandleButtonLoading = vi.fn() +const mockOpenErrorModal = vi.fn() +const mockTranslation = vi.fn((key: string) => key) + +mockNuxtImport('useButtonControl', () => () => ({ + handleButtonLoading: mockHandleButtonLoading +})) + +mockNuxtImport('useStrrModals', () => () => ({ + openConfirmActionModal: vi.fn(), + openErrorModal: mockOpenErrorModal, + close: vi.fn() +})) + +mockNuxtImport('useNuxtApp', () => () => ({ + $i18n: { t: mockTranslation } +})) + +describe('useExaminerActions', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should correctly set button loading, args, refresh, reset states', async () => { + const { manageAction } = useExaminerActions() + const actionFn = vi.fn().mockResolvedValue(undefined) + const refresh = vi.fn() + + await manageAction({ id: 42 }, RegistrationActionsE.CANCEL, actionFn, 'left', 1, refresh, ['extra', 'args'] as any) + + expect(mockHandleButtonLoading).toHaveBeenNthCalledWith(1, false, 'left', 1) + expect(actionFn).toHaveBeenCalledWith(42, 'extra', 'args') + expect(refresh).toHaveBeenCalledOnce() + expect(mockHandleButtonLoading).toHaveBeenNthCalledWith(2, true) + expect(mockOpenErrorModal).not.toHaveBeenCalled() + }) + + it('should skip actionFn and refresh when validateFn returns false, and proceed when true', async () => { + const { manageAction } = useExaminerActions() + const actionFn = vi.fn().mockResolvedValue(undefined) + const refresh = vi.fn() + const item = { id: '1234567890' } + + const validateFn = vi.fn().mockResolvedValue(false) + await manageAction(item, ApplicationActionsE.REJECT, actionFn, 'right', 0, refresh, [] as any, validateFn) + + expect(validateFn).toHaveBeenCalledOnce() + expect(actionFn).not.toHaveBeenCalled() + expect(refresh).not.toHaveBeenCalled() + expect(mockHandleButtonLoading).toHaveBeenCalledWith(true) + + vi.clearAllMocks() + + const validateTrue = vi.fn().mockResolvedValue(true) + await manageAction(item, ApplicationActionsE.REJECT, actionFn, 'right', 0, refresh, [] as any, validateTrue) + + expect(actionFn).toHaveBeenCalledWith('1234567890') + expect(refresh).toHaveBeenCalledOnce() + }) + + it('should open error modal with action key and reset loading on error', async () => { + const { manageAction } = useExaminerActions() + const item = { id: '1234567890' } + const refresh = vi.fn() + + const actionFn = vi.fn().mockRejectedValue(new Error('action failed')) + await manageAction(item, ApplicationActionsE.REJECT, actionFn, 'right', 0, refresh) + + expect(refresh).not.toHaveBeenCalled() + expect(mockTranslation).toHaveBeenCalledWith('error.action.reject') + expect(mockOpenErrorModal).toHaveBeenCalledWith('Error', 'error.action.reject', false) + expect(mockHandleButtonLoading).toHaveBeenCalledWith(true) + }) +}) From a94dadcf8f07b2af3c05227b4d59d8b7def95157 Mon Sep 17 00:00:00 2001 From: Dima K Date: Mon, 16 Mar 2026 14:24:20 -0700 Subject: [PATCH 3/3] chore: update version --- strr-examiner-web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strr-examiner-web/package.json b/strr-examiner-web/package.json index 9931306ee..cd9394909 100644 --- a/strr-examiner-web/package.json +++ b/strr-examiner-web/package.json @@ -2,7 +2,7 @@ "name": "strr-examiner-web", "private": true, "type": "module", - "version": "0.2.24", + "version": "0.2.25", "scripts": { "build-check": "nuxt build", "build": "nuxt generate",