Skip to content

Commit d2b172d

Browse files
feat: harden stale error recovery.
1 parent 2fe5383 commit d2b172d

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

playwright/rendering-modes.spec.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ test('react mode typecheck loads types without malformed URL fetches', async ({
111111
}
112112
})
113113

114+
await setComponentEditorSource(
115+
page,
116+
[
117+
"import React from 'react'",
118+
'const App = () => <button type="button">react types loaded</button>',
119+
].join('\n'),
120+
)
121+
114122
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
115123
await page.getByRole('button', { name: 'Typecheck' }).click()
116124

@@ -347,6 +355,58 @@ test('post-render runtime exceptions from iframe are reported in preview panel',
347355
await expect(page.locator('#preview-host pre')).toContainText('clicked boom')
348356
})
349357

358+
test('post-render runtime errors fully recover after source fix', async ({ page }) => {
359+
await waitForInitialRender(page)
360+
await ensurePanelToolsVisible(page, 'component')
361+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
362+
363+
await setComponentEditorSource(
364+
page,
365+
[
366+
"import React, { useState } from 'react'",
367+
'export const App = () => {',
368+
' const [count, setCount] = useState(0)',
369+
' return (',
370+
' <button type="button" onClick={() => {',
371+
" throw new Error('clicked boom')",
372+
' }}>',
373+
' click boom',
374+
' </button>',
375+
' )',
376+
'}',
377+
].join('\n'),
378+
)
379+
380+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
381+
await getPreviewFrame(page).getByRole('button', { name: 'click boom' }).click()
382+
383+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
384+
await expect(page.locator('#preview-host pre')).toContainText('clicked boom')
385+
386+
await setComponentEditorSource(
387+
page,
388+
[
389+
"import React, { useState } from 'react'",
390+
'export const App = () => {',
391+
' const [count, setCount] = useState(0)',
392+
' return (',
393+
' <button type="button" onClick={() => setCount(prev => prev + 1)}>',
394+
' safe click {count}',
395+
' </button>',
396+
' )',
397+
'}',
398+
].join('\n'),
399+
)
400+
401+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
402+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
403+
await getPreviewFrame(page).getByRole('button', { name: 'safe click 0' }).click()
404+
await expect(
405+
getPreviewFrame(page).getByRole('button', { name: 'safe click 1' }),
406+
).toBeVisible()
407+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
408+
})
409+
350410
test('requires render button when auto render is disabled', async ({ page }) => {
351411
await waitForInitialRender(page)
352412

@@ -763,6 +823,64 @@ test('workspace graph errors for circular imports remain deterministic', async (
763823
await expect(page.locator('#preview-host pre')).toContainText('Import chain: ./module')
764824
})
765825

826+
test('children runtime errors recover after module fix and mode switches', async ({
827+
page,
828+
}) => {
829+
await waitForInitialRender(page)
830+
831+
await ensurePanelToolsVisible(page, 'component')
832+
await addWorkspaceTab(page)
833+
834+
await setWorkspaceTabSource(page, {
835+
fileName: 'module.tsx',
836+
source: [
837+
'export const ItemWrap = ({ children: string }) => {',
838+
' return <span className="item-wrap">{children}</span>',
839+
'}',
840+
].join('\n'),
841+
})
842+
843+
await setWorkspaceTabSource(page, {
844+
fileName: 'App.tsx',
845+
source: [
846+
"import { ItemWrap } from './module.tsx'",
847+
'export const App = () => (',
848+
' <div>',
849+
' <ItemWrap>hello children</ItemWrap>',
850+
' </div>',
851+
')',
852+
].join('\n'),
853+
})
854+
855+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
856+
await expect(page.locator('#preview-host pre')).toContainText(
857+
'[runtime] children is not defined',
858+
)
859+
860+
await setWorkspaceTabSource(page, {
861+
fileName: 'module.tsx',
862+
source: [
863+
'export const ItemWrap = ({ children }: { children: string }) => {',
864+
' return <span className="item-wrap">{children}</span>',
865+
'}',
866+
].join('\n'),
867+
})
868+
869+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
870+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
871+
await expect(getPreviewFrame(page).getByText('hello children')).toBeVisible()
872+
873+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
874+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
875+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
876+
await expect(getPreviewFrame(page).getByText('hello children')).toBeVisible()
877+
878+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('dom')
879+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
880+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
881+
await expect(getPreviewFrame(page).getByText('hello children')).toBeVisible()
882+
})
883+
766884
test('auto-render skips unrelated component tab edits outside entry dependency graph', async ({
767885
page,
768886
}) => {

src/modules/preview-runtime/iframe-preview-executor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export const executeWorkspaceIframePreview = ({
239239
const runtimeError = toIframeRuntimeError(data)
240240

241241
if (hasRendered) {
242+
cleanup()
242243
if (typeof onRuntimeError === 'function') {
243244
onRuntimeError(runtimeError)
244245
}

0 commit comments

Comments
 (0)