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
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,20 @@ final class RunnerTests: XCTestCase {
}
needsPostSnapshotInteractionDelay = true
return Response(ok: true, data: snapshotFast(app: activeApp, options: options))
case .screenshot:
guard
let requestedOutPath = command.outPath?.trimmingCharacters(in: .whitespacesAndNewlines),
!requestedOutPath.isEmpty
else {
return Response(ok: false, error: ErrorPayload(message: "screenshot requires outPath"))
}
let screenshot = XCUIScreen.main.screenshot()
do {
try screenshot.pngRepresentation.write(to: URL(fileURLWithPath: requestedOutPath))
return Response(ok: true, data: DataPayload(message: "screenshot captured"))
} catch {
return Response(ok: false, error: ErrorPayload(message: "failed to write screenshot: \(error.localizedDescription)"))
}
case .back:
if tapNavigationBack(app: activeApp) {
return Response(ok: true, data: DataPayload(message: "back"))
Expand Down Expand Up @@ -935,7 +949,7 @@ final class RunnerTests: XCTestCase {

private func isReadOnlyCommand(_ command: Command) -> Bool {
switch command.command {
case .findText, .snapshot:
case .findText, .snapshot, .screenshot:
return true
case .alert:
let action = (command.action ?? "get").lowercased()
Expand All @@ -962,7 +976,7 @@ final class RunnerTests: XCTestCase {

private func isRunnerLifecycleCommand(_ command: CommandType) -> Bool {
switch command {
case .shutdown, .recordStop:
case .shutdown, .recordStop, .screenshot:
return true
default:
return false
Expand Down Expand Up @@ -1818,6 +1832,7 @@ enum CommandType: String, Codable {
case swipe
case findText
case snapshot
case screenshot
case back
case home
case appSwitcher
Expand Down
15 changes: 15 additions & 0 deletions src/platforms/ios/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
setIosSetting,
writeIosClipboardText,
} from '../index.ts';
import { shouldFallbackToRunnerForIosScreenshot } from '../apps.ts';
import type { DeviceInfo } from '../../../utils/device.ts';
import { AppError } from '../../../utils/errors.ts';

Expand Down Expand Up @@ -114,6 +115,20 @@ test('openIosApp custom scheme deep links on iOS devices require app bundle cont
);
});

test('shouldFallbackToRunnerForIosScreenshot detects removed devicectl subcommand output', () => {
const error = new AppError('COMMAND_FAILED', 'Failed to capture iOS screenshot', {
stderr: "error: Unknown option '--device'",
});
assert.equal(shouldFallbackToRunnerForIosScreenshot(error), true);
});

test('shouldFallbackToRunnerForIosScreenshot ignores unrelated command failures', () => {
const error = new AppError('COMMAND_FAILED', 'Failed to capture iOS screenshot', {
stderr: 'error: device is busy connecting',
});
assert.equal(shouldFallbackToRunnerForIosScreenshot(error), false);
});

test('openIosApp web URL on iOS device without app falls back to Safari', async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-ios-safari-test-'));
const xcrunPath = path.join(tmpDir, 'xcrun');
Expand Down
32 changes: 28 additions & 4 deletions src/platforms/ios/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type PermissionSettingOptions,
} from '../permission-utils.ts';
import { parseAppearanceAction } from '../appearance.ts';
import { runIosRunnerCommand } from './runner-client.ts';

import { IOS_APP_LAUNCH_TIMEOUT_MS, IOS_DEVICECTL_TIMEOUT_MS } from './config.ts';
import {
Expand Down Expand Up @@ -221,10 +222,33 @@ export async function screenshotIos(device: DeviceInfo, outPath: string): Promis
return;
}

await runIosDevicectl(['device', 'screenshot', '--device', device.id, outPath], {
action: 'capture iOS screenshot',
deviceId: device.id,
});
try {
await runIosDevicectl(['device', 'screenshot', '--device', device.id, outPath], {
action: 'capture iOS screenshot',
deviceId: device.id,
});
return;
} catch (error) {
if (!shouldFallbackToRunnerForIosScreenshot(error)) {
throw error;
}
}

await runIosRunnerCommand(device, { command: 'screenshot', outPath });
}

export function shouldFallbackToRunnerForIosScreenshot(error: unknown): boolean {
if (!(error instanceof AppError)) return false;
if (error.code !== 'COMMAND_FAILED') return false;
const details = (error.details ?? {}) as { stdout?: unknown; stderr?: unknown };
const stdout = typeof details.stdout === 'string' ? details.stdout : '';
const stderr = typeof details.stderr === 'string' ? details.stderr : '';
const combined = `${error.message}\n${stdout}\n${stderr}`.toLowerCase();
return (
combined.includes("unknown option '--device'") ||
(combined.includes('unknown subcommand') && combined.includes('screenshot')) ||
(combined.includes('unrecognized subcommand') && combined.includes('screenshot'))
);
}

export async function readIosClipboardText(device: DeviceInfo): Promise<string> {
Expand Down
3 changes: 2 additions & 1 deletion src/platforms/ios/runner-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type RunnerCommand = {
| 'swipe'
| 'findText'
| 'snapshot'
| 'screenshot'
| 'back'
| 'home'
| 'appSwitcher'
Expand Down Expand Up @@ -654,7 +655,7 @@ export function isRetryableRunnerError(err: unknown): boolean {
}

function isReadOnlyRunnerCommand(command: RunnerCommand['command']): boolean {
return command === 'snapshot' || command === 'findText' || command === 'alert';
return command === 'snapshot' || command === 'screenshot' || command === 'findText' || command === 'alert';
}

function assertRunnerRequestActive(requestId: string | undefined): void {
Expand Down
Loading