@@ -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+
350410test ( '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+
766884test ( 'auto-render skips unrelated component tab edits outside entry dependency graph' , async ( {
767885 page,
768886} ) => {
0 commit comments