fix: guard whiteboard permission rejections

This commit is contained in:
lucaronin
2026-05-27 18:56:36 +02:00
parent 908994c253
commit 03a667661e
2 changed files with 78 additions and 0 deletions

View File

@@ -125,6 +125,13 @@ function mockEditor(): Editor {
} as unknown as Editor
}
function dispatchUnhandledRejection(reason: unknown): Event {
const event = new Event('unhandledrejection', { cancelable: true })
Object.defineProperty(event, 'reason', { value: reason })
window.dispatchEvent(event)
return event
}
function measuredTextElement(): HTMLElement {
const element = document.createElement('div')
element.textContent = 'Label'
@@ -246,6 +253,31 @@ describe('TldrawWhiteboard', () => {
expect(() => editor.textMeasure.measureElementTextNodeSpans(measuredTextElement())).toThrow('top')
})
it('suppresses whiteboard platform permission rejections while mounted', () => {
render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
const cleanup = renderedTldrawProps().onMount(mockEditor())
const denied = {
name: 'NotAllowedError',
message: 'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.',
}
expect(dispatchUnhandledRejection(denied).defaultPrevented).toBe(true)
expect(dispatchUnhandledRejection(new Error('save failed')).defaultPrevented).toBe(false)
cleanup()
expect(dispatchUnhandledRejection(denied).defaultPrevented).toBe(false)
})
it('resets the drawing store when switching to a blank board snapshot', () => {
const boardASnapshot = { records: { shape: 'from-board-a' } }
const { rerender } = render(

View File

@@ -101,6 +101,50 @@ function ignoreTldrawUserPreferencesUpdate(preferences: TLUserPreferences) {
void preferences
}
function rejectionName(error: unknown): string {
if (error instanceof Error) return error.name
if (typeof error !== 'object' || error === null || !('name' in error)) return ''
const { name } = error
return typeof name === 'string' ? name : ''
}
function rejectionMessage(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error
if (typeof error !== 'object' || error === null || !('message' in error)) return ''
const { message } = error
return typeof message === 'string' ? message : ''
}
function isWhiteboardPlatformPermissionRejection(reason: unknown): boolean {
const name = rejectionName(reason).toLowerCase()
const message = rejectionMessage(reason).toLowerCase()
if (name === 'notallowederror') return true
return message.includes('notallowederror') || (
message.includes('not allowed')
&& (
message.includes('permission')
|| message.includes('platform')
|| message.includes('user agent')
)
)
}
function installTldrawPlatformPermissionGuard(): () => void {
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
if (!isWhiteboardPlatformPermissionRejection(event.reason)) return
event.preventDefault()
}
window.addEventListener('unhandledrejection', handleUnhandledRejection)
return () => {
window.removeEventListener('unhandledrejection', handleUnhandledRejection)
}
}
function parseSnapshot(source: string): TLStoreSnapshot | null {
if (!source.trim()) return null
@@ -211,8 +255,10 @@ function installZoomAwareViewport(editor: Editor): () => void {
function installWhiteboardRuntimeGuards(editor: Editor): () => void {
const cleanupTextMeasurementGuard = installTldrawTextMeasurementGuard(editor)
const cleanupZoomAwareViewport = installZoomAwareViewport(editor)
const cleanupPlatformPermissionGuard = installTldrawPlatformPermissionGuard()
return () => {
cleanupPlatformPermissionGuard()
cleanupZoomAwareViewport()
cleanupTextMeasurementGuard()
}