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
4 changes: 2 additions & 2 deletions scripts/integration-progress-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ function summarizeProviderScenarioFlagExclusions() {
keys: ['headless'],
},
{
name: 'Apple screenshot status-bar normalization',
name: 'Apple simulator screenshot rendering options',
owner: 'iOS platform and screenshot-diff runtime tests',
keys: ['screenshotNormalizeStatusBar'],
keys: ['screenshotNormalizeStatusBar', 'screenshotPixelDensity'],
},
];
}
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,11 @@ test('client capture.screenshot normalizes overlay refs from daemon response dat
ok: true,
data: {
path: '/tmp/screenshot.png',
width: 402,
height: 874,
logicalWidth: 402,
logicalHeight: 874,
pixelDensity: 1,
overlayRefs: [
{
ref: '@e1',
Expand Down Expand Up @@ -864,6 +869,11 @@ test('client capture.screenshot normalizes overlay refs from daemon response dat

assert.deepEqual(result, {
path: '/tmp/screenshot.png',
width: 402,
height: 874,
logicalWidth: 402,
logicalHeight: 874,
pixelDensity: 1,
overlayRefs: [
{
ref: '@e1',
Expand Down Expand Up @@ -929,6 +939,11 @@ test('capture.screenshot normalizes the default-level result (unchanged)', async
ok: true,
data: {
path: '/tmp/shot.png',
width: 1206,
height: 2622,
logicalWidth: 402,
logicalHeight: 874,
pixelDensity: 3,
overlayRefs: [{ ref: 'e1', label: 'Login', x: 0, y: 0, width: 10, height: 10 }],
},
};
Expand All @@ -938,6 +953,11 @@ test('capture.screenshot normalizes the default-level result (unchanged)', async
const result = await client.capture.screenshot();

assert.equal(result.path, '/tmp/shot.png');
assert.equal(result.width, 1206);
assert.equal(result.height, 2622);
assert.equal(result.logicalWidth, 402);
assert.equal(result.logicalHeight, 874);
assert.equal(result.pixelDensity, 3);
assert.deepEqual(result.identifiers, { session: 'qa' });
});

Expand Down
1 change: 1 addition & 0 deletions src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export type BackendFindTextResult = {
export type BackendScreenshotOptions = {
fullscreen?: boolean;
overlayRefs?: boolean;
pixelDensity?: number;
stabilize?: boolean;
normalizeStatusBar?: boolean;
surface?: SessionSurface;
Expand Down
13 changes: 11 additions & 2 deletions src/cli/commands/__tests__/screenshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,26 @@ test('screenshot --level digest --json preserves the digest payload through the
assert.deepEqual(parsed.data, digest);
});

test('screenshot --json at the default level still emits the normalized { path, overlayRefs } shape', async () => {
test('screenshot --json at the default level still emits normalized screenshot metadata', async () => {
const full = {
path: '/tmp/shot.png',
width: 402,
height: 874,
logicalWidth: 402,
logicalHeight: 874,
pixelDensity: 1,
overlayRefs: [{ ref: 'e1', label: 'Login', x: 0, y: 0, width: 10, height: 10 }],
};
const client = clientReturning(full);
const flags = { json: true } as CliFlags;

const out = await captureStdout(() => screenshotCommand({ positionals: [], flags, client }));
const parsed = JSON.parse(out) as { data: { path: string; overlayCount?: number } };
const parsed = JSON.parse(out) as {
data: { path: string; width?: number; pixelDensity?: number };
};

assert.equal(parsed.data.path, '/tmp/shot.png');
assert.equal(parsed.data.width, 402);
assert.equal(parsed.data.pixelDensity, 1);
assert.ok(!('overlayCount' in parsed.data));
});
22 changes: 13 additions & 9 deletions src/cli/commands/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolveUserPath } from '../../utils/path-resolution.ts';
import type { AgentDeviceBackend } from '../../backend.ts';
import type { AgentDeviceClient, CaptureScreenshotResult } from '../../client/client.ts';
import { runCliCommand } from '../../commands/cli-runner.ts';
import { pickScreenshotResultData } from '../../utils/screenshot-result.ts';
import type { CliFlags } from '../parser/cli-flags.ts';
import { writeCommandOutput } from './shared.ts';
import type { ClientCommandHandler } from './router-types.ts';
Expand All @@ -23,14 +24,11 @@ export const screenshotCommand: ClientCommandHandler = async ({ positionals, fla
writeCommandOutput(flags, result, () => JSON.stringify(result, null, 2));
return true;
}
const data = {
path: result.path,
...(result.overlayRefs ? { overlayRefs: result.overlayRefs } : {}),
};
const data = pickScreenshotResultData(result);
writeCommandOutput(flags, data, () =>
result.overlayRefs
? `Annotated ${result.overlayRefs.length} refs onto ${result.path}`
: result.path,
: formatScreenshotSummary(result),
);
return true;
};
Expand Down Expand Up @@ -99,19 +97,25 @@ function createClientScreenshotBackend(
path: outPath,
session: context.session,
overlayRefs: options?.overlayRefs,
pixelDensity: options?.pixelDensity,
fullscreen: options?.fullscreen,
normalizeStatusBar: options?.normalizeStatusBar,
stabilize: options?.stabilize,
surface: options?.surface,
});
return {
path: result.path,
...(result.overlayRefs ? { overlayRefs: result.overlayRefs } : {}),
};
return pickScreenshotResultData(result);
},
};
}

function formatScreenshotSummary(result: CaptureScreenshotResult): string {
if (typeof result.width !== 'number' || typeof result.height !== 'number') {
return result.path;
}
const densitySuffix = typeof result.pixelDensity === 'number' ? ` @${result.pixelDensity}x` : '';
return `${result.path} (${result.width}x${result.height}${densitySuffix})`;
}

function resolveClientBackendPlatform(flags: CliFlags): AgentDeviceBackend['platform'] {
switch (flags.platform) {
case 'android':
Expand Down
3 changes: 3 additions & 0 deletions src/cli/parser/__tests__/args-parse-interaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ test('parseArgs recognizes screenshot flags', () => {
[
'screenshot',
'page.png',
'--pixel-density',
'2',
'--full',
'-f',
'--fullscreen',
Expand All @@ -241,6 +243,7 @@ test('parseArgs recognizes screenshot flags', () => {
);
assert.equal(parsed.command, 'screenshot');
assert.deepEqual(parsed.positionals, ['page.png']);
assert.equal(parsed.flags.screenshotPixelDensity, 2);
assert.equal(parsed.flags.screenshotFullscreen, true);
assert.equal(parsed.flags.screenshotMaxSize, 1024);
assert.equal(parsed.flags.screenshotNoStabilize, true);
Expand Down
2 changes: 1 addition & 1 deletion src/cli/parser/__tests__/cli-help-topics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ test('usage renders concise commands inline with descriptions', async () => {
assert.match(help, / test <path-or-glob>\.\.\.\s{2,}Run replay test suites/);
assert.match(
help,
/ screenshot \[path\]\s{2,}Capture screenshot with optional web full-page, desktop/,
/ screenshot \[path\]\s{2,}Capture screenshot with optional density, full-page, desktop/,
);
assert.match(
help,
Expand Down
1 change: 1 addition & 0 deletions src/client/client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export type CaptureSnapshotResult = {
export type CaptureScreenshotOptions = AgentDeviceRequestOverrides & {
path?: string;
overlayRefs?: boolean;
pixelDensity?: number;
fullscreen?: boolean;
maxSize?: number;
stabilize?: boolean;
Expand Down
5 changes: 5 additions & 0 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ export function createAgentDeviceClient(
const screenshot = readScreenshotResultData(data);
return {
path: readRequiredString(data, 'path'),
width: screenshot?.width,
height: screenshot?.height,
logicalWidth: screenshot?.logicalWidth,
logicalHeight: screenshot?.logicalHeight,
pixelDensity: screenshot?.pixelDensity,
overlayRefs: screenshot?.overlayRefs,
identifiers: { session },
};
Expand Down
4 changes: 3 additions & 1 deletion src/commands/capture/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,19 @@ describe('capture command interface', () => {
test('reads screenshot path and writes screenshot flags', () => {
const input = screenshotCliReader(
['page.png'],
flags({ screenshotFullscreen: true, screenshotMaxSize: 1024 }),
flags({ screenshotPixelDensity: 2, screenshotFullscreen: true, screenshotMaxSize: 1024 }),
);
expect(input).toMatchObject({
path: 'page.png',
pixelDensity: 2,
fullscreen: true,
maxSize: 1024,
});
expect(screenshotDaemonWriter(input)).toMatchObject({
command: 'screenshot',
positionals: ['page.png'],
options: {
screenshotPixelDensity: 2,
screenshotFullscreen: true,
screenshotMaxSize: 1024,
},
Expand Down
1 change: 1 addition & 0 deletions src/commands/capture/runtime/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const screenshotCommand: RuntimeCommand<
{
fullscreen: options.fullscreen,
overlayRefs: options.overlayRefs,
pixelDensity: options.pixelDensity,
stabilize: options.stabilize,
normalizeStatusBar: options.normalizeStatusBar,
surface: options.surface,
Expand Down
10 changes: 10 additions & 0 deletions src/commands/capture/screenshot-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ test('screenshot flag projection maps CLI flags to runtime options', () => {
assert.deepEqual(
screenshotOptionsFromFlags({
overlayRefs: true,
screenshotPixelDensity: 2,
screenshotFullscreen: true,
screenshotMaxSize: 1024,
screenshotNoStabilize: true,
screenshotNormalizeStatusBar: true,
}),
{
overlayRefs: true,
pixelDensity: 2,
fullscreen: true,
maxSize: 1024,
stabilize: false,
Expand All @@ -33,13 +35,15 @@ test('screenshot flag projection maps public options to request flags', () => {
assert.deepEqual(
screenshotFlagsFromOptions({
overlayRefs: true,
pixelDensity: 3,
fullscreen: true,
maxSize: 512,
stabilize: false,
normalizeStatusBar: true,
}),
{
overlayRefs: true,
screenshotPixelDensity: 3,
screenshotFullscreen: true,
screenshotMaxSize: 512,
screenshotNoStabilize: true,
Expand All @@ -64,17 +68,22 @@ test('screenshot script flags use the shared recorded flag contract', () => {
assert.deepEqual(result, { handled: true, nextIndex: 0 });
result = readScreenshotScriptFlag({ args: ['--normalize-status-bar'], index: 0, flags });
assert.deepEqual(result, { handled: true, nextIndex: 0 });
result = readScreenshotScriptFlag({ args: ['--pixel-density', '3'], index: 0, flags });
assert.deepEqual(result, { handled: true, nextIndex: 1 });

appendScreenshotScriptFlags(parts, flags);

assert.deepEqual(parts, [
'--pixel-density',
'3',
'--fullscreen',
'--max-size',
'640',
'--no-stabilize',
'--normalize-status-bar',
]);
assert.deepEqual(SCREENSHOT_ACTION_FLAG_KEYS, [
'screenshotPixelDensity',
'screenshotFullscreen',
'screenshotMaxSize',
'screenshotNoStabilize',
Expand All @@ -87,6 +96,7 @@ test('screenshot script flags use the shared recorded flag contract', () => {
assert.deepEqual(SCREENSHOT_COMMAND_FLAG_KEYS, [
'out',
'overlayRefs',
'screenshotPixelDensity',
'screenshotFullscreen',
'screenshotMaxSize',
'screenshotNoStabilize',
Expand Down
7 changes: 5 additions & 2 deletions src/commands/capture/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const screenshotCommandMetadata = defineFieldCommandMetadata(
{
path: stringField('Output path.'),
overlayRefs: booleanField(),
pixelDensity: integerField('Output screenshot pixel density in pixels per logical point.', {
min: 1,
}),
fullscreen: booleanField(),
maxSize: integerField(),
stabilize: booleanField(),
Expand All @@ -38,9 +41,9 @@ const screenshotCommandDefinition = defineExecutableCommand(

const screenshotCliSchema = {
helpDescription:
'Capture screenshot (web defaults to the viewport; use --fullscreen, --full, or -f for the entire page. macOS app sessions default to the app window; use --fullscreen for full desktop, --max-size to downscale, --overlay-refs to annotate current refs, --normalize-status-bar for deterministic iOS simulator chrome, or --no-stabilize for low-latency Android capture loops)',
'Capture screenshot (web defaults to the viewport; use --fullscreen, --full, or -f for the entire page. iOS simulators default to 1x logical-point output; use --pixel-density to request a different screenshot density. macOS app sessions default to the app window; use --fullscreen for full desktop, --max-size to downscale, --overlay-refs to annotate current refs, --normalize-status-bar for deterministic iOS simulator chrome, or --no-stabilize for low-latency Android capture loops)',
summary:
'Capture screenshot with optional web full-page, desktop, downscale, or ref overlay modes',
'Capture screenshot with optional density, full-page, desktop, downscale, or ref overlay modes',
positionalArgs: ['path?'],
allowedFlags: SCREENSHOT_COMMAND_FLAG_KEYS,
} as const;
Expand Down
1 change: 1 addition & 0 deletions src/commands/runtime-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type ScreenshotCommandOptions = CommandContext & {
out?: FileOutputRef;
fullscreen?: boolean;
overlayRefs?: boolean;
pixelDensity?: number;
maxSize?: number;
stabilize?: boolean;
normalizeStatusBar?: boolean;
Expand Down
Loading
Loading