From 49ce4d9812a3a854a256c36b7e7ef7fcdb696ba4 Mon Sep 17 00:00:00 2001 From: Mikkel Laursen Date: Sat, 17 Jul 2021 20:52:32 -0600 Subject: [PATCH] test(form): Added tests for useFileUpload --- .../file-input/__tests__/useFileUpload.tsx | 636 ++++++++++++++++++ .../form/src/file-input/__tests__/utils.ts | 210 ++++++ packages/form/src/file-input/useFileUpload.ts | 16 +- packages/form/src/file-input/utils.ts | 3 +- 4 files changed, 860 insertions(+), 5 deletions(-) create mode 100644 packages/form/src/file-input/__tests__/useFileUpload.tsx create mode 100644 packages/form/src/file-input/__tests__/utils.ts diff --git a/packages/form/src/file-input/__tests__/useFileUpload.tsx b/packages/form/src/file-input/__tests__/useFileUpload.tsx new file mode 100644 index 0000000000..d4f7b925d7 --- /dev/null +++ b/packages/form/src/file-input/__tests__/useFileUpload.tsx @@ -0,0 +1,636 @@ +import React, { Fragment, ReactElement } from "react"; +import filesize from "filesize"; +import { + act, + fireEvent, + getByRole as getByRoleGlobal, + render, + waitFor, +} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Button } from "@react-md/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@react-md/dialog"; +import { Configuration } from "@react-md/layout"; +import { List, SimpleListItem } from "@react-md/list"; +import { + CheckCircleSVGIcon, + CloseSVGIcon, + FileUploadSVGIcon, + WatchSVGIcon, +} from "@react-md/material-icons"; +import { StatesConfig } from "@react-md/states"; +import { Text } from "@react-md/typography"; +import { useDropzone } from "@react-md/utils"; + +import { FileInput } from "../FileInput"; +import { FileUploadOptions, useFileUpload } from "../useFileUpload"; +import { + FileExtensionError, + FileSizeError, + FileValidationError, + isFileSizeError, + isTooManyFilesError, + TooManyFilesError, +} from "../utils"; + +function createFile(name: string, bytes: number): File { + const content = new Uint8Array(bytes); + for (let i = 0; i < bytes; i += 1) { + content[i] = 32 + i; + } + + return new File([content.buffer], name); +} + +class MockFileReader implements FileReader { + error = null; + result: string | ArrayBuffer | null = null; + EMPTY = 0; + LOADING = 1; + DONE = 2; + readyState = 0; + + _progressEvents: ((event: Event) => void)[] = []; + _loadEvents: ((event: Event) => void)[] = []; + + onerror = jest.fn(); + onabort = jest.fn(); + onload = jest.fn(); + onloadstart = jest.fn(); + onloadend = jest.fn(); + onprogress = jest.fn(); + + abort = jest.fn(); + readAsText = jest.fn(); + readAsDataURL = jest.fn(); + readAsArrayBuffer = jest.fn(); + readAsBinaryString = jest.fn(); + + removeEventListener = jest.fn(); + dispatchEvent = jest.fn(); + + addEventListener( + name: "progress" | "load", + callback: (event: Event) => void + ) { + if (name === "progress") { + this._progressEvents.push(callback); + } else { + this._loadEvents.push(callback); + } + } + + triggerProgressEvent(loaded: number, total: number) { + act(() => { + const event = new ProgressEvent("progress", { + total, + loaded, + lengthComputable: true, + }); + + this._progressEvents.forEach((callback) => { + callback(event); + }); + }); + } + + triggerLoadEvent(result: string | ArrayBuffer | null) { + act(() => { + const event = new Event("load"); + this.result = result; + this._loadEvents.forEach((callback) => { + callback(event); + }); + }); + } +} + +const abort = jest.fn(); +const readAsText = jest.fn(); +const readAsDataURL = jest.fn(); +const readAsArrayBuffer = jest.fn(); +const readAsBinaryString = jest.fn(); +let mockFileReader = new MockFileReader(); + +let fileReader = jest.spyOn(window, "FileReader"); + +beforeEach(() => { + jest.clearAllMocks(); + mockFileReader = new MockFileReader(); + fileReader = jest.spyOn(window, "FileReader"); + + fileReader.mockImplementation(() => mockFileReader); + mockFileReader.abort = abort; + mockFileReader.readAsText = readAsText; + mockFileReader.readAsDataURL = readAsDataURL; + mockFileReader.readAsArrayBuffer = readAsArrayBuffer; + mockFileReader.readAsBinaryString = readAsBinaryString; +}); + +function SingleFileTest(props: FileUploadOptions) { + const { onChange, stats, reset, errors, clearErrors } = useFileUpload(props); + const [stat] = stats; + + return ( + + + Upload + +
{stat?.status}
+
{stat?.file.name}
+
{stat?.progress}
+
+ {stat?.status === "complete" && stat.result} +
+
    + {errors.map((error) => ( +
  • + {error.name} +
  • + ))} +
+ + +
+ ); +} + +// all of this test setup is basically copied from +// `packages/documentation/src/components/Demos/Form/FileInputs/ServerUploadExample.tsx` +const IMAGE_VIDEO_EXTENSIONS = [ + "svg", + "jpeg", + "jpg", + "png", + "apng", + "mkv", + "mp4", + "mpeg", + "mpg", + "webm", + "mov", +]; + +interface ErrorHeaderProps { + error: TooManyFilesError | FileSizeError | FileExtensionError; +} + +function ErrorHeader({ error }: ErrorHeaderProps): ReactElement { + if (isFileSizeError(error)) { + const { type } = error; + const limit = filesize(error.limit); + if (type === "total") { + return ( + + {`Unable to upload the following files due to total upload size limit (${limit})`} + + ); + } + + const range = type === "min" ? "greater" : "less"; + return ( + + {`Unable to upload the following files because files must be ${range} than ${limit}`} + + ); + } + if (isTooManyFilesError(error)) { + const { limit } = error; + return ( + + {`Unable to upload the following files due to total files allowed limit (${limit})`} + + ); + } + + const { extensions } = error; + return ( + + {`Invalid file extension. Must be one of ${extensions.join(", ")}`} + + ); +} + +interface ErrorRendererProps { + error: FileValidationError; +} + +function ErrorRenderer({ error }: ErrorRendererProps): ReactElement { + if ("files" in error) { + const { key, files } = error; + return ( + + + + {files.map((file, i) => ( + + ))} + + + ); + } + + // error + /* ^ is a {@link FileAccessError} */ + return ( + + File access is restricted. Try a different file or folder. + + ); +} + +interface ErrorModalProps { + errors: readonly FileValidationError[]; + clearErrors(): void; +} + +function ErrorModal({ errors, clearErrors }: ErrorModalProps): ReactElement { + return ( + + + File Upload Errors + + + {errors.map((error) => ( + + ))} + + + + + + ); +} + +const MAX_UPLOAD_SIZE = 5 * 1024 * 1024; + +function ServerUploadExample({ + maxFiles = 5, + concurrency = 1, + maxFileSize = MAX_UPLOAD_SIZE, + totalFileSize = MAX_UPLOAD_SIZE, + extensions = IMAGE_VIDEO_EXTENSIONS, + getFileParser = () => "readAsArrayBuffer", + ...options +}: FileUploadOptions): ReactElement { + const { + stats, + errors, + onChange, + clearErrors, + remove, + onDrop, + accept, + totalBytes, + totalFiles, + } = useFileUpload({ + concurrency, + maxFiles, + maxFileSize, + totalFileSize, + extensions, + getFileParser, + ...options, + }); + const [_isOver, dndHandlers] = useDropzone({ onDrop }); + + return ( + <> + + + {stats.map((uploadStats) => ( + + ) : uploadStats.status === "uploading" ? ( + + ) : ( + + ) + } + rightAddon={ + + } + primaryText={uploadStats.file.name} + secondaryText={filesize(uploadStats.file.size)} + /> + ))} + {Array.from({ length: Math.max(0, maxFiles - totalFiles) }, (_, i) => ( + } + primaryText={`Remaining File ${totalFiles + i + 1}`} + height="extra-large" + disabled + disabledOpacity + /> + ))} + + + Upload + +
{totalBytes}
+
{totalFiles}
+ + ); +} + +// utils for ServerUploadExample +const getErrorDialog = () => + getByRoleGlobal(document.body, "dialog", { name: "File Upload Errors" }); + +const getUploadListItem = (name: string) => + getByRoleGlobal(document.body, "listitem", { + name: (_name, li) => (li.textContent || "").includes(name), + }); + +function renderComplex(props: FileUploadOptions = {}) { + return render(, { + wrapper: ({ children }) => ( + {children} + ), + }); +} + +describe("useFileUpload", () => { + it("should work correctly for a single file upload flow and reset", () => { + const file = new File(["pretend-bytes"], "README.txt"); + const { getAllByRole, getByLabelText, getByRole, getByTestId } = render( + + ); + const input = getByLabelText("Upload") as HTMLInputElement; + const status = getByTestId("status"); + const fileName = getByTestId("fileName"); + const progress = getByTestId("progress"); + const result = getByTestId("result"); + + expect(status).toHaveTextContent(""); + expect(fileName).toHaveTextContent(""); + expect(progress).toHaveTextContent(""); + expect(result).toHaveTextContent(""); + expect(() => getAllByRole("listitem")).toThrow(); + + userEvent.upload(input, file); + + expect(readAsText).toBeCalledWith(file); + expect(status).toHaveTextContent("uploading"); + expect(fileName).toHaveTextContent("README.txt"); + expect(progress).toHaveTextContent("0"); + expect(result).toHaveTextContent(""); + expect(() => getAllByRole("listitem")).toThrow(); + + mockFileReader.triggerProgressEvent(100, 1000); + expect(status).toHaveTextContent("uploading"); + expect(fileName).toHaveTextContent("README.txt"); + expect(progress).toHaveTextContent("10"); + expect(result).toHaveTextContent(""); + expect(() => getAllByRole("listitem")).toThrow(); + + const content = "pretend-bytes"; + mockFileReader.triggerLoadEvent(content); + expect(status).toHaveTextContent("complete"); + expect(fileName).toHaveTextContent("README.txt"); + expect(progress).toHaveTextContent("100"); + expect(result).toHaveTextContent(content); + expect(() => getAllByRole("listitem")).toThrow(); + + fireEvent.click(getByRole("button", { name: "Reset" })); + expect(status).toHaveTextContent(""); + expect(fileName).toHaveTextContent(""); + expect(progress).toHaveTextContent(""); + expect(result).toHaveTextContent(""); + expect(() => getAllByRole("listitem")).toThrow(); + }); + + it("should abort any FileReaders when the reset function is called", () => { + const file = new File(["pretend-bytes"], "README.txt"); + const { getByLabelText, getByRole } = render(); + const input = getByLabelText("Upload") as HTMLInputElement; + + userEvent.upload(input, file); + expect(abort).not.toBeCalled(); + fireEvent.click(getByRole("button", { name: "Reset" })); + expect(abort).toBeCalledTimes(1); + }); + + it("should allow for some default validation", () => { + const { getByLabelText, getByRole, getByTestId } = render( + + ); + const input = getByLabelText("Upload") as HTMLInputElement; + const status = getByTestId("status"); + const fileName = getByTestId("fileName"); + const reset = getByRole("button", { name: "Reset" }); + const clearErrors = getByRole("button", { name: "Clear Errors" }); + + const file1 = createFile("file1.txt", 32); + expect(file1.size).toBe(32); + userEvent.upload(input, file1); + expect(() => + getByRole("listitem", { name: "FileSizeError" }) + ).not.toThrow(); + expect(status).toHaveTextContent(""); + expect(fileName).toHaveTextContent(""); + + fireEvent.click(reset); + expect(() => getByRole("listitem", { name: "FileSizeError" })).toThrow(); + + const file2 = createFile("file2.txt", 2000); + expect(file2.size).toBe(2000); + userEvent.upload(input, file2); + + expect(() => + getByRole("listitem", { name: "FileSizeError" }) + ).not.toThrow(); + expect(status).toHaveTextContent(""); + expect(fileName).toHaveTextContent(""); + + fireEvent.click(reset); + expect(() => getByRole("listitem", { name: "FileSizeError" })).toThrow(); + + const file3 = createFile("file3.txt", 800); + expect(file3.size).toBe(800); + userEvent.upload(input, file3); + + expect(() => getByRole("listitem")).toThrow(); + expect(status).toHaveTextContent("uploading"); + expect(fileName).toHaveTextContent(file3.name); + + mockFileReader.triggerLoadEvent("fake-contents"); + expect(status).toHaveTextContent("complete"); + expect(fileName).toHaveTextContent(file3.name); + + const file4 = createFile("file4.txt", 1); + expect(file4.size).toBe(1); + userEvent.upload(input, file4); + expect(status).toHaveTextContent("complete"); + expect(fileName).toHaveTextContent(file3.name); + expect(() => + getByRole("listitem", { name: "FileSizeError" }) + ).not.toThrow(); + + fireEvent.click(clearErrors); + expect(status).toHaveTextContent("complete"); + expect(fileName).toHaveTextContent(file3.name); + expect(() => getByRole("listitem", { name: "FileSizeError" })).toThrow(); + }); + + it("should restrict files based on the provided extensions", async () => { + const extensions = ["svg", "png"]; + const { getByLabelText } = renderComplex({ extensions }); + const input = getByLabelText(/Upload/) as HTMLInputElement; + expect(input).toHaveAttribute("accept", ".svg,.png"); + + userEvent.upload(input, createFile("Invalid.txt", 1000)); + + expect(readAsArrayBuffer).not.toBeCalled(); + + expect(getErrorDialog).not.toThrow(); + }); + + it("should allow for specific files to be removed", () => { + const { getByLabelText } = renderComplex(); + const input = getByLabelText(/Upload/) as HTMLInputElement; + const file1 = createFile("example1.png", 1000); + const file2 = createFile("example2.png", 1024); + + userEvent.upload(input, file1); + userEvent.upload(input, file2); + + const file1Item = getUploadListItem(file1.name); + expect(() => getUploadListItem(file2.name)).not.toThrow(); + + fireEvent.click( + getByRoleGlobal(file1Item, "button", { name: "Remove File" }) + ); + expect(() => getUploadListItem(file1.name)).toThrow(); + expect(() => getUploadListItem(file2.name)).not.toThrow(); + }); + + it("should allow files to be dropped when connected with useDropzone", () => { + const { getByRole } = renderComplex(); + const list = getByRole("none"); + expect(list.tagName).toBe("OL"); + + const file = createFile("file1.png", 1024); + fireEvent.drop(list, { + dataTransfer: { + files: [file], + }, + }); + + expect(() => getUploadListItem(file.name)).not.toThrow(); + }); + + it("should queue the FileAccessError if an error occurs while uploading a file through drag and drop or onChange", async () => { + const onDrop = jest.fn(); + const onChange = jest.fn(); + const { getByLabelText, getByRole, getByText } = renderComplex({ + onDrop, + onChange, + }); + + const list = getByRole("none"); + expect(list.tagName).toBe("OL"); + const input = getByLabelText(/Upload/) as HTMLInputElement; + + fireEvent.drop(list, { + dataTransfer: { + get files(): File[] { + throw new Error(); + }, + }, + }); + expect(onDrop).toBeCalledTimes(1); + expect(onChange).not.toBeCalled(); + expect(getErrorDialog).not.toThrow(); + expect(() => getByText(/File access is restricted/)).not.toThrow(); + + fireEvent.click(getByRole("button", { name: "Okay" })); + await waitFor(getErrorDialog); + + // throwing an error crashed the test for some reason here + fireEvent.change(input, { target: { files: null } }); + expect(onDrop).toBeCalledTimes(1); + expect(onChange).toBeCalledTimes(1); + expect(getErrorDialog).not.toThrow(); + expect(() => getByText(/File access is restricted/)).not.toThrow(); + + fireEvent.click(getByRole("button", { name: "Okay" })); + }); + + it("should allow for a custom validateFiles function", () => { + const validateFiles = jest.fn((files) => ({ + pending: files, + errors: [], + })); + + const { getByLabelText } = renderComplex({ validateFiles }); + const input = getByLabelText(/Upload/) as HTMLInputElement; + + const file = createFile("file1.png", 1024); + expect(validateFiles).not.toBeCalled(); + userEvent.upload(input, file); + expect(validateFiles).toBeCalledWith([file], { + maxFiles: 5, + extensions: IMAGE_VIDEO_EXTENSIONS, + minFileSize: -1, + maxFileSize: MAX_UPLOAD_SIZE, + totalBytes: 0, + totalFiles: 0, + totalFileSize: MAX_UPLOAD_SIZE, + }); + }); + + it("should throw a TooManyFilesError if too many files are uploaded", () => { + const { getByLabelText, getByText } = renderComplex({ maxFiles: 1 }); + const input = getByLabelText(/Upload/) as HTMLInputElement; + const file1 = createFile("file1.png", 1000); + const file2 = createFile("file2.png", 1000); + + userEvent.upload(input, file1); + expect(getErrorDialog).toThrow(); + + userEvent.upload(input, file2); + expect(getErrorDialog).not.toThrow(); + expect(() => + getByText( + "Unable to upload the following files due to total files allowed limit (1)" + ) + ).not.toThrow(); + }); +}); diff --git a/packages/form/src/file-input/__tests__/utils.ts b/packages/form/src/file-input/__tests__/utils.ts new file mode 100644 index 0000000000..f47aeb133b --- /dev/null +++ b/packages/form/src/file-input/__tests__/utils.ts @@ -0,0 +1,210 @@ +import { + FileAccessError, + FileExtensionError, + FileSizeError, + FileUploadStats, + GenericFileError, + getFileParser, + getSplitFileUploads, + isFileAccessError, + isFileExtensionError, + isGenericFileError, + validateFiles, +} from "../utils"; + +function createFile(name: string, bytes: number): File { + const content = new Uint8Array(bytes); + for (let i = 0; i < bytes; i += 1) { + content[i] = 32 + i; + } + + return new File([content.buffer], name); +} + +const file = new File(["pretend-bytes"], "file1.txt"); +const png = new File([""], "file.png"); +const apng = new File([""], "file.apng"); +const avif = new File([""], "file.avif"); +const tiff = new File([""], "file.tiff"); +const gif = new File([""], "file.gif"); +const gifv = new File([""], "file.gifv"); +const jpg = new File([""], "file.jpg"); +const jpeg = new File([""], "file.jpeg"); +const mp3 = new File([""], "file.mp3"); +const wav = new File([""], "file.wav"); +const ogg = new File([""], "file.ogg"); +const m4p = new File([""], "file.m4p"); +const flac = new File([""], "file.flac"); +const mkv = new File([""], "file.mkv"); +const mpeg = new File([""], "file.mpeg"); +const mpg = new File([""], "file.mpg"); +const mov = new File([""], "file.mov"); +const avi = new File([""], "file.avi"); +const flv = new File([""], "file.flv"); +const webm = new File([""], "file.webm"); +const mp4 = new File([""], "file.mp4"); +const js = new File([""], "file.js"); +const jsx = new File([""], "file.jsx"); +const ts = new File([""], "file.ts"); +const tsx = new File([""], "file.tsx"); +const json = new File([""], "file.json"); +const lock = new File([""], "file.lock"); +const hbs = new File([""], "file.hbs"); +const yml = new File([""], "file.yml"); +const yaml = new File([""], "file.yaml"); +const log = new File([""], "file.log"); +const txt = new File([""], "file.txt"); +const md = new File([""], "file.md"); + +const MEDIA_LIKE_FILES = [ + png, + apng, + avif, + tiff, + gif, + gifv, + jpg, + jpeg, + mp3, + wav, + ogg, + m4p, + flac, + mkv, + mpeg, + mpg, + mov, + avi, + flv, + webm, + mp4, +] as const; +const TEXT_LIKE_FILES = [ + js, + jsx, + ts, + tsx, + json, + lock, + hbs, + yml, + yaml, + log, + txt, + md, +] as const; + +describe("isGenericFileError", () => { + it("should return true for instances of GenericFileError", () => { + expect(isGenericFileError(new GenericFileError([file]))).toBe(true); + expect(isGenericFileError(new FileAccessError())).toBe(false); + }); +}); + +describe("isFileAccessError", () => { + it("should return true for instances of FileAccessError", () => { + expect(isFileAccessError(new FileAccessError())).toBe(true); + expect(isFileAccessError(new GenericFileError([file]))).toBe(false); + }); +}); + +describe("isFileExtensionError", () => { + it("should return true for instances of FileExtensionError", () => { + expect(isFileExtensionError(new FileExtensionError([file], ["png"]))).toBe( + true + ); + expect(isFileExtensionError(new GenericFileError([file]))).toBe(false); + }); +}); + +describe("getFileParser", () => { + it("should have reasonable defaults for known file extensions", () => { + MEDIA_LIKE_FILES.forEach((file) => { + expect(getFileParser(file)).toBe("readAsDataURL"); + }); + TEXT_LIKE_FILES.forEach((file) => { + expect(getFileParser(file)).toBe("readAsText"); + }); + + expect(getFileParser(new File([""], "file.jar"))).toBe("readAsArrayBuffer"); + expect(getFileParser(new File([""], "file.tar"))).toBe("readAsArrayBuffer"); + expect(getFileParser(new File([""], "file.zip"))).toBe("readAsArrayBuffer"); + }); +}); + +describe("validateFiles", () => { + it("should prevent both maxFileSize and totalFileSize errors", () => { + const file1 = createFile("file1.txt", 1024); + const file2 = createFile("file2.txt", 2048); + const file3 = createFile("file3.txt", 1000); + const result1 = validateFiles([file1, file2], { + maxFiles: -1, + extensions: [], + minFileSize: -1, + maxFileSize: 1024, + totalBytes: 0, + totalFiles: 0, + totalFileSize: 1024, + }); + + const result2 = validateFiles([file1, file3], { + maxFiles: -1, + extensions: [], + minFileSize: -1, + maxFileSize: 1024, + totalBytes: 0, + totalFiles: 0, + totalFileSize: 2000, + }); + + expect(result1).toEqual({ + pending: [file1], + errors: [new FileSizeError([file2], "max", 1024)], + }); + expect(result2).toEqual({ + pending: [file1], + errors: [new FileSizeError([file3], "total", 2000)], + }); + }); +}); + +describe("getSplitFileUploads", () => { + it("should correctly split the file upload stats", () => { + const file1 = createFile("file1.txt", 1024); + const file2 = createFile("file2.txt", 2048); + const file3 = createFile("file3.txt", 1000); + const pending: FileUploadStats[] = [ + { + key: "pending-key", + status: "pending", + file: file1, + progress: 0, + }, + ]; + const uploading: FileUploadStats[] = [ + { + key: "uploading-key", + status: "uploading", + file: file2, + progress: 10, + }, + ]; + const complete: FileUploadStats[] = [ + { + key: "complete-key", + status: "complete", + file: file3, + result: "fake-contents", + progress: 100, + }, + ]; + + expect( + getSplitFileUploads([...pending, ...complete, ...uploading]) + ).toEqual({ + pending, + uploading, + complete, + }); + }); +}); diff --git a/packages/form/src/file-input/useFileUpload.ts b/packages/form/src/file-input/useFileUpload.ts index 29dc34de0f..dc6294dddd 100644 --- a/packages/form/src/file-input/useFileUpload.ts +++ b/packages/form/src/file-input/useFileUpload.ts @@ -263,6 +263,7 @@ export function useFileUpload({ }; case "start": { const { key, reader } = action; + /* istanbul ignore next */ if (!state.stats[key]) { throw new Error(`Missing file with key "${key}"`); } @@ -288,6 +289,7 @@ export function useFileUpload({ } case "progress": { const { key, progress } = action; + /* istanbul ignore next */ if (!state.stats[key]) { throw new Error(`Missing file with key "${key}"`); } @@ -305,6 +307,7 @@ export function useFileUpload({ } case "complete": { const { key, result } = action; + /* istanbul ignore next */ if (!state.stats[key]) { throw new Error(`Missing file with key "${key}"`); } @@ -330,6 +333,7 @@ export function useFileUpload({ case "clearErrors": return { ...state, errors: [] }; default: + /* istanbul ignore next */ return state; } }, @@ -400,6 +404,8 @@ export function useFileUpload({ const files = event.currentTarget.files; if (files) { queueFiles(Array.from(files)); + } else { + throw new Error(); } } catch (e) { dispatch({ @@ -476,14 +482,16 @@ export function useFileUpload({ const { key, file } = stats; const reader = new FileReader(); - reader.onprogress = createProgressEventHandler(key); - - reader.onload = () => { + // using `addEventListener` instead of directly setting to + // `reader.progress`/`reader.load` so it's easier to test + reader.addEventListener("progress", createProgressEventHandler(key)); + reader.addEventListener("load", () => { complete(key, reader.result); - }; + }); start(key, reader); const parser = getFileParser(file); + /* istanbul ignore next */ if ( process.env.NODE_ENV !== "production" && ![ diff --git a/packages/form/src/file-input/utils.ts b/packages/form/src/file-input/utils.ts index 0c8f765d87..bce5235db6 100644 --- a/packages/form/src/file-input/utils.ts +++ b/packages/form/src/file-input/utils.ts @@ -488,7 +488,7 @@ export function validateFiles( * @remarks \@since 2.9.0 */ export function isTextFile(file: File): boolean { - return /\.((j|t)sx?|json|lock|hbs|ya?ml|log)$/i.test(file.name); + return /\.((j|t)sx?|json|lock|hbs|ya?ml|log|txt|md)$/i.test(file.name); } /** @@ -643,6 +643,7 @@ export function getSplitFileUploads( } else if (stat.status === "complete") { complete.push(stat); } else { + /* istanbul ignore next */ throw new Error("Invalid upload stat"); } });