Skip to content
Open
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
2 changes: 2 additions & 0 deletions PROOFSHOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
51 changes: 41 additions & 10 deletions src/browser/capture.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -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,
});
}

/**
Expand All @@ -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 {
Expand Down
14 changes: 11 additions & 3 deletions src/browser/session.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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.
Expand All @@ -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 });
}

Expand Down
4 changes: 2 additions & 2 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export async function startCommand(options: StartOptions): Promise<void> {

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();
Expand All @@ -130,7 +130,7 @@ export async function startCommand(options: StartOptions): Promise<void> {

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;
Expand Down
22 changes: 22 additions & 0 deletions src/utils/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
38 changes: 26 additions & 12 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,31 @@ 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;
defaultPages: string[];
viewport: ViewportConfig;
headless: boolean;
browser: BrowserConfig;
timeouts: TimeoutConfig;
}

const CONFIG_FILENAME = 'proofshot.config.json';
Expand All @@ -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) {
Expand All @@ -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 };
Expand All @@ -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,
Expand All @@ -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;
}