From 3faf51d323282506cc76e74b93e9020912b8c477 Mon Sep 17 00:00:00 2001 From: Jonathan Beebe Date: Sun, 29 Mar 2026 09:40:28 -0500 Subject: [PATCH 1/2] fix: reject pickJsonFile promise when user cancels file picker The cancel event on was not handled, leaving the promise pending forever when the user dismissed the file dialog. Add an oncancel handler that rejects with a descriptive error. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/__tests__/io.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++ src/io.ts | 1 + 2 files changed, 46 insertions(+) create mode 100644 src/__tests__/io.test.ts diff --git a/src/__tests__/io.test.ts b/src/__tests__/io.test.ts new file mode 100644 index 0000000..383c916 --- /dev/null +++ b/src/__tests__/io.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { pickJsonFile } from '../io' + +describe('pickJsonFile', () => { + const originalCreateElement = document.createElement.bind(document) + let createdInput: HTMLInputElement + + beforeEach(() => { + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'input') { + createdInput = el as HTMLInputElement + vi.spyOn(createdInput, 'click').mockImplementation(() => {}) + } + return el + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('resolves with file contents when a file is selected', async () => { + const promise = pickJsonFile() + + const file = new File(['{"hello":"world"}'], 'test.json', { + type: 'application/json', + }) + Object.defineProperty(createdInput, 'files', { + value: [file], + configurable: true, + }) + createdInput.dispatchEvent(new Event('change')) + + await expect(promise).resolves.toBe('{"hello":"world"}') + }) + + it('rejects when the user cancels the file picker', async () => { + const promise = pickJsonFile() + + createdInput.dispatchEvent(new Event('cancel')) + + await expect(promise).rejects.toThrow('File selection cancelled') + }) +}) diff --git a/src/io.ts b/src/io.ts index 18e1b80..39f6eb6 100644 --- a/src/io.ts +++ b/src/io.ts @@ -24,6 +24,7 @@ export function pickJsonFile(): Promise { reader.onerror = () => reject(new Error('Failed to read file')) reader.readAsText(file) } + input.oncancel = () => reject(new Error('File selection cancelled')) input.click() }) } From 95b223deb5ccaa0760d163a2f9d6a02d87965270 Mon Sep 17 00:00:00 2001 From: Jonathan Beebe Date: Sun, 29 Mar 2026 09:43:46 -0500 Subject: [PATCH 2/2] fix: resolve pickJsonFile cancel with null instead of rejecting Cancelling a file picker is a normal user action, not an error. Resolve with null so callers can silently ignore it rather than surfacing an error to the user. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/__tests__/io.test.ts | 4 ++-- src/hooks/useShareImport.ts | 1 + src/io.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/__tests__/io.test.ts b/src/__tests__/io.test.ts index 383c916..a6434dc 100644 --- a/src/__tests__/io.test.ts +++ b/src/__tests__/io.test.ts @@ -35,11 +35,11 @@ describe('pickJsonFile', () => { await expect(promise).resolves.toBe('{"hello":"world"}') }) - it('rejects when the user cancels the file picker', async () => { + it('resolves with null when the user cancels the file picker', async () => { const promise = pickJsonFile() createdInput.dispatchEvent(new Event('cancel')) - await expect(promise).rejects.toThrow('File selection cancelled') + await expect(promise).resolves.toBeNull() }) }) diff --git a/src/hooks/useShareImport.ts b/src/hooks/useShareImport.ts index 1dee8a5..0f6b5b6 100644 --- a/src/hooks/useShareImport.ts +++ b/src/hooks/useShareImport.ts @@ -133,6 +133,7 @@ export function useShareImport({ getFramework, navigate, addRaw, replace, addImp (onImported: (fw: Framework) => void) => { pickJsonFile() .then((text) => { + if (text === null) return const fw = JSON.parse(text) if (fw.name && fw.quadrants && fw.quadrants.length === 4) { const imported: Framework = { diff --git a/src/io.ts b/src/io.ts index 39f6eb6..761e95a 100644 --- a/src/io.ts +++ b/src/io.ts @@ -8,7 +8,7 @@ export function downloadJson(filename: string, data: string): void { URL.revokeObjectURL(url) } -export function pickJsonFile(): Promise { +export function pickJsonFile(): Promise { return new Promise((resolve, reject) => { const input = document.createElement('input') input.type = 'file' @@ -24,7 +24,7 @@ export function pickJsonFile(): Promise { reader.onerror = () => reject(new Error('Failed to read file')) reader.readAsText(file) } - input.oncancel = () => reject(new Error('File selection cancelled')) + input.oncancel = () => resolve(null) input.click() }) }