import { resolveDroppedFiles } from "../src/components/resolveDroppedFiles"; type FileDropItem = Omit & { getAsFileSystemHandle?: () => Promise; webkitGetAsEntry?: () => unknown; }; function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); } function assertFiles(actual: File[], expected: File[], message: string): void { assert(actual.length === expected.length, `${message}: expected ${expected.length} file(s), got ${actual.length}`); expected.forEach((file, index) => { assert(actual[index] === file, `${message}: file ${index + 1} did not preserve object identity`); }); } function transfer(files: File[] = [], items: FileDropItem[] = []): Pick { return { files: files as unknown as FileList, items: items as unknown as DataTransferItemList }; } function item(overrides: Partial = {}): FileDropItem { return { kind: "file", type: "application/octet-stream", getAsFile: () => null, getAsString: () => undefined, ...overrides } as FileDropItem; } function namedFile(name: string): File { return new File([name], name, { type: "text/plain" }); } async function* handles(...entries: unknown[]): AsyncIterable { for (const entry of entries) yield entry; } async function run(): Promise { const direct = namedFile("direct.txt"); let directItemCalls = 0; const directResult = await resolveDroppedFiles(transfer([direct], [item({ getAsFile: () => { directItemCalls += 1; return direct; } })])); assertFiles(directResult, [direct], "native DataTransfer.files takes precedence"); assert(directItemCalls === 0, "native files must not also be resolved from items"); const itemA = namedFile("item-a.txt"); const itemB = namedFile("item-b.txt"); let itemHandleCalls = 0; const itemResult = await resolveDroppedFiles(transfer([], [ item({ getAsFile: () => itemA, getAsFileSystemHandle: async () => { itemHandleCalls += 1; return { kind: "file", getFile: async () => itemA }; } }), item({ getAsFile: () => itemB }), item({ kind: "string", getAsFile: () => null }) ])); assertFiles(itemResult, [itemA, itemB], "DataTransferItem.getAsFile fallback resolves file items"); assert(itemHandleCalls === 0, "item files must not also be resolved from file-system handles"); const topLevelFile = namedFile("top-level.txt"); const rootFile = namedFile("root.txt"); const nestedFile = namedFile("nested.txt"); const deepFile = namedFile("deep.txt"); const deepDirectory = { kind: "directory", values: () => handles({ kind: "file", getFile: async () => deepFile }) }; const nestedDirectory = { kind: "directory", values: () => handles( { kind: "file", getFile: async () => nestedFile }, deepDirectory ) }; const rootDirectory = { kind: "directory", values: () => handles( { kind: "file", getFile: async () => rootFile }, nestedDirectory ) }; const handleResult = await resolveDroppedFiles(transfer([], [ item({ getAsFileSystemHandle: async () => ({ kind: "file", getFile: async () => topLevelFile }) }), item({ getAsFileSystemHandle: async () => rootDirectory }) ])); assertFiles( handleResult, [topLevelFile, rootFile, nestedFile, deepFile], "File System Access file handles resolve and directory handles recurse in enumeration order" ); const legacy = namedFile("legacy.txt"); const legacyResult = await resolveDroppedFiles(transfer([], [item({ getAsFileSystemHandle: async () => { throw new Error("file-system handles unavailable"); }, webkitGetAsEntry: () => ({ isFile: true, isDirectory: false, file: (resolve: (file: File) => void) => resolve(legacy) }) })])); assertFiles(legacyResult, [legacy], "legacy webkit file entry remains a final fallback"); const emptyResult = await resolveDroppedFiles(transfer([], [ item({ kind: "string" }), item({ getAsFileSystemHandle: async () => ({ kind: "directory" }), webkitGetAsEntry: () => ({ isFile: false, isDirectory: true }) }) ])); assertFiles(emptyResult, [], "empty and non-readable directory paths resolve to an empty drop"); let unreadableRejected = false; try { await resolveDroppedFiles(transfer([], [item({ getAsFileSystemHandle: async () => ({ kind: "file", getFile: async () => { throw new Error("permission denied"); } }) })])); } catch (error) { unreadableRejected = error instanceof Error && error.message === "permission denied"; } assert(unreadableRejected, "an unreadable file handle rejects so the component can report the unreadable reason"); } void run().catch((error) => { throw error; });