Skip to content

Commit 23cbd4a

Browse files
authored
@uppy/xhr-upload: add browser tests (#6169)
Good to have actual browser tests for this uploader.
1 parent 53f5b44 commit 23cbd4a

File tree

6 files changed

+206
-1
lines changed

6 files changed

+206
-1
lines changed

packages/@uppy/xhr-upload/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"scripts": {
99
"build": "tsc --build tsconfig.build.json",
1010
"typecheck": "tsc --build",
11-
"test": "vitest run --environment=jsdom --silent='passed-only'"
11+
"test": "vitest run --silent='passed-only'",
12+
"test:e2e": "vitest run --project browser"
1213
},
1314
"keywords": [
1415
"file uploader",
@@ -44,8 +45,12 @@
4445
},
4546
"devDependencies": {
4647
"@uppy/core": "workspace:^",
48+
"@uppy/dashboard": "workspace:^",
49+
"@vitest/browser": "^3.2.4",
4750
"jsdom": "^26.1.0",
51+
"msw": "^2.10.4",
4852
"nock": "^13.1.0",
53+
"playwright": "1.57.0",
4954
"typescript": "^5.8.3",
5055
"vitest": "^3.2.4"
5156
},
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { setupWorker } from 'msw/browser'
2+
3+
export const worker = setupWorker()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test as testBase } from 'vitest'
2+
import { worker } from './mocks/browser.js'
3+
4+
export const test = testBase.extend<{ worker: typeof worker }>({
5+
worker: [
6+
// biome-ignore lint/correctness/noEmptyPattern: dunno
7+
async ({}, use) => {
8+
// Start the worker before the test.
9+
await worker.start()
10+
11+
// Expose the worker object on the test's context.
12+
await use(worker)
13+
14+
// Remove any request handlers added in individual test cases.
15+
// This prevents them from affecting unrelated tests.
16+
worker.resetHandlers()
17+
},
18+
{
19+
auto: true,
20+
},
21+
],
22+
})
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import Uppy, { type UppyEventMap } from '@uppy/core'
2+
import Dashboard from '@uppy/dashboard'
3+
import { page, userEvent } from '@vitest/browser/context'
4+
import { HttpResponse, http } from 'msw'
5+
import { afterEach, beforeEach, describe, expect } from 'vitest'
6+
import '@uppy/core/css/style.css'
7+
import '@uppy/dashboard/css/style.css'
8+
import XHRUpload from './index.js'
9+
import { test } from './test-extend.js'
10+
11+
type UploadCompleteResult = Parameters<UppyEventMap<any, any>['complete']>[0]
12+
13+
type MockUploadRequest = {
14+
fieldNames: string[]
15+
fileNames: string[]
16+
}
17+
18+
let uppy: Uppy | undefined
19+
20+
function createUppy({ bundle = false }: { bundle?: boolean } = {}) {
21+
const target = document.createElement('div')
22+
document.body.appendChild(target)
23+
24+
uppy = new Uppy({ debug: true })
25+
.use(Dashboard, {
26+
target,
27+
inline: true,
28+
})
29+
.use(XHRUpload, {
30+
endpoint: 'http://localhost/upload',
31+
bundle,
32+
})
33+
34+
return uppy
35+
}
36+
37+
function createMockFile(name: string, size: number = 16) {
38+
return new File(['a'.repeat(size)], name, { type: 'text/plain' })
39+
}
40+
41+
function waitForUploadComplete(uppy: Uppy<any, any>) {
42+
return new Promise<UploadCompleteResult>((resolve) =>
43+
uppy.once('complete', resolve),
44+
)
45+
}
46+
47+
beforeEach(() => {
48+
document.body.innerHTML = ''
49+
})
50+
51+
afterEach(() => {
52+
uppy?.destroy()
53+
uppy = undefined
54+
})
55+
56+
describe('XHRUpload browser mode', () => {
57+
test('uploads a file in non-bundle mode', async ({ worker }) => {
58+
const requests: MockUploadRequest[] = []
59+
worker.use(
60+
http.post('http://localhost/upload', async ({ request }) => {
61+
const formData = await request.formData()
62+
const uploadedFiles = Array.from(formData.entries()).filter(
63+
(entry): entry is [string, File] => entry[1] instanceof File,
64+
)
65+
66+
requests.push({
67+
fieldNames: uploadedFiles.map(([fieldName]) => fieldName),
68+
fileNames: uploadedFiles.map(([, file]) => file.name),
69+
})
70+
71+
return HttpResponse.json({ url: 'http://localhost/uploads/regular' })
72+
}),
73+
)
74+
75+
const uppy = createUppy()
76+
const completePromise = waitForUploadComplete(uppy)
77+
const fileInput = document.querySelector('.uppy-Dashboard-input')!
78+
79+
await userEvent.upload(fileInput, createMockFile('regular.txt'))
80+
await page.getByRole('button', { name: 'Upload 1 file' }).click()
81+
82+
const result = await completePromise
83+
84+
expect(result.failed).toHaveLength(0)
85+
expect(result.successful).toHaveLength(1)
86+
expect(requests).toHaveLength(1)
87+
expect(requests[0]).toEqual({
88+
fieldNames: ['file'],
89+
fileNames: ['regular.txt'],
90+
})
91+
expect(uppy.getFiles()[0].response?.uploadURL).toBe(
92+
'http://localhost/uploads/regular',
93+
)
94+
await expect
95+
.element(page.getByText('Complete', { exact: true }))
96+
.toBeVisible()
97+
})
98+
99+
test('uploads files in a single request with bundle: true', async ({
100+
worker,
101+
}) => {
102+
const requests: MockUploadRequest[] = []
103+
worker.use(
104+
http.post('http://localhost/upload', async ({ request }) => {
105+
const formData = await request.formData()
106+
const uploadedFiles = Array.from(formData.entries()).filter(
107+
(entry): entry is [string, File] => entry[1] instanceof File,
108+
)
109+
110+
requests.push({
111+
fieldNames: uploadedFiles.map(([fieldName]) => fieldName),
112+
fileNames: uploadedFiles.map(([, file]) => file.name),
113+
})
114+
115+
return HttpResponse.json({ url: 'http://localhost/uploads/bundled' })
116+
}),
117+
)
118+
119+
const uppy = createUppy({ bundle: true })
120+
const completePromise = waitForUploadComplete(uppy)
121+
const fileInput = document.querySelector('.uppy-Dashboard-input')!
122+
123+
await userEvent.upload(fileInput, [
124+
createMockFile('bundle-a.txt'),
125+
createMockFile('bundle-b.txt'),
126+
])
127+
await page.getByRole('button', { name: 'Upload 2 files' }).click()
128+
129+
const result = await completePromise
130+
131+
expect(result.failed).toHaveLength(0)
132+
expect(result.successful).toHaveLength(2)
133+
expect(requests).toHaveLength(1)
134+
const sortedFileNames = [...requests[0].fileNames].sort()
135+
expect(requests[0].fieldNames).toEqual(['files[]', 'files[]'])
136+
expect(sortedFileNames).toEqual(['bundle-a.txt', 'bundle-b.txt'])
137+
expect(uppy.getFiles().every((file) => file.response?.uploadURL)).toBe(true)
138+
await expect
139+
.element(page.getByText('Complete', { exact: true }))
140+
.toBeVisible()
141+
})
142+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({
4+
test: {
5+
projects: [
6+
{
7+
test: {
8+
name: 'unit',
9+
include: [
10+
'src/**/*.test.{ts,tsx}',
11+
'!src/**/*.browser.test.{ts,tsx}',
12+
],
13+
environment: 'jsdom',
14+
},
15+
},
16+
{
17+
test: {
18+
name: 'browser',
19+
include: ['src/**/*.browser.test.{ts,tsx}'],
20+
browser: {
21+
enabled: true,
22+
provider: 'playwright',
23+
instances: [{ browser: 'chromium' }],
24+
},
25+
},
26+
},
27+
],
28+
},
29+
})

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10480,9 +10480,13 @@ __metadata:
1048010480
dependencies:
1048110481
"@uppy/companion-client": "workspace:^"
1048210482
"@uppy/core": "workspace:^"
10483+
"@uppy/dashboard": "workspace:^"
1048310484
"@uppy/utils": "workspace:^"
10485+
"@vitest/browser": "npm:^3.2.4"
1048410486
jsdom: "npm:^26.1.0"
10487+
msw: "npm:^2.10.4"
1048510488
nock: "npm:^13.1.0"
10489+
playwright: "npm:1.57.0"
1048610490
typescript: "npm:^5.8.3"
1048710491
vitest: "npm:^3.2.4"
1048810492
peerDependencies:

0 commit comments

Comments
 (0)