@veolab/discoverylab 1.2.1 → 1.3.0
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/.mcp.json +2 -2
- package/README.md +182 -0
- package/dist/{chunk-TAODYZ52.js → chunk-3QRQEDWR.js} +510 -213
- package/dist/{chunk-L4SA5F5W.js → chunk-4L76GPRC.js} +1162 -58
- package/dist/chunk-6EGBXRDK.js +30 -0
- package/dist/{chunk-I6YD3QFM.js → chunk-FIL7IWEL.js} +5 -3
- package/dist/{chunk-4KLG6DDE.js → chunk-FNUN7EPB.js} +6 -6
- package/dist/chunk-GAKEFJ5T.js +481 -0
- package/dist/chunk-LB3RNE3O.js +109 -0
- package/dist/chunk-N6JJ2RGV.js +2680 -0
- package/dist/{chunk-XUKWS2CE.js → chunk-VRM42PML.js} +3546 -926
- package/dist/{chunk-TJ3H23LL.js → chunk-VVIOB362.js} +3 -1
- package/dist/{chunk-W3WJGYR6.js → chunk-XFVDP332.js} +8 -2
- package/dist/{chunk-QJXXHOV7.js → chunk-XKX6NBHF.js} +5 -1
- package/dist/cli.js +405 -11
- package/dist/{db-ADBEBNH6.js → db-6WLEVKUV.js} +3 -1
- package/dist/esvp-GSISVXLC.js +52 -0
- package/dist/esvp-mobile-GC7MAGMI.js +20 -0
- package/dist/index.d.ts +123 -1
- package/dist/index.html +11689 -8690
- package/dist/index.js +67 -11
- package/dist/{ocr-UTWC7537.js → ocr-QDYNCSPE.js} +1 -1
- package/dist/{playwright-R7Y5HREH.js → playwright-VZ7PXDC5.js} +2 -2
- package/dist/runtime/esvp-host-runtime/darwin-arm64/esvp-host-runtime +0 -0
- package/dist/runtime/esvp-host-runtime/manifest.json +10 -0
- package/dist/{server-3FBHBA7L.js → server-6N3KIEGP.js} +2 -1
- package/dist/server-FO3UVUZU.js +22 -0
- package/dist/{setup-27CQAX6K.js → setup-2SQC5UHJ.js} +4 -3
- package/dist/{tools-L6PKKQPY.js → tools-OCRMOQ4U.js} +63 -8
- package/package.json +36 -5
- package/dist/chunk-22OCFYHG.js +0 -6283
- package/dist/chunk-24VARQVO.js +0 -7818
- package/dist/chunk-2OGZX6C4.js +0 -588
- package/dist/chunk-2WCNIFRO.js +0 -6191
- package/dist/chunk-43U6UYV7.js +0 -590
- package/dist/chunk-4H2E3K2G.js +0 -7638
- package/dist/chunk-4MS6YW2B.js +0 -6490
- package/dist/chunk-4NNTRJOI.js +0 -7791
- package/dist/chunk-5F76VWME.js +0 -6397
- package/dist/chunk-5NEFN42O.js +0 -7791
- package/dist/chunk-63MEQ6UH.js +0 -7673
- package/dist/chunk-6H3NXFX3.js +0 -6861
- package/dist/chunk-7IDQLLBW.js +0 -311
- package/dist/chunk-7NP64TGJ.js +0 -6822
- package/dist/chunk-AATLY4KT.js +0 -6505
- package/dist/chunk-C7QUR7XX.js +0 -6397
- package/dist/chunk-CGKCE6MC.js +0 -6279
- package/dist/chunk-D25V6IWE.js +0 -6487
- package/dist/chunk-EQOZSXAT.js +0 -6822
- package/dist/chunk-FPHD7HSQ.js +0 -6812
- package/dist/chunk-GGJJUCFK.js +0 -7160
- package/dist/chunk-GLHOY3NN.js +0 -7805
- package/dist/chunk-GML5MKQA.js +0 -6398
- package/dist/chunk-GOL6FUJL.js +0 -6045
- package/dist/chunk-GSWHWEYC.js +0 -1346
- package/dist/chunk-HDKEQOF5.js +0 -7788
- package/dist/chunk-HZGSWVVS.js +0 -7111
- package/dist/chunk-IGZ5TICZ.js +0 -334
- package/dist/chunk-IRKQG33A.js +0 -7054
- package/dist/chunk-JFTBF4JR.js +0 -6040
- package/dist/chunk-JVLVBPUJ.js +0 -6180
- package/dist/chunk-JY3KC67R.js +0 -6504
- package/dist/chunk-KUFBCBNJ.js +0 -6815
- package/dist/chunk-KV7KDJ43.js +0 -7639
- package/dist/chunk-L5IJZV5F.js +0 -6822
- package/dist/chunk-MFFPQLU4.js +0 -7102
- package/dist/chunk-MJS2YKNR.js +0 -6397
- package/dist/chunk-MN6LCZHZ.js +0 -1320
- package/dist/chunk-NBAUZ7X2.js +0 -1336
- package/dist/chunk-NDBW6ELQ.js +0 -7638
- package/dist/chunk-O2HBSDI2.js +0 -6175
- package/dist/chunk-OFFIUYMG.js +0 -6341
- package/dist/chunk-OVCQGF2J.js +0 -1321
- package/dist/chunk-P4S7ZY6G.js +0 -7638
- package/dist/chunk-PBHUHSC3.js +0 -6002
- package/dist/chunk-PC4LR4ZI.js +0 -6359
- package/dist/chunk-PMTGGZ7R.js +0 -6397
- package/dist/chunk-PTXSB3UV.js +0 -497
- package/dist/chunk-PYUCY3U6.js +0 -1340
- package/dist/chunk-RDZDSOAL.js +0 -7750
- package/dist/chunk-RLW2OI2L.js +0 -6383
- package/dist/chunk-RUGHHO4K.js +0 -6395
- package/dist/chunk-SIOQVM2E.js +0 -6819
- package/dist/chunk-SR67SRIT.js +0 -1336
- package/dist/chunk-SSRXIO2V.js +0 -6822
- package/dist/chunk-SWSEKFON.js +0 -6487
- package/dist/chunk-TBG76CYG.js +0 -6395
- package/dist/chunk-V3CBINLD.js +0 -6812
- package/dist/chunk-VPYSLEGM.js +0 -6710
- package/dist/chunk-VY3BLXBW.js +0 -329
- package/dist/chunk-WTFOGVJQ.js +0 -6365
- package/dist/chunk-X64SFUT5.js +0 -6099
- package/dist/chunk-XIBF5LBD.js +0 -6395
- package/dist/chunk-Y5VDMSYC.js +0 -6701
- package/dist/chunk-YUBL36H4.js +0 -6605
- package/dist/chunk-YWVXFVSW.js +0 -6456
- package/dist/chunk-ZXZACOLD.js +0 -6822
- package/dist/db-IWIL65EX.js +0 -33
- package/dist/gridCompositor-ENKLFPWR.js +0 -409
- package/dist/playwright-A3OGSDRG.js +0 -38
- package/dist/playwright-ATDC4NYW.js +0 -38
- package/dist/playwright-E6EUFIJG.js +0 -38
- package/dist/server-2DXLKLFM.js +0 -13
- package/dist/server-2ICEWJVK.js +0 -13
- package/dist/server-2MQV3FNY.js +0 -13
- package/dist/server-2NGD7GE3.js +0 -13
- package/dist/server-2VKO76UK.js +0 -14
- package/dist/server-3BK2VFU7.js +0 -13
- package/dist/server-4LDOB3NX.js +0 -13
- package/dist/server-4YI44KDR.js +0 -13
- package/dist/server-64XMXA5P.js +0 -13
- package/dist/server-6IPHVUYT.js +0 -14
- package/dist/server-73ORHMJN.js +0 -13
- package/dist/server-73P7M3QB.js +0 -14
- package/dist/server-BPVRW5LJ.js +0 -14
- package/dist/server-BW4RKZIX.js +0 -13
- package/dist/server-CFS5SM5K.js +0 -13
- package/dist/server-DX7VYHHM.js +0 -13
- package/dist/server-F3YPX6ET.js +0 -13
- package/dist/server-FUXTR33I.js +0 -13
- package/dist/server-G2SY3DOS.js +0 -13
- package/dist/server-G32U7VOQ.js +0 -13
- package/dist/server-IOOZK4NP.js +0 -14
- package/dist/server-J52LMTBT.js +0 -13
- package/dist/server-JG7UKFGK.js +0 -14
- package/dist/server-JSCHEBOD.js +0 -13
- package/dist/server-K6KC4ZOM.js +0 -13
- package/dist/server-KJVRGWFE.js +0 -13
- package/dist/server-LCPB2L4U.js +0 -13
- package/dist/server-M7LDYKAJ.js +0 -13
- package/dist/server-MKVK6ZQQ.js +0 -13
- package/dist/server-MU52LCXT.js +0 -13
- package/dist/server-NM5CKDUU.js +0 -13
- package/dist/server-NPZN3FWO.js +0 -14
- package/dist/server-O5FIAHSY.js +0 -14
- package/dist/server-OESJUEYC.js +0 -13
- package/dist/server-ONSKQO4W.js +0 -13
- package/dist/server-P27BZXBL.js +0 -14
- package/dist/server-Q4FBWQUA.js +0 -13
- package/dist/server-RNQ7VUAL.js +0 -13
- package/dist/server-S6B5WUBT.js +0 -14
- package/dist/server-SRYNSGSP.js +0 -14
- package/dist/server-SUN3W2YK.js +0 -13
- package/dist/server-UA62LHZB.js +0 -13
- package/dist/server-UJB44EW5.js +0 -13
- package/dist/server-X3TLP6DX.js +0 -14
- package/dist/server-YT2UGEZK.js +0 -13
- package/dist/server-ZBPQ33V6.js +0 -14
- package/dist/setup-AQX4JQVR.js +0 -17
- package/dist/setup-EQTU7FI6.js +0 -17
- package/dist/tools-2KPB37GK.js +0 -178
- package/dist/tools-3H6IOWXV.js +0 -178
- package/dist/tools-3KYHPDCJ.js +0 -178
- package/dist/tools-75BAPCUM.js +0 -177
- package/dist/tools-BUVCUCRL.js +0 -178
- package/dist/tools-HDNODRS6.js +0 -178
- package/dist/tools-HP5MNY3D.js +0 -177
- package/dist/tools-N5N2IO7V.js +0 -178
- package/dist/tools-NFJEZ2FF.js +0 -177
- package/dist/tools-TLCKABUW.js +0 -178
|
@@ -1,9 +1,231 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PROJECTS_DIR
|
|
3
|
-
|
|
2
|
+
PROJECTS_DIR,
|
|
3
|
+
TEMPLATES_DIR
|
|
4
|
+
} from "./chunk-VVIOB362.js";
|
|
5
|
+
|
|
6
|
+
// src/core/templates/loader.ts
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var MANIFEST_FILE = "manifest.json";
|
|
11
|
+
var BUNDLE_DIR = "bundle";
|
|
12
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
var BUNDLED_TEMPLATES_DIR = join(__dirname, "..", "..", "templates");
|
|
14
|
+
var cachedManifest = null;
|
|
15
|
+
var cachedTemplatesDir = null;
|
|
16
|
+
var cachedAt = 0;
|
|
17
|
+
var CACHE_TTL = 3e4;
|
|
18
|
+
function resolveTemplatesDir() {
|
|
19
|
+
const localManifest = join(TEMPLATES_DIR, MANIFEST_FILE);
|
|
20
|
+
const localBundle = join(TEMPLATES_DIR, BUNDLE_DIR);
|
|
21
|
+
if (existsSync(localManifest) && existsSync(localBundle)) {
|
|
22
|
+
return TEMPLATES_DIR;
|
|
23
|
+
}
|
|
24
|
+
const bundledManifest = join(BUNDLED_TEMPLATES_DIR, MANIFEST_FILE);
|
|
25
|
+
const bundledBundle = join(BUNDLED_TEMPLATES_DIR, BUNDLE_DIR);
|
|
26
|
+
if (existsSync(bundledManifest) && existsSync(bundledBundle)) {
|
|
27
|
+
return BUNDLED_TEMPLATES_DIR;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function loadManifest() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (cachedManifest && now - cachedAt < CACHE_TTL) {
|
|
34
|
+
return cachedManifest;
|
|
35
|
+
}
|
|
36
|
+
const dir = resolveTemplatesDir();
|
|
37
|
+
if (!dir) {
|
|
38
|
+
cachedManifest = null;
|
|
39
|
+
cachedTemplatesDir = null;
|
|
40
|
+
cachedAt = now;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(join(dir, MANIFEST_FILE), "utf-8");
|
|
45
|
+
const manifest = JSON.parse(raw);
|
|
46
|
+
cachedManifest = manifest;
|
|
47
|
+
cachedTemplatesDir = dir;
|
|
48
|
+
cachedAt = now;
|
|
49
|
+
return manifest;
|
|
50
|
+
} catch {
|
|
51
|
+
cachedManifest = null;
|
|
52
|
+
cachedTemplatesDir = null;
|
|
53
|
+
cachedAt = now;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function isTemplatesInstalled() {
|
|
58
|
+
return loadManifest() !== null;
|
|
59
|
+
}
|
|
60
|
+
function getAvailableTemplates() {
|
|
61
|
+
const manifest = loadManifest();
|
|
62
|
+
return manifest?.templates ?? [];
|
|
63
|
+
}
|
|
64
|
+
function getTemplate(id) {
|
|
65
|
+
const templates = getAvailableTemplates();
|
|
66
|
+
return templates.find((t) => t.id === id) ?? null;
|
|
67
|
+
}
|
|
68
|
+
function getBundlePath() {
|
|
69
|
+
loadManifest();
|
|
70
|
+
if (!cachedTemplatesDir) return null;
|
|
71
|
+
const bundlePath = join(cachedTemplatesDir, BUNDLE_DIR);
|
|
72
|
+
if (!existsSync(bundlePath)) return null;
|
|
73
|
+
return bundlePath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/core/android/adb.ts
|
|
77
|
+
import { execSync } from "child_process";
|
|
78
|
+
import { existsSync as existsSync2 } from "fs";
|
|
79
|
+
import { homedir } from "os";
|
|
80
|
+
import { join as join2 } from "path";
|
|
81
|
+
var ADB_COMMAND_CACHE_SUCCESS_TTL_MS = 5 * 60 * 1e3;
|
|
82
|
+
var ADB_COMMAND_CACHE_FAILURE_TTL_MS = 5 * 1e3;
|
|
83
|
+
var adbCommandCache = null;
|
|
84
|
+
function quoteCommand(cmd) {
|
|
85
|
+
return cmd.includes(" ") ? `"${cmd}"` : cmd;
|
|
86
|
+
}
|
|
87
|
+
function shellQuoteArg(value) {
|
|
88
|
+
const str = String(value ?? "");
|
|
89
|
+
if (!str) return "''";
|
|
90
|
+
return `'${str.replace(/'/g, `'"'"'`)}'`;
|
|
91
|
+
}
|
|
92
|
+
function findAndroidSdkPath() {
|
|
93
|
+
const envPaths = [
|
|
94
|
+
process.env.ANDROID_HOME,
|
|
95
|
+
process.env.ANDROID_SDK_ROOT,
|
|
96
|
+
process.env.ANDROID_SDK
|
|
97
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
98
|
+
for (const envPath of envPaths) {
|
|
99
|
+
if (existsSync2(join2(envPath, "platform-tools", "adb"))) {
|
|
100
|
+
return envPath;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const home = homedir();
|
|
104
|
+
const commonPaths = [
|
|
105
|
+
join2(home, "Library", "Android", "sdk"),
|
|
106
|
+
join2(home, "Android", "Sdk"),
|
|
107
|
+
"/opt/android-sdk",
|
|
108
|
+
"/usr/local/android-sdk"
|
|
109
|
+
];
|
|
110
|
+
for (const sdkPath of commonPaths) {
|
|
111
|
+
if (existsSync2(join2(sdkPath, "platform-tools", "adb"))) {
|
|
112
|
+
return sdkPath;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function getAdbPath() {
|
|
118
|
+
const sdkPath = findAndroidSdkPath();
|
|
119
|
+
if (!sdkPath) return null;
|
|
120
|
+
const adbPath = join2(sdkPath, "platform-tools", "adb");
|
|
121
|
+
return existsSync2(adbPath) ? adbPath : null;
|
|
122
|
+
}
|
|
123
|
+
function getEmulatorPath() {
|
|
124
|
+
const sdkPath = findAndroidSdkPath();
|
|
125
|
+
if (!sdkPath) return null;
|
|
126
|
+
const emulatorPath = join2(sdkPath, "emulator", "emulator");
|
|
127
|
+
return existsSync2(emulatorPath) ? emulatorPath : null;
|
|
128
|
+
}
|
|
129
|
+
function getAdbCommand(options) {
|
|
130
|
+
const forceRefresh = options?.forceRefresh === true;
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
if (!forceRefresh && adbCommandCache) {
|
|
133
|
+
const ttlMs = adbCommandCache.value ? ADB_COMMAND_CACHE_SUCCESS_TTL_MS : ADB_COMMAND_CACHE_FAILURE_TTL_MS;
|
|
134
|
+
if (now - adbCommandCache.checkedAt < ttlMs) {
|
|
135
|
+
return adbCommandCache.value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const candidates = [
|
|
139
|
+
getAdbPath(),
|
|
140
|
+
"/opt/homebrew/bin/adb",
|
|
141
|
+
"/usr/local/bin/adb",
|
|
142
|
+
"adb"
|
|
143
|
+
].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
144
|
+
for (const candidate of candidates) {
|
|
145
|
+
try {
|
|
146
|
+
if (candidate === "adb") {
|
|
147
|
+
execSync("which adb", { stdio: "pipe", timeout: 2e3 });
|
|
148
|
+
} else if (!existsSync2(candidate)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
adbCommandCache = { value: candidate, checkedAt: Date.now() };
|
|
152
|
+
return candidate;
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
adbCommandCache = { value: null, checkedAt: Date.now() };
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function normalizeAndroidDeviceToken(value) {
|
|
160
|
+
return value.trim().replace(/^android:/i, "").replace(/\s+/g, "_").toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
function listConnectedAndroidDevices(adbCommand = getAdbCommand()) {
|
|
163
|
+
if (!adbCommand) return [];
|
|
164
|
+
try {
|
|
165
|
+
const adbOutput = execSync(`${quoteCommand(adbCommand)} devices -l`, {
|
|
166
|
+
encoding: "utf8",
|
|
167
|
+
timeout: 4e3
|
|
168
|
+
});
|
|
169
|
+
const lines = adbOutput.split("\n").slice(1);
|
|
170
|
+
const devices = [];
|
|
171
|
+
for (const rawLine of lines) {
|
|
172
|
+
const line = rawLine.trim();
|
|
173
|
+
if (!line) continue;
|
|
174
|
+
const parts = line.split(/\s+/);
|
|
175
|
+
const serial = parts[0];
|
|
176
|
+
const state = parts[1];
|
|
177
|
+
if (!serial || !state) continue;
|
|
178
|
+
const modelMatch = line.match(/model:(\S+)/);
|
|
179
|
+
const deviceMatch = line.match(/device:(\S+)/);
|
|
180
|
+
const deviceInfo = {
|
|
181
|
+
serial,
|
|
182
|
+
state,
|
|
183
|
+
model: modelMatch?.[1],
|
|
184
|
+
device: deviceMatch?.[1]
|
|
185
|
+
};
|
|
186
|
+
if (serial.startsWith("emulator-") && state === "device") {
|
|
187
|
+
try {
|
|
188
|
+
const avdNameOutput = execSync(
|
|
189
|
+
`${quoteCommand(adbCommand)} -s ${shellQuoteArg(serial)} emu avd name`,
|
|
190
|
+
{
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
timeout: 1500
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
const avdName = avdNameOutput.split("\n").map((value) => value.trim()).find((value) => value && value !== "OK");
|
|
196
|
+
if (avdName) {
|
|
197
|
+
deviceInfo.avdName = avdName;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
devices.push(deviceInfo);
|
|
203
|
+
}
|
|
204
|
+
return devices;
|
|
205
|
+
} catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function resolveAndroidDeviceSerial(deviceId, adbCommand = getAdbCommand()) {
|
|
210
|
+
const connectedDevices = listConnectedAndroidDevices(adbCommand);
|
|
211
|
+
const onlineDevices = connectedDevices.filter((device) => device.state === "device");
|
|
212
|
+
if (onlineDevices.length === 0) return null;
|
|
213
|
+
const requested = typeof deviceId === "string" ? deviceId.trim() : "";
|
|
214
|
+
if (!requested) {
|
|
215
|
+
return onlineDevices[0]?.serial || null;
|
|
216
|
+
}
|
|
217
|
+
const requestedToken = normalizeAndroidDeviceToken(requested);
|
|
218
|
+
for (const device of onlineDevices) {
|
|
219
|
+
const candidates = [device.serial, device.avdName, device.model, device.device].filter((value) => Boolean(value)).map((value) => normalizeAndroidDeviceToken(value));
|
|
220
|
+
if (candidates.includes(requestedToken)) {
|
|
221
|
+
return device.serial;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
4
226
|
|
|
5
227
|
// src/core/testing/maestro.ts
|
|
6
|
-
import { exec, execSync, spawn } from "child_process";
|
|
228
|
+
import { exec, execSync as execSync2, spawn } from "child_process";
|
|
7
229
|
import { promisify } from "util";
|
|
8
230
|
import * as fs from "fs";
|
|
9
231
|
import * as path from "path";
|
|
@@ -18,14 +240,21 @@ var maestroCommandCache = null;
|
|
|
18
240
|
var maestroCommandResolveInFlight = null;
|
|
19
241
|
var maestroDeviceListCache = null;
|
|
20
242
|
var maestroDeviceListInFlight = null;
|
|
21
|
-
function
|
|
243
|
+
function quoteCommand2(cmd) {
|
|
22
244
|
return cmd.includes(" ") ? `"${cmd}"` : cmd;
|
|
23
245
|
}
|
|
24
|
-
function
|
|
246
|
+
function shellQuoteArg2(value) {
|
|
25
247
|
const str = String(value ?? "");
|
|
26
248
|
if (!str) return "''";
|
|
27
249
|
return `'${str.replace(/'/g, `'"'"'`)}'`;
|
|
28
250
|
}
|
|
251
|
+
function getAdbCommandOrThrow() {
|
|
252
|
+
const adbCommand = getAdbCommand();
|
|
253
|
+
if (!adbCommand) {
|
|
254
|
+
throw new Error("ADB not found. Set ANDROID_HOME/ANDROID_SDK_ROOT or add adb to PATH.");
|
|
255
|
+
}
|
|
256
|
+
return adbCommand;
|
|
257
|
+
}
|
|
29
258
|
function getMaestroCommandCandidates() {
|
|
30
259
|
const candidates = [];
|
|
31
260
|
const maestroHomePath = path.join(os.homedir(), ".maestro", "bin", "maestro");
|
|
@@ -59,7 +288,7 @@ async function resolveMaestroCommand(options) {
|
|
|
59
288
|
maestroCommandResolveInFlight = (async () => {
|
|
60
289
|
for (const candidate of getMaestroCommandCandidates()) {
|
|
61
290
|
try {
|
|
62
|
-
const cmd =
|
|
291
|
+
const cmd = quoteCommand2(candidate);
|
|
63
292
|
await execAsync(`${cmd} --version`);
|
|
64
293
|
maestroCommandCache = { value: candidate, checkedAt: Date.now() };
|
|
65
294
|
return candidate;
|
|
@@ -114,7 +343,7 @@ async function getMaestroVersion() {
|
|
|
114
343
|
try {
|
|
115
344
|
const command = await resolveMaestroCommand();
|
|
116
345
|
if (!command) return null;
|
|
117
|
-
const cmd =
|
|
346
|
+
const cmd = quoteCommand2(command);
|
|
118
347
|
const { stdout } = await execAsync(`${cmd} --version`);
|
|
119
348
|
return stdout.trim();
|
|
120
349
|
} catch {
|
|
@@ -152,19 +381,22 @@ async function listMaestroDevices(options) {
|
|
|
152
381
|
} catch {
|
|
153
382
|
}
|
|
154
383
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
384
|
+
const adbCommand = getAdbCommand();
|
|
385
|
+
if (adbCommand) {
|
|
386
|
+
const { stdout: androidOutput } = await execAsync(`${quoteCommand2(adbCommand)} devices -l`);
|
|
387
|
+
const lines = androidOutput.split("\n").slice(1);
|
|
388
|
+
for (const line of lines) {
|
|
389
|
+
const match = line.match(/^(\S+)\s+device\s+(.*)$/);
|
|
390
|
+
if (match) {
|
|
391
|
+
const [, id, info] = match;
|
|
392
|
+
const modelMatch = info.match(/model:(\S+)/);
|
|
393
|
+
devices.push({
|
|
394
|
+
id,
|
|
395
|
+
name: modelMatch ? modelMatch[1] : id,
|
|
396
|
+
platform: "android",
|
|
397
|
+
status: "connected"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
168
400
|
}
|
|
169
401
|
}
|
|
170
402
|
} catch {
|
|
@@ -320,7 +552,7 @@ async function runMaestroTest(options) {
|
|
|
320
552
|
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
321
553
|
const startTime = Date.now();
|
|
322
554
|
const args = ["test", flowPath];
|
|
323
|
-
const maestroCmd =
|
|
555
|
+
const maestroCmd = quoteCommand2(command);
|
|
324
556
|
if (device) {
|
|
325
557
|
args.push("--device", device);
|
|
326
558
|
}
|
|
@@ -330,7 +562,7 @@ async function runMaestroTest(options) {
|
|
|
330
562
|
args.push("--format", "junit");
|
|
331
563
|
args.push("--output", path.join(outputDir, "report.xml"));
|
|
332
564
|
try {
|
|
333
|
-
const shellArgs = args.map(
|
|
565
|
+
const shellArgs = args.map(shellQuoteArg2).join(" ");
|
|
334
566
|
const { stdout, stderr } = await execAsync(`${maestroCmd} ${shellArgs}`, {
|
|
335
567
|
timeout,
|
|
336
568
|
env: { ...process.env, ...env }
|
|
@@ -387,7 +619,8 @@ async function runMaestroWithCapture(options, onProgress) {
|
|
|
387
619
|
if (targetDevice?.platform === "ios") {
|
|
388
620
|
recordingProcess = spawn("xcrun", ["simctl", "io", device, "recordVideo", videoPath]);
|
|
389
621
|
} else if (targetDevice?.platform === "android") {
|
|
390
|
-
|
|
622
|
+
const adbCommand = getAdbCommandOrThrow();
|
|
623
|
+
recordingProcess = spawn(adbCommand, ["-s", device, "shell", "screenrecord", "/sdcard/recording.mp4"]);
|
|
391
624
|
}
|
|
392
625
|
}
|
|
393
626
|
onProgress?.("Running Maestro test...");
|
|
@@ -399,8 +632,9 @@ async function runMaestroWithCapture(options, onProgress) {
|
|
|
399
632
|
const devices = await listMaestroDevices();
|
|
400
633
|
const targetDevice = devices.find((d) => d.id === device || d.name === device);
|
|
401
634
|
if (targetDevice?.platform === "android") {
|
|
402
|
-
|
|
403
|
-
await execAsync(
|
|
635
|
+
const adbCommand = getAdbCommandOrThrow();
|
|
636
|
+
await execAsync(`${quoteCommand2(adbCommand)} -s ${shellQuoteArg2(device)} pull /sdcard/recording.mp4 "${videoPath}"`);
|
|
637
|
+
await execAsync(`${quoteCommand2(adbCommand)} -s ${shellQuoteArg2(device)} shell rm /sdcard/recording.mp4`);
|
|
404
638
|
}
|
|
405
639
|
}
|
|
406
640
|
return {
|
|
@@ -499,6 +733,212 @@ function createNavigationTestFlow(appId, tabs) {
|
|
|
499
733
|
steps
|
|
500
734
|
};
|
|
501
735
|
}
|
|
736
|
+
function parseMaestroActionsFromYaml(yamlContent) {
|
|
737
|
+
const actions = [];
|
|
738
|
+
const lines = yamlContent.split("\n");
|
|
739
|
+
let currentAction = null;
|
|
740
|
+
let actionIndex = 0;
|
|
741
|
+
const nextId = () => `action_${String(++actionIndex).padStart(3, "0")}`;
|
|
742
|
+
const stripInlineComment = (value) => {
|
|
743
|
+
let inSingle = false;
|
|
744
|
+
let inDouble = false;
|
|
745
|
+
let escaped = false;
|
|
746
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
747
|
+
const char = value[index];
|
|
748
|
+
if (escaped) {
|
|
749
|
+
escaped = false;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (char === "\\") {
|
|
753
|
+
escaped = true;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (char === '"' && !inSingle) {
|
|
757
|
+
inDouble = !inDouble;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (char === "'" && !inDouble) {
|
|
761
|
+
inSingle = !inSingle;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (char === "#" && !inSingle && !inDouble) {
|
|
765
|
+
return value.slice(0, index).trimEnd();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return value;
|
|
769
|
+
};
|
|
770
|
+
const cleanValue = (value) => stripInlineComment(value).trim().replace(/^["']|["']$/g, "");
|
|
771
|
+
const pushAction = (action) => {
|
|
772
|
+
actions.push({
|
|
773
|
+
id: action.id || nextId(),
|
|
774
|
+
type: action.type || "tap",
|
|
775
|
+
timestamp: action.timestamp || Date.now(),
|
|
776
|
+
description: action.description || action.type || "Action",
|
|
777
|
+
x: action.x,
|
|
778
|
+
y: action.y,
|
|
779
|
+
endX: action.endX,
|
|
780
|
+
endY: action.endY,
|
|
781
|
+
text: action.text,
|
|
782
|
+
direction: action.direction,
|
|
783
|
+
seconds: action.seconds,
|
|
784
|
+
appId: action.appId,
|
|
785
|
+
duration: action.duration,
|
|
786
|
+
screenshotPath: action.screenshotPath
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
for (const line of lines) {
|
|
790
|
+
const trimmed = line.trim();
|
|
791
|
+
if (trimmed.startsWith("#") || trimmed === "" || trimmed === "---") {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (trimmed.startsWith("appId:")) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (trimmed.startsWith("- tapOn:")) {
|
|
798
|
+
const inlineValue = cleanValue(trimmed.replace("- tapOn:", ""));
|
|
799
|
+
if (inlineValue) {
|
|
800
|
+
pushAction({
|
|
801
|
+
type: "tap",
|
|
802
|
+
description: `Tap on "${inlineValue}"`,
|
|
803
|
+
text: inlineValue
|
|
804
|
+
});
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
currentAction = { type: "tap", description: "Tap" };
|
|
808
|
+
} else if (trimmed.startsWith("- tap:")) {
|
|
809
|
+
currentAction = { type: "tap", description: "Tap" };
|
|
810
|
+
} else if (trimmed.startsWith("- swipe:")) {
|
|
811
|
+
currentAction = { type: "swipe", description: "Swipe" };
|
|
812
|
+
} else if (trimmed.startsWith("- scroll:")) {
|
|
813
|
+
currentAction = { type: "scroll", description: "Scroll" };
|
|
814
|
+
} else if (trimmed.startsWith("- scrollUntilVisible:")) {
|
|
815
|
+
currentAction = { type: "scroll", description: "Scroll" };
|
|
816
|
+
} else if (trimmed.startsWith("- inputText:")) {
|
|
817
|
+
const text = cleanValue(trimmed.replace("- inputText:", ""));
|
|
818
|
+
pushAction({
|
|
819
|
+
type: "input",
|
|
820
|
+
description: `Input: ${text.slice(0, 20)}${text.length > 20 ? "..." : ""}`,
|
|
821
|
+
text
|
|
822
|
+
});
|
|
823
|
+
continue;
|
|
824
|
+
} else if (trimmed.startsWith("- launchApp:")) {
|
|
825
|
+
const appId = cleanValue(trimmed.replace("- launchApp:", ""));
|
|
826
|
+
pushAction({
|
|
827
|
+
type: "launch",
|
|
828
|
+
description: `Launch: ${appId}`,
|
|
829
|
+
text: appId,
|
|
830
|
+
appId
|
|
831
|
+
});
|
|
832
|
+
continue;
|
|
833
|
+
} else if (trimmed === "- launchApp") {
|
|
834
|
+
pushAction({
|
|
835
|
+
type: "launch",
|
|
836
|
+
description: "Launch app"
|
|
837
|
+
});
|
|
838
|
+
continue;
|
|
839
|
+
} else if (trimmed.startsWith("- assertVisible:")) {
|
|
840
|
+
const inlineValue = cleanValue(trimmed.replace("- assertVisible:", ""));
|
|
841
|
+
if (inlineValue) {
|
|
842
|
+
pushAction({
|
|
843
|
+
type: "assert",
|
|
844
|
+
description: `Assert visible "${inlineValue}"`,
|
|
845
|
+
text: inlineValue
|
|
846
|
+
});
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
currentAction = { type: "assert", description: "Assert visible" };
|
|
850
|
+
} else if (trimmed.startsWith("- extendedWaitUntil:")) {
|
|
851
|
+
currentAction = { type: "wait", description: "Wait" };
|
|
852
|
+
} else if (trimmed.startsWith("- pressKey:")) {
|
|
853
|
+
const key = cleanValue(trimmed.replace("- pressKey:", "")).toLowerCase();
|
|
854
|
+
if (key === "back") {
|
|
855
|
+
pushAction({ type: "back", description: "Back button" });
|
|
856
|
+
} else if (key === "home") {
|
|
857
|
+
pushAction({ type: "home", description: "Home button" });
|
|
858
|
+
} else {
|
|
859
|
+
pushAction({
|
|
860
|
+
type: "pressKey",
|
|
861
|
+
description: `Press: ${key}`,
|
|
862
|
+
text: key
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
} else if (trimmed.startsWith("- back")) {
|
|
867
|
+
pushAction({ type: "back", description: "Back button" });
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (currentAction && trimmed.startsWith("point:")) {
|
|
871
|
+
const point = cleanValue(trimmed.replace("point:", ""));
|
|
872
|
+
const [x, y] = point.split(",").map((n) => parseInt(n.trim()));
|
|
873
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
874
|
+
currentAction.x = x;
|
|
875
|
+
currentAction.y = y;
|
|
876
|
+
currentAction.description = `Tap at (${x}, ${y})`;
|
|
877
|
+
pushAction(currentAction);
|
|
878
|
+
currentAction = null;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (currentAction && trimmed.startsWith("text:")) {
|
|
883
|
+
const text = cleanValue(trimmed.replace("text:", ""));
|
|
884
|
+
currentAction.text = text;
|
|
885
|
+
if (currentAction.type === "assert") {
|
|
886
|
+
currentAction.description = `Assert visible "${text}"`;
|
|
887
|
+
} else {
|
|
888
|
+
currentAction.description = `Tap on "${text}"`;
|
|
889
|
+
}
|
|
890
|
+
pushAction(currentAction);
|
|
891
|
+
currentAction = null;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (currentAction && trimmed.startsWith("id:")) {
|
|
895
|
+
const id = cleanValue(trimmed.replace("id:", ""));
|
|
896
|
+
currentAction.text = id;
|
|
897
|
+
currentAction.description = `Tap on id: ${id}`;
|
|
898
|
+
pushAction(currentAction);
|
|
899
|
+
currentAction = null;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (currentAction && (currentAction.type === "swipe" || currentAction.type === "scroll") && trimmed.startsWith("direction:")) {
|
|
903
|
+
const direction = cleanValue(trimmed.replace("direction:", "")).toLowerCase();
|
|
904
|
+
currentAction.direction = direction;
|
|
905
|
+
currentAction.description = currentAction.type === "swipe" ? `Swipe ${direction}` : `Scroll ${direction}`;
|
|
906
|
+
pushAction(currentAction);
|
|
907
|
+
currentAction = null;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (currentAction && currentAction.type === "wait" && trimmed.startsWith("timeout:")) {
|
|
911
|
+
const timeoutMs = parseInt(cleanValue(trimmed.replace("timeout:", "")), 10);
|
|
912
|
+
const seconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.round(timeoutMs / 1e3)) : 3;
|
|
913
|
+
currentAction.seconds = seconds;
|
|
914
|
+
currentAction.description = `Wait ${seconds}s`;
|
|
915
|
+
pushAction(currentAction);
|
|
916
|
+
currentAction = null;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (currentAction && currentAction.type === "swipe") {
|
|
920
|
+
if (trimmed.startsWith("start:")) {
|
|
921
|
+
const point = cleanValue(trimmed.replace("start:", ""));
|
|
922
|
+
const [x, y] = point.split(",").map((n) => parseInt(n.trim()));
|
|
923
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
924
|
+
currentAction.x = x;
|
|
925
|
+
currentAction.y = y;
|
|
926
|
+
}
|
|
927
|
+
} else if (trimmed.startsWith("end:")) {
|
|
928
|
+
const point = cleanValue(trimmed.replace("end:", ""));
|
|
929
|
+
const [x, y] = point.split(",").map((n) => parseInt(n.trim()));
|
|
930
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
931
|
+
currentAction.endX = x;
|
|
932
|
+
currentAction.endY = y;
|
|
933
|
+
currentAction.description = `Swipe from (${currentAction.x}, ${currentAction.y}) to (${x}, ${y})`;
|
|
934
|
+
pushAction(currentAction);
|
|
935
|
+
currentAction = null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return actions;
|
|
941
|
+
}
|
|
502
942
|
var MaestroRecorder = class extends EventEmitter {
|
|
503
943
|
session = null;
|
|
504
944
|
adbProcess = null;
|
|
@@ -527,7 +967,7 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
527
967
|
*/
|
|
528
968
|
isMaestroInPath() {
|
|
529
969
|
try {
|
|
530
|
-
|
|
970
|
+
execSync2("which maestro", { encoding: "utf-8", timeout: 2e3 });
|
|
531
971
|
return true;
|
|
532
972
|
} catch {
|
|
533
973
|
return false;
|
|
@@ -535,7 +975,7 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
535
975
|
}
|
|
536
976
|
canRunMaestro(cmd) {
|
|
537
977
|
try {
|
|
538
|
-
|
|
978
|
+
execSync2(`"${cmd}" --version`, { encoding: "utf-8", timeout: 3e3 });
|
|
539
979
|
return true;
|
|
540
980
|
} catch {
|
|
541
981
|
return false;
|
|
@@ -652,6 +1092,7 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
652
1092
|
};
|
|
653
1093
|
this.actionCounter = 0;
|
|
654
1094
|
try {
|
|
1095
|
+
const adbCommand = platform === "android" ? getAdbCommandOrThrow() : null;
|
|
655
1096
|
const flowPath = path.join(baseDir, "test.yaml");
|
|
656
1097
|
this.session.flowPath = flowPath;
|
|
657
1098
|
const videoPath = path.join(baseDir, "recording.mp4");
|
|
@@ -729,7 +1170,7 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
729
1170
|
this.videoProcess = spawn("xcrun", ["simctl", "io", deviceId, "recordVideo", videoPath]);
|
|
730
1171
|
} else {
|
|
731
1172
|
const tempVideoPath = "/sdcard/maestro-recording.mp4";
|
|
732
|
-
this.videoProcess = spawn(
|
|
1173
|
+
this.videoProcess = spawn(adbCommand, ["-s", deviceId, "shell", "screenrecord", "--time-limit", "180", tempVideoPath]);
|
|
733
1174
|
}
|
|
734
1175
|
} else {
|
|
735
1176
|
if (useNativeRecord) {
|
|
@@ -737,7 +1178,7 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
737
1178
|
}
|
|
738
1179
|
if (platform === "android") {
|
|
739
1180
|
const tempVideoPath = "/sdcard/maestro-recording.mp4";
|
|
740
|
-
this.videoProcess = spawn(
|
|
1181
|
+
this.videoProcess = spawn(adbCommand, ["-s", deviceId, "shell", "screenrecord", "--time-limit", "180", tempVideoPath]);
|
|
741
1182
|
this.startLegacyCapture(deviceId, platform);
|
|
742
1183
|
} else {
|
|
743
1184
|
this.videoProcess = spawn("xcrun", ["simctl", "io", deviceId, "recordVideo", videoPath]);
|
|
@@ -765,12 +1206,17 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
765
1206
|
* Capture Android touch events via ADB
|
|
766
1207
|
*/
|
|
767
1208
|
startAndroidEventCapture(deviceId) {
|
|
768
|
-
|
|
1209
|
+
const adbCommand = getAdbCommand();
|
|
1210
|
+
if (!adbCommand) {
|
|
1211
|
+
console.error("[MaestroRecorder] ADB not found. Android event capture is unavailable.");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
exec(`${quoteCommand2(adbCommand)} -s ${shellQuoteArg2(deviceId)} shell wm size`, (err, stdout) => {
|
|
769
1215
|
if (err) return;
|
|
770
1216
|
const match = stdout.match(/(\d+)x(\d+)/);
|
|
771
1217
|
const screenWidth = match ? parseInt(match[1]) : 1080;
|
|
772
1218
|
const screenHeight = match ? parseInt(match[2]) : 1920;
|
|
773
|
-
this.adbProcess = spawn(
|
|
1219
|
+
this.adbProcess = spawn(adbCommand, ["-s", deviceId, "shell", "getevent", "-lt"]);
|
|
774
1220
|
let touchStartTime = 0;
|
|
775
1221
|
let touchStartX = 0;
|
|
776
1222
|
let touchStartY = 0;
|
|
@@ -910,7 +1356,8 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
910
1356
|
const screenshotPath = path.join(this.session.screenshotsDir, screenshotName);
|
|
911
1357
|
try {
|
|
912
1358
|
if (this.session.platform === "android") {
|
|
913
|
-
|
|
1359
|
+
const adbCommand = getAdbCommandOrThrow();
|
|
1360
|
+
await execAsync(`${quoteCommand2(adbCommand)} -s ${shellQuoteArg2(this.session.deviceId)} exec-out screencap -p > "${screenshotPath}"`);
|
|
914
1361
|
} else {
|
|
915
1362
|
await execAsync(`xcrun simctl io ${this.session.deviceId} screenshot "${screenshotPath}"`);
|
|
916
1363
|
}
|
|
@@ -1010,8 +1457,9 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
1010
1457
|
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
1011
1458
|
if (this.session.platform === "android" && this.session.videoPath) {
|
|
1012
1459
|
try {
|
|
1013
|
-
|
|
1014
|
-
await execAsync(
|
|
1460
|
+
const adbCommand = getAdbCommandOrThrow();
|
|
1461
|
+
await execAsync(`${quoteCommand2(adbCommand)} -s ${shellQuoteArg2(this.session.deviceId)} pull /sdcard/maestro-recording.mp4 "${this.session.videoPath}"`);
|
|
1462
|
+
await execAsync(`${quoteCommand2(adbCommand)} -s ${shellQuoteArg2(this.session.deviceId)} shell rm /sdcard/maestro-recording.mp4`);
|
|
1015
1463
|
} catch (e) {
|
|
1016
1464
|
console.error("Failed to pull video:", e);
|
|
1017
1465
|
}
|
|
@@ -1027,6 +1475,18 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
1027
1475
|
console.log("[MaestroRecorder] Generated flow YAML from recorded actions");
|
|
1028
1476
|
}
|
|
1029
1477
|
}
|
|
1478
|
+
if (this.session.actions.length === 0 && this.session.flowPath && fs.existsSync(this.session.flowPath)) {
|
|
1479
|
+
try {
|
|
1480
|
+
const fallbackYaml = await fs.promises.readFile(this.session.flowPath, "utf-8");
|
|
1481
|
+
const parsedActions = parseMaestroActionsFromYaml(fallbackYaml);
|
|
1482
|
+
if (parsedActions.length > 0) {
|
|
1483
|
+
this.session.actions = parsedActions;
|
|
1484
|
+
console.log(`[MaestroRecorder] Rehydrated ${parsedActions.length} actions from YAML for manual capture`);
|
|
1485
|
+
}
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
console.error("[MaestroRecorder] Failed to rehydrate actions from YAML:", error);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1030
1490
|
const metadataPath = path.join(this.session.screenshotsDir, "..", "session.json");
|
|
1031
1491
|
await fs.promises.writeFile(metadataPath, JSON.stringify(this.session, null, 2));
|
|
1032
1492
|
this.clearPendingSession();
|
|
@@ -1040,186 +1500,13 @@ var MaestroRecorder = class extends EventEmitter {
|
|
|
1040
1500
|
* Parse actions from Maestro YAML content
|
|
1041
1501
|
*/
|
|
1042
1502
|
parseActionsFromYaml(yamlContent) {
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const cleanValue = (value) => value.trim().replace(/^["']|["']$/g, "");
|
|
1049
|
-
const pushAction = (action) => {
|
|
1050
|
-
actions.push({
|
|
1051
|
-
id: action.id || nextId(),
|
|
1052
|
-
type: action.type || "tap",
|
|
1053
|
-
timestamp: action.timestamp || Date.now(),
|
|
1054
|
-
description: action.description || action.type || "Action",
|
|
1055
|
-
x: action.x,
|
|
1056
|
-
y: action.y,
|
|
1057
|
-
endX: action.endX,
|
|
1058
|
-
endY: action.endY,
|
|
1059
|
-
text: action.text,
|
|
1060
|
-
direction: action.direction,
|
|
1061
|
-
seconds: action.seconds,
|
|
1062
|
-
appId: action.appId,
|
|
1063
|
-
duration: action.duration,
|
|
1064
|
-
screenshotPath: action.screenshotPath
|
|
1065
|
-
});
|
|
1066
|
-
};
|
|
1067
|
-
for (const line of lines) {
|
|
1068
|
-
const trimmed = line.trim();
|
|
1069
|
-
if (trimmed.startsWith("#") || trimmed === "" || trimmed === "---") {
|
|
1070
|
-
continue;
|
|
1071
|
-
}
|
|
1072
|
-
if (trimmed.startsWith("appId:")) {
|
|
1073
|
-
const parsedAppId = cleanValue(trimmed.replace("appId:", ""));
|
|
1074
|
-
if (this.session && parsedAppId && !this.session.appId) {
|
|
1075
|
-
this.session.appId = parsedAppId;
|
|
1076
|
-
}
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
if (trimmed.startsWith("- tapOn:")) {
|
|
1080
|
-
const inlineValue = cleanValue(trimmed.replace("- tapOn:", ""));
|
|
1081
|
-
if (inlineValue) {
|
|
1082
|
-
pushAction({
|
|
1083
|
-
type: "tap",
|
|
1084
|
-
description: `Tap on "${inlineValue}"`,
|
|
1085
|
-
text: inlineValue
|
|
1086
|
-
});
|
|
1087
|
-
continue;
|
|
1088
|
-
}
|
|
1089
|
-
currentAction = { type: "tap", description: "Tap" };
|
|
1090
|
-
} else if (trimmed.startsWith("- tap:")) {
|
|
1091
|
-
currentAction = { type: "tap", description: "Tap" };
|
|
1092
|
-
} else if (trimmed.startsWith("- swipe:")) {
|
|
1093
|
-
currentAction = { type: "swipe", description: "Swipe" };
|
|
1094
|
-
} else if (trimmed.startsWith("- scroll:")) {
|
|
1095
|
-
currentAction = { type: "scroll", description: "Scroll" };
|
|
1096
|
-
} else if (trimmed.startsWith("- scrollUntilVisible:")) {
|
|
1097
|
-
currentAction = { type: "scroll", description: "Scroll" };
|
|
1098
|
-
} else if (trimmed.startsWith("- inputText:")) {
|
|
1099
|
-
const text = cleanValue(trimmed.replace("- inputText:", ""));
|
|
1100
|
-
pushAction({
|
|
1101
|
-
type: "input",
|
|
1102
|
-
description: `Input: ${text.slice(0, 20)}${text.length > 20 ? "..." : ""}`,
|
|
1103
|
-
text
|
|
1104
|
-
});
|
|
1105
|
-
continue;
|
|
1106
|
-
} else if (trimmed.startsWith("- launchApp:")) {
|
|
1107
|
-
const appId = cleanValue(trimmed.replace("- launchApp:", ""));
|
|
1108
|
-
pushAction({
|
|
1109
|
-
type: "launch",
|
|
1110
|
-
description: `Launch: ${appId}`,
|
|
1111
|
-
text: appId,
|
|
1112
|
-
appId
|
|
1113
|
-
});
|
|
1114
|
-
continue;
|
|
1115
|
-
} else if (trimmed === "- launchApp") {
|
|
1116
|
-
pushAction({
|
|
1117
|
-
type: "launch",
|
|
1118
|
-
description: "Launch app"
|
|
1119
|
-
});
|
|
1120
|
-
continue;
|
|
1121
|
-
} else if (trimmed.startsWith("- assertVisible:")) {
|
|
1122
|
-
const inlineValue = cleanValue(trimmed.replace("- assertVisible:", ""));
|
|
1123
|
-
if (inlineValue) {
|
|
1124
|
-
pushAction({
|
|
1125
|
-
type: "assert",
|
|
1126
|
-
description: `Assert visible "${inlineValue}"`,
|
|
1127
|
-
text: inlineValue
|
|
1128
|
-
});
|
|
1129
|
-
continue;
|
|
1130
|
-
}
|
|
1131
|
-
currentAction = { type: "assert", description: "Assert visible" };
|
|
1132
|
-
} else if (trimmed.startsWith("- extendedWaitUntil:")) {
|
|
1133
|
-
currentAction = { type: "wait", description: "Wait" };
|
|
1134
|
-
} else if (trimmed.startsWith("- pressKey:")) {
|
|
1135
|
-
const key = cleanValue(trimmed.replace("- pressKey:", "")).toLowerCase();
|
|
1136
|
-
if (key === "back") {
|
|
1137
|
-
pushAction({ type: "back", description: "Back button" });
|
|
1138
|
-
} else if (key === "home") {
|
|
1139
|
-
pushAction({ type: "home", description: "Home button" });
|
|
1140
|
-
} else {
|
|
1141
|
-
pushAction({
|
|
1142
|
-
type: "pressKey",
|
|
1143
|
-
description: `Press: ${key}`,
|
|
1144
|
-
text: key
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
continue;
|
|
1148
|
-
} else if (trimmed.startsWith("- back")) {
|
|
1149
|
-
pushAction({ type: "back", description: "Back button" });
|
|
1150
|
-
continue;
|
|
1151
|
-
}
|
|
1152
|
-
if (currentAction && trimmed.startsWith("point:")) {
|
|
1153
|
-
const point = cleanValue(trimmed.replace("point:", ""));
|
|
1154
|
-
const [x, y] = point.split(",").map((n) => parseInt(n.trim()));
|
|
1155
|
-
if (!isNaN(x) && !isNaN(y)) {
|
|
1156
|
-
currentAction.x = x;
|
|
1157
|
-
currentAction.y = y;
|
|
1158
|
-
currentAction.description = `Tap at (${x}, ${y})`;
|
|
1159
|
-
pushAction(currentAction);
|
|
1160
|
-
currentAction = null;
|
|
1161
|
-
continue;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
if (currentAction && trimmed.startsWith("text:")) {
|
|
1165
|
-
const text = cleanValue(trimmed.replace("text:", ""));
|
|
1166
|
-
currentAction.text = text;
|
|
1167
|
-
if (currentAction.type === "assert") {
|
|
1168
|
-
currentAction.description = `Assert visible "${text}"`;
|
|
1169
|
-
} else {
|
|
1170
|
-
currentAction.description = `Tap on "${text}"`;
|
|
1171
|
-
}
|
|
1172
|
-
pushAction(currentAction);
|
|
1173
|
-
currentAction = null;
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
if (currentAction && trimmed.startsWith("id:")) {
|
|
1177
|
-
const id = cleanValue(trimmed.replace("id:", ""));
|
|
1178
|
-
currentAction.text = id;
|
|
1179
|
-
currentAction.description = `Tap on id: ${id}`;
|
|
1180
|
-
pushAction(currentAction);
|
|
1181
|
-
currentAction = null;
|
|
1182
|
-
continue;
|
|
1183
|
-
}
|
|
1184
|
-
if (currentAction && (currentAction.type === "swipe" || currentAction.type === "scroll") && trimmed.startsWith("direction:")) {
|
|
1185
|
-
const direction = cleanValue(trimmed.replace("direction:", "")).toLowerCase();
|
|
1186
|
-
currentAction.direction = direction;
|
|
1187
|
-
currentAction.description = currentAction.type === "swipe" ? `Swipe ${direction}` : `Scroll ${direction}`;
|
|
1188
|
-
pushAction(currentAction);
|
|
1189
|
-
currentAction = null;
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1192
|
-
if (currentAction && currentAction.type === "wait" && trimmed.startsWith("timeout:")) {
|
|
1193
|
-
const timeoutMs = parseInt(cleanValue(trimmed.replace("timeout:", "")), 10);
|
|
1194
|
-
const seconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.round(timeoutMs / 1e3)) : 3;
|
|
1195
|
-
currentAction.seconds = seconds;
|
|
1196
|
-
currentAction.description = `Wait ${seconds}s`;
|
|
1197
|
-
pushAction(currentAction);
|
|
1198
|
-
currentAction = null;
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
if (currentAction && currentAction.type === "swipe") {
|
|
1202
|
-
if (trimmed.startsWith("start:")) {
|
|
1203
|
-
const point = cleanValue(trimmed.replace("start:", ""));
|
|
1204
|
-
const [x, y] = point.split(",").map((n) => parseInt(n.trim()));
|
|
1205
|
-
if (!isNaN(x) && !isNaN(y)) {
|
|
1206
|
-
currentAction.x = x;
|
|
1207
|
-
currentAction.y = y;
|
|
1208
|
-
}
|
|
1209
|
-
} else if (trimmed.startsWith("end:")) {
|
|
1210
|
-
const point = cleanValue(trimmed.replace("end:", ""));
|
|
1211
|
-
const [x, y] = point.split(",").map((n) => parseInt(n.trim()));
|
|
1212
|
-
if (!isNaN(x) && !isNaN(y)) {
|
|
1213
|
-
currentAction.endX = x;
|
|
1214
|
-
currentAction.endY = y;
|
|
1215
|
-
currentAction.description = `Swipe from (${currentAction.x}, ${currentAction.y}) to (${x}, ${y})`;
|
|
1216
|
-
pushAction(currentAction);
|
|
1217
|
-
currentAction = null;
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1503
|
+
const parsedActions = parseMaestroActionsFromYaml(yamlContent);
|
|
1504
|
+
const appIdMatch = yamlContent.match(/^\s*appId:\s*([^\n#]+)/m);
|
|
1505
|
+
const parsedAppId = appIdMatch?.[1]?.trim().replace(/^["']|["']$/g, "") || "";
|
|
1506
|
+
if (this.session && parsedAppId && !this.session.appId) {
|
|
1507
|
+
this.session.appId = parsedAppId;
|
|
1221
1508
|
}
|
|
1222
|
-
return
|
|
1509
|
+
return parsedActions;
|
|
1223
1510
|
}
|
|
1224
1511
|
mergeParsedActionsWithRecorded(parsedActions, recordedActions) {
|
|
1225
1512
|
if (parsedActions.length === 0) return recordedActions;
|
|
@@ -1376,6 +1663,15 @@ function getMaestroRecorder() {
|
|
|
1376
1663
|
}
|
|
1377
1664
|
|
|
1378
1665
|
export {
|
|
1666
|
+
isTemplatesInstalled,
|
|
1667
|
+
getAvailableTemplates,
|
|
1668
|
+
getTemplate,
|
|
1669
|
+
getBundlePath,
|
|
1670
|
+
findAndroidSdkPath,
|
|
1671
|
+
getEmulatorPath,
|
|
1672
|
+
getAdbCommand,
|
|
1673
|
+
listConnectedAndroidDevices,
|
|
1674
|
+
resolveAndroidDeviceSerial,
|
|
1379
1675
|
isMaestroInstalled,
|
|
1380
1676
|
isIdbInstalled,
|
|
1381
1677
|
tapViaIdb,
|
|
@@ -1389,5 +1685,6 @@ export {
|
|
|
1389
1685
|
createLoginFlow,
|
|
1390
1686
|
createOnboardingFlow,
|
|
1391
1687
|
createNavigationTestFlow,
|
|
1688
|
+
parseMaestroActionsFromYaml,
|
|
1392
1689
|
getMaestroRecorder
|
|
1393
1690
|
};
|