@uppy/xhr-upload: add browser tests (#6169)

Good to have actual browser tests for this uploader.
This commit is contained in:
Merlijn Vos 2026-02-19 15:10:17 +01:00 committed by GitHub
parent 53f5b44f4b
commit 23cbd4a05a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 206 additions and 1 deletions

View file

@ -8,7 +8,8 @@
"scripts": {
"build": "tsc --build tsconfig.build.json",
"typecheck": "tsc --build",
"test": "vitest run --environment=jsdom --silent='passed-only'"
"test": "vitest run --silent='passed-only'",
"test:e2e": "vitest run --project browser"
},
"keywords": [
"file uploader",
@ -44,8 +45,12 @@
},
"devDependencies": {
"@uppy/core": "workspace:^",
"@uppy/dashboard": "workspace:^",
"@vitest/browser": "^3.2.4",
"jsdom": "^26.1.0",
"msw": "^2.10.4",
"nock": "^13.1.0",
"playwright": "1.57.0",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},

View file

@ -0,0 +1,3 @@
import { setupWorker } from 'msw/browser'
export const worker = setupWorker()

View file

@ -0,0 +1,22 @@
import { test as testBase } from 'vitest'
import { worker } from './mocks/browser.js'
export const test = testBase.extend<{ worker: typeof worker }>({
worker: [
// biome-ignore lint/correctness/noEmptyPattern: dunno
async ({}, use) => {
// Start the worker before the test.
await worker.start()
// Expose the worker object on the test's context.
await use(worker)
// Remove any request handlers added in individual test cases.
// This prevents them from affecting unrelated tests.
worker.resetHandlers()
},
{
auto: true,
},
],
})

View file

@ -0,0 +1,142 @@
import Uppy, { type UppyEventMap } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import { page, userEvent } from '@vitest/browser/context'
import { HttpResponse, http } from 'msw'
import { afterEach, beforeEach, describe, expect } from 'vitest'
import '@uppy/core/css/style.css'
import '@uppy/dashboard/css/style.css'
import XHRUpload from './index.js'
import { test } from './test-extend.js'
type UploadCompleteResult = Parameters<UppyEventMap<any, any>['complete']>[0]
type MockUploadRequest = {
fieldNames: string[]
fileNames: string[]
}
let uppy: Uppy | undefined
function createUppy({ bundle = false }: { bundle?: boolean } = {}) {
const target = document.createElement('div')
document.body.appendChild(target)
uppy = new Uppy({ debug: true })
.use(Dashboard, {
target,
inline: true,
})
.use(XHRUpload, {
endpoint: 'http://localhost/upload',
bundle,
})
return uppy
}
function createMockFile(name: string, size: number = 16) {
return new File(['a'.repeat(size)], name, { type: 'text/plain' })
}
function waitForUploadComplete(uppy: Uppy<any, any>) {
return new Promise<UploadCompleteResult>((resolve) =>
uppy.once('complete', resolve),
)
}
beforeEach(() => {
document.body.innerHTML = ''
})
afterEach(() => {
uppy?.destroy()
uppy = undefined
})
describe('XHRUpload browser mode', () => {
test('uploads a file in non-bundle mode', async ({ worker }) => {
const requests: MockUploadRequest[] = []
worker.use(
http.post('http://localhost/upload', async ({ request }) => {
const formData = await request.formData()
const uploadedFiles = Array.from(formData.entries()).filter(
(entry): entry is [string, File] => entry[1] instanceof File,
)
requests.push({
fieldNames: uploadedFiles.map(([fieldName]) => fieldName),
fileNames: uploadedFiles.map(([, file]) => file.name),
})
return HttpResponse.json({ url: 'http://localhost/uploads/regular' })
}),
)
const uppy = createUppy()
const completePromise = waitForUploadComplete(uppy)
const fileInput = document.querySelector('.uppy-Dashboard-input')!
await userEvent.upload(fileInput, createMockFile('regular.txt'))
await page.getByRole('button', { name: 'Upload 1 file' }).click()
const result = await completePromise
expect(result.failed).toHaveLength(0)
expect(result.successful).toHaveLength(1)
expect(requests).toHaveLength(1)
expect(requests[0]).toEqual({
fieldNames: ['file'],
fileNames: ['regular.txt'],
})
expect(uppy.getFiles()[0].response?.uploadURL).toBe(
'http://localhost/uploads/regular',
)
await expect
.element(page.getByText('Complete', { exact: true }))
.toBeVisible()
})
test('uploads files in a single request with bundle: true', async ({
worker,
}) => {
const requests: MockUploadRequest[] = []
worker.use(
http.post('http://localhost/upload', async ({ request }) => {
const formData = await request.formData()
const uploadedFiles = Array.from(formData.entries()).filter(
(entry): entry is [string, File] => entry[1] instanceof File,
)
requests.push({
fieldNames: uploadedFiles.map(([fieldName]) => fieldName),
fileNames: uploadedFiles.map(([, file]) => file.name),
})
return HttpResponse.json({ url: 'http://localhost/uploads/bundled' })
}),
)
const uppy = createUppy({ bundle: true })
const completePromise = waitForUploadComplete(uppy)
const fileInput = document.querySelector('.uppy-Dashboard-input')!
await userEvent.upload(fileInput, [
createMockFile('bundle-a.txt'),
createMockFile('bundle-b.txt'),
])
await page.getByRole('button', { name: 'Upload 2 files' }).click()
const result = await completePromise
expect(result.failed).toHaveLength(0)
expect(result.successful).toHaveLength(2)
expect(requests).toHaveLength(1)
const sortedFileNames = [...requests[0].fileNames].sort()
expect(requests[0].fieldNames).toEqual(['files[]', 'files[]'])
expect(sortedFileNames).toEqual(['bundle-a.txt', 'bundle-b.txt'])
expect(uppy.getFiles().every((file) => file.response?.uploadURL)).toBe(true)
await expect
.element(page.getByText('Complete', { exact: true }))
.toBeVisible()
})
})

View file

@ -0,0 +1,29 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: [
'src/**/*.test.{ts,tsx}',
'!src/**/*.browser.test.{ts,tsx}',
],
environment: 'jsdom',
},
},
{
test: {
name: 'browser',
include: ['src/**/*.browser.test.{ts,tsx}'],
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
},
],
},
})

View file

@ -10480,9 +10480,13 @@ __metadata:
dependencies:
"@uppy/companion-client": "workspace:^"
"@uppy/core": "workspace:^"
"@uppy/dashboard": "workspace:^"
"@uppy/utils": "workspace:^"
"@vitest/browser": "npm:^3.2.4"
jsdom: "npm:^26.1.0"
msw: "npm:^2.10.4"
nock: "npm:^13.1.0"
playwright: "npm:1.57.0"
typescript: "npm:^5.8.3"
vitest: "npm:^3.2.4"
peerDependencies: