@seed-design/react-file-upload 0.0.0-alpha-20260416134952
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/FileUpload-12s-1EmFGBYg.cjs +585 -0
- package/lib/FileUpload-12s-BsPs5ndy.js +569 -0
- package/lib/index.cjs +60 -0
- package/lib/index.d.ts +8012 -0
- package/lib/index.js +43 -0
- package/package.json +48 -0
- package/src/FileUpload.namespace.ts +26 -0
- package/src/FileUpload.tsx +253 -0
- package/src/accept-utils.test.ts +87 -0
- package/src/accept-utils.ts +83 -0
- package/src/index.ts +54 -0
- package/src/types.ts +24 -0
- package/src/useFileUpload.test.tsx +1854 -0
- package/src/useFileUpload.ts +500 -0
- package/src/useFileUploadContext.tsx +50 -0
- package/src/utils.test.ts +46 -0
- package/src/utils.ts +24 -0
|
@@ -0,0 +1,1854 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
FileUploadRoot,
|
|
10
|
+
FileUploadDropzone,
|
|
11
|
+
FileUploadTrigger,
|
|
12
|
+
FileUploadHiddenInput,
|
|
13
|
+
FileUploadItemName,
|
|
14
|
+
FileUploadItemSize,
|
|
15
|
+
FileUploadItemRemoveButton,
|
|
16
|
+
FileUploadContext,
|
|
17
|
+
type FileUploadRootProps,
|
|
18
|
+
} from "./FileUpload";
|
|
19
|
+
import { FileUploadItemProvider } from "./useFileUploadContext";
|
|
20
|
+
import { useFileUploadItem } from "./useFileUpload";
|
|
21
|
+
import type { FileEntry, FileStatusDetails } from "./types";
|
|
22
|
+
|
|
23
|
+
function setUp(jsx: ReactElement) {
|
|
24
|
+
return {
|
|
25
|
+
user: userEvent.setup(),
|
|
26
|
+
...render(jsx),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createMockFile(name: string, size: number, type: string): File {
|
|
31
|
+
const content = new Array(size).fill("a").join("");
|
|
32
|
+
|
|
33
|
+
return new File([content], name, { type });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function BasicFileUploadItem({ fileEntry, index }: { fileEntry: FileEntry; index: number }) {
|
|
37
|
+
const api = useFileUploadItem(fileEntry);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<FileUploadItemProvider value={api}>
|
|
41
|
+
<li data-testid={`item-${index}`}>
|
|
42
|
+
<FileUploadItemName />
|
|
43
|
+
<FileUploadItemSize formatBytes={(bytes) => `${bytes} bytes`} />
|
|
44
|
+
<FileUploadItemRemoveButton data-testid={`delete-${index}`}>
|
|
45
|
+
Delete
|
|
46
|
+
</FileUploadItemRemoveButton>
|
|
47
|
+
</li>
|
|
48
|
+
</FileUploadItemProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BasicFileUpload = React.forwardRef<HTMLInputElement, FileUploadRootProps>((props, ref) => {
|
|
53
|
+
return (
|
|
54
|
+
<FileUploadRoot {...props}>
|
|
55
|
+
<FileUploadTrigger>Choose files</FileUploadTrigger>
|
|
56
|
+
<FileUploadDropzone data-testid="dropzone">
|
|
57
|
+
<span>Drop files here</span>
|
|
58
|
+
</FileUploadDropzone>
|
|
59
|
+
<ul data-testid="item-group">
|
|
60
|
+
<FileUploadContext>
|
|
61
|
+
{({ acceptedFileEntries }) =>
|
|
62
|
+
acceptedFileEntries.map((fileEntry, index) => (
|
|
63
|
+
<BasicFileUploadItem key={fileEntry.id} fileEntry={fileEntry} index={index} />
|
|
64
|
+
))
|
|
65
|
+
}
|
|
66
|
+
</FileUploadContext>
|
|
67
|
+
</ul>
|
|
68
|
+
<FileUploadHiddenInput ref={ref} data-testid="hidden-input" />
|
|
69
|
+
</FileUploadRoot>
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("useFileUpload", () => {
|
|
74
|
+
describe("basic rendering", () => {
|
|
75
|
+
it("should render the file upload correctly", () => {
|
|
76
|
+
const { getByText, getByTestId } = setUp(<BasicFileUpload />);
|
|
77
|
+
|
|
78
|
+
expect(getByText("Choose files")).toBeDefined();
|
|
79
|
+
expect(getByTestId("dropzone")).toBeDefined();
|
|
80
|
+
expect(getByTestId("hidden-input")).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should have hidden input with correct attributes", () => {
|
|
84
|
+
const { getByTestId } = setUp(<BasicFileUpload name="files" />);
|
|
85
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
86
|
+
|
|
87
|
+
expect(input.type).toBe("file");
|
|
88
|
+
expect(input.name).toBe("files");
|
|
89
|
+
expect(input.tabIndex).toBe(-1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should render with multiple attribute when maxFiles > 1", () => {
|
|
93
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={5} />);
|
|
94
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
95
|
+
|
|
96
|
+
expect(input.multiple).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should render with accept attribute", () => {
|
|
100
|
+
const { getByTestId } = setUp(<BasicFileUpload accept="image/*" />);
|
|
101
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
102
|
+
|
|
103
|
+
expect(input.accept).toBe("image/*");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should render with accept array attribute", () => {
|
|
107
|
+
const { getByTestId } = setUp(<BasicFileUpload accept={["image/*", ".pdf"]} />);
|
|
108
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
109
|
+
|
|
110
|
+
expect(input.accept).toBe("image/*,.pdf");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("file selection via trigger", () => {
|
|
115
|
+
it("should open file picker when trigger is clicked", async () => {
|
|
116
|
+
const { getByText, getByTestId, user } = setUp(<BasicFileUpload />);
|
|
117
|
+
const trigger = getByText("Choose files");
|
|
118
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
119
|
+
|
|
120
|
+
const clickSpy = mock(() => {});
|
|
121
|
+
input.click = clickSpy;
|
|
122
|
+
|
|
123
|
+
await user.click(trigger);
|
|
124
|
+
|
|
125
|
+
expect(clickSpy).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should not open file picker when disabled", async () => {
|
|
129
|
+
const { getByText, getByTestId, user } = setUp(<BasicFileUpload disabled />);
|
|
130
|
+
const trigger = getByText("Choose files");
|
|
131
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
132
|
+
|
|
133
|
+
const clickSpy = mock(() => {});
|
|
134
|
+
input.click = clickSpy;
|
|
135
|
+
|
|
136
|
+
await user.click(trigger);
|
|
137
|
+
|
|
138
|
+
expect(clickSpy).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("file change handling", () => {
|
|
143
|
+
it("should accept files via input change", async () => {
|
|
144
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
145
|
+
const { getByTestId } = setUp(
|
|
146
|
+
<BasicFileUpload onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
150
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
151
|
+
|
|
152
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
153
|
+
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
156
|
+
expect.objectContaining({ file, status: "pending" }),
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should call onAcceptedFileEntriesChange with cumulative files", async () => {
|
|
162
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
163
|
+
const { getByTestId } = setUp(
|
|
164
|
+
<BasicFileUpload maxFiles={5} onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
168
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
169
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
170
|
+
|
|
171
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
175
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
176
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const file3 = createMockFile("file3.txt", 300, "text/plain");
|
|
181
|
+
|
|
182
|
+
fireEvent.change(input, { target: { files: [file3] } });
|
|
183
|
+
|
|
184
|
+
await waitFor(() => {
|
|
185
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
186
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
187
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
188
|
+
expect.objectContaining({ file: file3, status: "pending" }),
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should display accepted files", async () => {
|
|
194
|
+
const { getByTestId, getByText } = setUp(<BasicFileUpload />);
|
|
195
|
+
|
|
196
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
197
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
198
|
+
|
|
199
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(getByText("test.txt")).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should display file size", async () => {
|
|
207
|
+
const { getByTestId, getByText } = setUp(<BasicFileUpload />);
|
|
208
|
+
|
|
209
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
210
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
211
|
+
|
|
212
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
213
|
+
|
|
214
|
+
await waitFor(() => {
|
|
215
|
+
expect(getByText("1024 bytes")).toBeDefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should reset input value after file selection to allow re-selecting the same file", async () => {
|
|
220
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={5} />);
|
|
221
|
+
|
|
222
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
223
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
224
|
+
|
|
225
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(getByTestId("item-0")).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(input.value).toBe("");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("file selection via dropzone", () => {
|
|
236
|
+
it("should accept dropped files on dropzone", async () => {
|
|
237
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
238
|
+
const { getByTestId } = setUp(
|
|
239
|
+
<BasicFileUpload onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
240
|
+
);
|
|
241
|
+
const dropzone = getByTestId("dropzone");
|
|
242
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
243
|
+
|
|
244
|
+
fireEvent.drop(dropzone, {
|
|
245
|
+
dataTransfer: { files: [file] },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
250
|
+
expect.objectContaining({ file, status: "pending" }),
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should not accept dropped files when disabled", async () => {
|
|
256
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
257
|
+
const { getByTestId } = setUp(
|
|
258
|
+
<BasicFileUpload disabled onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
259
|
+
);
|
|
260
|
+
const dropzone = getByTestId("dropzone");
|
|
261
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
262
|
+
|
|
263
|
+
fireEvent.drop(dropzone, {
|
|
264
|
+
dataTransfer: { files: [file] },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(onAcceptedFileEntriesChange).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("file change handling via dropzone", () => {
|
|
274
|
+
it("should accept files via drop", async () => {
|
|
275
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
276
|
+
const { getByTestId } = setUp(
|
|
277
|
+
<BasicFileUpload onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const dropzone = getByTestId("dropzone");
|
|
281
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
282
|
+
|
|
283
|
+
fireEvent.drop(dropzone, {
|
|
284
|
+
dataTransfer: { files: [file] },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
289
|
+
expect.objectContaining({ file, status: "pending" }),
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should call onAcceptedFileEntriesChange with cumulative files via drop", async () => {
|
|
295
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
296
|
+
const { getByTestId } = setUp(
|
|
297
|
+
<BasicFileUpload maxFiles={5} onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const dropzone = getByTestId("dropzone");
|
|
301
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
302
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
303
|
+
|
|
304
|
+
fireEvent.drop(dropzone, {
|
|
305
|
+
dataTransfer: { files: [file1, file2] },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
310
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
311
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const file3 = createMockFile("file3.txt", 300, "text/plain");
|
|
316
|
+
|
|
317
|
+
fireEvent.drop(dropzone, {
|
|
318
|
+
dataTransfer: { files: [file3] },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
323
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
324
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
325
|
+
expect.objectContaining({ file: file3, status: "pending" }),
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should display accepted files via drop", async () => {
|
|
331
|
+
const { getByTestId, getByText } = setUp(<BasicFileUpload />);
|
|
332
|
+
|
|
333
|
+
const dropzone = getByTestId("dropzone");
|
|
334
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
335
|
+
|
|
336
|
+
fireEvent.drop(dropzone, {
|
|
337
|
+
dataTransfer: { files: [file] },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await waitFor(() => {
|
|
341
|
+
expect(getByText("test.txt")).toBeDefined();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should display file size for dropped files", async () => {
|
|
346
|
+
const { getByTestId, getByText } = setUp(<BasicFileUpload />);
|
|
347
|
+
|
|
348
|
+
const dropzone = getByTestId("dropzone");
|
|
349
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
350
|
+
|
|
351
|
+
fireEvent.drop(dropzone, {
|
|
352
|
+
dataTransfer: { files: [file] },
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await waitFor(() => {
|
|
356
|
+
expect(getByText("1024 bytes")).toBeDefined();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("file deletion", () => {
|
|
362
|
+
it("should delete file when delete trigger is clicked", async () => {
|
|
363
|
+
const { getByTestId, queryByTestId, user } = setUp(<BasicFileUpload />);
|
|
364
|
+
|
|
365
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
366
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
367
|
+
|
|
368
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
369
|
+
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(getByTestId("item-0")).toBeDefined();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const deleteButton = getByTestId("delete-0");
|
|
375
|
+
await user.click(deleteButton);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(queryByTestId("item-0")).toBeNull();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("disabled state", () => {
|
|
384
|
+
it("should have data-disabled attribute when disabled", () => {
|
|
385
|
+
const { getByTestId } = setUp(<BasicFileUpload disabled />);
|
|
386
|
+
|
|
387
|
+
const dropzone = getByTestId("dropzone");
|
|
388
|
+
expect(dropzone.getAttribute("data-disabled")).toBe("");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should disable trigger when disabled", () => {
|
|
392
|
+
const { getByText } = setUp(<BasicFileUpload disabled />);
|
|
393
|
+
const trigger = getByText("Choose files") as HTMLButtonElement;
|
|
394
|
+
|
|
395
|
+
expect(trigger.disabled).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should disable hidden input when disabled", () => {
|
|
399
|
+
const { getByTestId } = setUp(<BasicFileUpload disabled />);
|
|
400
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
401
|
+
|
|
402
|
+
expect(input.disabled).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should disable trigger but not input when maxFiles is reached", async () => {
|
|
406
|
+
const { getByTestId, getByText } = setUp(<BasicFileUpload maxFiles={2} />);
|
|
407
|
+
|
|
408
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
409
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
410
|
+
const file2 = createMockFile("b.txt", 100, "text/plain");
|
|
411
|
+
|
|
412
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
413
|
+
|
|
414
|
+
await waitFor(() => {
|
|
415
|
+
const trigger = getByText("Choose files") as HTMLButtonElement;
|
|
416
|
+
expect(trigger.disabled).toBe(true);
|
|
417
|
+
// input should remain enabled so form submission includes the files
|
|
418
|
+
expect(input.disabled).toBe(false);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("should have data-disabled on dropzone when maxFiles is reached", async () => {
|
|
423
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={1} />);
|
|
424
|
+
|
|
425
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
426
|
+
const file = createMockFile("a.txt", 100, "text/plain");
|
|
427
|
+
|
|
428
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
429
|
+
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
const dropzone = getByTestId("dropzone");
|
|
432
|
+
expect(dropzone.getAttribute("data-disabled")).toBe("");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should re-enable trigger after removing a file when maxFiles was reached", async () => {
|
|
437
|
+
const { getByTestId, getByText } = setUp(<BasicFileUpload maxFiles={1} />);
|
|
438
|
+
|
|
439
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
440
|
+
const file = createMockFile("a.txt", 100, "text/plain");
|
|
441
|
+
|
|
442
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
443
|
+
|
|
444
|
+
await waitFor(() => {
|
|
445
|
+
const trigger = getByText("Choose files") as HTMLButtonElement;
|
|
446
|
+
expect(trigger.disabled).toBe(true);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const deleteButton = getByTestId("delete-0");
|
|
450
|
+
await userEvent.click(deleteButton);
|
|
451
|
+
|
|
452
|
+
await waitFor(() => {
|
|
453
|
+
const trigger = getByText("Choose files") as HTMLButtonElement;
|
|
454
|
+
expect(trigger.disabled).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should not accept dropped files when maxFiles is reached", async () => {
|
|
459
|
+
const onFileReject = mock(() => {});
|
|
460
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={1} onFileReject={onFileReject} />);
|
|
461
|
+
|
|
462
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
463
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
464
|
+
fireEvent.change(input, { target: { files: [file1] } });
|
|
465
|
+
|
|
466
|
+
await waitFor(() => {
|
|
467
|
+
expect(getByTestId("item-0")).toBeDefined();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const dropzone = getByTestId("dropzone");
|
|
471
|
+
const file2 = createMockFile("b.txt", 100, "text/plain");
|
|
472
|
+
fireEvent.drop(dropzone, {
|
|
473
|
+
dataTransfer: { files: [file2] },
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file: file2, errors: ["TOO_MANY_FILES"] }]);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("status change", () => {
|
|
483
|
+
it("should call onAcceptedFileEntriesChange when file status changes", async () => {
|
|
484
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
485
|
+
|
|
486
|
+
function StatusChangeItem({ fileEntry, index }: { fileEntry: FileEntry; index: number }) {
|
|
487
|
+
const api = useFileUploadItem(fileEntry);
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<FileUploadItemProvider value={api}>
|
|
491
|
+
<li data-testid={`file-${index}`}>
|
|
492
|
+
<FileUploadItemName />
|
|
493
|
+
</li>
|
|
494
|
+
</FileUploadItemProvider>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const StatusChangeUpload = () => {
|
|
499
|
+
return (
|
|
500
|
+
<FileUploadRoot onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}>
|
|
501
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
502
|
+
<FileUploadContext>
|
|
503
|
+
{({ acceptedFileEntries, updateFileEntryStatus }) => (
|
|
504
|
+
<>
|
|
505
|
+
<ul>
|
|
506
|
+
{acceptedFileEntries.map((fileEntry, index) => (
|
|
507
|
+
<StatusChangeItem key={fileEntry.id} fileEntry={fileEntry} index={index} />
|
|
508
|
+
))}
|
|
509
|
+
</ul>
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
data-testid="start-upload"
|
|
513
|
+
onClick={() => {
|
|
514
|
+
for (const f of acceptedFileEntries) {
|
|
515
|
+
updateFileEntryStatus(f.id, { status: "uploading", progress: 50 });
|
|
516
|
+
}
|
|
517
|
+
}}
|
|
518
|
+
>
|
|
519
|
+
Start Upload
|
|
520
|
+
</button>
|
|
521
|
+
</>
|
|
522
|
+
)}
|
|
523
|
+
</FileUploadContext>
|
|
524
|
+
</FileUploadRoot>
|
|
525
|
+
);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const { getByTestId, user } = setUp(<StatusChangeUpload />);
|
|
529
|
+
|
|
530
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
531
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
532
|
+
|
|
533
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
534
|
+
|
|
535
|
+
await waitFor(() => {
|
|
536
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
537
|
+
expect.objectContaining({ file, status: "pending" }),
|
|
538
|
+
]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
await user.click(getByTestId("start-upload"));
|
|
542
|
+
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
545
|
+
expect.objectContaining({ file, status: "uploading", progress: 50 }),
|
|
546
|
+
]);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe("updateFileEntryStatus", () => {
|
|
552
|
+
it("should update a file's status", async () => {
|
|
553
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
554
|
+
const UpdateStatusUpload = () => (
|
|
555
|
+
<FileUploadRoot onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}>
|
|
556
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
557
|
+
<FileUploadContext>
|
|
558
|
+
{({ acceptedFileEntries, updateFileEntryStatus }) => (
|
|
559
|
+
<>
|
|
560
|
+
<ul>
|
|
561
|
+
{acceptedFileEntries.map((f, i) => (
|
|
562
|
+
<li key={f.id} data-testid={`file-${i}`}>
|
|
563
|
+
{f.file.name} - {f.status}
|
|
564
|
+
{"progress" in f && `-${f.progress}`}
|
|
565
|
+
</li>
|
|
566
|
+
))}
|
|
567
|
+
</ul>
|
|
568
|
+
<button
|
|
569
|
+
type="button"
|
|
570
|
+
data-testid="update-status"
|
|
571
|
+
onClick={() => {
|
|
572
|
+
for (const f of acceptedFileEntries) {
|
|
573
|
+
updateFileEntryStatus(f.id, { status: "uploading", progress: 50 });
|
|
574
|
+
}
|
|
575
|
+
}}
|
|
576
|
+
>
|
|
577
|
+
Update
|
|
578
|
+
</button>
|
|
579
|
+
</>
|
|
580
|
+
)}
|
|
581
|
+
</FileUploadContext>
|
|
582
|
+
</FileUploadRoot>
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const { getByTestId } = setUp(<UpdateStatusUpload />);
|
|
586
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
587
|
+
fireEvent.change(input, {
|
|
588
|
+
target: { files: [createMockFile("a.txt", 100, "text/plain")] },
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await waitFor(() => expect(getByTestId("file-0")).toBeDefined());
|
|
592
|
+
|
|
593
|
+
fireEvent.click(getByTestId("update-status"));
|
|
594
|
+
|
|
595
|
+
await waitFor(() => {
|
|
596
|
+
expect(getByTestId("file-0").textContent).toContain("uploading");
|
|
597
|
+
expect(getByTestId("file-0").textContent).toContain("50");
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("should only update the targeted file", async () => {
|
|
602
|
+
const UpdateOneUpload = () => (
|
|
603
|
+
<FileUploadRoot maxFiles={3}>
|
|
604
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
605
|
+
<FileUploadContext>
|
|
606
|
+
{({ acceptedFileEntries, updateFileEntryStatus }) => (
|
|
607
|
+
<>
|
|
608
|
+
<ul>
|
|
609
|
+
{acceptedFileEntries.map((f, i) => (
|
|
610
|
+
<li key={f.id} data-testid={`file-${i}`}>
|
|
611
|
+
{f.file.name} - {f.status}
|
|
612
|
+
</li>
|
|
613
|
+
))}
|
|
614
|
+
</ul>
|
|
615
|
+
{acceptedFileEntries.length > 0 && (
|
|
616
|
+
<button
|
|
617
|
+
type="button"
|
|
618
|
+
data-testid="update-first"
|
|
619
|
+
onClick={() =>
|
|
620
|
+
updateFileEntryStatus(acceptedFileEntries[0].id, { status: "success" })
|
|
621
|
+
}
|
|
622
|
+
>
|
|
623
|
+
Update First
|
|
624
|
+
</button>
|
|
625
|
+
)}
|
|
626
|
+
</>
|
|
627
|
+
)}
|
|
628
|
+
</FileUploadContext>
|
|
629
|
+
</FileUploadRoot>
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const { getByTestId } = setUp(<UpdateOneUpload />);
|
|
633
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
634
|
+
fireEvent.change(input, {
|
|
635
|
+
target: {
|
|
636
|
+
files: [
|
|
637
|
+
createMockFile("a.txt", 100, "text/plain"),
|
|
638
|
+
createMockFile("b.txt", 200, "text/plain"),
|
|
639
|
+
],
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
await waitFor(() => expect(getByTestId("file-1")).toBeDefined());
|
|
644
|
+
|
|
645
|
+
fireEvent.click(getByTestId("update-first"));
|
|
646
|
+
|
|
647
|
+
await waitFor(() => {
|
|
648
|
+
expect(getByTestId("file-0").textContent).toContain("success");
|
|
649
|
+
expect(getByTestId("file-1").textContent).toContain("pending");
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe("removeFileEntry", () => {
|
|
655
|
+
it("should remove a specific file", async () => {
|
|
656
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
657
|
+
const RemoveUpload = () => (
|
|
658
|
+
<FileUploadRoot maxFiles={3} onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}>
|
|
659
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
660
|
+
<FileUploadContext>
|
|
661
|
+
{({ acceptedFileEntries, removeFileEntry }) => (
|
|
662
|
+
<ul>
|
|
663
|
+
{acceptedFileEntries.map((f, i) => (
|
|
664
|
+
<li key={f.id} data-testid={`file-${i}`}>
|
|
665
|
+
{f.file.name}
|
|
666
|
+
<button
|
|
667
|
+
type="button"
|
|
668
|
+
data-testid={`remove-${i}`}
|
|
669
|
+
onClick={() => removeFileEntry(f.id)}
|
|
670
|
+
>
|
|
671
|
+
Remove
|
|
672
|
+
</button>
|
|
673
|
+
</li>
|
|
674
|
+
))}
|
|
675
|
+
</ul>
|
|
676
|
+
)}
|
|
677
|
+
</FileUploadContext>
|
|
678
|
+
</FileUploadRoot>
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const { getByTestId, queryByTestId } = setUp(<RemoveUpload />);
|
|
682
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
683
|
+
fireEvent.change(input, {
|
|
684
|
+
target: {
|
|
685
|
+
files: [
|
|
686
|
+
createMockFile("a.txt", 100, "text/plain"),
|
|
687
|
+
createMockFile("b.txt", 200, "text/plain"),
|
|
688
|
+
],
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
await waitFor(() => expect(getByTestId("file-1")).toBeDefined());
|
|
693
|
+
|
|
694
|
+
fireEvent.click(getByTestId("remove-0"));
|
|
695
|
+
|
|
696
|
+
await waitFor(() => {
|
|
697
|
+
expect(getByTestId("file-0").textContent).toContain("b.txt");
|
|
698
|
+
expect(queryByTestId("file-1")).toBeNull();
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("clearFileEntries", () => {
|
|
704
|
+
it("should remove all files", async () => {
|
|
705
|
+
const ClearUpload = () => (
|
|
706
|
+
<FileUploadRoot maxFiles={3}>
|
|
707
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
708
|
+
<FileUploadContext>
|
|
709
|
+
{({ acceptedFileEntries, clearFileEntries }) => (
|
|
710
|
+
<>
|
|
711
|
+
<ul>
|
|
712
|
+
{acceptedFileEntries.map((f, i) => (
|
|
713
|
+
<li key={f.id} data-testid={`file-${i}`}>
|
|
714
|
+
{f.file.name}
|
|
715
|
+
</li>
|
|
716
|
+
))}
|
|
717
|
+
</ul>
|
|
718
|
+
<button type="button" data-testid="clear" onClick={clearFileEntries}>
|
|
719
|
+
Clear
|
|
720
|
+
</button>
|
|
721
|
+
</>
|
|
722
|
+
)}
|
|
723
|
+
</FileUploadContext>
|
|
724
|
+
</FileUploadRoot>
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const { getByTestId, queryByTestId } = setUp(<ClearUpload />);
|
|
728
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
729
|
+
fireEvent.change(input, {
|
|
730
|
+
target: {
|
|
731
|
+
files: [
|
|
732
|
+
createMockFile("a.txt", 100, "text/plain"),
|
|
733
|
+
createMockFile("b.txt", 200, "text/plain"),
|
|
734
|
+
],
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
await waitFor(() => expect(getByTestId("file-1")).toBeDefined());
|
|
739
|
+
|
|
740
|
+
fireEvent.click(getByTestId("clear"));
|
|
741
|
+
|
|
742
|
+
await waitFor(() => {
|
|
743
|
+
expect(queryByTestId("file-0")).toBeNull();
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
describe("controlled mode", () => {
|
|
749
|
+
it("should work with controlled acceptedFileEntries", () => {
|
|
750
|
+
const file = createMockFile("controlled.txt", 512, "text/plain");
|
|
751
|
+
const fileEntries: FileEntry[] = [{ id: "mock-1", file, status: "pending" }];
|
|
752
|
+
const { getByText } = setUp(<BasicFileUpload acceptedFileEntries={fileEntries} />);
|
|
753
|
+
|
|
754
|
+
expect(getByText("controlled.txt")).toBeDefined();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("should work with defaultAcceptedFileEntries", () => {
|
|
758
|
+
const file = createMockFile("default.txt", 512, "text/plain");
|
|
759
|
+
const fileEntries: FileEntry[] = [{ id: "mock-1", file, status: "pending" }];
|
|
760
|
+
const { getByText } = setUp(<BasicFileUpload defaultAcceptedFileEntries={fileEntries} />);
|
|
761
|
+
|
|
762
|
+
expect(getByText("default.txt")).toBeDefined();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("should reflect external acceptedFileEntries changes on rerender", () => {
|
|
766
|
+
const file1 = createMockFile("first.txt", 512, "text/plain");
|
|
767
|
+
const file2 = createMockFile("second.txt", 512, "text/plain");
|
|
768
|
+
|
|
769
|
+
const { getByText, queryByText, rerender } = setUp(
|
|
770
|
+
<BasicFileUpload
|
|
771
|
+
acceptedFileEntries={[{ id: "mock-1", file: file1, status: "pending" }]}
|
|
772
|
+
/>,
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
expect(getByText("first.txt")).toBeDefined();
|
|
776
|
+
expect(queryByText("second.txt")).toBeNull();
|
|
777
|
+
|
|
778
|
+
rerender(
|
|
779
|
+
<BasicFileUpload
|
|
780
|
+
acceptedFileEntries={[{ id: "mock-2", file: file2, status: "pending" }]}
|
|
781
|
+
/>,
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
expect(queryByText("first.txt")).toBeNull();
|
|
785
|
+
expect(getByText("second.txt")).toBeDefined();
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("should call onAcceptedFileEntriesChange when removing a file in controlled mode", async () => {
|
|
789
|
+
const file = createMockFile("controlled.txt", 512, "text/plain");
|
|
790
|
+
const fileEntries: FileEntry[] = [{ id: "mock-1", file, status: "pending" }];
|
|
791
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
792
|
+
|
|
793
|
+
const { getByTestId } = setUp(
|
|
794
|
+
<BasicFileUpload
|
|
795
|
+
acceptedFileEntries={fileEntries}
|
|
796
|
+
onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}
|
|
797
|
+
/>,
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
const deleteButton = getByTestId("delete-0");
|
|
801
|
+
await userEvent.click(deleteButton);
|
|
802
|
+
|
|
803
|
+
await waitFor(() => {
|
|
804
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([]);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("should call onAcceptedFileEntriesChange when adding files in controlled mode", async () => {
|
|
809
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
810
|
+
|
|
811
|
+
const { getByTestId } = setUp(
|
|
812
|
+
<BasicFileUpload
|
|
813
|
+
maxFiles={3}
|
|
814
|
+
acceptedFileEntries={[]}
|
|
815
|
+
onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}
|
|
816
|
+
/>,
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
820
|
+
const newFile = createMockFile("new.txt", 512, "text/plain");
|
|
821
|
+
|
|
822
|
+
fireEvent.change(input, { target: { files: [newFile] } });
|
|
823
|
+
|
|
824
|
+
await waitFor(() => {
|
|
825
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
826
|
+
expect.objectContaining({ file: newFile, status: "pending" }),
|
|
827
|
+
]);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
describe("validation", () => {
|
|
833
|
+
it("should reject files exceeding maxFileSize", async () => {
|
|
834
|
+
const onFileReject = mock(() => {});
|
|
835
|
+
const { getByTestId } = setUp(
|
|
836
|
+
<BasicFileUpload maxFileSize={1024} onFileReject={onFileReject} />,
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
840
|
+
const largeFile = createMockFile("large.txt", 2048, "text/plain");
|
|
841
|
+
|
|
842
|
+
fireEvent.change(input, { target: { files: [largeFile] } });
|
|
843
|
+
|
|
844
|
+
await waitFor(() => {
|
|
845
|
+
expect(onFileReject).toHaveBeenCalled();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it("should reject files below minFileSize", async () => {
|
|
850
|
+
const onFileReject = mock(() => {});
|
|
851
|
+
const { getByTestId } = setUp(
|
|
852
|
+
<BasicFileUpload minFileSize={1024} onFileReject={onFileReject} />,
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
856
|
+
const smallFile = createMockFile("small.txt", 100, "text/plain");
|
|
857
|
+
|
|
858
|
+
fireEvent.change(input, { target: { files: [smallFile] } });
|
|
859
|
+
|
|
860
|
+
await waitFor(() => {
|
|
861
|
+
expect(onFileReject).toHaveBeenCalled();
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it("should reject files exceeding maxFiles limit", async () => {
|
|
866
|
+
const onFileReject = mock(() => {});
|
|
867
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={1} onFileReject={onFileReject} />);
|
|
868
|
+
|
|
869
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
870
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
871
|
+
const file2 = createMockFile("file2.txt", 100, "text/plain");
|
|
872
|
+
|
|
873
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
874
|
+
|
|
875
|
+
await waitFor(() => {
|
|
876
|
+
expect(onFileReject).toHaveBeenCalled();
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("should reject files with invalid type", async () => {
|
|
881
|
+
const onFileReject = mock(() => {});
|
|
882
|
+
const { getByTestId } = setUp(
|
|
883
|
+
<BasicFileUpload accept="image/*" onFileReject={onFileReject} />,
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
887
|
+
const textFile = createMockFile("document.txt", 100, "text/plain");
|
|
888
|
+
|
|
889
|
+
fireEvent.change(input, { target: { files: [textFile] } });
|
|
890
|
+
|
|
891
|
+
await waitFor(() => {
|
|
892
|
+
expect(onFileReject).toHaveBeenCalled();
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it("should call custom validate function", async () => {
|
|
897
|
+
const customValidate = mock((file: File): string[] | null => {
|
|
898
|
+
if (file.name.includes("invalid")) {
|
|
899
|
+
return ["CUSTOM"];
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
const { getByTestId } = setUp(<BasicFileUpload validate={customValidate} />);
|
|
905
|
+
|
|
906
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
907
|
+
const file = createMockFile("invalid-file.txt", 100, "text/plain");
|
|
908
|
+
|
|
909
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
910
|
+
|
|
911
|
+
await waitFor(() => {
|
|
912
|
+
expect(customValidate).toHaveBeenCalledWith(file);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
describe("drag and drop", () => {
|
|
918
|
+
it("should set dragging state on dragover", async () => {
|
|
919
|
+
const { getByTestId } = setUp(<BasicFileUpload />);
|
|
920
|
+
const dropzone = getByTestId("dropzone");
|
|
921
|
+
|
|
922
|
+
fireEvent.dragOver(dropzone);
|
|
923
|
+
|
|
924
|
+
await waitFor(() => {
|
|
925
|
+
expect(dropzone.getAttribute("data-dragging-over")).toBe("");
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it("should clear dragging state on dragleave", async () => {
|
|
930
|
+
const { getByTestId } = setUp(<BasicFileUpload />);
|
|
931
|
+
const dropzone = getByTestId("dropzone");
|
|
932
|
+
|
|
933
|
+
fireEvent.dragOver(dropzone);
|
|
934
|
+
fireEvent.dragLeave(dropzone);
|
|
935
|
+
|
|
936
|
+
await waitFor(() => {
|
|
937
|
+
expect(dropzone.getAttribute("data-dragging-over")).toBeNull();
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("should accept dropped files", async () => {
|
|
942
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
943
|
+
const { getByTestId } = setUp(
|
|
944
|
+
<BasicFileUpload onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
945
|
+
);
|
|
946
|
+
const dropzone = getByTestId("dropzone");
|
|
947
|
+
|
|
948
|
+
const file = createMockFile("dropped.txt", 1024, "text/plain");
|
|
949
|
+
|
|
950
|
+
fireEvent.drop(dropzone, {
|
|
951
|
+
dataTransfer: {
|
|
952
|
+
files: [file],
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
await waitFor(() => {
|
|
957
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
958
|
+
expect.objectContaining({ file, status: "pending" }),
|
|
959
|
+
]);
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it("should call onAcceptedFileEntriesChange with multiple dropped files", async () => {
|
|
964
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
965
|
+
const { getByTestId } = setUp(
|
|
966
|
+
<BasicFileUpload maxFiles={5} onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
967
|
+
);
|
|
968
|
+
const dropzone = getByTestId("dropzone");
|
|
969
|
+
|
|
970
|
+
const file1 = createMockFile("photo1.png", 500, "image/png");
|
|
971
|
+
const file2 = createMockFile("photo2.png", 600, "image/png");
|
|
972
|
+
|
|
973
|
+
fireEvent.drop(dropzone, {
|
|
974
|
+
dataTransfer: { files: [file1, file2] },
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
await waitFor(() => {
|
|
978
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
979
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
980
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
981
|
+
]);
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it("should accumulate files across multiple drops", async () => {
|
|
986
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
987
|
+
const { getByTestId } = setUp(
|
|
988
|
+
<BasicFileUpload maxFiles={5} onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
989
|
+
);
|
|
990
|
+
const dropzone = getByTestId("dropzone");
|
|
991
|
+
|
|
992
|
+
const file1 = createMockFile("first.txt", 100, "text/plain");
|
|
993
|
+
fireEvent.drop(dropzone, {
|
|
994
|
+
dataTransfer: { files: [file1] },
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
await waitFor(() => {
|
|
998
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
999
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
1000
|
+
]);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const file2 = createMockFile("second.txt", 200, "text/plain");
|
|
1004
|
+
fireEvent.drop(dropzone, {
|
|
1005
|
+
dataTransfer: { files: [file2] },
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await waitFor(() => {
|
|
1009
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1010
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
1011
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
1012
|
+
]);
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
describe("form integration", () => {
|
|
1018
|
+
it("should have aria-required when required", () => {
|
|
1019
|
+
const { getByTestId } = setUp(<BasicFileUpload required />);
|
|
1020
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1021
|
+
|
|
1022
|
+
expect(input.getAttribute("aria-required")).toBe("true");
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it("should not have aria-required when not required", () => {
|
|
1026
|
+
const { getByTestId } = setUp(<BasicFileUpload />);
|
|
1027
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1028
|
+
|
|
1029
|
+
expect(input.getAttribute("aria-required")).toBeNull();
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it("should have name attribute when name is provided", () => {
|
|
1033
|
+
const { getByTestId } = setUp(<BasicFileUpload name="attachment" />);
|
|
1034
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1035
|
+
|
|
1036
|
+
expect(input.getAttribute("name")).toBe("attachment");
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it("should have disabled attribute when disabled", () => {
|
|
1040
|
+
const { getByTestId } = setUp(<BasicFileUpload disabled />);
|
|
1041
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1042
|
+
|
|
1043
|
+
expect(input.disabled).toBe(true);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it("should not generate an id on the hidden input", () => {
|
|
1047
|
+
const { getByTestId } = setUp(<BasicFileUpload />);
|
|
1048
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1049
|
+
|
|
1050
|
+
expect(input.getAttribute("id")).toBeNull();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("should keep input.files in sync with accepted files via DataTransfer", async () => {
|
|
1054
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={3} name="files" />);
|
|
1055
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1056
|
+
|
|
1057
|
+
// Add 2 files
|
|
1058
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
1059
|
+
const file2 = createMockFile("b.txt", 200, "text/plain");
|
|
1060
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
1061
|
+
|
|
1062
|
+
await waitFor(() => {
|
|
1063
|
+
expect(input.files).toHaveLength(2);
|
|
1064
|
+
expect(input.files?.[0]?.name).toBe("a.txt");
|
|
1065
|
+
expect(input.files?.[1]?.name).toBe("b.txt");
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Remove first file → input.files should update
|
|
1069
|
+
await userEvent.click(getByTestId("delete-0"));
|
|
1070
|
+
|
|
1071
|
+
await waitFor(() => {
|
|
1072
|
+
expect(input.files).toHaveLength(1);
|
|
1073
|
+
expect(input.files?.[0]?.name).toBe("b.txt");
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Remove last file → input.files should be empty
|
|
1077
|
+
await userEvent.click(getByTestId("delete-0"));
|
|
1078
|
+
|
|
1079
|
+
await waitFor(() => {
|
|
1080
|
+
expect(input.files).toHaveLength(0);
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
describe("duplicate file names", () => {
|
|
1086
|
+
it("should accept multiple files with the same name", async () => {
|
|
1087
|
+
const { getByTestId, getAllByText } = setUp(<BasicFileUpload maxFiles={3} />);
|
|
1088
|
+
|
|
1089
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1090
|
+
const file1 = createMockFile("photo.png", 1024, "image/png");
|
|
1091
|
+
const file2 = createMockFile("photo.png", 2048, "image/png");
|
|
1092
|
+
const file3 = createMockFile("photo.png", 4096, "image/png");
|
|
1093
|
+
|
|
1094
|
+
fireEvent.change(input, { target: { files: [file1, file2, file3] } });
|
|
1095
|
+
|
|
1096
|
+
await waitFor(() => {
|
|
1097
|
+
expect(getAllByText("photo.png")).toHaveLength(3);
|
|
1098
|
+
expect(getByTestId("item-0")).toBeDefined();
|
|
1099
|
+
expect(getByTestId("item-1")).toBeDefined();
|
|
1100
|
+
expect(getByTestId("item-2")).toBeDefined();
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("should delete only the targeted file among duplicates", async () => {
|
|
1105
|
+
const { getByTestId, getAllByText, user } = setUp(<BasicFileUpload maxFiles={3} />);
|
|
1106
|
+
|
|
1107
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1108
|
+
const file1 = createMockFile("photo.png", 1024, "image/png");
|
|
1109
|
+
const file2 = createMockFile("photo.png", 2048, "image/png");
|
|
1110
|
+
const file3 = createMockFile("photo.png", 4096, "image/png");
|
|
1111
|
+
|
|
1112
|
+
fireEvent.change(input, { target: { files: [file1, file2, file3] } });
|
|
1113
|
+
|
|
1114
|
+
await waitFor(() => {
|
|
1115
|
+
expect(getAllByText("photo.png")).toHaveLength(3);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Delete the middle file (index 1)
|
|
1119
|
+
await user.click(getByTestId("delete-1"));
|
|
1120
|
+
|
|
1121
|
+
await waitFor(() => {
|
|
1122
|
+
expect(getAllByText("photo.png")).toHaveLength(2);
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it("should delete all duplicate files one by one", async () => {
|
|
1127
|
+
const { getByTestId, queryAllByText, user } = setUp(<BasicFileUpload maxFiles={3} />);
|
|
1128
|
+
|
|
1129
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1130
|
+
const file1 = createMockFile("photo.png", 1024, "image/png");
|
|
1131
|
+
const file2 = createMockFile("photo.png", 2048, "image/png");
|
|
1132
|
+
|
|
1133
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
1134
|
+
|
|
1135
|
+
await waitFor(() => {
|
|
1136
|
+
expect(queryAllByText("photo.png")).toHaveLength(2);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
await user.click(getByTestId("delete-0"));
|
|
1140
|
+
|
|
1141
|
+
await waitFor(() => {
|
|
1142
|
+
expect(queryAllByText("photo.png")).toHaveLength(1);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
await user.click(getByTestId("delete-0"));
|
|
1146
|
+
|
|
1147
|
+
await waitFor(() => {
|
|
1148
|
+
expect(queryAllByText("photo.png")).toHaveLength(0);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
describe("reorder files", () => {
|
|
1154
|
+
// Helper component that exposes reorderFileEntry for testing
|
|
1155
|
+
const ReorderTestComponent = () => {
|
|
1156
|
+
return (
|
|
1157
|
+
<FileUploadRoot maxFiles={5}>
|
|
1158
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
1159
|
+
<FileUploadContext>
|
|
1160
|
+
{({ acceptedFileEntries, reorderFileEntry }) => (
|
|
1161
|
+
<>
|
|
1162
|
+
<ul data-testid="file-list">
|
|
1163
|
+
{acceptedFileEntries.map((f, index) => (
|
|
1164
|
+
<li key={f.id} data-testid={`file-${index}`}>
|
|
1165
|
+
{f.file.name}
|
|
1166
|
+
</li>
|
|
1167
|
+
))}
|
|
1168
|
+
</ul>
|
|
1169
|
+
<button
|
|
1170
|
+
type="button"
|
|
1171
|
+
data-testid="reorder-0-to-2"
|
|
1172
|
+
onClick={() => reorderFileEntry(0, 2)}
|
|
1173
|
+
>
|
|
1174
|
+
Move first to third
|
|
1175
|
+
</button>
|
|
1176
|
+
<button
|
|
1177
|
+
type="button"
|
|
1178
|
+
data-testid="reorder-2-to-0"
|
|
1179
|
+
onClick={() => reorderFileEntry(2, 0)}
|
|
1180
|
+
>
|
|
1181
|
+
Move third to first
|
|
1182
|
+
</button>
|
|
1183
|
+
<button
|
|
1184
|
+
type="button"
|
|
1185
|
+
data-testid="reorder-invalid"
|
|
1186
|
+
onClick={() => reorderFileEntry(-1, 10)}
|
|
1187
|
+
>
|
|
1188
|
+
Invalid reorder
|
|
1189
|
+
</button>
|
|
1190
|
+
</>
|
|
1191
|
+
)}
|
|
1192
|
+
</FileUploadContext>
|
|
1193
|
+
</FileUploadRoot>
|
|
1194
|
+
);
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
it("should reorder files from index 0 to index 2", async () => {
|
|
1198
|
+
const { getByTestId, user } = setUp(<ReorderTestComponent />);
|
|
1199
|
+
|
|
1200
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1201
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
1202
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
1203
|
+
const file3 = createMockFile("file3.txt", 300, "text/plain");
|
|
1204
|
+
|
|
1205
|
+
fireEvent.change(input, { target: { files: [file1, file2, file3] } });
|
|
1206
|
+
|
|
1207
|
+
await waitFor(() => {
|
|
1208
|
+
expect(getByTestId("file-0").textContent).toBe("file1.txt");
|
|
1209
|
+
expect(getByTestId("file-1").textContent).toBe("file2.txt");
|
|
1210
|
+
expect(getByTestId("file-2").textContent).toBe("file3.txt");
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
await user.click(getByTestId("reorder-0-to-2"));
|
|
1214
|
+
|
|
1215
|
+
await waitFor(() => {
|
|
1216
|
+
expect(getByTestId("file-0").textContent).toBe("file2.txt");
|
|
1217
|
+
expect(getByTestId("file-1").textContent).toBe("file3.txt");
|
|
1218
|
+
expect(getByTestId("file-2").textContent).toBe("file1.txt");
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it("should reorder files from index 2 to index 0", async () => {
|
|
1223
|
+
const { getByTestId, user } = setUp(<ReorderTestComponent />);
|
|
1224
|
+
|
|
1225
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1226
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
1227
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
1228
|
+
const file3 = createMockFile("file3.txt", 300, "text/plain");
|
|
1229
|
+
|
|
1230
|
+
fireEvent.change(input, { target: { files: [file1, file2, file3] } });
|
|
1231
|
+
|
|
1232
|
+
await waitFor(() => {
|
|
1233
|
+
expect(getByTestId("file-0").textContent).toBe("file1.txt");
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
await user.click(getByTestId("reorder-2-to-0"));
|
|
1237
|
+
|
|
1238
|
+
await waitFor(() => {
|
|
1239
|
+
expect(getByTestId("file-0").textContent).toBe("file3.txt");
|
|
1240
|
+
expect(getByTestId("file-1").textContent).toBe("file1.txt");
|
|
1241
|
+
expect(getByTestId("file-2").textContent).toBe("file2.txt");
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it("should not change order with invalid indices", async () => {
|
|
1246
|
+
const { getByTestId, user } = setUp(<ReorderTestComponent />);
|
|
1247
|
+
|
|
1248
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1249
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
1250
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
1251
|
+
|
|
1252
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
1253
|
+
|
|
1254
|
+
await waitFor(() => {
|
|
1255
|
+
expect(getByTestId("file-0").textContent).toBe("file1.txt");
|
|
1256
|
+
expect(getByTestId("file-1").textContent).toBe("file2.txt");
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
await user.click(getByTestId("reorder-invalid"));
|
|
1260
|
+
|
|
1261
|
+
await waitFor(() => {
|
|
1262
|
+
expect(getByTestId("file-0").textContent).toBe("file1.txt");
|
|
1263
|
+
expect(getByTestId("file-1").textContent).toBe("file2.txt");
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it("should not reorder when disabled", async () => {
|
|
1268
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
1269
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
1270
|
+
const fileEntries: FileEntry[] = [
|
|
1271
|
+
{ id: "mock-1", file: file1, status: "pending" },
|
|
1272
|
+
{ id: "mock-2", file: file2, status: "pending" },
|
|
1273
|
+
];
|
|
1274
|
+
|
|
1275
|
+
const DisabledReorderComponent = () => {
|
|
1276
|
+
return (
|
|
1277
|
+
<FileUploadRoot maxFiles={5} disabled defaultAcceptedFileEntries={fileEntries}>
|
|
1278
|
+
<FileUploadContext>
|
|
1279
|
+
{({ acceptedFileEntries, reorderFileEntry }) => (
|
|
1280
|
+
<>
|
|
1281
|
+
<ul data-testid="file-list">
|
|
1282
|
+
{acceptedFileEntries.map((file, index) => (
|
|
1283
|
+
<li key={file.id} data-testid={`file-${index}`}>
|
|
1284
|
+
{file.file.name}
|
|
1285
|
+
</li>
|
|
1286
|
+
))}
|
|
1287
|
+
</ul>
|
|
1288
|
+
<button
|
|
1289
|
+
type="button"
|
|
1290
|
+
data-testid="reorder-btn"
|
|
1291
|
+
onClick={() => reorderFileEntry(0, 1)}
|
|
1292
|
+
>
|
|
1293
|
+
Reorder
|
|
1294
|
+
</button>
|
|
1295
|
+
</>
|
|
1296
|
+
)}
|
|
1297
|
+
</FileUploadContext>
|
|
1298
|
+
</FileUploadRoot>
|
|
1299
|
+
);
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
const { getByTestId, user } = setUp(<DisabledReorderComponent />);
|
|
1303
|
+
|
|
1304
|
+
await waitFor(() => {
|
|
1305
|
+
expect(getByTestId("file-0").textContent).toBe("file1.txt");
|
|
1306
|
+
expect(getByTestId("file-1").textContent).toBe("file2.txt");
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
await user.click(getByTestId("reorder-btn"));
|
|
1310
|
+
|
|
1311
|
+
// Should remain unchanged because disabled
|
|
1312
|
+
await waitFor(() => {
|
|
1313
|
+
expect(getByTestId("file-0").textContent).toBe("file1.txt");
|
|
1314
|
+
expect(getByTestId("file-1").textContent).toBe("file2.txt");
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
describe("onFileReject details", () => {
|
|
1320
|
+
it("should include FILE_TOO_LARGE error code", async () => {
|
|
1321
|
+
const onFileReject = mock(() => {});
|
|
1322
|
+
const { getByTestId } = setUp(
|
|
1323
|
+
<BasicFileUpload maxFileSize={1024} onFileReject={onFileReject} />,
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1327
|
+
const file = createMockFile("big.txt", 2048, "text/plain");
|
|
1328
|
+
|
|
1329
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1330
|
+
|
|
1331
|
+
await waitFor(() => {
|
|
1332
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file, errors: ["FILE_TOO_LARGE"] }]);
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it("should include FILE_TOO_SMALL error code", async () => {
|
|
1337
|
+
const onFileReject = mock(() => {});
|
|
1338
|
+
const { getByTestId } = setUp(
|
|
1339
|
+
<BasicFileUpload minFileSize={1024} onFileReject={onFileReject} />,
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1342
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1343
|
+
const file = createMockFile("tiny.txt", 100, "text/plain");
|
|
1344
|
+
|
|
1345
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1346
|
+
|
|
1347
|
+
await waitFor(() => {
|
|
1348
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file, errors: ["FILE_TOO_SMALL"] }]);
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
it("should include INVALID_TYPE error code", async () => {
|
|
1353
|
+
const onFileReject = mock(() => {});
|
|
1354
|
+
const { getByTestId } = setUp(
|
|
1355
|
+
<BasicFileUpload accept="image/*" onFileReject={onFileReject} />,
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1359
|
+
const file = createMockFile("doc.txt", 100, "text/plain");
|
|
1360
|
+
|
|
1361
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1362
|
+
|
|
1363
|
+
await waitFor(() => {
|
|
1364
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file, errors: ["INVALID_TYPE"] }]);
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it("should include TOO_MANY_FILES error code for excess files", async () => {
|
|
1369
|
+
const onFileReject = mock(() => {});
|
|
1370
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={1} onFileReject={onFileReject} />);
|
|
1371
|
+
|
|
1372
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1373
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
1374
|
+
const file2 = createMockFile("b.txt", 100, "text/plain");
|
|
1375
|
+
|
|
1376
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
1377
|
+
|
|
1378
|
+
await waitFor(() => {
|
|
1379
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file: file2, errors: ["TOO_MANY_FILES"] }]);
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it("should include multiple error codes for same file", async () => {
|
|
1384
|
+
const onFileReject = mock(() => {});
|
|
1385
|
+
const { getByTestId } = setUp(
|
|
1386
|
+
<BasicFileUpload accept="image/*" maxFileSize={500} onFileReject={onFileReject} />,
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1390
|
+
const file = createMockFile("big.txt", 1024, "text/plain");
|
|
1391
|
+
|
|
1392
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1393
|
+
|
|
1394
|
+
await waitFor(() => {
|
|
1395
|
+
expect(onFileReject).toHaveBeenCalledWith([
|
|
1396
|
+
{ file, errors: ["INVALID_TYPE", "FILE_TOO_LARGE"] },
|
|
1397
|
+
]);
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
it("should include custom error code from validate", async () => {
|
|
1402
|
+
const onFileReject = mock(() => {});
|
|
1403
|
+
const validate = (file: File) => (file.name.startsWith("bad") ? ["CUSTOM_ERROR"] : null);
|
|
1404
|
+
|
|
1405
|
+
const { getByTestId } = setUp(
|
|
1406
|
+
<BasicFileUpload validate={validate} onFileReject={onFileReject} />,
|
|
1407
|
+
);
|
|
1408
|
+
|
|
1409
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1410
|
+
const file = createMockFile("bad-file.txt", 100, "text/plain");
|
|
1411
|
+
|
|
1412
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1413
|
+
|
|
1414
|
+
await waitFor(() => {
|
|
1415
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file, errors: ["CUSTOM_ERROR"] }]);
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it("should reject invalid files on drop", async () => {
|
|
1420
|
+
const onFileReject = mock(() => {});
|
|
1421
|
+
const { getByTestId } = setUp(
|
|
1422
|
+
<BasicFileUpload accept="image/*" onFileReject={onFileReject} />,
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1425
|
+
const dropzone = getByTestId("dropzone");
|
|
1426
|
+
const file = createMockFile("doc.txt", 100, "text/plain");
|
|
1427
|
+
|
|
1428
|
+
fireEvent.drop(dropzone, {
|
|
1429
|
+
dataTransfer: { files: [file] },
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
await waitFor(() => {
|
|
1433
|
+
expect(onFileReject).toHaveBeenCalledWith([{ file, errors: ["INVALID_TYPE"] }]);
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
describe("onAcceptedFileEntriesChange on removal", () => {
|
|
1439
|
+
it("should call onAcceptedFileEntriesChange with empty array when last file is removed", async () => {
|
|
1440
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
1441
|
+
const { getByTestId, user } = setUp(
|
|
1442
|
+
<BasicFileUpload onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
1443
|
+
);
|
|
1444
|
+
|
|
1445
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1446
|
+
const file = createMockFile("test.txt", 100, "text/plain");
|
|
1447
|
+
|
|
1448
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1449
|
+
|
|
1450
|
+
await waitFor(() => {
|
|
1451
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1452
|
+
expect.objectContaining({ file, status: "pending" }),
|
|
1453
|
+
]);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
onAcceptedFileEntriesChange.mockClear();
|
|
1457
|
+
await user.click(getByTestId("delete-0"));
|
|
1458
|
+
|
|
1459
|
+
await waitFor(() => {
|
|
1460
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([]);
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
it("should call onAcceptedFileEntriesChange with remaining files after removal", async () => {
|
|
1465
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
1466
|
+
const { getByTestId, user } = setUp(
|
|
1467
|
+
<BasicFileUpload maxFiles={3} onAcceptedFileEntriesChange={onAcceptedFileEntriesChange} />,
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1471
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
1472
|
+
const file2 = createMockFile("b.txt", 200, "text/plain");
|
|
1473
|
+
const file3 = createMockFile("c.txt", 300, "text/plain");
|
|
1474
|
+
|
|
1475
|
+
fireEvent.change(input, { target: { files: [file1, file2, file3] } });
|
|
1476
|
+
|
|
1477
|
+
await waitFor(() => {
|
|
1478
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1479
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
1480
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
1481
|
+
expect.objectContaining({ file: file3, status: "pending" }),
|
|
1482
|
+
]);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
onAcceptedFileEntriesChange.mockClear();
|
|
1486
|
+
await user.click(getByTestId("delete-1"));
|
|
1487
|
+
|
|
1488
|
+
await waitFor(() => {
|
|
1489
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1490
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
1491
|
+
expect.objectContaining({ file: file3, status: "pending" }),
|
|
1492
|
+
]);
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
describe("both callbacks", () => {
|
|
1498
|
+
it("should call both callbacks when batch has valid and invalid files", async () => {
|
|
1499
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
1500
|
+
const onFileReject = mock(() => {});
|
|
1501
|
+
const { getByTestId } = setUp(
|
|
1502
|
+
<BasicFileUpload
|
|
1503
|
+
maxFiles={5}
|
|
1504
|
+
accept="image/*"
|
|
1505
|
+
onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}
|
|
1506
|
+
onFileReject={onFileReject}
|
|
1507
|
+
/>,
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1511
|
+
const validFile = createMockFile("photo.png", 100, "image/png");
|
|
1512
|
+
const invalidFile = createMockFile("doc.txt", 100, "text/plain");
|
|
1513
|
+
|
|
1514
|
+
fireEvent.change(input, { target: { files: [validFile, invalidFile] } });
|
|
1515
|
+
|
|
1516
|
+
await waitFor(() => {
|
|
1517
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1518
|
+
expect.objectContaining({ file: validFile, status: "pending" }),
|
|
1519
|
+
]);
|
|
1520
|
+
expect(onFileReject).toHaveBeenCalledWith([
|
|
1521
|
+
{ file: invalidFile, errors: ["INVALID_TYPE"] },
|
|
1522
|
+
]);
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("should partially accept and reject when maxFiles is reached mid-batch", async () => {
|
|
1527
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
1528
|
+
const onFileReject = mock(() => {});
|
|
1529
|
+
const { getByTestId } = setUp(
|
|
1530
|
+
<BasicFileUpload
|
|
1531
|
+
maxFiles={3}
|
|
1532
|
+
onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}
|
|
1533
|
+
onFileReject={onFileReject}
|
|
1534
|
+
/>,
|
|
1535
|
+
);
|
|
1536
|
+
|
|
1537
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1538
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
1539
|
+
const file2 = createMockFile("b.txt", 100, "text/plain");
|
|
1540
|
+
|
|
1541
|
+
// first batch: add 2 files
|
|
1542
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
1543
|
+
|
|
1544
|
+
await waitFor(() => {
|
|
1545
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1546
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
1547
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
1548
|
+
]);
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
onAcceptedFileEntriesChange.mockClear();
|
|
1552
|
+
onFileReject.mockClear();
|
|
1553
|
+
|
|
1554
|
+
const file3 = createMockFile("c.txt", 100, "text/plain");
|
|
1555
|
+
const file4 = createMockFile("d.txt", 100, "text/plain");
|
|
1556
|
+
const file5 = createMockFile("e.txt", 100, "text/plain");
|
|
1557
|
+
|
|
1558
|
+
// second batch: add 3 more, but only 1 slot remaining
|
|
1559
|
+
fireEvent.change(input, { target: { files: [file3, file4, file5] } });
|
|
1560
|
+
|
|
1561
|
+
await waitFor(() => {
|
|
1562
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1563
|
+
expect.objectContaining({ file: file1, status: "pending" }),
|
|
1564
|
+
expect.objectContaining({ file: file2, status: "pending" }),
|
|
1565
|
+
expect.objectContaining({ file: file3, status: "pending" }),
|
|
1566
|
+
]);
|
|
1567
|
+
expect(onFileReject).toHaveBeenCalledWith([
|
|
1568
|
+
{ file: file4, errors: ["TOO_MANY_FILES"] },
|
|
1569
|
+
{ file: file5, errors: ["TOO_MANY_FILES"] },
|
|
1570
|
+
]);
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it("should only call onFileReject when all files in batch are invalid", async () => {
|
|
1575
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
1576
|
+
const onFileReject = mock(() => {});
|
|
1577
|
+
const { getByTestId } = setUp(
|
|
1578
|
+
<BasicFileUpload
|
|
1579
|
+
maxFiles={5}
|
|
1580
|
+
accept="image/*"
|
|
1581
|
+
onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}
|
|
1582
|
+
onFileReject={onFileReject}
|
|
1583
|
+
/>,
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1587
|
+
const file1 = createMockFile("a.txt", 100, "text/plain");
|
|
1588
|
+
const file2 = createMockFile("b.txt", 100, "text/plain");
|
|
1589
|
+
|
|
1590
|
+
fireEvent.change(input, { target: { files: [file1, file2] } });
|
|
1591
|
+
|
|
1592
|
+
await waitFor(() => {
|
|
1593
|
+
expect(onFileReject).toHaveBeenCalledWith([
|
|
1594
|
+
{ file: file1, errors: ["INVALID_TYPE"] },
|
|
1595
|
+
{ file: file2, errors: ["INVALID_TYPE"] },
|
|
1596
|
+
]);
|
|
1597
|
+
expect(onAcceptedFileEntriesChange).not.toHaveBeenCalled();
|
|
1598
|
+
});
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
describe("preventDocumentDrop", () => {
|
|
1603
|
+
it("should prevent default on document drop by default", () => {
|
|
1604
|
+
setUp(<BasicFileUpload />);
|
|
1605
|
+
|
|
1606
|
+
const dropEvent = new Event("drop", { bubbles: true, cancelable: true });
|
|
1607
|
+
document.dispatchEvent(dropEvent);
|
|
1608
|
+
|
|
1609
|
+
expect(dropEvent.defaultPrevented).toBe(true);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
it("should prevent default on document dragover by default", () => {
|
|
1613
|
+
setUp(<BasicFileUpload />);
|
|
1614
|
+
|
|
1615
|
+
const dragOverEvent = new Event("dragover", { bubbles: true, cancelable: true });
|
|
1616
|
+
document.dispatchEvent(dragOverEvent);
|
|
1617
|
+
|
|
1618
|
+
expect(dragOverEvent.defaultPrevented).toBe(true);
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
it("should not prevent default when preventDocumentDrop is false", () => {
|
|
1622
|
+
setUp(<BasicFileUpload preventDocumentDrop={false} />);
|
|
1623
|
+
|
|
1624
|
+
const dropEvent = new Event("drop", { bubbles: true, cancelable: true });
|
|
1625
|
+
document.dispatchEvent(dropEvent);
|
|
1626
|
+
|
|
1627
|
+
expect(dropEvent.defaultPrevented).toBe(false);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
it("should not register listeners when disabled", () => {
|
|
1631
|
+
setUp(<BasicFileUpload disabled preventDocumentDrop />);
|
|
1632
|
+
|
|
1633
|
+
const dropEvent = new Event("drop", { bubbles: true, cancelable: true });
|
|
1634
|
+
document.dispatchEvent(dropEvent);
|
|
1635
|
+
|
|
1636
|
+
expect(dropEvent.defaultPrevented).toBe(false);
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
it("should clean up listeners on unmount", () => {
|
|
1640
|
+
const { unmount } = setUp(<BasicFileUpload />);
|
|
1641
|
+
|
|
1642
|
+
unmount();
|
|
1643
|
+
|
|
1644
|
+
const dropEvent = new Event("drop", { bubbles: true, cancelable: true });
|
|
1645
|
+
document.dispatchEvent(dropEvent);
|
|
1646
|
+
|
|
1647
|
+
expect(dropEvent.defaultPrevented).toBe(false);
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
it("should not interfere with drops inside the dropzone", () => {
|
|
1651
|
+
const onFileReject = mock(() => {});
|
|
1652
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={5} onFileReject={onFileReject} />);
|
|
1653
|
+
|
|
1654
|
+
const dropzone = getByTestId("dropzone");
|
|
1655
|
+
const file = createMockFile("test.txt", 100, "text/plain");
|
|
1656
|
+
|
|
1657
|
+
fireEvent.drop(dropzone, {
|
|
1658
|
+
dataTransfer: { files: [file] },
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
// Drop on dropzone should still process files normally
|
|
1662
|
+
expect(onFileReject).not.toHaveBeenCalled();
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
describe("onFileAccept", () => {
|
|
1667
|
+
it("should call onFileAccept with newly added entries via input", async () => {
|
|
1668
|
+
const onFileAccept = mock(() => {});
|
|
1669
|
+
const { getByTestId } = setUp(<BasicFileUpload onFileAccept={onFileAccept} />);
|
|
1670
|
+
|
|
1671
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1672
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
1673
|
+
|
|
1674
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1675
|
+
|
|
1676
|
+
await waitFor(() => {
|
|
1677
|
+
expect(onFileAccept).toHaveBeenCalledTimes(1);
|
|
1678
|
+
expect(onFileAccept).toHaveBeenCalledWith(
|
|
1679
|
+
[expect.objectContaining({ file, status: "pending" })],
|
|
1680
|
+
{ updateFileEntryStatus: expect.any(Function) },
|
|
1681
|
+
);
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
it("should call onFileAccept with only newly added entries, not all entries", async () => {
|
|
1686
|
+
const onFileAccept = mock(() => {});
|
|
1687
|
+
const { getByTestId } = setUp(<BasicFileUpload maxFiles={5} onFileAccept={onFileAccept} />);
|
|
1688
|
+
|
|
1689
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1690
|
+
const file1 = createMockFile("file1.txt", 100, "text/plain");
|
|
1691
|
+
const file2 = createMockFile("file2.txt", 200, "text/plain");
|
|
1692
|
+
|
|
1693
|
+
fireEvent.change(input, { target: { files: [file1] } });
|
|
1694
|
+
|
|
1695
|
+
await waitFor(() => {
|
|
1696
|
+
expect(onFileAccept).toHaveBeenCalledTimes(1);
|
|
1697
|
+
expect(onFileAccept).toHaveBeenCalledWith(
|
|
1698
|
+
[expect.objectContaining({ file: file1 })],
|
|
1699
|
+
expect.any(Object),
|
|
1700
|
+
);
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
fireEvent.change(input, { target: { files: [file2] } });
|
|
1704
|
+
|
|
1705
|
+
await waitFor(() => {
|
|
1706
|
+
expect(onFileAccept).toHaveBeenCalledTimes(2);
|
|
1707
|
+
expect(onFileAccept).toHaveBeenLastCalledWith(
|
|
1708
|
+
[expect.objectContaining({ file: file2 })],
|
|
1709
|
+
expect.any(Object),
|
|
1710
|
+
);
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
it("should provide a working updateFileEntryStatus helper", async () => {
|
|
1715
|
+
const onFileAccept = mock(
|
|
1716
|
+
(
|
|
1717
|
+
entries: FileEntry[],
|
|
1718
|
+
helpers: {
|
|
1719
|
+
updateFileEntryStatus: (id: string, details: FileStatusDetails) => void;
|
|
1720
|
+
},
|
|
1721
|
+
) => {
|
|
1722
|
+
for (const entry of entries) {
|
|
1723
|
+
helpers.updateFileEntryStatus(entry.id, { status: "uploading", progress: 0 });
|
|
1724
|
+
}
|
|
1725
|
+
},
|
|
1726
|
+
);
|
|
1727
|
+
const onAcceptedFileEntriesChange = mock(() => {});
|
|
1728
|
+
|
|
1729
|
+
const { getByTestId } = setUp(
|
|
1730
|
+
<BasicFileUpload
|
|
1731
|
+
onFileAccept={onFileAccept}
|
|
1732
|
+
onAcceptedFileEntriesChange={onAcceptedFileEntriesChange}
|
|
1733
|
+
/>,
|
|
1734
|
+
);
|
|
1735
|
+
|
|
1736
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1737
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
1738
|
+
|
|
1739
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1740
|
+
|
|
1741
|
+
await waitFor(() => {
|
|
1742
|
+
expect(onAcceptedFileEntriesChange).toHaveBeenCalledWith([
|
|
1743
|
+
expect.objectContaining({ file, status: "uploading", progress: 0 }),
|
|
1744
|
+
]);
|
|
1745
|
+
});
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
it("should not call onFileAccept when files are rejected", async () => {
|
|
1749
|
+
const onFileAccept = mock(() => {});
|
|
1750
|
+
const onFileReject = mock(() => {});
|
|
1751
|
+
const { getByTestId } = setUp(
|
|
1752
|
+
<BasicFileUpload
|
|
1753
|
+
accept="image/*"
|
|
1754
|
+
onFileAccept={onFileAccept}
|
|
1755
|
+
onFileReject={onFileReject}
|
|
1756
|
+
/>,
|
|
1757
|
+
);
|
|
1758
|
+
|
|
1759
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1760
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
1761
|
+
|
|
1762
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1763
|
+
|
|
1764
|
+
await waitFor(() => {
|
|
1765
|
+
expect(onFileReject).toHaveBeenCalled();
|
|
1766
|
+
expect(onFileAccept).not.toHaveBeenCalled();
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
it("should not call onFileAccept when removing or clearing files", async () => {
|
|
1771
|
+
const onFileAccept = mock(() => {});
|
|
1772
|
+
|
|
1773
|
+
const RemoveTestUpload = () => (
|
|
1774
|
+
<FileUploadRoot maxFiles={3} onFileAccept={onFileAccept}>
|
|
1775
|
+
<FileUploadHiddenInput data-testid="hidden-input" />
|
|
1776
|
+
<FileUploadContext>
|
|
1777
|
+
{({ acceptedFileEntries, removeFileEntry, clearFileEntries }) => (
|
|
1778
|
+
<>
|
|
1779
|
+
<ul>
|
|
1780
|
+
{acceptedFileEntries.map((f, i) => (
|
|
1781
|
+
<li key={f.id} data-testid={`file-${i}`}>
|
|
1782
|
+
{f.file.name}
|
|
1783
|
+
<button
|
|
1784
|
+
type="button"
|
|
1785
|
+
data-testid={`remove-${i}`}
|
|
1786
|
+
onClick={() => removeFileEntry(f.id)}
|
|
1787
|
+
>
|
|
1788
|
+
Remove
|
|
1789
|
+
</button>
|
|
1790
|
+
</li>
|
|
1791
|
+
))}
|
|
1792
|
+
</ul>
|
|
1793
|
+
<button type="button" data-testid="clear" onClick={clearFileEntries}>
|
|
1794
|
+
Clear
|
|
1795
|
+
</button>
|
|
1796
|
+
</>
|
|
1797
|
+
)}
|
|
1798
|
+
</FileUploadContext>
|
|
1799
|
+
</FileUploadRoot>
|
|
1800
|
+
);
|
|
1801
|
+
|
|
1802
|
+
const { getByTestId } = setUp(<RemoveTestUpload />);
|
|
1803
|
+
const input = getByTestId("hidden-input") as HTMLInputElement;
|
|
1804
|
+
const file = createMockFile("test.txt", 100, "text/plain");
|
|
1805
|
+
|
|
1806
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
1807
|
+
await waitFor(() => expect(getByTestId("file-0")).toBeDefined());
|
|
1808
|
+
|
|
1809
|
+
// Reset mock after initial accept call
|
|
1810
|
+
onFileAccept.mockClear();
|
|
1811
|
+
|
|
1812
|
+
fireEvent.click(getByTestId("remove-0"));
|
|
1813
|
+
await waitFor(() => {
|
|
1814
|
+
expect(onFileAccept).not.toHaveBeenCalled();
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
it("should call onFileAccept via drop on dropzone", async () => {
|
|
1819
|
+
const onFileAccept = mock(() => {});
|
|
1820
|
+
const { getByTestId } = setUp(<BasicFileUpload onFileAccept={onFileAccept} />);
|
|
1821
|
+
|
|
1822
|
+
const dropzone = getByTestId("dropzone");
|
|
1823
|
+
const file = createMockFile("dropped.txt", 1024, "text/plain");
|
|
1824
|
+
|
|
1825
|
+
fireEvent.drop(dropzone, {
|
|
1826
|
+
dataTransfer: { files: [file] },
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
await waitFor(() => {
|
|
1830
|
+
expect(onFileAccept).toHaveBeenCalledTimes(1);
|
|
1831
|
+
expect(onFileAccept).toHaveBeenCalledWith(
|
|
1832
|
+
[expect.objectContaining({ file, status: "pending" })],
|
|
1833
|
+
{ updateFileEntryStatus: expect.any(Function) },
|
|
1834
|
+
);
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
it("should not call onFileAccept when disabled", async () => {
|
|
1839
|
+
const onFileAccept = mock(() => {});
|
|
1840
|
+
const { getByTestId } = setUp(<BasicFileUpload disabled onFileAccept={onFileAccept} />);
|
|
1841
|
+
|
|
1842
|
+
const dropzone = getByTestId("dropzone");
|
|
1843
|
+
const file = createMockFile("test.txt", 1024, "text/plain");
|
|
1844
|
+
|
|
1845
|
+
fireEvent.drop(dropzone, {
|
|
1846
|
+
dataTransfer: { files: [file] },
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
await waitFor(() => {
|
|
1850
|
+
expect(onFileAccept).not.toHaveBeenCalled();
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
});
|