@@ -14,6 +14,12 @@ function resolveRuntime(program: Command): string {
1414 ?? DEFAULT_RUNTIME
1515}
1616
17+ function buildRuntimeUrl ( baseUrl : string , projectJson : string ) : string {
18+ const encoded = Buffer . from ( projectJson ) . toString ( "base64" )
19+ const sep = baseUrl . includes ( "?" ) ? "&" : "?"
20+ return `${ baseUrl } ${ sep } project=${ encodeURIComponent ( encoded ) } &autoplay=true`
21+ }
22+
1723async function loadPlaywright ( ) {
1824 try {
1925 return await import ( "playwright" )
@@ -48,77 +54,48 @@ async function renderWithPlaywright(
4854 const sp2 = new Spinner ( "Launching Chrome (WebGPU)" ) . spin ( )
4955 const browser = await pw . chromium . launch ( {
5056 headless : false ,
57+ channel : "chrome" ,
5158 args : [
5259 "--enable-unsafe-webgpu" ,
5360 "--enable-features=Vulkan" ,
5461 "--use-angle=metal" ,
55- "--window-position=-2400,-2400" ,
56- "--window-size=1,1" ,
5762 ] ,
5863 } )
5964 sp2 . succeed ( "Chrome ready" )
6065
6166 try {
62- const context = await browser . newContext ( { acceptDownloads : true } )
67+ const context = await browser . newContext ( {
68+ acceptDownloads : true ,
69+ viewport : { width : 1920 , height : 1080 } ,
70+ } )
6371 const page = await context . newPage ( )
6472
65- let hostname : string
66- try { hostname = new URL ( runtimeUrl ) . hostname } catch { hostname = runtimeUrl }
67- const sp3 = new Spinner ( `Loading ${ hostname } ` ) . spin ( )
68- await page . goto ( runtimeUrl , { waitUntil : "networkidle" } )
69- await page . waitForTimeout ( 3000 )
70- sp3 . succeed ( "Runtime loaded" )
71-
7273 const project = JSON . parse ( projectJson )
7374 const layerCount = project . layers ?. length ?? 0
74- const sp4 = new Spinner ( `Injecting project (${ layerCount } layers, ${ opts . duration } s)` ) . spin ( )
75- await page . evaluate ( ( json : string ) => {
76- const projectFile = JSON . parse ( json )
77- const { useLayerStore, useTimelineStore, useEditorStore } = ( window as any ) . __SHADER_LAB_STORES__ ?? { }
78- if ( useLayerStore ) {
79- useLayerStore . getState ( ) . replaceState (
80- projectFile . layers ,
81- projectFile . selectedLayerId ,
82- null
83- )
84- }
85- if ( useTimelineStore ) {
86- useTimelineStore . getState ( ) . replaceState ( {
87- currentTime : 0 ,
88- duration : projectFile . timeline . duration ,
89- isPlaying : true ,
90- loop : projectFile . timeline . loop ,
91- selectedKeyframeId : null ,
92- selectedTrackId : null ,
93- tracks : projectFile . timeline . tracks ,
94- } )
95- }
96- if ( useEditorStore && projectFile . sceneConfig ) {
97- useEditorStore . getState ( ) . updateSceneConfig ( projectFile . sceneConfig )
98- useEditorStore . getState ( ) . setOutputSize (
99- projectFile . composition . width ,
100- projectFile . composition . height
101- )
102- }
103- } , projectJson )
104- await page . waitForTimeout ( 2000 )
105- sp4 . succeed ( "Project injected" )
75+
76+ let hostname : string
77+ try { hostname = new URL ( runtimeUrl ) . hostname } catch { hostname = runtimeUrl }
78+ const sp3 = new Spinner ( `Loading ${ hostname } (${ layerCount } layers)` ) . spin ( )
79+ const fullUrl = buildRuntimeUrl ( runtimeUrl , projectJson )
80+ await page . goto ( fullUrl , { waitUntil : "networkidle" } )
81+ // Wait for URL params loader to apply project (1.5s delay in component + render time)
82+ await page . waitForTimeout ( 5000 )
83+ sp3 . succeed ( "Runtime loaded & project injected" )
10684
10785 if ( opts . format === "png" ) {
10886 const sp5 = new Spinner ( "Exporting PNG" ) . spin ( )
87+
88+ // Open export dialog using Playwright locator
89+ await page . locator ( 'button[aria-label="Export"]' ) . first ( ) . click ( )
90+ await page . waitForTimeout ( 1500 )
91+
10992 const downloadPromise = page . waitForEvent ( "download" , { timeout : 30000 } )
110- await page . evaluate ( ( _time : number ) => {
111- const exportBtn = document . querySelector ( 'button[aria-label="Export"]' ) as HTMLButtonElement
112- exportBtn ?. click ( )
113- setTimeout ( ( ) => {
114- const imgTab = [ ...document . querySelectorAll ( "button" ) ] . find ( b => b . textContent ?. trim ( ) === "image" )
115- imgTab ?. click ( )
116- setTimeout ( ( ) => {
117- const exportPng = [ ...document . querySelectorAll ( "button" ) ] . find ( b => b . textContent ?. includes ( "Export PNG" ) )
118- exportPng ?. click ( )
119- } , 500 )
120- } , 1000 )
121- } , opts . time )
93+ // Click image tab (default, but ensure)
94+ await page . getByRole ( "button" , { name : "image" , exact : true } ) . click ( )
95+ await page . waitForTimeout ( 500 )
96+ // Click Export PNG
97+ await page . getByRole ( "button" , { name : "Export PNG" } ) . click ( )
98+
12299 const download = await downloadPromise
123100 sp5 . succeed ( "PNG captured" )
124101
@@ -131,27 +108,21 @@ async function renderWithPlaywright(
131108 const pb = new ProgressBar ( `Recording ${ formatLabel } ` , expectedMs )
132109 const spRec = new Spinner ( `Recording ${ formatLabel } (${ opts . duration } s @ ${ opts . fps } fps)` ) . spin ( )
133110
111+ // Open export dialog
112+ await page . locator ( 'button[aria-label="Export"]' ) . first ( ) . click ( )
113+ await page . waitForTimeout ( 1500 )
114+
134115 const downloadPromise = page . waitForEvent ( "download" , { timeout : 120000 } )
135- await page . evaluate ( ( format : string ) => {
136- const exportBtn = document . querySelector ( 'button[aria-label="Export"]' ) as HTMLButtonElement
137- exportBtn ?. click ( )
138- setTimeout ( ( ) => {
139- const videoTab = [ ...document . querySelectorAll ( "button" ) ] . find ( b => b . textContent ?. trim ( ) === "video" )
140- videoTab ?. click ( )
141- setTimeout ( ( ) => {
142- if ( format === "mp4" ) {
143- const mp4Btn = [ ...document . querySelectorAll ( "button" ) ] . find ( b => b . textContent ?. trim ( ) === "MP4" )
144- mp4Btn ?. click ( )
145- }
146- setTimeout ( ( ) => {
147- const btn = [ ...document . querySelectorAll ( "button" ) ] . find (
148- b => b . textContent ?. includes ( "Export WEBM" ) || b . textContent ?. includes ( "Export MP4" )
149- )
150- btn ?. click ( )
151- } , 500 )
152- } , 500 )
153- } , 1000 )
154- } , opts . format )
116+ // Click video tab
117+ await page . getByRole ( "button" , { name : "video" , exact : true } ) . click ( )
118+ await page . waitForTimeout ( 500 )
119+ // Select format if MP4
120+ if ( opts . format === "mp4" ) {
121+ await page . getByRole ( "button" , { name : "MP4" , exact : true } ) . click ( )
122+ await page . waitForTimeout ( 300 )
123+ }
124+ // Click Export WEBM / Export MP4
125+ await page . getByRole ( "button" , { name : `Export ${ formatLabel } ` } ) . click ( )
155126
156127 const recStart = Date . now ( )
157128 const tick = setInterval ( ( ) => pb . update ( Date . now ( ) - recStart ) , 200 )
0 commit comments