Skip to content

Commit f7593c8

Browse files
committed
fix: use URL params injection + getByRole selectors for reliable export (v0.1.2)
1 parent 212df4a commit f7593c8

3 files changed

Lines changed: 47 additions & 76 deletions

File tree

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@resciencelab/shader-cli",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Agent-native CLI for Shader Lab — compose and export WebGPU shader scenes from the terminal",
55
"type": "module",
66
"bin": {

cli/src/commands/export.ts

Lines changed: 45 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
1723
async 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)

cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const session = new Session()
1717
const program = new Command()
1818
.name("shader-cli")
1919
.description("Agent-native CLI for Shader Lab — compose and export WebGPU shader scenes from the terminal")
20-
.version("0.1.1")
20+
.version("0.1.2")
2121
.option("--json", "Output as JSON (agent-friendly)")
2222
.option("--project <path>", "Open a .lab project file")
2323
.option("--runtime <url>", "Shader Lab runtime URL (default: https://shader-lab.rescience.dev/tools/shader-lab)")

0 commit comments

Comments
 (0)