agent-device 0.12.3 → 0.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/src/152.js +1 -1
  2. package/dist/src/320.js +1 -1
  3. package/dist/src/57.js +1 -1
  4. package/dist/src/641.js +38 -0
  5. package/dist/src/818.js +1 -1
  6. package/dist/src/974.js +2 -2
  7. package/dist/src/backend.d.ts +205 -0
  8. package/dist/src/backend.js +1 -0
  9. package/dist/src/bin.js +63 -63
  10. package/dist/src/commands/index.d.ts +908 -0
  11. package/dist/src/commands/index.js +1 -0
  12. package/dist/src/contracts.d.ts +1 -1
  13. package/dist/src/daemon.js +15 -15
  14. package/dist/src/index.d.ts +898 -3
  15. package/dist/src/index.js +3 -3
  16. package/dist/src/io.d.ts +85 -0
  17. package/dist/src/io.js +1 -0
  18. package/dist/src/metro-companion.js +1 -1
  19. package/dist/src/metro.d.ts +10 -0
  20. package/dist/src/metro.js +1 -1
  21. package/dist/src/selectors.js +1 -1
  22. package/dist/src/testing/conformance.d.ts +416 -0
  23. package/dist/src/testing/conformance.js +1 -0
  24. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +12 -3
  25. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +1 -0
  26. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +24 -5
  27. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +2 -0
  28. package/ios-runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift +182 -0
  29. package/ios-runner/RUNNER_PROTOCOL.md +1 -1
  30. package/package.json +17 -1
  31. package/skills/agent-device/references/bootstrap-install.md +13 -0
  32. package/skills/agent-device/references/remote-tenancy.md +15 -0
  33. package/skills/agent-device/references/verification.md +1 -0
  34. package/dist/src/155.js +0 -38
  35. package/dist/src/940.js +0 -1
@@ -0,0 +1,416 @@
1
+ declare type AgentDeviceBackend = {
2
+ platform: AgentDeviceBackendPlatform;
3
+ capabilities?: BackendCapabilitySet;
4
+ escapeHatches?: BackendEscapeHatches;
5
+ captureSnapshot?(context: BackendCommandContext, options?: BackendSnapshotOptions): Promise<BackendSnapshotResult>;
6
+ captureScreenshot?(context: BackendCommandContext, outPath: string, options?: BackendScreenshotOptions): Promise<BackendScreenshotResult | void>;
7
+ readText?(context: BackendCommandContext, node: SnapshotNode): Promise<BackendReadTextResult>;
8
+ findText?(context: BackendCommandContext, text: string): Promise<BackendFindTextResult>;
9
+ tap?(context: BackendCommandContext, point: Point, options?: BackendTapOptions): Promise<BackendActionResult>;
10
+ fill?(context: BackendCommandContext, point: Point, text: string, options?: BackendFillOptions): Promise<BackendActionResult>;
11
+ typeText?(context: BackendCommandContext, text: string, options?: {
12
+ delayMs?: number;
13
+ }): Promise<BackendActionResult>;
14
+ pressKey?(context: BackendCommandContext, key: string, options?: {
15
+ modifiers?: string[];
16
+ }): Promise<BackendActionResult>;
17
+ openApp?(context: BackendCommandContext, target: BackendOpenTarget): Promise<BackendActionResult>;
18
+ closeApp?(context: BackendCommandContext, app?: string): Promise<BackendActionResult>;
19
+ installApp?(context: BackendCommandContext, target: BackendInstallTarget): Promise<BackendActionResult>;
20
+ };
21
+
22
+ declare type AgentDeviceBackendPlatform = 'ios' | 'android' | 'macos' | 'linux';
23
+
24
+ declare type AgentDeviceRuntime = {
25
+ backend: AgentDeviceBackend;
26
+ artifacts: ArtifactAdapter;
27
+ sessions: CommandSessionStore;
28
+ policy: CommandPolicy;
29
+ diagnostics?: DiagnosticsSink;
30
+ clock?: CommandClock;
31
+ signal?: AbortSignal;
32
+ };
33
+
34
+ declare type ArtifactAdapter = {
35
+ resolveInput(ref: FileInputRef, options: ResolveInputOptions): Promise<ResolvedInputFile>;
36
+ reserveOutput(ref: FileOutputRef | undefined, options: ReserveOutputOptions): Promise<ReservedOutputFile>;
37
+ createTempFile(options: CreateTempFileOptions): Promise<TemporaryFile>;
38
+ };
39
+
40
+ declare type ArtifactDescriptor = {
41
+ kind: 'localPath';
42
+ field: string;
43
+ path: string;
44
+ fileName?: string;
45
+ metadata?: Record<string, unknown>;
46
+ } | {
47
+ kind: 'artifact';
48
+ field: string;
49
+ artifactId: string;
50
+ fileName?: string;
51
+ url?: string;
52
+ clientPath?: string;
53
+ metadata?: Record<string, unknown>;
54
+ };
55
+
56
+ export declare function assertCommandConformance(target: CommandConformanceTarget, options?: {
57
+ suites?: readonly CommandConformanceSuite[];
58
+ }): Promise<CommandConformanceReport>;
59
+
60
+ declare const BACKEND_CAPABILITY_NAMES: readonly ["android.shell", "ios.runnerCommand", "macos.desktopScreenshot"];
61
+
62
+ declare type BackendActionResult = Record<string, unknown> | void;
63
+
64
+ declare type BackendCapabilityName = (typeof BACKEND_CAPABILITY_NAMES)[number];
65
+
66
+ declare type BackendCapabilitySet = readonly BackendCapabilityName[];
67
+
68
+ declare type BackendCommandContext = {
69
+ session?: string;
70
+ requestId?: string;
71
+ appId?: string;
72
+ appBundleId?: string;
73
+ signal?: AbortSignal;
74
+ metadata?: Record<string, unknown>;
75
+ };
76
+
77
+ declare type BackendEscapeHatches = {
78
+ androidShell?(context: BackendCommandContext, args: readonly string[]): Promise<BackendShellResult>;
79
+ iosRunnerCommand?(context: BackendCommandContext, command: BackendRunnerCommand): Promise<BackendActionResult>;
80
+ macosDesktopScreenshot?(context: BackendCommandContext, outPath: string, options?: BackendScreenshotOptions): Promise<BackendScreenshotResult | void>;
81
+ };
82
+
83
+ declare type BackendFillOptions = {
84
+ delayMs?: number;
85
+ };
86
+
87
+ declare type BackendFindTextResult = {
88
+ found: boolean;
89
+ };
90
+
91
+ declare type BackendInstallTarget = {
92
+ app: string;
93
+ artifactPath: string;
94
+ };
95
+
96
+ declare type BackendOpenTarget = {
97
+ app?: string;
98
+ url?: string;
99
+ activity?: string;
100
+ };
101
+
102
+ declare type BackendReadTextResult = {
103
+ text: string;
104
+ };
105
+
106
+ declare type BackendRunnerCommand = {
107
+ command: string;
108
+ args?: readonly string[];
109
+ payload?: Record<string, unknown>;
110
+ };
111
+
112
+ declare type BackendScreenshotOptions = {
113
+ fullscreen?: boolean;
114
+ overlayRefs?: boolean;
115
+ surface?: 'app' | 'frontmost-app' | 'desktop' | 'menubar';
116
+ };
117
+
118
+ declare type BackendScreenshotResult = {
119
+ path?: string;
120
+ overlayRefs?: ScreenshotOverlayRef[];
121
+ };
122
+
123
+ declare type BackendShellResult = {
124
+ exitCode: number;
125
+ stdout: string;
126
+ stderr: string;
127
+ };
128
+
129
+ declare type BackendSnapshotAnalysis = {
130
+ rawNodeCount?: number;
131
+ maxDepth?: number;
132
+ };
133
+
134
+ declare type BackendSnapshotFreshness = {
135
+ action: string;
136
+ retryCount: number;
137
+ staleAfterRetries: boolean;
138
+ reason?: 'empty-interactive' | 'sharp-drop' | 'stuck-route';
139
+ };
140
+
141
+ declare type BackendSnapshotOptions = SnapshotOptions & {
142
+ outPath?: string;
143
+ };
144
+
145
+ declare type BackendSnapshotResult = {
146
+ nodes?: SnapshotNode[];
147
+ truncated?: boolean;
148
+ backend?: string;
149
+ snapshot?: SnapshotState;
150
+ analysis?: BackendSnapshotAnalysis;
151
+ freshness?: BackendSnapshotFreshness;
152
+ warnings?: string[];
153
+ appName?: string;
154
+ appBundleId?: string;
155
+ };
156
+
157
+ declare type BackendTapOptions = {
158
+ button?: 'primary' | 'secondary' | 'middle';
159
+ count?: number;
160
+ intervalMs?: number;
161
+ holdMs?: number;
162
+ jitterPx?: number;
163
+ doubleTap?: boolean;
164
+ };
165
+
166
+ export declare const captureConformanceSuite: CommandConformanceSuite;
167
+
168
+ declare type CommandClock = {
169
+ now(): number;
170
+ sleep(ms: number): Promise<void>;
171
+ };
172
+
173
+ export declare type CommandConformanceCase = {
174
+ name: string;
175
+ command: string;
176
+ run(runtime: AgentDeviceRuntime, fixtures: CommandConformanceFixtures): Promise<void>;
177
+ };
178
+
179
+ export declare type CommandConformanceCaseContext = {
180
+ suite: string;
181
+ caseName: string;
182
+ fixtures: CommandConformanceFixtures;
183
+ };
184
+
185
+ export declare type CommandConformanceFailure = {
186
+ suite: string;
187
+ caseName: string;
188
+ command: string;
189
+ error: unknown;
190
+ };
191
+
192
+ export declare type CommandConformanceFixtures = {
193
+ session: string;
194
+ visibleSelector: string;
195
+ visibleText: string;
196
+ editableTarget: InteractionTarget;
197
+ fillText: string;
198
+ point: Point;
199
+ };
200
+
201
+ export declare type CommandConformanceReport = {
202
+ target: string;
203
+ passed: number;
204
+ failed: number;
205
+ failures: CommandConformanceFailure[];
206
+ suites: CommandConformanceSuiteResult[];
207
+ };
208
+
209
+ export declare type CommandConformanceSuite = {
210
+ name: string;
211
+ cases: readonly CommandConformanceCase[];
212
+ run(target: CommandConformanceTarget): Promise<CommandConformanceSuiteResult>;
213
+ };
214
+
215
+ export declare type CommandConformanceSuiteResult = {
216
+ suite: string;
217
+ passed: number;
218
+ failed: number;
219
+ failures: CommandConformanceFailure[];
220
+ };
221
+
222
+ export declare const commandConformanceSuites: readonly CommandConformanceSuite[];
223
+
224
+ export declare type CommandConformanceTarget = {
225
+ name: string;
226
+ createRuntime: ConformanceRuntimeFactory;
227
+ fixtures?: Partial<CommandConformanceFixtures>;
228
+ beforeEach?(context: CommandConformanceCaseContext): void | Promise<void>;
229
+ afterEach?(context: CommandConformanceCaseContext): void | Promise<void>;
230
+ };
231
+
232
+ declare type CommandPolicy = {
233
+ allowLocalInputPaths: boolean;
234
+ allowLocalOutputPaths: boolean;
235
+ maxImagePixels: number;
236
+ allowNamedBackendCapabilities: readonly BackendCapabilityName[];
237
+ };
238
+
239
+ declare type CommandSessionRecord = {
240
+ name: string;
241
+ appId?: string;
242
+ appBundleId?: string;
243
+ appName?: string;
244
+ backendSessionId?: string;
245
+ snapshot?: SnapshotState;
246
+ metadata?: Record<string, unknown>;
247
+ };
248
+
249
+ declare type CommandSessionStore = {
250
+ get(name: string): CommandSessionRecord | undefined | Promise<CommandSessionRecord | undefined>;
251
+ set(record: CommandSessionRecord): void | Promise<void>;
252
+ delete?(name: string): void | Promise<void>;
253
+ list?(): readonly CommandSessionRecord[] | Promise<readonly CommandSessionRecord[]>;
254
+ };
255
+
256
+ export declare type ConformanceRuntimeFactory = () => AgentDeviceRuntime | Promise<AgentDeviceRuntime>;
257
+
258
+ declare type CreateTempFileOptions = {
259
+ prefix: string;
260
+ ext: string;
261
+ };
262
+
263
+ export declare const defaultCommandConformanceFixtures: CommandConformanceFixtures;
264
+
265
+ declare type DiagnosticsSink = {
266
+ emit(event: {
267
+ level: 'debug' | 'info' | 'warn' | 'error';
268
+ message: string;
269
+ data?: unknown;
270
+ }): void;
271
+ };
272
+
273
+ declare type ElementTarget = SelectorTarget | RefTarget;
274
+
275
+ declare type FileInputRef = {
276
+ kind: 'path';
277
+ path: string;
278
+ } | {
279
+ kind: 'uploadedArtifact';
280
+ id: string;
281
+ };
282
+
283
+ declare type FileOutputRef = {
284
+ kind: 'path';
285
+ path: string;
286
+ } | {
287
+ kind: 'downloadableArtifact';
288
+ clientPath?: string;
289
+ fileName?: string;
290
+ };
291
+
292
+ export declare const interactionConformanceSuite: CommandConformanceSuite;
293
+
294
+ declare type InteractionTarget = ElementTarget | PointTarget;
295
+
296
+ declare type OutputVisibility = 'client-visible' | 'internal';
297
+
298
+ declare type Point = {
299
+ x: number;
300
+ y: number;
301
+ };
302
+
303
+ declare type PointTarget = {
304
+ kind: 'point';
305
+ x: number;
306
+ y: number;
307
+ };
308
+
309
+ declare type RawSnapshotNode = {
310
+ index: number;
311
+ type?: string;
312
+ role?: string;
313
+ subrole?: string;
314
+ label?: string;
315
+ value?: string;
316
+ identifier?: string;
317
+ rect?: Rect;
318
+ enabled?: boolean;
319
+ selected?: boolean;
320
+ hittable?: boolean;
321
+ depth?: number;
322
+ parentIndex?: number;
323
+ pid?: number;
324
+ bundleId?: string;
325
+ appName?: string;
326
+ windowTitle?: string;
327
+ surface?: string;
328
+ hiddenContentAbove?: boolean;
329
+ hiddenContentBelow?: boolean;
330
+ };
331
+
332
+ declare type Rect = {
333
+ x: number;
334
+ y: number;
335
+ width: number;
336
+ height: number;
337
+ };
338
+
339
+ declare type RefTarget = {
340
+ kind: 'ref';
341
+ ref: string;
342
+ fallbackLabel?: string;
343
+ };
344
+
345
+ declare type ReservedOutputFile = {
346
+ path: string;
347
+ visibility: OutputVisibility;
348
+ publish: () => Promise<ArtifactDescriptor | undefined>;
349
+ cleanup?: () => Promise<void>;
350
+ };
351
+
352
+ declare type ReserveOutputOptions = {
353
+ field: string;
354
+ ext: string;
355
+ requestedClientPath?: string;
356
+ visibility?: OutputVisibility;
357
+ };
358
+
359
+ declare type ResolvedInputFile = {
360
+ path: string;
361
+ cleanup?: () => Promise<void>;
362
+ };
363
+
364
+ declare type ResolveInputOptions = {
365
+ usage: string;
366
+ field?: string;
367
+ };
368
+
369
+ export declare function runCommandConformance(target: CommandConformanceTarget, options?: {
370
+ suites?: readonly CommandConformanceSuite[];
371
+ }): Promise<CommandConformanceReport>;
372
+
373
+ declare type ScreenshotOverlayRef = {
374
+ ref: string;
375
+ label?: string;
376
+ rect: Rect;
377
+ overlayRect: Rect;
378
+ center: Point;
379
+ };
380
+
381
+ export declare const selectorConformanceSuite: CommandConformanceSuite;
382
+
383
+ declare type SelectorTarget = {
384
+ kind: 'selector';
385
+ selector: string;
386
+ };
387
+
388
+ declare type SnapshotBackend = 'xctest' | 'android' | 'macos-helper' | 'linux-atspi';
389
+
390
+ declare type SnapshotNode = RawSnapshotNode & {
391
+ ref: string;
392
+ };
393
+
394
+ declare type SnapshotOptions = {
395
+ interactiveOnly?: boolean;
396
+ compact?: boolean;
397
+ depth?: number;
398
+ scope?: string;
399
+ raw?: boolean;
400
+ };
401
+
402
+ declare type SnapshotState = {
403
+ nodes: SnapshotNode[];
404
+ createdAt: number;
405
+ truncated?: boolean;
406
+ backend?: SnapshotBackend;
407
+ comparisonSafe?: boolean;
408
+ };
409
+
410
+ declare type TemporaryFile = {
411
+ path: string;
412
+ visibility: 'internal';
413
+ cleanup: () => Promise<void>;
414
+ };
415
+
416
+ export { }
@@ -0,0 +1 @@
1
+ import e from"node:assert/strict";import{commands as t,selector as s}from"../commands/index.js";let a={session:"default",visibleSelector:"label=Continue",visibleText:"Continue",editableTarget:s("label=Email"),fillText:"hello@example.com",point:{x:4,y:8}},n=m({name:"capture",cases:[{name:"captures screenshots through the backend primitive",command:"capture.screenshot",run:async(s,a)=>{let n=await t.capture.screenshot(s,{session:a.session});e.equal(typeof n.path,"string"),e.ok(n.path.length>0)}},{name:"captures snapshots with nodes",command:"capture.snapshot",run:async(s,a)=>{let n=await t.capture.snapshot(s,{session:a.session});e.ok(Array.isArray(n.nodes))}}]}),i=m({name:"selectors",cases:[{name:"finds visible text",command:"selectors.find",run:async(s,a)=>{let n=await t.selectors.find(s,{session:a.session,query:a.visibleText,action:"exists"});e.equal(n.kind,"found"),e.equal(n.found,!0)}},{name:"reads text from a selector",command:"selectors.getText",run:async(a,n)=>{let i=await t.selectors.getText(a,{session:n.session,target:s(n.visibleSelector)});e.equal(i.kind,"text"),e.equal(i.text,n.visibleText)}},{name:"checks selector visibility",command:"selectors.isVisible",run:async(a,n)=>{let i=await t.selectors.isVisible(a,{session:n.session,target:s(n.visibleSelector)});e.equal(i.pass,!0)}},{name:"waits for visible text",command:"selectors.waitForText",run:async(s,a)=>{let n=await t.selectors.waitForText(s,{session:a.session,text:a.visibleText,timeoutMs:1});e.equal(n.kind,"text"),e.equal(n.text,a.visibleText)}}]}),o=m({name:"interactions",cases:[{name:"clicks selector targets",command:"interactions.click",run:async(a,n)=>{let i=await t.interactions.click(a,{session:n.session,target:s(n.visibleSelector)});e.equal(i.kind,"selector")}},{name:"presses explicit points",command:"interactions.press",run:async(s,a)=>{let n=await t.interactions.press(s,{session:a.session,target:{kind:"point",...a.point}});e.deepEqual(n.point,a.point)}},{name:"fills editable targets",command:"interactions.fill",run:async(s,a)=>{let n=await t.interactions.fill(s,{session:a.session,target:a.editableTarget,text:a.fillText});e.equal(n.text,a.fillText)}},{name:"types text without a target",command:"interactions.typeText",run:async(s,a)=>{let n=await t.interactions.typeText(s,{session:a.session,text:a.fillText});e.equal(n.text,a.fillText)}}]}),r=[n,i,o];async function l(e,t={}){let s=t.suites??r,a=[];for(let t of s)a.push(await t.run(e));let n=a.flatMap(e=>e.failures);return{target:e.name,passed:a.reduce((e,t)=>e+t.passed,0),failed:a.reduce((e,t)=>e+t.failed,0),failures:n,suites:a}}async function c(e,t={}){let s=await l(e,t);if(s.failed>0)throw AggregateError(s.failures.map(e=>e.error),`${e.name} failed ${s.failed} agent-device conformance case${1===s.failed?"":"s"}`);return s}function m(e){return{name:e.name,cases:e.cases,run:async t=>{let s={...a,...t.fixtures},n=[],i=0;for(let a of e.cases){let o={suite:e.name,caseName:a.name,fixtures:s};try{await t.beforeEach?.(o);let e=await t.createRuntime();await a.run(e,s),i+=1}catch(t){n.push({suite:e.name,caseName:a.name,command:a.command,error:t})}finally{await t.afterEach?.(o)}}return{suite:e.name,passed:i,failed:n.length,failures:n}}}}export{c as assertCommandConformance,n as captureConformanceSuite,r as commandConformanceSuites,a as defaultCommandConformanceFixtures,o as interactionConformanceSuite,l as runCommandConformance,i as selectorConformanceSuite};
@@ -183,16 +183,25 @@ extension RunnerTests {
183
183
  if let requestedFps = command.fps, (requestedFps < minRecordingFps || requestedFps > maxRecordingFps) {
184
184
  return Response(ok: false, error: ErrorPayload(message: "recordStart fps must be between \(minRecordingFps) and \(maxRecordingFps)"))
185
185
  }
186
+ if let requestedQuality = command.quality, (requestedQuality < minRecordingQuality || requestedQuality > maxRecordingQuality) {
187
+ return Response(ok: false, error: ErrorPayload(message: "recordStart quality must be between \(minRecordingQuality) and \(maxRecordingQuality)"))
188
+ }
186
189
  do {
187
190
  let resolvedOutPath = resolveRecordingOutPath(requestedOutPath)
188
191
  let fpsLabel = command.fps.map(String.init) ?? String(RunnerTests.defaultRecordingFps)
192
+ let qualityLabel = command.quality.map(String.init) ?? "native"
189
193
  NSLog(
190
- "AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@",
194
+ "AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@ quality=%@",
191
195
  requestedOutPath,
192
196
  resolvedOutPath,
193
- fpsLabel
197
+ fpsLabel,
198
+ qualityLabel
199
+ )
200
+ let recorder = ScreenRecorder(
201
+ outputPath: resolvedOutPath,
202
+ fps: command.fps.map { Int32($0) },
203
+ quality: command.quality
194
204
  )
195
- let recorder = ScreenRecorder(outputPath: resolvedOutPath, fps: command.fps.map { Int32($0) })
196
205
  try recorder.start { [weak self] in
197
206
  return self?.captureRunnerFrame()
198
207
  }
@@ -52,6 +52,7 @@ struct Command: Codable {
52
52
  let scale: Double?
53
53
  let outPath: String?
54
54
  let fps: Int?
55
+ let quality: Int?
55
56
  let interactiveOnly: Bool?
56
57
  let compact: Bool?
57
58
  let depth: Int?
@@ -7,6 +7,7 @@ extension RunnerTests {
7
7
  final class ScreenRecorder {
8
8
  private let outputPath: String
9
9
  private let fps: Int32?
10
+ private let quality: Int?
10
11
  private var effectiveFps: Int32 {
11
12
  max(1, fps ?? RunnerTests.defaultRecordingFps)
12
13
  }
@@ -25,9 +26,10 @@ extension RunnerTests {
25
26
  private var startedSession = false
26
27
  private var startError: Error?
27
28
 
28
- init(outputPath: String, fps: Int32?) {
29
+ init(outputPath: String, fps: Int32?, quality: Int?) {
29
30
  self.outputPath = outputPath
30
31
  self.fps = fps
32
+ self.quality = quality
31
33
  }
32
34
 
33
35
  func start(captureFrame: @escaping () -> RunnerImage?) throws {
@@ -48,7 +50,7 @@ extension RunnerTests {
48
50
  while Date() < bootstrapDeadline {
49
51
  if let image = captureFrame(), let cgImage = runnerCGImage(from: image) {
50
52
  bootstrapImage = image
51
- dimensions = CGSize(width: cgImage.width, height: cgImage.height)
53
+ dimensions = scaledDimensions(width: cgImage.width, height: cgImage.height)
52
54
  break
53
55
  }
54
56
  Thread.sleep(forTimeInterval: 0.05)
@@ -240,11 +242,13 @@ extension RunnerTests {
240
242
 
241
243
  CVPixelBufferLockBaseAddress(pixelBuffer, [])
242
244
  defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) }
245
+ let width = CVPixelBufferGetWidth(pixelBuffer)
246
+ let height = CVPixelBufferGetHeight(pixelBuffer)
243
247
  guard
244
248
  let context = CGContext(
245
249
  data: CVPixelBufferGetBaseAddress(pixelBuffer),
246
- width: image.width,
247
- height: image.height,
250
+ width: width,
251
+ height: height,
248
252
  bitsPerComponent: 8,
249
253
  bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),
250
254
  space: CGColorSpaceCreateDeviceRGB(),
@@ -253,8 +257,23 @@ extension RunnerTests {
253
257
  else {
254
258
  return nil
255
259
  }
256
- context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
260
+ context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
257
261
  return pixelBuffer
258
262
  }
263
+
264
+ private func scaledDimensions(width: Int, height: Int) -> CGSize {
265
+ guard let quality, quality < 10 else {
266
+ return CGSize(width: width, height: height)
267
+ }
268
+ let scale = Double(quality) / 10.0
269
+ return CGSize(
270
+ width: scaledEvenDimension(width, scale: scale),
271
+ height: scaledEvenDimension(height, scale: scale)
272
+ )
273
+ }
274
+
275
+ private func scaledEvenDimension(_ value: Int, scale: Double) -> Int {
276
+ max(2, Int((Double(value) * scale / 2.0).rounded()) * 2)
277
+ }
259
278
  }
260
279
  }
@@ -48,6 +48,8 @@ final class RunnerTests: XCTestCase {
48
48
  let tvRemoteDoublePressDelayDefault: TimeInterval = 0.0
49
49
  let minRecordingFps = 1
50
50
  let maxRecordingFps = 120
51
+ let minRecordingQuality = 5
52
+ let maxRecordingQuality = 10
51
53
  var needsPostSnapshotInteractionDelay = false
52
54
  var needsFirstInteractionDelay = false
53
55
  var activeRecording: ScreenRecorder?