Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 66 additions & 14 deletions src/__tests__/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />)

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(<App />)
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(<App />)
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(<App />)
const user = userEvent.setup()
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 5 additions & 4 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -80,7 +80,8 @@ export default function App() {
}

if (!sourceMap) {
setIsSourceMapInputError(Boolean(text) && true)
setSourceMapInputValue(text)
setIsSourceMapInputError(Boolean(text))
return
}

Expand Down Expand Up @@ -198,7 +199,7 @@ export default function App() {
>
Choose files
<input
accept=".map,.txt"
accept=".map,.txt,.json"
aria-labelledby="file-upload-button"
className="file-input file-input-bordered"
hidden
Expand Down
Loading