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
5 changes: 5 additions & 0 deletions .changeset/feat-headed-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'heroshot': minor
---

Add `--headed` flag to run browser in visible mode for debugging slow captures or timeout issues
5 changes: 5 additions & 0 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ heroshot --workers 4 # Capture with 4 parallel workers
| ------------------- | --------------------------------------------------------------------- |
| `--clean` | Delete stale files from output directory (only works without pattern) |
| `--workers <count>` | Number of parallel capture workers (default: 1) |
| `--headed` | Show browser window (for debugging slow captures or timeouts) |

The pattern matches against:

Expand Down Expand Up @@ -344,4 +345,8 @@ heroshot list --json
# Verbose output (works with any command)
heroshot -v
heroshot config -v

# Debug slow captures with visible browser window
heroshot --headed
heroshot dashboard --headed
```
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ export default tseslint.config(
'ArrowFunctionExpression[params.length=1][params.0.type=Identifier][body.type=MemberExpression][body.computed=false]',
message: 'Use destructuring in callback: prefer ({ prop }) => prop over f => f.prop',
},
{
// bans `!!x` - prefer `Boolean(x)` for clarity
selector:
"UnaryExpression[operator='!'][argument.type='UnaryExpression'][argument.operator='!']",
message: 'Use Boolean(x) instead of !!x for explicit type coercion.',
},
],

// Let TypeScript infer return types
Expand Down
1 change: 1 addition & 0 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ program
.option('--save', 'Save screenshot definition to config')
.option('--clean', 'Delete stale files in output directory')
.option('--workers <count>', 'Number of parallel capture workers', Number.parseInt)
.option('--headed', 'Run browser in headed mode (visible window) for debugging')
.action(async (url?: string, options?: ShotCommandOptions) => {
const globalOptions = program.opts<GlobalOptions>();
const success = await shotAction(url, options, globalOptions);
Expand Down
38 changes: 23 additions & 15 deletions src/cli/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export async function handleUrlCapture(
sessionKey,
skipStaleCheck: true, // Don't check for stale files in oneshot mode
viewportOnly: options?.viewportOnly,
headed: options?.headed,
});

if (options?.save && result.failed === 0) {
Expand All @@ -75,18 +76,23 @@ export async function handleUrlCapture(
return result.failed === 0;
}

type DefaultCommandOptions = {
configPath: string;
sessionKey: string | undefined;
hasExplicitConfig: boolean;
clean?: boolean;
workers?: number;
headed?: boolean;
};

/**
* Handle default command (setup or sync).
*/
export async function handleDefaultCommand(
configPath: string,
sessionKey: string | undefined,
hasExplicitConfig: boolean,
clean?: boolean,
workers?: number
): Promise<boolean> {
export async function handleDefaultCommand(options: DefaultCommandOptions): Promise<boolean> {
const { configPath, sessionKey, hasExplicitConfig, clean, workers, headed } = options;

if (existsSync(configPath)) {
const result = await sync({ configPath, sessionKey, clean, workers });
const result = await sync({ configPath, sessionKey, clean, workers, headed });
return result.failed === 0;
}

Expand All @@ -97,7 +103,7 @@ export async function handleDefaultCommand(

const { hasScreenshots } = await setup();
if (hasScreenshots) {
const result = await sync({ clean, workers });
const result = await sync({ clean, workers, headed });
return result.failed === 0;
}
return true;
Expand Down Expand Up @@ -126,17 +132,19 @@ export async function shotAction(
filter: url,
clean: options?.clean,
workers: options?.workers,
headed: options?.headed,
});
return result.failed === 0;
}

return handleDefaultCommand(
return handleDefaultCommand({
configPath,
globalOptions.sessionKey,
!!globalOptions.config,
options?.clean,
options?.workers
);
sessionKey: globalOptions.sessionKey,
hasExplicitConfig: Boolean(globalOptions.config),
clean: options?.clean,
workers: options?.workers,
headed: options?.headed,
});
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export const shotCommandOptionsSchema = shotCliOptionsSchema.extend({
clean: z.boolean().optional(),
/** Number of parallel capture workers */
workers: z.number().int().min(1).optional(),
/** Run browser in headed mode (visible window) for debugging */
headed: z.boolean().optional(),
});

/** Global config */
Expand Down
3 changes: 2 additions & 1 deletion src/sync/parallelCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type ParallelCaptureOptions = {
bypassCSP?: boolean;
reducedMotion?: 'reduce' | 'no-preference';
userAgent?: string;
headed?: boolean;
};
workers: number;
captureSpinner: ReturnType<typeof spinner>;
Expand Down Expand Up @@ -96,7 +97,7 @@ async function executeBatch(

// Launch browser for this batch
const { browser, context } = await launchBrowser({
headless: true,
headless: !browserOptions.headed,
viewport: browserOptions.viewport,
deviceScaleFactor: browserOptions.deviceScaleFactor,
storageState: browserOptions.storageState,
Expand Down
3 changes: 2 additions & 1 deletion src/sync/schemeCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type CaptureSchemeOptions = {
bypassCSP?: boolean;
reducedMotion?: 'reduce' | 'no-preference';
userAgent?: string;
headed?: boolean;
};
colorScheme: 'light' | 'dark' | undefined;
schemes: ('light' | 'dark')[];
Expand Down Expand Up @@ -49,7 +50,7 @@ export async function captureWithScheme(
const results: ScreenshotResult[] = [];

const { browser, context } = await launchBrowser({
headless: true,
headless: !browserOptions.headed,
viewport: browserOptions.viewport,
deviceScaleFactor: browserOptions.deviceScaleFactor,
storageState: browserOptions.storageState,
Expand Down
2 changes: 2 additions & 0 deletions src/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type CaptureContext = {
bypassCSP?: boolean;
reducedMotion?: 'reduce' | 'no-preference';
userAgent?: string;
headed?: boolean;
};
schemes: ('light' | 'dark')[];
workers: number;
Expand Down Expand Up @@ -130,6 +131,7 @@ export async function sync(options: SyncOptions = {}): Promise<SyncResult> {
const browserOptions = {
...buildBrowserOptions(config),
storageState,
headed: options.headed,
};

// Start capture
Expand Down
2 changes: 2 additions & 0 deletions src/sync/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export type SyncOptions = {
viewportOnly?: boolean;
/** Number of parallel capture workers (default: 1) */
workers?: number;
/** Run browser in headed mode (visible window) for debugging */
headed?: boolean;
};

/** Padding dimensions */
Expand Down