frontend/tests/pages/asset-uploads.test.ts
Rama main · réplica de lectura del repositorio original.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime'
import { flushPromises } from '@vue/test-utils'
import { ref } from 'vue'
import AssetPage from '../../app/pages/assets/[id]/index.vue'
const { api, toast, refreshAsset, refreshAttachments } = vi.hoisted(() => ({
api: vi.fn(), toast: vi.fn(), refreshAsset: vi.fn(), refreshAttachments: vi.fn()
}))
mockNuxtImport('useApi', () => api)
mockNuxtImport('useToast', () => () => ({ add: toast }))
mockNuxtImport('useRoute', () => () => ({ params: { id: 'asset' }, query: {} }))
mockNuxtImport('useFeatures', () => () => ({ features: ref({ warranties: false }) }))
describe('Asset attachment uploads', () => {
const upload = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('fetch', upload)
upload.mockReset().mockResolvedValue({ ok: true })
api.mockImplementation((url: () => string) => {
const path = url()
return {
data: ref(path.endsWith('/attachments')
? []
: path.endsWith('/warranty')
? null
: {
id: 'asset', name: 'Desk', quantity: 1
}),
refresh: path.endsWith('/attachments') ? refreshAttachments : refreshAsset,
status: ref('success'), error: ref(null)
}
})
})
afterEach(() => vi.unstubAllGlobals())
const mountPage = () => mountSuspended(AssetPage, {
global: { stubs: { AssetHistory: true, UModal: true } }
})
const file = (name: string) => new File([name], name, { type: 'text/plain' })
const sentNames = () => upload.mock.calls.map(([, options]) => options.body.get('file').name)
it('retains failed and unattempted files across new batches and retries only those files', async () => {
const wrapper = await mountPage()
const input = wrapper.get('input[type="file"]')
expect(input.attributes()).toHaveProperty('multiple')
upload.mockResolvedValueOnce({ ok: true }).mockResolvedValueOnce({ ok: false, status: 500 })
Object.defineProperty(input.element, 'files', {
configurable: true, value: [file('first.txt'), file('failed.txt'), file('remaining.txt')]
})
await input.trigger('change')
await flushPromises()
expect(sentNames()).toEqual(['first.txt', 'failed.txt'])
expect(wrapper.text()).toContain('2 files not uploaded')
expect(refreshAttachments).toHaveBeenCalledOnce()
expect(refreshAsset).toHaveBeenCalledOnce()
expect(toast).toHaveBeenCalledWith({ title: 'Uploaded 1 of 3 files', color: 'warning' })
const section = input.element.closest('section')!
section.dispatchEvent(Object.assign(new Event('drop', { bubbles: true, cancelable: true }), {
dataTransfer: { files: [file('new.txt')] }
}))
await flushPromises()
expect(wrapper.text()).toContain('2 files not uploaded')
Array.from(section.querySelectorAll('button')).find(button => button.textContent?.trim() === 'Retry')!.click()
await flushPromises()
expect(sentNames()).toEqual(['first.txt', 'failed.txt', 'new.txt', 'failed.txt', 'remaining.txt'])
expect(wrapper.text()).not.toContain('not uploaded')
wrapper.unmount()
})
it('queues a second drop while an upload is in progress', async () => {
const wrapper = await mountPage()
let complete!: (response: { ok: boolean }) => void
upload.mockImplementationOnce(() => new Promise((resolve) => {
complete = resolve
}))
const section = wrapper.get('input[type="file"]').element.closest('section')!
const drop = (name: string) => section.dispatchEvent(Object.assign(new Event('drop', { bubbles: true, cancelable: true }), {
dataTransfer: { files: [file(name)] }
}))
drop('first.txt')
drop('queued.txt')
expect(sentNames()).toEqual(['first.txt'])
complete({ ok: true })
await flushPromises()
expect(sentNames()).toEqual(['first.txt', 'queued.txt'])
wrapper.unmount()
})
})