From 8c5597313015f4b8a2fc85f5cfe064ac627c4cbe Mon Sep 17 00:00:00 2001 From: Ofri Harlev Date: Wed, 8 Apr 2026 00:20:00 -0700 Subject: [PATCH] feat: allow command timeouts in config --- PROOFSHOT.md | 2 ++ README.md | 12 ++++++++++ src/browser/capture.ts | 51 ++++++++++++++++++++++++++++++++-------- src/browser/session.ts | 14 ++++++++--- src/commands/start.ts | 4 ++-- src/utils/config.test.ts | 22 +++++++++++++++++ src/utils/config.ts | 38 ++++++++++++++++++++---------- 7 files changed, 116 insertions(+), 27 deletions(-) diff --git a/PROOFSHOT.md b/PROOFSHOT.md index 905ca26..e690ead 100644 --- a/PROOFSHOT.md +++ b/PROOFSHOT.md @@ -21,3 +21,5 @@ Artifacts saved to ./proofshot-artifacts/ including video, screenshots, errors, You can customize browser launch behavior in `proofshot.config.json`, including HTTPS error ignoring, a custom browser executable path, and a project-specific `agent-browser` config path. Use `proofshot doctor` when the local setup looks wrong. It prints the current config path, browser mode, viewport, installed binaries, and any active ProofShot session. + +If your environment is slower than the defaults, add a `timeouts` section to `proofshot.config.json` to increase browser open, exec, or video trim timeouts. diff --git a/README.md b/README.md index d157a65..fc1ff4e 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,18 @@ You can also configure browser launch behavior in `proofshot.config.json`: Set `browser.configPath` when you need ProofShot to run `agent-browser` against a project-specific config instead of inheriting `~/.agent-browser/config.json`. Relative paths are resolved from the directory that contains `proofshot.config.json`. +If your environment needs longer waits, you can tune command timeouts in `proofshot.config.json`: + +```json +{ + "timeouts": { + "browserOpenMs": 120000, + "execPassthroughMs": 90000, + "videoTrimMs": 120000 + } +} +``` + ### `proofshot stop` Stop recording, collect errors, generate proof artifacts. diff --git a/src/browser/capture.ts b/src/browser/capture.ts index 64c9be2..d2c2012 100644 --- a/src/browser/capture.ts +++ b/src/browser/capture.ts @@ -1,18 +1,34 @@ import { ab } from '../utils/exec.js'; +import { + DEFAULT_RECORDING_START_TIMEOUT_MS, + DEFAULT_RECORDING_STOP_TIMEOUT_MS, + DEFAULT_SCREENSHOT_TIMEOUT_MS, + type TimeoutConfig, +} from '../utils/config.js'; /** * Start video recording to the given file path. */ -export function startRecording(outputPath: string, sessionName?: string): void { - ab(`record start ${outputPath}`, { timeoutMs: 10000, session: sessionName }); +export function startRecording( + outputPath: string, + sessionName?: string, + timeouts?: TimeoutConfig, +): void { + ab(`record start ${outputPath}`, { + timeoutMs: timeouts?.recordingStartMs ?? DEFAULT_RECORDING_START_TIMEOUT_MS, + session: sessionName, + }); } /** * Stop the current recording. */ -export function stopRecording(sessionName?: string): void { +export function stopRecording(sessionName?: string, timeouts?: TimeoutConfig): void { try { - ab('record stop', { timeoutMs: 15000, session: sessionName }); + ab('record stop', { + timeoutMs: timeouts?.recordingStopMs ?? DEFAULT_RECORDING_STOP_TIMEOUT_MS, + session: sessionName, + }); } catch { // Recording may not have started — that's fine } @@ -21,16 +37,31 @@ export function stopRecording(sessionName?: string): void { /** * Take a screenshot and save to the given path. */ -export function takeScreenshot(outputPath: string, fullPage = true, sessionName?: string): void { +export function takeScreenshot( + outputPath: string, + fullPage = true, + sessionName?: string, + timeouts?: TimeoutConfig, +): void { const fullFlag = fullPage ? ' --full' : ''; - ab(`screenshot ${outputPath}${fullFlag}`, { timeoutMs: 15000, session: sessionName }); + ab(`screenshot ${outputPath}${fullFlag}`, { + timeoutMs: timeouts?.screenshotMs ?? DEFAULT_SCREENSHOT_TIMEOUT_MS, + session: sessionName, + }); } /** * Take an annotated screenshot (labels interactive elements). */ -export function takeAnnotatedScreenshot(outputPath: string, sessionName?: string): void { - ab(`screenshot ${outputPath} --annotate`, { timeoutMs: 15000, session: sessionName }); +export function takeAnnotatedScreenshot( + outputPath: string, + sessionName?: string, + timeouts?: TimeoutConfig, +): void { + ab(`screenshot ${outputPath} --annotate`, { + timeoutMs: timeouts?.screenshotMs ?? DEFAULT_SCREENSHOT_TIMEOUT_MS, + session: sessionName, + }); } /** @@ -42,13 +73,13 @@ export function diffScreenshots( current: string, outputPath: string, sessionName?: string, + timeouts?: TimeoutConfig, ): number | null { try { const result = ab(`diff screenshot ${baseline} ${current} ${outputPath}`, { - timeoutMs: 15000, + timeoutMs: timeouts?.screenshotMs ?? DEFAULT_SCREENSHOT_TIMEOUT_MS, session: sessionName, }); - // Parse mismatch percentage from output const match = result.match(/([\d.]+)%/); return match ? parseFloat(match[1]) : null; } catch { diff --git a/src/browser/session.ts b/src/browser/session.ts index e0637c4..0295d35 100644 --- a/src/browser/session.ts +++ b/src/browser/session.ts @@ -1,5 +1,10 @@ import { ab, ProofShotError } from '../utils/exec.js'; -import type { BrowserConfig, ViewportConfig } from '../utils/config.js'; +import { + DEFAULT_BROWSER_OPEN_TIMEOUT_MS, + type BrowserConfig, + type TimeoutConfig, + type ViewportConfig, +} from '../utils/config.js'; export function buildOpenBrowserCommand( url: string, @@ -15,7 +20,6 @@ export function buildOpenBrowserCommand( const suffix = flags.length > 0 ? ` ${flags.join(' ')}` : ''; return `open ${url}${suffix}`; } - /** * Initialize a browser session. * Opens the browser and sets viewport dimensions. @@ -26,8 +30,12 @@ export function openBrowser( headless = true, sessionName?: string, browserConfig?: BrowserConfig, + timeouts?: TimeoutConfig, ): void { - ab(buildOpenBrowserCommand(url, headless, browserConfig), { timeoutMs: 60000, session: sessionName }); + ab(buildOpenBrowserCommand(url, headless, browserConfig), { + timeoutMs: timeouts?.browserOpenMs ?? DEFAULT_BROWSER_OPEN_TIMEOUT_MS, + session: sessionName, + }); ab(`set viewport ${viewport.width} ${viewport.height}`, { session: sessionName }); } diff --git a/src/commands/start.ts b/src/commands/start.ts index f9632ac..1c4263f 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -111,7 +111,7 @@ export async function startCommand(options: StartOptions): Promise { console.log(chalk.dim('Opening browser...')); try { - openBrowser(openUrl, config.viewport, config.headless, sessionName, config.browser); + openBrowser(openUrl, config.viewport, config.headless, sessionName, config.browser, config.timeouts); console.log(chalk.green('✓') + ' Browser ready'); } catch (error: any) { closeBrowser(); @@ -130,7 +130,7 @@ export async function startCommand(options: StartOptions): Promise { for (let attempt = 1; attempt <= RECORDING_RETRIES; attempt++) { try { - startRecording(videoPath, sessionName); + startRecording(videoPath, sessionName, config.timeouts); recordingStarted = true; console.log(chalk.green('✓') + ' Recording started'); break; diff --git a/src/utils/config.test.ts b/src/utils/config.test.ts index 5bd1bfd..2a35432 100644 --- a/src/utils/config.test.ts +++ b/src/utils/config.test.ts @@ -40,4 +40,26 @@ describe('loadConfig', () => { ignoreHttpsErrors: false, }); }); + + it('merges nested timeout config with defaults', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proofshot-timeout-config-')); + fs.writeFileSync( + path.join(tempDir, 'proofshot.config.json'), + JSON.stringify({ + timeouts: { + browserOpenMs: 120000, + execPassthroughMs: 90000, + }, + }), + ); + + expect(loadConfig(tempDir).timeouts).toEqual({ + browserOpenMs: 120000, + recordingStartMs: 10000, + recordingStopMs: 15000, + screenshotMs: 15000, + execPassthroughMs: 90000, + videoTrimMs: 60000, + }); + }); }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 4209ceb..c31c752 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -16,7 +16,23 @@ export interface BrowserConfig { executablePath?: string; ignoreHttpsErrors: boolean; } +export interface TimeoutConfig { + browserOpenMs: number; + recordingStartMs: number; + recordingStopMs: number; + screenshotMs: number; + execPassthroughMs: number; + videoTrimMs: number; +} +// Keep timeout defaults named and centralized so operational tuning does not +// rely on unexplained literals spread across the command flow. +export const DEFAULT_BROWSER_OPEN_TIMEOUT_MS = 60_000; +export const DEFAULT_RECORDING_START_TIMEOUT_MS = 10_000; +export const DEFAULT_RECORDING_STOP_TIMEOUT_MS = 15_000; +export const DEFAULT_SCREENSHOT_TIMEOUT_MS = 15_000; +export const DEFAULT_EXEC_PASSTHROUGH_TIMEOUT_MS = 60_000; +export const DEFAULT_VIDEO_TRIM_TIMEOUT_MS = 60_000; export interface ProofShotConfig { devServer: DevServerConfig; output: string; @@ -24,6 +40,7 @@ export interface ProofShotConfig { viewport: ViewportConfig; headless: boolean; browser: BrowserConfig; + timeouts: TimeoutConfig; } const CONFIG_FILENAME = 'proofshot.config.json'; @@ -40,11 +57,16 @@ const DEFAULT_CONFIG: ProofShotConfig = { browser: { ignoreHttpsErrors: false, }, + timeouts: { + browserOpenMs: DEFAULT_BROWSER_OPEN_TIMEOUT_MS, + recordingStartMs: DEFAULT_RECORDING_START_TIMEOUT_MS, + recordingStopMs: DEFAULT_RECORDING_STOP_TIMEOUT_MS, + screenshotMs: DEFAULT_SCREENSHOT_TIMEOUT_MS, + execPassthroughMs: DEFAULT_EXEC_PASSTHROUGH_TIMEOUT_MS, + videoTrimMs: DEFAULT_VIDEO_TRIM_TIMEOUT_MS, + }, }; -/** - * Find the config file by walking up from cwd. - */ export function findConfigPath(startDir?: string): string | null { let dir = startDir || process.cwd(); while (true) { @@ -56,9 +78,6 @@ export function findConfigPath(startDir?: string): string | null { } } -/** - * Load config from disk, merging with defaults. - */ export function loadConfig(startDir?: string): ProofShotConfig { const configPath = findConfigPath(startDir); if (!configPath) return { ...DEFAULT_CONFIG }; @@ -80,15 +99,13 @@ export function loadConfig(startDir?: string): ProofShotConfig { devServer: { ...DEFAULT_CONFIG.devServer, ...parsed.devServer }, viewport: { ...DEFAULT_CONFIG.viewport, ...parsed.viewport }, browser: resolvedBrowser, + timeouts: { ...DEFAULT_CONFIG.timeouts, ...parsed.timeouts }, }; } catch { return { ...DEFAULT_CONFIG }; } } -/** - * Write config to disk. - */ export function writeConfig( config: ProofShotConfig, dir?: string, @@ -98,9 +115,6 @@ export function writeConfig( return configPath; } -/** - * Check if a config file exists in the current project. - */ export function configExists(dir?: string): boolean { return findConfigPath(dir) !== null; }