Skip to content

Commit 6cbba90

Browse files
committed
chore: wip
1 parent cead50c commit 6cbba90

14 files changed

Lines changed: 605 additions & 383 deletions

File tree

.claude/skills/stacks-browse/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ bun .claude/skills/stacks-browse/scripts/browse.ts screenshot <url> [--viewport
6565
- `--scale 2` renders at 2× (retina).
6666
- `--cookie` pre-seeds cookies before navigation (repeatable). Use it for gated pages, e.g. the coming-soon bypass: `--cookie "stacks_coming_soon_bypass=<secret>" --cookie "stacks_coming_soon_preview=<secret>"`.
6767
- `--settle` waits N ms after the load event before capturing (default 700). Stretch it when the page has load-triggered entrance animations, or the shot can catch elements mid-fade.
68+
- `--scheme light|dark` emulates `prefers-color-scheme`, so theme-aware pages (auto dark mode) can be captured in both schemes without changing the host OS setting.
6869
- Default output: `.stacks/shots/<path>.png`.
6970

7071
### Responsive

.claude/skills/stacks-browse/scripts/browse.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ function kill(s: Session): void {
221221

222222
interface PageState { consoleErrors: string[], console: string[], responses: { url: string, status: number, ms: number, type: string }[], mainStatus: number | null }
223223

224-
async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w: number, h: number }, scale?: number, timeoutMs?: number, cookies?: string[], settleMs?: number } = {}): Promise<PageState> {
224+
async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w: number, h: number }, scale?: number, timeoutMs?: number, cookies?: string[], settleMs?: number, scheme?: string } = {}): Promise<PageState> {
225225
const state: PageState = { consoleErrors: [], console: [], responses: [], mainStatus: null }
226226
const startById = new Map<string, number>()
227227

@@ -256,6 +256,14 @@ async function gotoAndInstrument(cdp: Cdp, url: string, opts: { viewport?: { w:
256256
})
257257
}
258258

259+
// Emulate light/dark so prefers-color-scheme pages can be QA'd in both
260+
// schemes without flipping the host OS setting.
261+
if (opts.scheme === 'light' || opts.scheme === 'dark') {
262+
await cdp.send('Emulation.setEmulatedMedia', {
263+
features: [{ name: 'prefers-color-scheme', value: opts.scheme }],
264+
})
265+
}
266+
259267
cdp.on((e) => {
260268
if (e.method === 'Runtime.consoleAPICalled') {
261269
const text = (e.params.args || []).map((a: any) => a.value ?? a.description ?? a.type).join(' ')
@@ -371,17 +379,19 @@ async function main() {
371379
console.log('Usage: bun browse.ts <navigate|screenshot|responsive|monitor|snapshot> <url> [flags]')
372380
console.log(' --cookie "name=value" repeatable; pre-seeds cookies (e.g. coming-soon bypass)')
373381
console.log(' --settle 1500 ms to wait after load before acting (default 700; stretch for entrance animations)')
382+
console.log(' --scheme dark emulate prefers-color-scheme (light|dark) for QA of theme-aware pages')
374383
process.exit(url ? 0 : 1)
375384
}
376385

377386
const session = await launch()
378387
const cookies = flagList(flags.cookie)
379388
const settleMs = flags.settle ? Number(flags.settle) : undefined
389+
const scheme = typeof flags.scheme === 'string' ? flags.scheme : undefined
380390
try {
381391
if (command === 'navigate' || command === 'go') {
382392
const cdp = await openPage(session.port)
383393
const t0 = performance.now()
384-
const state = await gotoAndInstrument(cdp, url, { cookies, settleMs })
394+
const state = await gotoAndInstrument(cdp, url, { cookies, settleMs, scheme })
385395
const loadMs = Math.round(performance.now() - t0)
386396
console.log(JSON.stringify({
387397
browser: session.browser,
@@ -401,7 +411,7 @@ async function main() {
401411
const scale = flags.scale ? Number(flags.scale) : 1
402412
const out = (flags.out as string) || `.stacks/shots/${new URL(url).pathname.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home'}.png`
403413
mkdirSync(out.split('/').slice(0, -1).join('/') || '.', { recursive: true })
404-
await gotoAndInstrument(cdp, url, { viewport: { w: vp[0], h: vp[1] }, scale, cookies, settleMs })
414+
await gotoAndInstrument(cdp, url, { viewport: { w: vp[0], h: vp[1] }, scale, cookies, settleMs, scheme })
405415
const png = await captureScreenshot(cdp, { full: !!flags.full, element: flags.element as string | undefined })
406416
await Bun.write(out, png)
407417
console.log(JSON.stringify({ url, out, viewport: `${vp[0]}x${vp[1]}`, scale, full: !!flags.full, element: flags.element ?? null, bytes: png.length }, null, 2))
@@ -414,7 +424,7 @@ async function main() {
414424
const results: any[] = []
415425
for (const bp of BREAKPOINTS) {
416426
const cdp = await openPage(session.port)
417-
await gotoAndInstrument(cdp, url, { viewport: { w: bp.w, h: bp.h }, cookies, settleMs })
427+
await gotoAndInstrument(cdp, url, { viewport: { w: bp.w, h: bp.h }, cookies, settleMs, scheme })
418428
const overflow = await cdp.send('Runtime.evaluate', {
419429
expression: 'document.documentElement.scrollWidth > window.innerWidth ? document.documentElement.scrollWidth - window.innerWidth : 0',
420430
returnByValue: true,
@@ -430,7 +440,7 @@ async function main() {
430440
else if (command === 'monitor') {
431441
const cdp = await openPage(session.port)
432442
const ms = flags.ms ? Number(flags.ms) : 5000
433-
const state = await gotoAndInstrument(cdp, url, { cookies, settleMs })
443+
const state = await gotoAndInstrument(cdp, url, { cookies, settleMs, scheme })
434444
await Bun.sleep(ms)
435445
const failed = state.responses.filter(r => r.status >= 400)
436446
const slow = state.responses.filter(r => r.ms > 3000)
@@ -447,7 +457,7 @@ async function main() {
447457

448458
else if (command === 'snapshot') {
449459
const cdp = await openPage(session.port)
450-
await gotoAndInstrument(cdp, url, { cookies, settleMs })
460+
await gotoAndInstrument(cdp, url, { cookies, settleMs, scheme })
451461
const expr = `(() => {
452462
const sel = (q) => Array.from(document.querySelectorAll(q));
453463
const txt = (e) => (e.innerText || e.textContent || '').trim().slice(0, 80);

0 commit comments

Comments
 (0)