openuispec 0.2.8 → 0.2.10
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.
- package/README.md +50 -385
- package/cli/index.ts +77 -1
- package/cli/init.ts +9 -4
- package/docs/cli.md +160 -0
- package/docs/file-formats.md +84 -0
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +5 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +45 -6
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +89 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +13 -6
- package/mcp-server/index.ts +94 -3
- package/mcp-server/screenshot-android.ts +199 -65
- package/mcp-server/screenshot-ios.ts +242 -30
- package/mcp-server/screenshot.ts +80 -0
- package/package.json +1 -1
package/mcp-server/index.ts
CHANGED
|
@@ -25,9 +25,9 @@ import { loadTargetDrift } from "../drift/index.js";
|
|
|
25
25
|
import { readFileSync as fsReadFileSync, existsSync, readdirSync } from "node:fs";
|
|
26
26
|
import { relative, resolve } from "node:path";
|
|
27
27
|
import YAML from "yaml";
|
|
28
|
-
import { takeScreenshot } from "./screenshot.js";
|
|
29
|
-
import { takeAndroidScreenshot } from "./screenshot-android.js";
|
|
30
|
-
import { takeIOSScreenshot } from "./screenshot-ios.js";
|
|
28
|
+
import { takeScreenshot, takeScreenshotBatch } from "./screenshot.js";
|
|
29
|
+
import { takeAndroidScreenshot, takeAndroidScreenshotBatch } from "./screenshot-android.js";
|
|
30
|
+
import { takeIOSScreenshot, takeIOSScreenshotBatch } from "./screenshot-ios.js";
|
|
31
31
|
|
|
32
32
|
// ── resolve project cwd ──────────────────────────────────────────────
|
|
33
33
|
|
|
@@ -777,6 +777,97 @@ server.registerTool(
|
|
|
777
777
|
}
|
|
778
778
|
);
|
|
779
779
|
|
|
780
|
+
// ── tool: openuispec_screenshot_web_batch ──────────────────────────────
|
|
781
|
+
|
|
782
|
+
const webBatchCaptureSchema = z.object({
|
|
783
|
+
screen: z.string().describe("Screen name for metadata and filename"),
|
|
784
|
+
route: z.string().describe("Route path (e.g. '/home', '/settings')"),
|
|
785
|
+
selector: z.string().optional().describe("CSS selector to screenshot a specific element"),
|
|
786
|
+
full_page: z.boolean().optional().describe("Capture full scrollable page"),
|
|
787
|
+
wait_for: z.number().optional().describe("Per-capture wait time in ms"),
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
server.registerTool(
|
|
791
|
+
"openuispec_screenshot_web_batch",
|
|
792
|
+
{
|
|
793
|
+
description: "Take multiple web screenshots in a single server session. Starts the dev server once, then captures all routes in sequence. Much faster than calling screenshot for each route individually.",
|
|
794
|
+
inputSchema: {
|
|
795
|
+
captures: z.array(webBatchCaptureSchema).describe("Array of captures — each with screen name and route"),
|
|
796
|
+
viewport: z.object({ width: z.number().default(1280), height: z.number().default(800) }).optional().describe("Viewport dimensions for all captures"),
|
|
797
|
+
theme: z.enum(["light", "dark"]).optional().describe("Force color scheme for all captures"),
|
|
798
|
+
output_dir: z.string().optional().describe("Directory to save all PNGs (relative to web app root)"),
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
async ({ captures, viewport, theme, output_dir }) => {
|
|
802
|
+
try {
|
|
803
|
+
return await takeScreenshotBatch(projectCwd, { captures, viewport, theme, output_dir });
|
|
804
|
+
} catch (err) {
|
|
805
|
+
return toolError(err);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
// ── tool: openuispec_screenshot_android_batch ─────────────────────────
|
|
811
|
+
|
|
812
|
+
const androidBatchCaptureSchema = z.object({
|
|
813
|
+
screen: z.string().describe("Screen name for metadata and filename"),
|
|
814
|
+
route: z.string().optional().describe("Deep link URI to launch"),
|
|
815
|
+
nav: z.array(z.string()).optional().describe("UI tap steps after launch"),
|
|
816
|
+
wait_for: z.number().optional().describe("Per-capture wait time in ms"),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
server.registerTool(
|
|
820
|
+
"openuispec_screenshot_android_batch",
|
|
821
|
+
{
|
|
822
|
+
description: "Take multiple Android screenshots in a single build+install cycle. Builds the APK once, installs once, then captures each screen in sequence via deep links or UI navigation. Much faster than calling screenshot_android for each screen individually.",
|
|
823
|
+
inputSchema: {
|
|
824
|
+
captures: z.array(androidBatchCaptureSchema).describe("Array of captures — each with screen name and optional route/nav"),
|
|
825
|
+
theme: z.enum(["light", "dark"]).optional().describe("Force light or dark mode for all captures"),
|
|
826
|
+
output_dir: z.string().optional().describe("Directory to save all PNGs (relative to Android project root)"),
|
|
827
|
+
project_dir: z.string().optional().describe("Direct path to Android project root"),
|
|
828
|
+
module: z.string().optional().describe("App module name (default: auto-detect)"),
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
async ({ captures, theme, output_dir, project_dir, module }) => {
|
|
832
|
+
try {
|
|
833
|
+
return await takeAndroidScreenshotBatch(projectCwd, { captures, theme, output_dir, project_dir, module });
|
|
834
|
+
} catch (err) {
|
|
835
|
+
return toolError(err);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// ── tool: openuispec_screenshot_ios_batch ──────────────────────────────
|
|
841
|
+
|
|
842
|
+
const iosBatchCaptureSchema = z.object({
|
|
843
|
+
screen: z.string().describe("Screen name for metadata and filename"),
|
|
844
|
+
nav: z.array(z.string()).optional().describe("UI tap steps after launch"),
|
|
845
|
+
wait_for: z.number().optional().describe("Per-capture wait time in ms"),
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
server.registerTool(
|
|
849
|
+
"openuispec_screenshot_ios_batch",
|
|
850
|
+
{
|
|
851
|
+
description: "Take multiple iOS screenshots in a single build+install cycle. Builds the app once, then captures each screen — no-nav screens via simctl, nav screens batched into a single XCUITest run. Much faster than calling screenshot_ios for each screen individually.",
|
|
852
|
+
inputSchema: {
|
|
853
|
+
captures: z.array(iosBatchCaptureSchema).describe("Array of captures — each with screen name and optional nav steps"),
|
|
854
|
+
device: z.string().optional().describe("Simulator device name"),
|
|
855
|
+
theme: z.enum(["light", "dark"]).optional().describe("Force light or dark appearance for all captures"),
|
|
856
|
+
output_dir: z.string().optional().describe("Directory to save all PNGs (relative to iOS project root)"),
|
|
857
|
+
project_dir: z.string().optional().describe("Direct path to iOS project root"),
|
|
858
|
+
scheme: z.string().optional().describe("Xcode scheme name"),
|
|
859
|
+
bundle_id: z.string().optional().describe("App bundle identifier"),
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
async ({ captures, device, theme, output_dir, project_dir, scheme, bundle_id }) => {
|
|
863
|
+
try {
|
|
864
|
+
return await takeIOSScreenshotBatch(projectCwd, { captures, device, theme, output_dir, project_dir, scheme, bundle_id });
|
|
865
|
+
} catch (err) {
|
|
866
|
+
return toolError(err);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
|
|
780
871
|
// ── start server ─────────────────────────────────────────────────────
|
|
781
872
|
|
|
782
873
|
export async function startMcpServer() {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./screenshot-shared.js";
|
|
18
18
|
|
|
19
19
|
const exec = promisify(execCb);
|
|
20
|
+
const androidScreenshotQueues = new Map<string, Promise<void>>();
|
|
20
21
|
|
|
21
22
|
// ── types ───────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -31,6 +32,21 @@ export interface AndroidScreenshotOptions {
|
|
|
31
32
|
module?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
export interface AndroidBatchCapture {
|
|
36
|
+
screen: string;
|
|
37
|
+
route?: string;
|
|
38
|
+
nav?: string[];
|
|
39
|
+
wait_for?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AndroidScreenshotBatchOptions {
|
|
43
|
+
captures: AndroidBatchCapture[];
|
|
44
|
+
theme?: "light" | "dark";
|
|
45
|
+
output_dir?: string;
|
|
46
|
+
project_dir?: string;
|
|
47
|
+
module?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
// ── constants ───────────────────────────────────────────────────────
|
|
35
51
|
|
|
36
52
|
const ADB_SCREENSHOT_PATH = "/sdcard/openuispec_screenshot.png";
|
|
@@ -213,18 +229,14 @@ export async function adbExec(adb: string, serial: string, args: string): Promis
|
|
|
213
229
|
// ── emulator storage cleanup ─────────────────────────────────────────
|
|
214
230
|
|
|
215
231
|
export async function cleanEmulatorStorage(adb: string, serial: string): Promise<void> {
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await adbShell(adb, serial,
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
// Clear temp files
|
|
226
|
-
await adbShell(adb, serial, `rm -rf /data/local/tmp/*.apk`);
|
|
227
|
-
} catch { /* ignore */ }
|
|
232
|
+
const cmds = [
|
|
233
|
+
`pm trim-caches 2G`, // aggressively trim package cache
|
|
234
|
+
`rm -rf /data/local/tmp/*.apk`, // leftover APKs from previous installs
|
|
235
|
+
`rm -f /sdcard/openuispec_screenshot.png /sdcard/ui_dump.xml /sdcard/screenshot.png`,
|
|
236
|
+
];
|
|
237
|
+
for (const cmd of cmds) {
|
|
238
|
+
try { await adbShell(adb, serial, cmd); } catch { /* ignore */ }
|
|
239
|
+
}
|
|
228
240
|
}
|
|
229
241
|
|
|
230
242
|
// ── build APK ───────────────────────────────────────────────────────
|
|
@@ -269,23 +281,41 @@ export async function installAndLaunch(
|
|
|
269
281
|
appInfo: AppInfo,
|
|
270
282
|
route?: string,
|
|
271
283
|
): Promise<void> {
|
|
272
|
-
//
|
|
273
|
-
await adbExec(adb, serial, `install -r "${apkPath}"`);
|
|
274
|
-
|
|
275
|
-
// Force-stop and clear saved navigation state
|
|
284
|
+
// Force-stop and uninstall to free storage + wipe saved nav state
|
|
276
285
|
await adbShell(adb, serial, `am force-stop ${appInfo.applicationId}`);
|
|
286
|
+
try { await adbShell(adb, serial, `pm uninstall ${appInfo.applicationId}`); } catch { /* not installed */ }
|
|
277
287
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
const clearFlags = `-f 0x10008000`;
|
|
288
|
+
// Install fresh (not -r replace, since we uninstalled)
|
|
289
|
+
await adbExec(adb, serial, `install "${apkPath}"`);
|
|
281
290
|
|
|
282
291
|
if (route) {
|
|
283
292
|
await adbShell(adb, serial,
|
|
284
|
-
`am start -W -a android.intent.action.VIEW -d
|
|
293
|
+
`am start -W -a android.intent.action.VIEW -d '${route}' ` +
|
|
285
294
|
`${appInfo.applicationId}/${appInfo.launchActivity}`);
|
|
286
295
|
} else {
|
|
287
296
|
await adbShell(adb, serial,
|
|
288
|
-
`am start -W
|
|
297
|
+
`am start -W -n ${appInfo.applicationId}/${appInfo.launchActivity}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function launchInstalledApp(
|
|
302
|
+
adb: string,
|
|
303
|
+
serial: string,
|
|
304
|
+
appInfo: AppInfo,
|
|
305
|
+
route?: string,
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
await adbShell(adb, serial, `am force-stop ${appInfo.applicationId}`);
|
|
308
|
+
// Clear saved nav state so deep links route correctly
|
|
309
|
+
try { await adbShell(adb, serial, `pm clear ${appInfo.applicationId}`); } catch { /* ignore */ }
|
|
310
|
+
if (route) {
|
|
311
|
+
await adbShell(
|
|
312
|
+
adb,
|
|
313
|
+
serial,
|
|
314
|
+
`am start -W -a android.intent.action.VIEW -d '${route}' ` +
|
|
315
|
+
`${appInfo.applicationId}/${appInfo.launchActivity}`,
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
await adbShell(adb, serial, `am start -W -n ${appInfo.applicationId}/${appInfo.launchActivity}`);
|
|
289
319
|
}
|
|
290
320
|
}
|
|
291
321
|
|
|
@@ -365,9 +395,12 @@ export async function captureScreenshot(
|
|
|
365
395
|
serial: string,
|
|
366
396
|
localPath: string,
|
|
367
397
|
): Promise<void> {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
398
|
+
try {
|
|
399
|
+
await exec(`${adb} -s ${serial} exec-out screencap -p > "${localPath}"`, { timeout: 60_000, shell: "/bin/bash" });
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
const output = ((err.stderr ?? "") + "\n" + (err.stdout ?? "")).trim();
|
|
402
|
+
throw new Error(`Android screenshot capture failed${output ? `:\n${output}` : "."}`);
|
|
403
|
+
}
|
|
371
404
|
}
|
|
372
405
|
|
|
373
406
|
// ── wait for app ready ──────────────────────────────────────────────
|
|
@@ -402,6 +435,47 @@ async function waitForAppReady(
|
|
|
402
435
|
await new Promise(r => setTimeout(r, waitMs));
|
|
403
436
|
}
|
|
404
437
|
|
|
438
|
+
async function takeSingleAndroidCapture(
|
|
439
|
+
adb: string,
|
|
440
|
+
serial: string,
|
|
441
|
+
androidDir: string,
|
|
442
|
+
appInfo: AppInfo,
|
|
443
|
+
capture: AndroidBatchCapture,
|
|
444
|
+
theme: "light" | "dark" | undefined,
|
|
445
|
+
defaultOutputDir: string | undefined,
|
|
446
|
+
): Promise<{ screen: string; path: string; data: string }> {
|
|
447
|
+
await launchInstalledApp(adb, serial, appInfo, capture.route);
|
|
448
|
+
await waitForAppReady(adb, serial, appInfo.applicationId, capture.wait_for ?? 3000);
|
|
449
|
+
|
|
450
|
+
if (capture.nav && capture.nav.length > 0) {
|
|
451
|
+
await navigateByTaps(adb, serial, capture.nav);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const themeLabel = theme ?? "default";
|
|
455
|
+
const filename = `${capture.screen}_${themeLabel}.png`;
|
|
456
|
+
const tmpPath = join(androidDir, `.openuispec-screenshot-${capture.screen}.png`);
|
|
457
|
+
await captureScreenshot(adb, serial, tmpPath);
|
|
458
|
+
|
|
459
|
+
let savedPath = filename;
|
|
460
|
+
if (defaultOutputDir) {
|
|
461
|
+
const outDir = resolve(androidDir, defaultOutputDir);
|
|
462
|
+
mkdirSync(outDir, { recursive: true });
|
|
463
|
+
savedPath = join(outDir, filename);
|
|
464
|
+
copyFileSync(tmpPath, savedPath);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const data = readFileSync(tmpPath).toString("base64");
|
|
469
|
+
return {
|
|
470
|
+
screen: capture.screen,
|
|
471
|
+
path: savedPath,
|
|
472
|
+
data,
|
|
473
|
+
};
|
|
474
|
+
} finally {
|
|
475
|
+
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
405
479
|
// ── main entry point ────────────────────────────────────────────────
|
|
406
480
|
|
|
407
481
|
export async function takeAndroidScreenshot(
|
|
@@ -427,61 +501,121 @@ export async function takeAndroidScreenshot(
|
|
|
427
501
|
const adb = findAdb();
|
|
428
502
|
const serial = await getConnectedEmulator(adb);
|
|
429
503
|
|
|
430
|
-
|
|
431
|
-
|
|
504
|
+
const previousRun = androidScreenshotQueues.get(serial) ?? Promise.resolve();
|
|
505
|
+
let releaseQueue: (() => void) | undefined;
|
|
506
|
+
const currentRun = new Promise<void>((resolve) => {
|
|
507
|
+
releaseQueue = resolve;
|
|
508
|
+
});
|
|
509
|
+
androidScreenshotQueues.set(serial, previousRun.then(() => currentRun));
|
|
432
510
|
|
|
433
|
-
|
|
434
|
-
const apkPath = await buildApk(androidDir, appInfo.moduleName);
|
|
511
|
+
await previousRun;
|
|
435
512
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
await
|
|
439
|
-
}
|
|
513
|
+
try {
|
|
514
|
+
// 3. Free emulator storage before build/install
|
|
515
|
+
await cleanEmulatorStorage(adb, serial);
|
|
440
516
|
|
|
441
|
-
|
|
442
|
-
|
|
517
|
+
// 4. Build APK
|
|
518
|
+
const apkPath = await buildApk(androidDir, appInfo.moduleName);
|
|
443
519
|
|
|
444
|
-
|
|
445
|
-
|
|
520
|
+
// 5. Set theme if requested
|
|
521
|
+
if (theme) {
|
|
522
|
+
await setTheme(adb, serial, theme);
|
|
523
|
+
}
|
|
446
524
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
525
|
+
// 6. Install fresh once, then capture
|
|
526
|
+
await installAndLaunch(adb, serial, apkPath, appInfo, route);
|
|
527
|
+
|
|
528
|
+
const snapshot = await takeSingleAndroidCapture(
|
|
529
|
+
adb,
|
|
530
|
+
serial,
|
|
531
|
+
androidDir,
|
|
532
|
+
appInfo,
|
|
533
|
+
{ screen: screen ?? "main", route, nav, wait_for },
|
|
534
|
+
theme,
|
|
535
|
+
output_dir,
|
|
536
|
+
);
|
|
451
537
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
538
|
+
return buildScreenshotResponse([snapshot], (s) => ({
|
|
539
|
+
screen: s.screen,
|
|
540
|
+
path: snapshot.path ?? null,
|
|
541
|
+
emulator: serial,
|
|
542
|
+
theme: theme ?? "default",
|
|
543
|
+
applicationId: appInfo.applicationId,
|
|
544
|
+
}));
|
|
545
|
+
} finally {
|
|
546
|
+
releaseQueue?.();
|
|
547
|
+
if (androidScreenshotQueues.get(serial) === currentRun) {
|
|
548
|
+
androidScreenshotQueues.delete(serial);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
458
552
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
553
|
+
export async function takeAndroidScreenshotBatch(
|
|
554
|
+
projectCwd: string,
|
|
555
|
+
options: AndroidScreenshotBatchOptions,
|
|
556
|
+
): Promise<ScreenshotResult> {
|
|
557
|
+
const { captures, theme, output_dir, project_dir, module } = options;
|
|
558
|
+
if (captures.length === 0) {
|
|
559
|
+
return {
|
|
560
|
+
content: [{ type: "text", text: "No Android captures specified." }],
|
|
561
|
+
isError: true,
|
|
562
|
+
};
|
|
466
563
|
}
|
|
467
564
|
|
|
468
|
-
|
|
565
|
+
const androidDir = findAndroidAppDir(projectCwd, project_dir);
|
|
566
|
+
const appInfo = extractAppInfo(androidDir, module);
|
|
567
|
+
const adb = findAdb();
|
|
568
|
+
const serial = await getConnectedEmulator(adb);
|
|
569
|
+
|
|
570
|
+
const previousRun = androidScreenshotQueues.get(serial) ?? Promise.resolve();
|
|
571
|
+
let releaseQueue: (() => void) | undefined;
|
|
572
|
+
const currentRun = new Promise<void>((resolve) => {
|
|
573
|
+
releaseQueue = resolve;
|
|
574
|
+
});
|
|
575
|
+
androidScreenshotQueues.set(serial, previousRun.then(() => currentRun));
|
|
576
|
+
|
|
577
|
+
await previousRun;
|
|
578
|
+
|
|
469
579
|
try {
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
screen: screenLabel,
|
|
473
|
-
path: savedPath ?? filename,
|
|
474
|
-
data,
|
|
475
|
-
}];
|
|
580
|
+
await cleanEmulatorStorage(adb, serial);
|
|
581
|
+
const apkPath = await buildApk(androidDir, appInfo.moduleName);
|
|
476
582
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
583
|
+
if (theme) {
|
|
584
|
+
await setTheme(adb, serial, theme);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await installAndLaunch(adb, serial, apkPath, appInfo);
|
|
588
|
+
|
|
589
|
+
// Pre-create output dir once
|
|
590
|
+
if (output_dir) mkdirSync(resolve(androidDir, output_dir), { recursive: true });
|
|
591
|
+
|
|
592
|
+
const snapshots = [];
|
|
593
|
+
for (let index = 0; index < captures.length; index += 1) {
|
|
594
|
+
const capture = captures[index];
|
|
595
|
+
snapshots.push(
|
|
596
|
+
await takeSingleAndroidCapture(
|
|
597
|
+
adb,
|
|
598
|
+
serial,
|
|
599
|
+
androidDir,
|
|
600
|
+
appInfo,
|
|
601
|
+
capture,
|
|
602
|
+
theme,
|
|
603
|
+
output_dir,
|
|
604
|
+
),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return buildScreenshotResponse(snapshots, (snapshot) => ({
|
|
609
|
+
screen: snapshot.screen,
|
|
610
|
+
path: snapshot.path,
|
|
480
611
|
emulator: serial,
|
|
481
|
-
theme:
|
|
612
|
+
theme: theme ?? "default",
|
|
482
613
|
applicationId: appInfo.applicationId,
|
|
483
614
|
}));
|
|
484
615
|
} finally {
|
|
485
|
-
|
|
616
|
+
releaseQueue?.();
|
|
617
|
+
if (androidScreenshotQueues.get(serial) === currentRun) {
|
|
618
|
+
androidScreenshotQueues.delete(serial);
|
|
619
|
+
}
|
|
486
620
|
}
|
|
487
621
|
}
|