mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(editor): stop filing crash reports for expected lazy-chunk swaps
RichMarkdownErrorBoundary reported every caught error as a react-error-boundary crash, including the LazyChunkLoadError sentinel that lazy-with-retry throws after it has already exhausted its retries and its one guarded reload. That sentinel means "the chunk hash changed under a running window" (an app update), which is deliberate graceful degradation, not a crash. RecoverableRenderErrorBoundary already skips reporting it (#6206); this boundary was never updated. Crash b860def2 is exactly that path: a lazy_chunk_reload breadcrumb ("Unexpected token ':'") fires first, then the post-reload attempt surfaces LazyChunkLoadError and files a report. The fallback UI is unchanged, so the pane stays usable and offers retry.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { Suspense, act, type ReactElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { lazyWithRetry } from '@/lib/lazy-with-retry'
|
||||
import { RichMarkdownErrorBoundary } from './RichMarkdownErrorBoundary'
|
||||
|
||||
const reportCrashMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/lib/react-error-boundary-reporting', () => ({
|
||||
reportReactErrorBoundaryCrash: reportCrashMock
|
||||
}))
|
||||
|
||||
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
function createContainer(): { container: HTMLDivElement; root: Root } {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
return { container, root: createRoot(container) }
|
||||
}
|
||||
|
||||
// Mirrors EditorContent: the Suspense sits above the boundary, which wraps the lazy editor.
|
||||
function BoundaryHarness({ children }: { children: ReactNode }): ReactElement {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<RichMarkdownErrorBoundary fileId="file-1">{children}</RichMarkdownErrorBoundary>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
async function flushReactWork(): Promise<void> {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
})
|
||||
}
|
||||
|
||||
describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
|
||||
let root: Root | null = null
|
||||
let container: HTMLDivElement | null = null
|
||||
let consoleError: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
reportCrashMock.mockReset()
|
||||
window.sessionStorage.clear()
|
||||
consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
act(() => root?.unmount())
|
||||
}
|
||||
container?.remove()
|
||||
root = null
|
||||
container = null
|
||||
window.sessionStorage.clear()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
// Repro for crash b860def2: an app update swapped chunk hashes, lazy-with-retry
|
||||
// burned its one guarded reload, then surfaced LazyChunkLoadError here.
|
||||
it('renders the fallback without reporting after guarded dynamic import exhaustion', async () => {
|
||||
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
|
||||
const LazyRejectingImport = lazyWithRetry(
|
||||
() => Promise.reject(new SyntaxError("Unexpected token ':'")),
|
||||
{ retries: 0 }
|
||||
)
|
||||
;({ container, root } = createContainer())
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<BoundaryHarness>
|
||||
<LazyRejectingImport />
|
||||
</BoundaryHarness>
|
||||
)
|
||||
})
|
||||
await flushReactWork()
|
||||
await flushReactWork()
|
||||
|
||||
expect(container?.textContent).toContain('rich markdown editor')
|
||||
expect(reportCrashMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still reports ordinary render errors', async () => {
|
||||
const error = new Error('ordinary render failure')
|
||||
function BrokenEditor(): ReactElement {
|
||||
throw error
|
||||
}
|
||||
;({ container, root } = createContainer())
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<BoundaryHarness>
|
||||
<BrokenEditor />
|
||||
</BoundaryHarness>
|
||||
)
|
||||
})
|
||||
|
||||
expect(container?.textContent).toContain('rich markdown editor')
|
||||
expect(reportCrashMock).toHaveBeenCalledTimes(1)
|
||||
expect(reportCrashMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
boundaryId: 'editor.rich-markdown',
|
||||
surface: 'rich-markdown-editor',
|
||||
error
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { isLazyChunkLoadError } from '@/lib/lazy-with-retry'
|
||||
import { reportReactErrorBoundaryCrash } from '@/lib/react-error-boundary-reporting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
@@ -38,6 +39,12 @@ export class RichMarkdownErrorBoundary extends React.Component<Props, State> {
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo): void {
|
||||
console.error('[RichMarkdownEditor] render crash contained by boundary', error, info)
|
||||
// Why: lazy-with-retry already spent its retries and its one guarded reload, so a
|
||||
// chunk-hash swap after an app update is expected degradation, not a crash. The
|
||||
// fallback below still renders (matches RecoverableRenderErrorBoundary).
|
||||
if (isLazyChunkLoadError(error)) {
|
||||
return
|
||||
}
|
||||
void reportReactErrorBoundaryCrash({
|
||||
boundaryId: 'editor.rich-markdown',
|
||||
surface: 'rich-markdown-editor',
|
||||
|
||||
Reference in New Issue
Block a user