diff --git a/jest.config.js b/jest.config.js index 8678d5c..776ec03 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,9 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} **/ +/** @type {import('ts-jest').ProjectConfigTsJest} **/ module.exports = { testEnvironment: 'jsdom', + "setupFilesAfterEnv": [ + "/test-setup.js" + ], transform: { '^.+.tsx?$': [ 'ts-jest', diff --git a/src/file-selector.spec.ts b/src/file-selector.spec.ts index 2493ece..412420e 100644 --- a/src/file-selector.spec.ts +++ b/src/file-selector.spec.ts @@ -304,6 +304,64 @@ it('should use getAsFileSystemHandle when available', async () => { expect(file.path).toBe(`./${name}`); }); +it('should not use getAsFileSystemHandle when not in a secure context', async () => { + const f1Name = 'test.nosec.json'; + const f1 = createFile(f1Name, {ping: false}, { + type: 'application/json' + }); + const [_, h] = createFileSystemFileHandle('test.sec.json', {ping: true}, { + type: 'application/json' + }); + const evt = dragEvtFromItems([ + dataTransferItemWithFsHandle(f1, h) + ]); + + window.isSecureContext = false; + + const files = await fromEvent(evt); + expect(files).toHaveLength(1); + expect(files.every(file => file instanceof File)).toBe(true); + + const [file] = files as FileWithPath[]; + + expect(file.name).toBe(f1.name); + expect(file.type).toBe(f1.type); + expect(file.size).toBe(f1.size); + expect(file.lastModified).toBe(f1.lastModified); + expect(file.path).toBe(`./${f1Name}`); + + window.isSecureContext = true; +}); + +it('should reject when getAsFileSystemHandle resolves to null', async () => { + const evt = dragEvtFromItems([ + dataTransferItemWithFsHandle(null, null) + ]); + expect(fromEvent(evt)).rejects.toThrow('[object Object] is not a File'); +}); + +it('should fallback to getAsFile when getAsFileSystemHandle resolves to undefined', async () => { + const name = 'test.nosec.json'; + const mockFile = createFile(name, {ping: false}, { + type: 'application/json' + }); + const evt = dragEvtFromItems([ + dataTransferItemWithFsHandle(mockFile, undefined) + ]); + + const files = await fromEvent(evt); + expect(files).toHaveLength(1); + expect(files.every(file => file instanceof File)).toBe(true); + + const [file] = files as FileWithPath[]; + + expect(file.name).toBe(mockFile.name); + expect(file.type).toBe(mockFile.type); + expect(file.size).toBe(mockFile.size); + expect(file.lastModified).toBe(mockFile.lastModified); + expect(file.path).toBe(`./${name}`); +}); + function dragEvtFromItems(items: DataTransferItem | DataTransferItem[], type: string = 'drop'): DragEvent { return { type, @@ -374,7 +432,7 @@ function dataTransferItemFromEntry(entry: FileEntry | DirEntry, file?: File): Da } as any; } -function dataTransferItemWithFsHandle(file?: File, h?: FileSystemFileHandle): DataTransferItem { +function dataTransferItemWithFsHandle(file?: File | null, h?: FileSystemFileHandle | null): DataTransferItem { return { kind: 'file', getAsFile() { @@ -466,8 +524,7 @@ function createFile(name: string, data: T, options?: FilePropertyBag) { } function createFileSystemFileHandle(name: string, data: T, options?: FilePropertyBag): [File, FileSystemFileHandle] { - const json = JSON.stringify(data); - const file = new File([json], name, options); + const file = createFile(name, data, options); return [file, { getFile() { return Promise.resolve(file); @@ -482,7 +539,7 @@ function sortFiles(files: T[]) { interface FileSystemFileHandle { - getFile(): Promise; + getFile(): Promise; } type FileOrDirEntry = FileEntry | DirEntry diff --git a/src/file-selector.ts b/src/file-selector.ts index 4aaee19..33717d7 100644 --- a/src/file-selector.ts +++ b/src/file-selector.ts @@ -120,21 +120,32 @@ function flatten(items: any[]): T[] { ], []); } -function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEntry | null) { - if (typeof (item as any).getAsFileSystemHandle === 'function') { - return (item as any).getAsFileSystemHandle() - .then(async (h: any) => { - const file = await h.getFile(); - file.handle = h; - return toFileWithPath(file); - }); +async function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEntry | null) { + // Check if we're in a secure context; due to a bug in Chrome (as far as we know) + // the browser crashes when calling this API (yet to be confirmed as a consistent behaviour). + // + // See: + // - https://issues.chromium.org/issues/40186242 + // - https://github.com/react-dropzone/react-dropzone/issues/1397 + if (globalThis.isSecureContext && typeof (item as any).getAsFileSystemHandle === 'function') { + const h = await (item as any).getAsFileSystemHandle(); + if (h === null) { + throw new Error(`${item} is not a File`); + } + // It seems that the handle can be `undefined` (see https://github.com/react-dropzone/file-selector/issues/120), + // so we check if it isn't; if it is, the code path continues to the next API (`getAsFile`). + if (h !== undefined) { + const file = await h.getFile(); + file.handle = h; + return toFileWithPath(file); + } } const file = item.getAsFile(); if (!file) { - return Promise.reject(`${item} is not a File`); + throw new Error(`${item} is not a File`); } const fwp = toFileWithPath(file, entry?.fullPath ?? undefined); - return Promise.resolve(fwp); + return fwp; } // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemEntry diff --git a/test-setup.js b/test-setup.js new file mode 100644 index 0000000..671e925 --- /dev/null +++ b/test-setup.js @@ -0,0 +1,6 @@ +// NOTE: Let us test {isSecureContext}! +Object.defineProperty(globalThis, "isSecureContext", { + value: true, + writable: true, + enumerable: true, +});