Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/walk.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'bun:test'
import { shotBytes } from './walk.ts'
import { resolve } from 'node:path'
import { shotBytes, shotFile } from './walk.ts'

const png = Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
Expand All @@ -21,3 +22,16 @@ describe('shotBytes', () => {
expect(() => shotBytes(huge)).toThrow('over')
})
})

describe('shotFile', () => {
test('puts the PNG under the shots directory', () => {
expect(shotFile('/tmp/shots', 'home')).toBe(resolve('/tmp/shots', 'home.png'))
})

test('refuses a name that would leave the shots directory', () => {
expect(() => shotFile('/tmp/shots', '../outside')).toThrow('file name')
expect(() => shotFile('/tmp/shots', 'a/b')).toThrow('file name')
expect(() => shotFile('/tmp/shots', '..')).toThrow('file name')
expect(() => shotFile('/tmp/shots', '')).toThrow('file name')
})
})
30 changes: 24 additions & 6 deletions src/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { existsSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { createServer } from 'node:net'
import { join } from 'node:path'
import { basename, isAbsolute, join, relative, resolve as resolvePath } from 'node:path'
import process from 'node:process'
import { setTimeout as sleep } from 'node:timers/promises'
import { collapsed, covered, type LayoutBreak, overflow } from './page/layout.ts'
Expand Down Expand Up @@ -137,8 +137,26 @@
return bytes
}

async function shoot({ base, id }: Session, file: string) {
await writeFile(file, shotBytes(await call(base, 'GET', `/session/${id}/screenshot`)))
/** A PNG path under dir named for that screen: a slash or `..` would otherwise let the driver's bytes land outside
* the shots folder. */
export function shotFile(dir: string, name: string): string {
if (name === '' || name === '.' || name === '..' || name !== basename(name)) {
throw new Error('screenshot name is not a file name')
}
const root = resolvePath(dir)
const dest = resolvePath(root, `${name}.png`)
const rel = relative(root, dest)
if (rel.startsWith('..') || isAbsolute(rel)) {
throw new Error('screenshot path leaves the shots directory')
}
return dest
}

async function shoot({ base, id }: Session, dir: string, name: string) {
await writeFile(
shotFile(dir, name),
shotBytes(await call(base, 'GET', `/session/${id}/screenshot`)),
Comment thread
AlbinoGeek marked this conversation as resolved.
Dismissed
)
}

// Reaches one screen from the one before, reloading only when a step finds nothing, and returns its layout breaks. The
Expand All @@ -149,7 +167,7 @@
for (const s of screen.steps) {
reached = reached && (await click(session, s))
if (w.shots && s === screen.steps.at(-1)) {
await shoot(session, join(w.shots, 'loading', `${screen.name}.png`))
await shoot(session, join(w.shots, 'loading'), screen.name)
}
await still(session, HALF_SETTLE_MS)
}
Expand All @@ -163,7 +181,7 @@
breaks.push({ screen: screen.name, kind: 'missing', where: s.role, text: s.name })
}
if (w.shots && s === screen.steps.at(-1)) {
await shoot(session, join(w.shots, 'loading', `${screen.name}.png`))
await shoot(session, join(w.shots, 'loading'), screen.name)
}
await still(session, HALF_SETTLE_MS)
}
Expand All @@ -179,7 +197,7 @@
}
}
if (w.shots) {
await shoot(session, join(w.shots, `${screen.name}.png`))
await shoot(session, w.shots, screen.name)
}
return breaks
}
Expand Down
Loading