Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions strr-host-pm-web/app/stores/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ export const useDocumentStore = defineStore('host/document', () => {
id: string | number, // string for applications, number for registrations
type: 'applications' | 'registrations'
): Promise<void> {
const documentContext = storedDocuments.value
try {
uiDoc.loading = true

Expand All @@ -427,6 +428,9 @@ export const useDocumentStore = defineStore('host/document', () => {
body: formData
})

// A record load or reset replaces the document list while this request is pending.
if (storedDocuments.value !== documentContext) { return }

// Upload endpoints return the updated record, including the new document's unique file key.
const documents = 'registration' in res ? res.registration.documents : res.documents
const uploadedDocument = documents.find(doc =>
Expand All @@ -436,8 +440,10 @@ export const useDocumentStore = defineStore('host/document', () => {
if (uiDoc.uploadDate) { uploadedDocument.uploadDate = uiDoc.uploadDate }
storedDocuments.value.push(uiDoc)
} catch (e) {
logFetchError(e, 'Error uploading document')
strrModal.openErrorModal(t('error.docUpload.generic.title'), t('error.docUpload.generic.description'), false)
if (storedDocuments.value === documentContext) {
logFetchError(e, 'Error uploading document')
strrModal.openErrorModal(t('error.docUpload.generic.title'), t('error.docUpload.generic.description'), false)
}
throw e
} finally {
// cleanup loading on ui object
Expand Down
159 changes: 159 additions & 0 deletions strr-host-pm-web/tests/unit/document-upload-context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { mountSuspended, mockNuxtImport, registerEndpoint } from '@nuxt/test-utils/runtime'
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mockApplication, mockHostRegistration } from '../mocks/mockedData'
import SupportingInfo from '~/components/summary/SupportingInfo.vue'

const api = vi.fn()
const getApplication = vi.fn()
const getRegistration = vi.fn()
const openErrorModal = vi.fn()

mockNuxtImport('useRuntimeConfig', original => () => {
const config = original()
return { ...config, public: { ...config.public, strrApiURL: '' } }
})
registerEndpoint('/applications/APP-101/documents', { method: 'PUT', handler: () => api() })
registerEndpoint('/registrations/101/documents', { method: 'POST', handler: () => api() })
mockNuxtImport('useStrrApi', () => () => ({
getAccountApplication: getApplication,
getAccountRegistrations: getRegistration,
searchRegistrations: vi.fn(),
getApplicationReceipt: vi.fn(),
getRegistrationCert: vi.fn(),
updatePaymentDetails: vi.fn()
}))
mockNuxtImport('useStrrModals', () => () => ({ openErrorModal }))

enableAutoUnmount(afterEach)

function document (name: string): ApiDocument {
return {
fileName: `${name}.pdf`,
fileKey: `key-${name}`,
fileType: 'application/pdf',
documentType: DocumentUploadType.UTILITY_BILL
}
}

function incoming (): UiDocument {
const file = new File(['synthetic'], 'incoming-A.pdf', { type: 'application/pdf' })
return {
id: 'incoming-A',
file,
name: file.name,
type: DocumentUploadType.UTILITY_BILL,
apiDoc: {} as ApiDocument,
loading: false,
uploadStep: DocumentUploadStep.NOC
}
}

async function load (kind: 'application' | 'registration', id: number) {
const permit = useHostPermitStore()
if (kind === 'application') {
const value = structuredClone(mockApplication)
value.header.applicationNumber = `APP-${id}`
value.registration.documents = [document(`existing-${id}`)]
getApplication.mockResolvedValueOnce(value)
await permit.loadHostData(`APP-${id}`, false, true)
} else {
const value = structuredClone(mockHostRegistration)
value.id = id
value.registrationNumber = `REG-${id}`
value.documents = [document(`existing-${id}`)]
getRegistration.mockResolvedValueOnce(value)
await permit.loadHostRegistrationData(String(id))
}
}

function result (kind: 'application' | 'registration') {
const documents = [document('existing-101'), document('incoming-A')]
return kind === 'application' ? { registration: { documents } } : { documents }
}

function upload (kind: 'application' | 'registration', doc: UiDocument) {
const store = useDocumentStore()
return kind === 'application'
? store.addDocumentToApplication(doc, 'APP-101')
: store.addDocumentToRegistration(doc, 101)
}

let pinia: ReturnType<typeof createPinia>
beforeEach(() => {
pinia = createPinia()
setActivePinia(pinia)
vi.resetAllMocks()
})

describe.each(['application', 'registration'] as const)('%s upload document context', (kind) => {
it('adds the returned file while the same record remains loaded', async () => {
await load(kind, 101)
const doc = incoming()
api.mockResolvedValueOnce(result(kind))
await upload(kind, doc)
expect(doc.apiDoc.fileKey).toBe('key-incoming-A')
expect(useDocumentStore().apiDocuments.map(value => value.fileKey))
.toEqual(['key-existing-101', 'key-incoming-A'])
expect(doc.loading).toBe(false)
})

it('reports a failed upload while the same record remains loaded', async () => {
await load(kind, 101)
const doc = incoming()
api.mockRejectedValue(new Error('Synthetic active upload failure'))
await expect(upload(kind, doc)).rejects.toBeInstanceOf(Error)
expect(useDocumentStore().apiDocuments.map(value => value.fileKey)).toEqual(['key-existing-101'])
expect(openErrorModal).toHaveBeenCalledOnce()
expect(doc.loading).toBe(false)
})

it('does not restore documents after the current record has been reset', async () => {
await load(kind, 101)
const pending = Promise.withResolvers<ReturnType<typeof result>>()
api.mockReturnValue(pending.promise)
const doc = incoming()
const request = upload(kind, doc)
await vi.waitFor(() => expect(api).toHaveBeenCalled())
useHostPermitStore().$reset()
pending.resolve(result(kind))
await request
expect(useDocumentStore().storedDocuments).toEqual([])
expect(doc.loading).toBe(false)
})

it('keeps a newer record and its rendered documents after an older upload succeeds', async () => {
await load(kind, 101)
const pending = Promise.withResolvers<ReturnType<typeof result>>()
api.mockReturnValue(pending.promise)
const doc = incoming()
const request = upload(kind, doc)
await vi.waitFor(() => expect(api).toHaveBeenCalled())
await load(kind, 202)
const wrapper = await mountSuspended(SupportingInfo, { props: { isDashboard: true }, global: { plugins: [pinia] } })
expect(wrapper.text()).toContain('existing-202.pdf')
pending.resolve(result(kind))
await request
await flushPromises()
expect(useDocumentStore().apiDocuments.map(value => value.fileKey)).toEqual(['key-existing-202'])
expect(wrapper.text()).not.toContain('incoming-A.pdf')
expect(doc.loading).toBe(false)
})

it('keeps a newer record free of an older upload error dialog', async () => {
await load(kind, 101)
const pending = Promise.withResolvers<ReturnType<typeof result>>()
api.mockReturnValue(pending.promise)
const doc = incoming()
const request = upload(kind, doc).catch(error => error)
await vi.waitFor(() => expect(api).toHaveBeenCalled())
await load(kind, 202)
const failure = new Error('Synthetic old upload failure')
pending.reject(failure)
expect(await request).toBeInstanceOf(Error)
expect(useDocumentStore().apiDocuments.map(value => value.fileKey)).toEqual(['key-existing-202'])
expect(openErrorModal).not.toHaveBeenCalled()
expect(doc.loading).toBe(false)
})
})
10 changes: 8 additions & 2 deletions strr-strata-web/app/stores/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const useDocumentStore = defineStore('strata/document', () => {
* @returns {Promise<void>} A promise that resolves when the document has been added or rejects if an error occurs.
*/
async function addDocumentToApplication (uiDoc: UiDocument, applicationNumber: string): Promise<void> {
const documentContext = storedDocuments.value
try {
uiDoc.loading = true

Expand All @@ -77,12 +78,17 @@ export const useDocumentStore = defineStore('strata/document', () => {
body: formData
})

// A record load or reset replaces the document list while this request is pending.
if (storedDocuments.value !== documentContext) { return }

uiDoc.apiDoc = res.registration.documents!.find(doc =>
!storedDocuments.value.some(stored => stored.apiDoc.fileKey === doc.fileKey))!
storedDocuments.value.push(uiDoc)
} catch (e) {
logFetchError(e, 'Error uploading document')
strrModal.openErrorModal(t('error.docUpload.generic.title'), t('error.docUpload.generic.description'), false)
if (storedDocuments.value === documentContext) {
logFetchError(e, 'Error uploading document')
strrModal.openErrorModal(t('error.docUpload.generic.title'), t('error.docUpload.generic.description'), false)
}
throw e
} finally {
// cleanup loading on ui object
Expand Down
146 changes: 146 additions & 0 deletions strr-strata-web/tests/unit/document-upload-context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { mountSuspended, mockNuxtImport, registerEndpoint } from '@nuxt/test-utils/runtime'
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mockPermitDetailsData } from '../mocks/mockedData'
import DocumentList from '~/components/document/list/index.vue'

const api = vi.fn()
const getApplication = vi.fn()
const getRegistration = vi.fn()
const openErrorModal = vi.fn()

mockNuxtImport('useRuntimeConfig', original => () => {
const config = original()
return { ...config, public: { ...config.public, strrApiURL: '' } }
})
registerEndpoint('/applications/APP-101/documents', { method: 'PUT', handler: () => api() })
mockNuxtImport('useStrrApi', () => () => ({
getAccountApplication: getApplication,
getAccountRegistrations: getRegistration,
searchRegistrations: vi.fn(),
getApplicationReceipt: vi.fn(),
getRegistrationCert: vi.fn(),
updatePaymentDetails: vi.fn()
}))
mockNuxtImport('useStrrModals', () => () => ({ openErrorModal }))

enableAutoUnmount(afterEach)

function document (name: string): ApiDocument {
return {
fileName: `${name}.pdf`,
fileKey: `key-${name}`,
fileType: 'application/pdf',
documentType: DocumentUploadType.STRATA_HOTEL_DOCUMENTATION
}
}

function incoming (): UiDocument {
const file = new File(['synthetic'], 'incoming-A.pdf', { type: 'application/pdf' })
return {
id: 'incoming-A',
file,
name: file.name,
type: DocumentUploadType.STRATA_HOTEL_DOCUMENTATION,
apiDoc: {} as ApiDocument,
loading: false,
uploadStep: DocumentUploadStep.NOC
}
}

async function load (id: number) {
const registration = structuredClone(mockPermitDetailsData)
registration.documents = [document(`existing-${id}`)]
getApplication.mockResolvedValueOnce({
header: { applicationNumber: `APP-${id}`, status: ApplicationStatus.NOC_PENDING },
registration
})
await useStrrStrataStore().loadStrata(`APP-${id}`)
}

function result () {
return { registration: { documents: [document('existing-101'), document('incoming-A')] } }
}

function upload (doc: UiDocument) {
return useDocumentStore().addDocumentToApplication(doc, 'APP-101')
}

let pinia: ReturnType<typeof createPinia>
beforeEach(() => {
pinia = createPinia()
setActivePinia(pinia)
vi.resetAllMocks()
})

describe('Strata upload document context', () => {
it('adds the returned file while the same record remains loaded', async () => {
await load(101)
const doc = incoming()
api.mockResolvedValueOnce(result())
await upload(doc)
expect(doc.apiDoc.fileKey).toBe('key-incoming-A')
expect(useDocumentStore().apiDocuments.map(value => value.fileKey))
.toEqual(['key-existing-101', 'key-incoming-A'])
expect(doc.loading).toBe(false)
})

it('reports a failed upload while the same record remains loaded', async () => {
await load(101)
const doc = incoming()
api.mockRejectedValue(new Error('Synthetic active upload failure'))
await expect(upload(doc)).rejects.toBeInstanceOf(Error)
expect(useDocumentStore().apiDocuments.map(value => value.fileKey)).toEqual(['key-existing-101'])
expect(openErrorModal).toHaveBeenCalledOnce()
expect(doc.loading).toBe(false)
})

it('does not restore documents after the current record has been reset', async () => {
await load(101)
const pending = Promise.withResolvers<ReturnType<typeof result>>()
api.mockReturnValue(pending.promise)
const doc = incoming()
const request = upload(doc)
await vi.waitFor(() => expect(api).toHaveBeenCalled())
useStrrStrataStore().$reset()
pending.resolve(result())
await request
expect(useDocumentStore().storedDocuments).toEqual([])
expect(doc.loading).toBe(false)
})

it('keeps a newer record and its rendered documents after an older upload succeeds', async () => {
await load(101)
const pending = Promise.withResolvers<ReturnType<typeof result>>()
api.mockReturnValue(pending.promise)
const doc = incoming()
const request = upload(doc)
await vi.waitFor(() => expect(api).toHaveBeenCalled())
await load(202)
const wrapper = await mountSuspended(DocumentList, { global: { plugins: [pinia] } })
expect(wrapper.text()).toContain('existing-202.pdf')
pending.resolve(result())
await request
await flushPromises()
expect(useDocumentStore().apiDocuments.map(value => value.fileKey)).toEqual(['key-existing-202'])
expect(wrapper.text()).not.toContain('incoming-A.pdf')
expect(doc.loading).toBe(false)
})

it('keeps a newer record free of an older upload error dialog', async () => {
await load(101)
const pending = Promise.withResolvers<ReturnType<typeof result>>()
api.mockReturnValue(pending.promise)
const doc = incoming()
const request = upload(doc).catch(error => error)
await vi.waitFor(() => expect(api).toHaveBeenCalled())
await load(202)
const failure = new Error('Synthetic old upload failure')
pending.reject(failure)
expect(await request).toBeInstanceOf(Error)
expect(useDocumentStore().apiDocuments.map(value => value.fileKey)).toEqual(['key-existing-202'])
expect(openErrorModal).not.toHaveBeenCalled()
expect(doc.loading).toBe(false)
})
})
Loading