diff --git a/src/__tests__/app.test.tsx b/src/__tests__/app.test.tsx index aca8694..3da40a1 100644 --- a/src/__tests__/app.test.tsx +++ b/src/__tests__/app.test.tsx @@ -329,6 +329,58 @@ describe('source maps', () => { expect(sourcemapTextarea).toHaveValue('data:application/json;base64,!!!not-valid-base64!!!') }) + test('accepts .json files in the file picker', () => { + render() + + const sourceMapFileInput = screen.getByLabelText(/choose files/i) + expect(sourceMapFileInput).toHaveAttribute('accept', '.map,.txt,.json') + }) + + test('does not render the pasted text into the textarea while it is parsing', async () => { + const createSpy = delayFirstSourceMapCreate(100) + + render() + const user = userEvent.setup() + + const sourcemapTextarea = screen.getByRole('textbox', { name: /source map/i }) + + sourcemapTextarea.focus() + await user.paste(regular.sourcemaps[0].content) + + // An unrelated re-render while the map is parsing syncs the controlled + // textarea with its state, which must not hold the pasted text. + const stacktraceTextarea = screen.getByRole('textbox', { name: /minified stack trace/i }) + await user.type(stacktraceTextarea, 'x') + expect(sourcemapTextarea).toHaveValue('') + + // The delayed parse must still add the source map. + await screen.findByRole('list', { name: /sourcemaps list/i }) + expect(sourcemapTextarea).toHaveValue('') + + createSpy.mockRestore() + }) + + test('clears the textarea when the pasted source map is a rejected duplicate', async () => { + render() + const user = userEvent.setup() + + const sourcemapTextarea = screen.getByRole('textbox', { name: /source map/i }) + + sourcemapTextarea.focus() + await user.paste(regular.sourcemaps[0].content) + + const sourcemapList = await screen.findByRole('list', { name: /sourcemaps list/i }) + expect(sourcemapTextarea).toHaveValue('') + + // A duplicate does not change any state, so React's controlled-value + // restoration is the only thing keeping the textarea empty. + sourcemapTextarea.focus() + await user.paste(regular.sourcemaps[0].content) + + await waitFor(() => expect(sourcemapTextarea).toHaveValue('')) + expect(within(sourcemapList).getAllByRole('listitem')).toHaveLength(1) + }) + test('treats plain source map text as non-base64', async () => { render() const user = userEvent.setup() @@ -421,22 +473,22 @@ describe('source map lifecycle', () => { }) }) -describe('source map input race', () => { - // Delays the first SourceMap.create call so that a subsequent input event - // can finish parsing before the first one does. - function delayFirstSourceMapCreate(delayMs: number) { - const originalCreate = SourceMap.create.bind(SourceMap) - const createSpy = vi.spyOn(SourceMap, 'create') - - createSpy.mockImplementation(originalCreate) - createSpy.mockImplementationOnce(async (text, fileName) => { - await new Promise(resolve => setTimeout(resolve, delayMs)) - return originalCreate(text, fileName) - }) +// Delays the first SourceMap.create call so that a subsequent input event +// can finish parsing before the first one does. +function delayFirstSourceMapCreate(delayMs: number) { + const originalCreate = SourceMap.create.bind(SourceMap) + const createSpy = vi.spyOn(SourceMap, 'create') - return createSpy - } + createSpy.mockImplementation(originalCreate) + createSpy.mockImplementationOnce(async (text, fileName) => { + await new Promise(resolve => setTimeout(resolve, delayMs)) + return originalCreate(text, fileName) + }) + return createSpy +} + +describe('source map input race', () => { test('discards a stale successful parse instead of wiping newer input', async () => { const createSpy = delayFirstSourceMapCreate(100) const destroySpy = await spyOnConsumerDestroy() diff --git a/src/app.tsx b/src/app.tsx index 96f59db..56450ed 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -69,8 +69,8 @@ export default function App() { } } - setSourceMapInputValue(text) - + // The input state is only updated after parsing: rendering a large pasted + // source map into the controlled textarea for one frame causes jank. const sourceMap = await SourceMap.create(text) // A newer input event superseded this one while the source map was parsing. @@ -80,7 +80,8 @@ export default function App() { } if (!sourceMap) { - setIsSourceMapInputError(Boolean(text) && true) + setSourceMapInputValue(text) + setIsSourceMapInputError(Boolean(text)) return } @@ -198,7 +199,7 @@ export default function App() { > Choose files