diff --git a/e2e/drag-drop.e2e.ts b/e2e/drag-drop.e2e.ts index b017992..c227d17 100644 --- a/e2e/drag-drop.e2e.ts +++ b/e2e/drag-drop.e2e.ts @@ -26,6 +26,19 @@ test("drops a single file and returns it", async ({page}) => { expect(files[0].hasHandle).toBe(true); }); +test("the returned File stays readable after the drop (issue #1411)", async ({page}) => { + // The handle branch now hands back the File captured from getAsFile() rather than + // handle.getFile(). Reading its contents *after* the drop event resolved proves that File is a + // durable, readable snapshot — the property Electron's webUtils.getPathForFile() relies on, and + // what regressed when v1.x started returning the handle's File instead. + // https://github.com/react-dropzone/react-dropzone/issues/1411 + await cdpDrop(page, [fixture("files/hello.json")]); + + const files = await readResult(page); + expect(files[0].hasHandle).toBe(true); + expect(files[0].content).toBe('{"hello": true}\n'); +}); + test("drops a directory and flattens it recursively", async ({page}) => { await cdpDrop(page, [fixture("tree")]); diff --git a/e2e/fixture.html b/e2e/fixture.html index 8aed48c..0b434db 100644 --- a/e2e/fixture.html +++ b/e2e/fixture.html @@ -20,15 +20,20 @@ async function handle(evt) { try { const files = await fromEvent(evt); - window.__result = files.map(file => ({ - name: file.name, - type: file.type, - size: file.size, - path: file.path, - relativePath: file.relativePath, - isFile: file instanceof File, - hasHandle: typeof file.handle?.getFile === "function" - })); + // Read contents *after* fromEvent resolved (i.e. after the drop event and its awaits), to + // prove the returned File is still a durable, readable snapshot. + window.__result = await Promise.all( + files.map(async file => ({ + name: file.name, + type: file.type, + size: file.size, + path: file.path, + relativePath: file.relativePath, + isFile: file instanceof File, + hasHandle: typeof file.handle?.getFile === "function", + content: await file.text() + })) + ); } catch (err) { window.__error = String((err && err.message) || err); } diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 3cd3c65..a588f6c 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -22,6 +22,7 @@ export interface SelectedFile { relativePath?: string; isFile: boolean; hasHandle: boolean; + content: string; } const MIME: Record = { diff --git a/src/file-selector.spec.ts b/src/file-selector.spec.ts index 6f9b054..7cbc278 100644 --- a/src/file-selector.spec.ts +++ b/src/file-selector.spec.ts @@ -453,6 +453,53 @@ it("reads getAsFile() synchronously so awaiting getAsFileSystemHandle can't neut expect((files[0] as FileWithPath).name).toBe(name); }); +it("returns the original getAsFile() File (not handle.getFile()) so Electron's webUtils.getPathForFile keeps resolving a path (issue #1411)", async () => { + // In Electron, webUtils.getPathForFile() only resolves an on-disk path from the *original* File + // the drop produced (what DataTransferItem.getAsFile() returns). A File from + // FileSystemFileHandle.getFile() loses that path (electron/electron#33647), so when a handle is + // available we must still hand back the original File, with the handle attached. + // See https://github.com/react-dropzone/react-dropzone/issues/1411 + const name = "dropped.json"; + const originalFile = createFile(name, {ping: true}, {type: "application/json"}); + const handleFile = createFile(name, {ping: true}, {type: "application/json"}); + const h = { + getFile() { + return Promise.resolve(handleFile); + } + } as any; + const evt = dragEvtFromItems([dataTransferItemWithFsHandle(originalFile, h)]); + + const files = await fromEvent(evt); + expect(files).toHaveLength(1); + + const [file] = files as FileWithPath[]; + // Object identity is preserved: the returned File is the one from getAsFile(), not the handle's. + expect(file).toBe(originalFile); + expect(file).not.toBe(handleFile); + // The durable handle is still attached. + expect((file as any).handle).toBe(h); +}); + +it("falls back to handle.getFile() when getAsFile() returns null (e.g. a cross-window drop)", async () => { + // No original File to preserve, so the handle's File is the only source. It still carries the + // durable handle. + const name = "cross-window.json"; + const handleFile = createFile(name, {ping: true}, {type: "application/json"}); + const h = { + getFile() { + return Promise.resolve(handleFile); + } + } as any; + const evt = dragEvtFromItems([dataTransferItemWithFsHandle(null, h)]); + + const files = await fromEvent(evt); + expect(files).toHaveLength(1); + + const [file] = files as FileWithPath[]; + expect(file).toBe(handleFile); + expect((file as any).handle).toBe(h); +}); + it("guesses the {type} from the extension using the built-in defaults", async () => { const mockFile = createFile("test.png", {ping: true}); // typeless File const evt = inputEvtFromFiles(mockFile); diff --git a/src/file-selector.ts b/src/file-selector.ts index 317a0ef..5979270 100644 --- a/src/file-selector.ts +++ b/src/file-selector.ts @@ -154,10 +154,16 @@ async function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEn const syncFile = item.getAsFile(); const h = await getFsHandle(item); - // Prefer the File System Access handle when we got one: it carries a durable handle (and, for - // cross-window drops, the freshest File). if (h !== null && h !== undefined) { - const file = await h.getFile(); + // Hand back the File we captured synchronously from the DataTransferItem when we have it, + // rather than `handle.getFile()`. Both wrap the same dropped file, but only the original + // preserves the File object identity that Electron's `webUtils.getPathForFile()` needs to + // resolve an on-disk path; a File from `FileSystemFileHandle.getFile()` loses it + // (electron/electron#33647), which broke drag-and-drop path access in Electron. + // See https://github.com/react-dropzone/react-dropzone/issues/1411 + // `syncFile` is only null in rare cases (e.g. a cross-window drop where getAsFile() returned + // null), so fall back to the handle's File there. Either way, attach the durable handle. + const file = syncFile ?? (await h.getFile()); file.handle = h; return toFileWithPath(file); }