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/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",
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/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
new file mode 100644
index 000000000..de376a525
--- /dev/null
+++ b/strr-examiner-web/tests/unit/store-examiner.spec.ts
@@ -0,0 +1,438 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { mockNuxtImport } from '@nuxt/test-utils/runtime'
+import { ref, nextTick } from 'vue'
+import { setActivePinia, createPinia } from 'pinia'
+import {
+ mockHostApplication,
+ mockHostApplicationWithReviewer,
+ mockHostApplicationWithoutReviewer,
+ mockHostRegistration,
+ MOCK_UNIT_ADDRESS,
+ mockSnapshots
+} 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()
+}))
+
+mockNuxtImport('useKeycloak', () => () => ({
+ kcUser: ref({ userName: 'examiner1' })
+}))
+
+mockNuxtImport('useStrrModals', () => () => ({
+ openErrorModal: vi.fn()
+}))
+
+vi.mock('@/composables/useExaminerFeatureFlags', () => ({
+ useExaminerFeatureFlags: () => ({ isSplitDashboardTableEnabled: ref(false) })
+}))
+
+describe('Store - Examiner', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ vi.clearAllMocks()
+ mockStrrApi.mockResolvedValue({})
+ })
+
+ it('should have correct application and registration records', () => {
+ const store = useExaminerStore()
+
+ // no activeRecord
+ 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: '0987654321',
+ 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('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()
+
+ 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()
+
+ 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)
+ })
+})