/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/widgets/FileUploader/FileUploader.test.tsx
971 строка
30 KB
Lukas Masuch
Apply additional protobuf cleanups (#13767)
02 фев 2026, 23:58
Не верифицирован
02 фев 2026, 23:58
fa33900
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { act, fireEvent, screen, waitFor, within, } from "@testing-library/react" import { userEvent } from "@testing-library/user-event" import { FileUploader as FileUploaderProto, FileUploaderState as FileUploaderStateProto, FileURLs as FileURLsProto, IFileURLs, LabelVisibility as LabelVisibilityProto, UploadedFileInfo as UploadedFileInfoProto, } from "@streamlit/protobuf" import * as UseResizeObserver from "~lib/hooks/useResizeObserver" import { render } from "~lib/test_util" import { WidgetStateManager } from "~lib/WidgetStateManager" import FileUploader, { Props } from "./FileUploader" const createFile = ( filename = "filename.txt", webkitRelativePath?: string, type = "text/plain" ): File => { const file = new File(["Text in a file!"], filename, { type, lastModified: 0, }) if (webkitRelativePath) { Object.defineProperty(file, "webkitRelativePath", { value: webkitRelativePath, writable: false, }) } return file } const buildFileUploaderStateProto = ( fileUrlsArray: IFileURLs[] ): FileUploaderStateProto => new FileUploaderStateProto({ uploadedFileInfo: fileUrlsArray.map( fileUrls => new UploadedFileInfoProto({ fileId: fileUrls.fileId, fileUrls, name: fileUrls.fileId, size: 15, }) ), }) const getProps = ( elementProps: Partial<FileUploaderProto> = {}, widgetProps: Partial<Props> = {} ): Props => { return { element: FileUploaderProto.create({ id: "id", type: [], maxUploadSizeMb: 50, ...elementProps, }), width: 250, disabled: false, widgetMgr: new WidgetStateManager({ sendRerunBackMsg: vi.fn(), formsDataChanged: vi.fn(), }), // @ts-expect-error uploadClient: { uploadFile: vi.fn().mockImplementation(() => { return Promise.resolve() }), fetchFileURLs: vi.fn().mockImplementation((acceptedFiles: File[]) => { return Promise.resolve( acceptedFiles.map(file => { return new FileURLsProto({ fileId: file.name, uploadUrl: file.name, deleteUrl: file.name, }) }) ) }), deleteFile: vi.fn(), }, ...widgetProps, } } describe("FileUploader widget tests", () => { beforeEach(() => { vi.spyOn(UseResizeObserver, "useResizeObserver").mockReturnValue({ elementRef: { current: null }, values: [250], }) }) it("renders without crashing", () => { const props = getProps() render(<FileUploader {...props} />) const fileUploaderElement = screen.getByTestId("stFileUploader") expect(fileUploaderElement).toBeInTheDocument() }) it("sets initial value properly non-empty", () => { const props = getProps() const { element, widgetMgr } = props widgetMgr.setFileUploaderStateValue( element, buildFileUploaderStateProto([ new FileURLsProto({ fileId: "filename.txt", uploadUrl: "filename.txt", deleteUrl: "filename.txt", }), ]), { fromUi: false }, undefined ) render(<FileUploader {...props} />) const fileNameNode = screen.getByText("filename.txt") expect(fileNameNode).toBeInTheDocument() }) it("shows a label", () => { const props = getProps({ label: "Test label" }) render(<FileUploader {...props} />) const labelNode = screen.getByText("Test label") expect(labelNode).toBeInTheDocument() }) it("pass labelVisibility prop to StyledWidgetLabel correctly when hidden", () => { const props = getProps({ label: "Test label", labelVisibility: { value: LabelVisibilityProto.LabelVisibilityOptions.HIDDEN, }, }) render(<FileUploader {...props} />) const labelNode = screen.getByText("Test label") expect(labelNode).toBeInTheDocument() expect(labelNode).not.toBeVisible() }) it("pass labelVisibility prop to StyledWidgetLabel correctly when collapsed", () => { const props = getProps({ label: "Test label", labelVisibility: { value: LabelVisibilityProto.LabelVisibilityOptions.COLLAPSED, }, }) render(<FileUploader {...props} />) const labelNode = screen.getByText("Test label") expect(labelNode).toBeInTheDocument() expect(labelNode).not.toBeVisible() }) it("uploads a single file upload", async () => { const user = userEvent.setup() const props = getProps() vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput: HTMLInputElement = screen.getByTestId( "stFileUploaderDropzoneInput" ) const fileToUpload = createFile() await user.upload(fileDropZoneInput, fileToUpload) const fileName = screen.getByTestId("stFileUploaderFile") expect(fileName.textContent).toContain("filename.txt") expect(fileDropZoneInput.files?.[0]).toEqual(fileToUpload) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledWith( props.element, buildFileUploaderStateProto([ { fileId: "filename.txt", uploadUrl: "filename.txt", deleteUrl: "filename.txt", }, ]), { fromUi: true, }, undefined ) }) it("can pass fragmentId to setFileUploaderStateValue", async () => { const user = userEvent.setup() const props = getProps(undefined, { fragmentId: "myFragmentId" }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput: HTMLInputElement = screen.getByTestId( "stFileUploaderDropzoneInput" ) const fileToUpload = createFile() await user.upload(fileDropZoneInput, fileToUpload) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledWith( props.element, buildFileUploaderStateProto([ { fileId: "filename.txt", uploadUrl: "filename.txt", deleteUrl: "filename.txt", }, ]), { fromUi: true, }, "myFragmentId" ) }) it("uploads a single file even if too many files are selected", async () => { const props = getProps({ multipleFiles: false }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") const filesToUpload = [ new File(["Text in a file!"], "filename1.txt", { type: "text/plain", lastModified: 0, }), new File(["Text in an another file!"], "filename2.txt", { type: "text/plain", lastModified: 0, }), new File(["Another text in an another file!"], "filename3.txt", { type: "text/plain", lastModified: 0, }), ] fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files", "Files", "Files"], files: filesToUpload, }, }) await waitFor(() => expect(props.uploadClient.uploadFile).toHaveBeenCalledTimes(1) ) const fileElements = screen.getAllByTestId("stFileUploaderFile") // We should have 3 files. One will be uploading, the other two will // be in the error state. expect(fileElements.length).toBe(3) expect(fileElements[0].textContent).toContain("filename1.txt") const errors = screen.getAllByTestId("stFileUploaderFileErrorMessage") expect(errors.length).toBe(2) expect(errors[0].textContent).toContain("Only one file is allowed.") expect(errors[1].textContent).toContain("Only one file is allowed.") expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledWith( props.element, buildFileUploaderStateProto([ { fileId: "filename1.txt", uploadUrl: "filename1.txt", deleteUrl: "filename1.txt", }, ]), { fromUi: true, }, undefined ) }) it("replaces file on single file uploader", async () => { const user = userEvent.setup() const props = getProps() vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput: HTMLInputElement = screen.getByTestId( "stFileUploaderDropzoneInput" ) const firstFile = createFile() await user.upload(fileDropZoneInput, firstFile) const fileName = screen.getByTestId("stFileUploaderFile") expect(fileName.textContent).toContain("filename.txt") expect(fileDropZoneInput.files?.[0]).toEqual(firstFile) expect(props.uploadClient.uploadFile).toHaveBeenCalledTimes(1) // setFileUploaderStateValue should have been called once on init and once // when the file was uploaded. expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledTimes(2) const secondFile = new File(["Another text in a file"], "filename2.txt", { type: "text/plain", lastModified: 0, }) // Upload a replacement file await user.upload(fileDropZoneInput, secondFile) const currentFiles = screen.getAllByTestId("stFileUploaderFile") expect(currentFiles.length).toBe(1) expect(currentFiles[0].textContent).toContain("filename2.txt") expect(fileDropZoneInput.files?.[0]).toEqual(secondFile) expect(props.uploadClient.uploadFile).toHaveBeenCalledTimes(2) // setFileUploaderStateValue should have been called once on init (fromUi false), // once when the first file finished uploading, once when the existing file was // cleared before the replacement, and once for the replacement upload. expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledTimes(4) }) it("uploads multiple files, even if some have errors", async () => { const props = getProps({ multipleFiles: true, type: [".txt"] }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") const filesToUpload = [ new File(["Text in a file!"], "filename1.txt", { type: "text/plain", lastModified: 0, }), new File(["Text in a file?"], "filename2.txt", { type: "text/plain", lastModified: 0, }), new File(["Another PDF file"], "anotherpdffile.pdf", { type: "application/pdf", lastModified: 0, }), ] fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files"], files: filesToUpload, items: filesToUpload.map(file => ({ kind: "file", type: file.type, getAsFile: () => file, })), }, }) await waitFor(() => expect(props.uploadClient.uploadFile).toHaveBeenCalledTimes(2) ) const fileNames = screen.getAllByTestId("stFileUploaderFile") expect(fileNames.length).toBe(3) const errorFileNames = screen.getAllByTestId( "stFileUploaderFileErrorMessage" ) expect(errorFileNames.length).toBe(1) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledWith( props.element, buildFileUploaderStateProto([ { fileId: "filename1.txt", uploadUrl: "filename1.txt", deleteUrl: "filename1.txt", }, { fileId: "filename2.txt", uploadUrl: "filename2.txt", deleteUrl: "filename2.txt", }, ]), { fromUi: true, }, undefined ) }) it("uploads directory with multiple files successfully", async () => { const props = getProps({ multipleFiles: true, acceptDirectory: true, type: [".txt", ".py", ".md"], }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") // Simulate directory upload with files in different folders const directoryFiles = [ createFile("project/main.py", "project/main.py"), createFile("project/tests/test_main.py", "project/tests/test_main.py"), createFile("project/README.md", "project/README.md"), createFile("project/config.txt", "project/config.txt"), ] fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files"], files: directoryFiles, items: directoryFiles.map(file => ({ kind: "file", type: file.type, getAsFile: () => file, })), }, }) await waitFor(() => expect(props.uploadClient.uploadFile).toHaveBeenCalledTimes(4) ) const fileElements = screen.getAllByTestId("stFileUploaderFile") expect(fileElements.length).toBe(3) // Verify all files are accepted since they match the allowed types expect( screen.queryByTestId("stFileUploaderFileErrorMessage") ).not.toBeInTheDocument() // Verify that setFileUploaderStateValue was called (internal structure may vary) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalled() }) it("filters directory upload files by type restrictions", async () => { const props = getProps({ multipleFiles: true, acceptDirectory: true, type: [".txt"], // Only allow .txt files }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") // Mix of valid and invalid files for directory upload const mixedFiles = [ createFile("docs/valid.txt", "docs/valid.txt"), createFile("docs/subfolder/another.txt", "docs/subfolder/another.txt"), createFile("docs/image.jpg", "docs/image.jpg", "image/jpeg"), createFile("docs/document.pdf", "docs/document.pdf", "application/pdf"), ] fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files"], files: mixedFiles, items: mixedFiles.map(file => ({ kind: "file", type: file.type, getAsFile: () => file, })), }, }) await waitFor(() => expect(props.uploadClient.uploadFile).toHaveBeenCalledTimes(2) ) // Should show uploaded files (filtering appears to happen at react-dropzone level) const fileElements = screen.getAllByTestId("stFileUploaderFile") expect(fileElements.length).toBe(3) // Should have 1 error message for the rejected file that doesn't match file type const errorElements = screen.queryAllByTestId( "stFileUploaderFileErrorMessage" ) expect(errorElements.length).toBe(1) // Only valid .txt files should be uploaded - verify widget state was updated expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalled() }) it("renders directory upload button text correctly", () => { const props = getProps({ multipleFiles: true, acceptDirectory: true, }) render(<FileUploader {...props} />) const browseButton = screen.getByRole("button", { name: "Browse directories", }) expect(browseButton).toBeVisible() }) it("sets webkitdirectory attribute for directory uploads", () => { const props = getProps({ multipleFiles: true, acceptDirectory: true, }) render(<FileUploader {...props} />) const fileDropZoneInput: HTMLInputElement = screen.getByTestId( "stFileUploaderDropzoneInput" ) expect(fileDropZoneInput).toHaveAttribute("webkitdirectory", "") }) it("preserves directory structure in file names", async () => { const user = userEvent.setup() const props = getProps({ multipleFiles: true, acceptDirectory: true, }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput: HTMLInputElement = screen.getByTestId( "stFileUploaderDropzoneInput" ) // Create files with webkitRelativePath to simulate directory structure const directoryFiles = [ createFile("project/src/main.py", "project/src/main.py"), createFile("project/tests/test_main.py", "project/tests/test_main.py"), ] await user.upload(fileDropZoneInput, directoryFiles) // Files should show with their relative paths const fileElements = screen.getAllByTestId("stFileUploaderFile") expect(fileElements).toHaveLength(2) // Check that both files are present (order may vary) const fileTexts = fileElements.map(el => el.textContent) expect(fileTexts).toEqual( expect.arrayContaining([ expect.stringContaining("project/src/main.py"), expect.stringContaining("project/tests/test_main.py"), ]) ) }) it("handles empty directory upload gracefully", async () => { const props = getProps({ multipleFiles: true, acceptDirectory: true, }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") // Simulate empty directory fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files"], files: [], items: [], }, }) await waitFor(() => { // No upload calls should be made expect(props.uploadClient.uploadFile).not.toHaveBeenCalled() }) // No file elements should be created expect(screen.queryByTestId("stFileUploaderFile")).not.toBeInTheDocument() // Widget state should be initialized but not updated with files for empty directory expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledTimes(1) }) it("displays correct instructions for directory upload", () => { const props = getProps({ multipleFiles: true, acceptDirectory: true, }) render(<FileUploader {...props} />) // Check that browse button shows directory text const browseButton = screen.getByText("Browse directories") expect(browseButton).toBeVisible() // Verify dropzone has webkitdirectory attribute const input = screen.getByTestId("stFileUploaderDropzoneInput") expect(input).toHaveAttribute("webkitdirectory", "") }) it("can delete completed upload", async () => { const user = userEvent.setup() const props = getProps({ multipleFiles: true }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") // Upload two files await user.upload(fileDropZoneInput, createFile("filename1.txt")) await user.upload(fileDropZoneInput, createFile("filename2.txt")) const fileNames = screen.getAllByTestId("stFileUploaderFile") expect(fileNames.length).toBe(2) expect(fileNames[0].textContent).toContain("filename2.txt") expect(fileNames[1].textContent).toContain("filename1.txt") // WidgetStateManager should have been called with our two file IDs and first time with empty state expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledTimes(3) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenLastCalledWith( props.element, buildFileUploaderStateProto([ { fileId: "filename1.txt", uploadUrl: "filename1.txt", deleteUrl: "filename1.txt", }, { fileId: "filename2.txt", uploadUrl: "filename2.txt", deleteUrl: "filename2.txt", }, ]), { fromUi: true, }, undefined ) const firstDeleteBtn = screen.getAllByTestId("stFileUploaderDeleteBtn")[0] await user.click(within(firstDeleteBtn).getByRole("button")) // We should only have a single file - the second file from the original upload list (filename1.txt). const fileNamesAfterDelete = screen.getAllByTestId("stFileUploaderFile") expect(fileNamesAfterDelete.length).toBe(1) expect(fileNamesAfterDelete[0].textContent).toContain("filename1.txt") // WidgetStateManager should have been called with the file ID // of the remaining file. This should be the fourth time WidgetStateManager // has been updated. expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledTimes(4) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenLastCalledWith( props.element, buildFileUploaderStateProto([ { fileId: "filename1.txt", uploadUrl: "filename1.txt", deleteUrl: "filename1.txt", }, ]), { fromUi: true, }, undefined ) }) it("does not allow deleting files when disabled", async () => { const user = userEvent.setup() const props = getProps({ multipleFiles: true }, { disabled: true }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") // Seed an existing uploaded file before rendering (simulates server state) props.widgetMgr.setFileUploaderStateValue( props.element, buildFileUploaderStateProto([ new FileURLsProto({ fileId: "file1.txt", uploadUrl: "file1.txt", deleteUrl: "file1.txt", }), ]), { fromUi: false }, undefined ) render(<FileUploader {...props} />) // There should be one file displayed and a delete button present but disabled const deleteBtns = screen.getAllByTestId("stFileUploaderDeleteBtn") expect(deleteBtns.length).toBe(1) const buttonEl = within(deleteBtns[0]).getByRole("button") expect(buttonEl).toBeDisabled() // Clicking should not change files nor trigger state update await user.click(buttonEl) expect(screen.getAllByTestId("stFileUploaderFile").length).toBe(1) }) it("allows deleting files when enabled", async () => { const user = userEvent.setup() const props = getProps({ multipleFiles: true }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") await user.upload( fileDropZoneInput, new File(["a"], "file1.txt", { type: "text/plain" }) ) const deleteBtn = screen.getByTestId("stFileUploaderDeleteBtn") const buttonEl = within(deleteBtn).getByRole("button") expect(buttonEl).not.toBeDisabled() await user.click(buttonEl) expect(screen.queryByTestId("stFileUploaderFile")).not.toBeInTheDocument() }) it("can delete in-progress upload", async () => { const user = userEvent.setup() const props = getProps() // Mock the uploadFile method to return a promise that never resolves to test updating state props.uploadClient.uploadFile = vi.fn().mockImplementation(() => { return new Promise(() => {}) }) vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") await user.upload(fileDropZoneInput, createFile()) const progressBar = screen.getByRole("progressbar") expect(progressBar).toBeInTheDocument() // and then immediately delete it before upload "completes" const deleteBtn = screen.getByTestId("stFileUploaderDeleteBtn") await user.click(within(deleteBtn).getByRole("button")) expect(screen.queryByTestId("stFileUploaderFile")).not.toBeInTheDocument() // WidgetStateManager will still have been called once, during component mounting expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledTimes(1) expect(props.widgetMgr.setFileUploaderStateValue).toHaveBeenCalledWith( props.element, buildFileUploaderStateProto([]), { fromUi: false, }, undefined ) }) it("can delete file with ErrorStatus", async () => { const user = userEvent.setup() const props = getProps({ multipleFiles: false, type: [".txt"] }) render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") const filesToUpload = [ new File(["Another PDF file"], "anotherpdffile.pdf", { type: "application/pdf", lastModified: 0, }), ] // Drop a file with an error (wrong extension) fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files"], files: filesToUpload, items: filesToUpload.map(file => ({ kind: "file", type: file.type, getAsFile: () => file, })), }, }) await waitFor(() => expect(screen.getAllByTestId("stFileUploaderFile").length).toBe(1) ) const errorFileNames = screen.getAllByTestId( "stFileUploaderFileErrorMessage" ) expect(errorFileNames.length).toBe(1) // Delete the file const firstDeleteBtn = screen.getAllByTestId("stFileUploaderDeleteBtn")[0] await user.click(within(firstDeleteBtn).getByRole("button")) // File should be gone expect(screen.queryByTestId("stFileUploaderFile")).not.toBeInTheDocument() }) it("handles upload error", async () => { const user = userEvent.setup() const props = getProps() vi.spyOn(props.widgetMgr, "setFileUploaderStateValue") render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") // Upload a file that will be rejected by the server props.uploadClient.uploadFile = vi .fn() .mockRejectedValue(new Error("random upload error!")) await user.upload(fileDropZoneInput, createFile()) // Our file should have an error status const errorFileNames = screen.getByTestId("stFileUploaderFileErrorMessage") expect(errorFileNames.textContent).toContain("random upload error!") }) it("shows an ErrorStatus when File extension is not allowed", async () => { const props = getProps({ multipleFiles: false, type: [".png"] }) render(<FileUploader {...props} />) const fileDropZone = screen.getByTestId("stFileUploaderDropzone") const filesToUpload = [ new File(["TXT file"], "txtfile.txt", { type: "text/plain", lastModified: 0, }), ] // Drop a file with an error (wrong extension) fireEvent.drop(fileDropZone, { dataTransfer: { types: ["Files"], files: filesToUpload, items: filesToUpload.map(file => ({ kind: "file", type: file.type, getAsFile: () => file, })), }, }) await waitFor(() => expect(screen.getAllByTestId("stFileUploaderFile").length).toBe(1) ) const errorFileNames = screen.getByTestId("stFileUploaderFileErrorMessage") expect(errorFileNames.textContent).toContain( "text/plain files are not allowed." ) }) it("shows an ErrorStatus when maxUploadSizeMb = 0", async () => { const user = userEvent.setup() const props = getProps({ maxUploadSizeMb: 0 }) render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") await user.upload(fileDropZoneInput, createFile()) const errorFileNames = screen.getByTestId("stFileUploaderFileErrorMessage") expect(errorFileNames.textContent).toContain( "File must be 0.0B or smaller." ) }) it("marks files as error when fetching upload URLs fails", async () => { const user = userEvent.setup() const props = getProps() props.uploadClient.fetchFileURLs = vi .fn() .mockRejectedValue("fetch URLs failed") render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") await user.upload(fileDropZoneInput, createFile("failing.txt")) await waitFor(() => expect( screen.getByTestId("stFileUploaderFileErrorMessage") ).toHaveTextContent("fetch URLs failed") ) expect(props.uploadClient.uploadFile).not.toHaveBeenCalled() }) it("updates progress immediately when upload progress fires synchronously", async () => { const user = userEvent.setup() const props = getProps() props.uploadClient.uploadFile = vi .fn() .mockImplementation( ( _element, _url, _file, onUploadProgress: ((event: ProgressEvent) => void) | undefined ) => { onUploadProgress?.({ loaded: 50, total: 100 } as ProgressEvent) return new Promise(() => { /* never resolve to keep status uploading */ }) } ) render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") await user.upload(fileDropZoneInput, createFile("inflight.txt")) await waitFor(() => { expect(screen.getByRole("progressbar")).toHaveAttribute( "aria-valuenow", "50" ) }) }) it("resets its value when form is cleared", async () => { const user = userEvent.setup() const props = getProps({ multipleFiles: true, formId: "form-id" }) props.widgetMgr.setFormSubmitBehaviors("form-id", true) render(<FileUploader {...props} />) const fileDropZoneInput = screen.getByTestId("stFileUploaderDropzoneInput") await user.upload(fileDropZoneInput, createFile("filename1.txt")) await user.upload(fileDropZoneInput, createFile("filename2.txt")) expect(screen.getAllByTestId("stFileUploaderFile").length).toBe(2) act(() => { props.widgetMgr.submitForm("form-id", undefined) }) await waitFor(() => { expect(screen.queryAllByTestId("stFileUploaderFile")).toHaveLength(0) }) }) })