@deeeed/metamask-harness 0.26.5 → 0.28.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/CHANGELOG.md +17 -0
- package/adapters/mobile/open-device.sh +16 -4
- package/dist/adapters/mobile/prepare.js +114 -4
- package/dist/adapters/mobile/runtime-decision.js +4 -1
- package/dist/adapters.js +8 -6
- package/dist/cli-commands.js +1 -1
- package/dist/command-contract.js +1 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/device-target.js +4 -1
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/mobile-device-view.js +4 -1
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/devices.js +4 -1
- package/dist/doctor.js +25 -2
- package/dist/mm-harness-cli.js +2 -0
- package/dist/recipe-security.js +1 -0
- package/dist/run-recording.js +128 -92
- package/library/actions/extension/platform/cdp.mjs +170 -117
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/mobile/platform/bridge.mjs +14 -5
- package/library/actions/mobile/platform/observe-ui.mjs +437 -0
- package/library/actions/mobile/platform/tool-paths.mjs +122 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/analytics/collector.mjs +50 -5
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +43 -1
- package/package.json +1 -1
|
@@ -84,76 +84,107 @@ function resolveRelativeArtifactPath(artifactsDir, relPath) {
|
|
|
84
84
|
if (absolute !== artifactsRoot && !absolute.startsWith(`${artifactsRoot}${path.sep}`)) {
|
|
85
85
|
throw new Error(`Refusing Extension screenshot artifact path outside artifacts dir: ${relative}`);
|
|
86
86
|
}
|
|
87
|
-
return { relative: normalized, absolute };
|
|
87
|
+
return { relative: normalized, absolute, artifactsRoot };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async function
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
await
|
|
94
|
-
await
|
|
95
|
-
await refuseUnsafeDestination(destination.absolute);
|
|
96
|
-
const stagingDir = await mkdtemp(path.join(artifactsRoot, '.extension-evidence-'));
|
|
90
|
+
async function preparePrivateEvidenceDestination(absolute, artifactsRoot) {
|
|
91
|
+
const outputDir = path.dirname(absolute);
|
|
92
|
+
await ensureEvidenceDirectory(artifactsRoot, outputDir);
|
|
93
|
+
await refuseEvidenceDestinationSymlink(absolute);
|
|
94
|
+
const stagingDir = await mkdtemp(path.join(outputDir, '.mm-harness-extension-evidence-'));
|
|
97
95
|
await chmod(stagingDir, 0o700);
|
|
98
96
|
return {
|
|
99
|
-
|
|
97
|
+
absolute,
|
|
100
98
|
stagingDir,
|
|
101
|
-
|
|
99
|
+
stagedPath: path.join(stagingDir, 'captured.png'),
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
async function
|
|
106
|
-
|
|
103
|
+
async function ensureEvidenceDirectory(artifactsRoot, outputDir) {
|
|
104
|
+
await mkdir(artifactsRoot, { recursive: true });
|
|
105
|
+
await requireEvidenceDirectory(artifactsRoot);
|
|
106
|
+
const relative = path.relative(artifactsRoot, outputDir);
|
|
107
107
|
let current = artifactsRoot;
|
|
108
108
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
109
109
|
current = path.join(current, segment);
|
|
110
110
|
try {
|
|
111
|
-
|
|
112
|
-
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
113
|
-
throw new Error(`Refusing Extension evidence parent that is not a real directory: ${current}`);
|
|
114
|
-
}
|
|
111
|
+
await mkdir(current);
|
|
115
112
|
} catch (error) {
|
|
116
|
-
if (error?.code !== '
|
|
117
|
-
await mkdir(current, { mode: 0o700 });
|
|
113
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
118
114
|
}
|
|
115
|
+
await requireEvidenceDirectory(current);
|
|
119
116
|
}
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
async function
|
|
119
|
+
async function requireEvidenceDirectory(directory) {
|
|
120
|
+
const entry = await lstat(directory);
|
|
121
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) return;
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Refusing unsafe Extension evidence directory: ${directory}\n` +
|
|
124
|
+
`Next: replace ${JSON.stringify(directory)} with a regular directory and retry the recipe.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function publishPrivateEvidence(destination) {
|
|
129
|
+
let handle;
|
|
123
130
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
try {
|
|
132
|
+
handle = await open(
|
|
133
|
+
destination.stagedPath,
|
|
134
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
|
135
|
+
);
|
|
136
|
+
const stagedStat = await handle.stat();
|
|
137
|
+
if (!stagedStat.isFile()) {
|
|
138
|
+
throw unsafeEvidenceArtifactError(destination.stagedPath);
|
|
139
|
+
}
|
|
140
|
+
await handle.chmod(0o600);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof Error && error.message.includes('Next:')) throw error;
|
|
143
|
+
throw unsafeEvidenceArtifactError(
|
|
144
|
+
destination.stagedPath,
|
|
145
|
+
error instanceof Error ? error.message : String(error),
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
await handle?.close();
|
|
130
149
|
}
|
|
131
|
-
|
|
132
|
-
|
|
150
|
+
await refuseEvidenceDestinationSymlink(destination.absolute);
|
|
151
|
+
await rename(destination.stagedPath, destination.absolute);
|
|
152
|
+
} finally {
|
|
153
|
+
await cleanupEvidenceStaging(destination.stagingDir);
|
|
133
154
|
}
|
|
134
155
|
}
|
|
135
156
|
|
|
136
|
-
async function
|
|
137
|
-
|
|
138
|
-
stage.staged,
|
|
139
|
-
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
|
140
|
-
);
|
|
157
|
+
async function refuseEvidenceDestinationSymlink(absolute) {
|
|
158
|
+
let destination;
|
|
141
159
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
destination = await lstat(absolute);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error?.code === 'ENOENT') return;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
if (destination.isSymbolicLink()) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Refusing Extension evidence destination symlink: ${absolute}\n` +
|
|
168
|
+
`Next: rm -- ${JSON.stringify(absolute)} and retry the recipe.`,
|
|
169
|
+
);
|
|
151
170
|
}
|
|
152
|
-
|
|
171
|
+
if (!destination.isFile()) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Refusing non-file Extension evidence destination: ${absolute}\n` +
|
|
174
|
+
`Next: remove ${JSON.stringify(absolute)} and retry the recipe.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function unsafeEvidenceArtifactError(stagedPath, detail) {
|
|
180
|
+
return new Error(
|
|
181
|
+
`Captured Extension evidence is not a safe regular file: ${stagedPath}${detail ? ` (${detail})` : ''}.\n` +
|
|
182
|
+
'Next: mm-harness doctor --adapter extension --json',
|
|
183
|
+
);
|
|
153
184
|
}
|
|
154
185
|
|
|
155
|
-
async function
|
|
156
|
-
await rm(
|
|
186
|
+
async function cleanupEvidenceStaging(stagingDir) {
|
|
187
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
function captureHelperPath() {
|
|
@@ -203,10 +234,12 @@ async function captureHelperBrowserPid(context, port) {
|
|
|
203
234
|
}
|
|
204
235
|
|
|
205
236
|
async function captureCdpViewportSnapshot(page, context, relPath, metadata, captureHelperError = null) {
|
|
206
|
-
const
|
|
237
|
+
const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
|
|
238
|
+
const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
|
|
239
|
+
let result;
|
|
207
240
|
try {
|
|
208
241
|
const timeoutMs = Number(metadata?.cdpTimeoutMs ?? 5000);
|
|
209
|
-
|
|
242
|
+
result = await Promise.race([
|
|
210
243
|
page.session.call('Page.captureScreenshot', {
|
|
211
244
|
format: 'png',
|
|
212
245
|
fromSurface: true,
|
|
@@ -217,33 +250,41 @@ async function captureCdpViewportSnapshot(page, context, relPath, metadata, capt
|
|
|
217
250
|
if (typeof result?.data !== 'string' || result.data.length === 0) {
|
|
218
251
|
throw new Error('Chrome Page.captureScreenshot returned no image data.');
|
|
219
252
|
}
|
|
220
|
-
await writeFile(stage.staged, Buffer.from(result.data, 'base64'), {
|
|
221
|
-
flag: 'wx',
|
|
222
|
-
mode: 0o600,
|
|
223
|
-
});
|
|
224
|
-
await publishPrivateArtifact(stage);
|
|
225
|
-
return {
|
|
226
|
-
path: stage.relative,
|
|
227
|
-
type: 'screenshot',
|
|
228
|
-
nodeId: context.nodeId,
|
|
229
|
-
label: metadata?.label ?? `${context.nodeId} screenshot`,
|
|
230
|
-
category: metadata?.category ?? 'evidence',
|
|
231
|
-
mimeType: 'image/png',
|
|
232
|
-
metadata: {
|
|
233
|
-
provider: 'cdp',
|
|
234
|
-
mode: 'Page.captureScreenshot',
|
|
235
|
-
...(captureHelperError ? { fallbackFrom: 'capture-helper', captureHelperError } : {}),
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
253
|
} catch (error) {
|
|
239
|
-
await
|
|
254
|
+
await cleanupEvidenceStaging(destination.stagingDir);
|
|
240
255
|
const cdpError = error instanceof Error ? error.message : String(error);
|
|
241
256
|
return captureDomRasterSnapshot(page, context, relPath, metadata, captureHelperError, cdpError);
|
|
242
257
|
}
|
|
258
|
+
try {
|
|
259
|
+
await writeFile(
|
|
260
|
+
destination.stagedPath,
|
|
261
|
+
Buffer.from(result.data, 'base64'),
|
|
262
|
+
{ flag: 'wx', mode: 0o600 },
|
|
263
|
+
);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
await cleanupEvidenceStaging(destination.stagingDir);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
await publishPrivateEvidence(destination);
|
|
269
|
+
return {
|
|
270
|
+
path: relative,
|
|
271
|
+
type: 'screenshot',
|
|
272
|
+
nodeId: context.nodeId,
|
|
273
|
+
label: metadata?.label ?? `${context.nodeId} screenshot`,
|
|
274
|
+
category: metadata?.category ?? 'evidence',
|
|
275
|
+
mimeType: 'image/png',
|
|
276
|
+
metadata: {
|
|
277
|
+
provider: 'cdp',
|
|
278
|
+
mode: 'Page.captureScreenshot',
|
|
279
|
+
...(captureHelperError ? { fallbackFrom: 'capture-helper', captureHelperError } : {}),
|
|
280
|
+
},
|
|
281
|
+
};
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
async function captureDomRasterSnapshot(page, context, relPath, metadata, captureHelperError, cdpError) {
|
|
246
|
-
const
|
|
285
|
+
const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
|
|
286
|
+
const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
|
|
287
|
+
const dataUrlPromise = page.evaluate(`(async () => {
|
|
247
288
|
const width = Math.max(1, window.innerWidth);
|
|
248
289
|
const height = Math.max(1, window.innerHeight);
|
|
249
290
|
const source = document.documentElement;
|
|
@@ -282,23 +323,24 @@ async function captureDomRasterSnapshot(page, context, relPath, metadata, captur
|
|
|
282
323
|
context.drawImage(image, 0, 0, width, height);
|
|
283
324
|
return canvas.toDataURL('image/png');
|
|
284
325
|
})()`);
|
|
285
|
-
|
|
286
|
-
throw new Error(`Extension screenshot fallbacks failed: capture-helper=${captureHelperError ?? 'not attempted'}; cdp=${cdpError}; DOM raster returned no PNG.`);
|
|
287
|
-
}
|
|
288
|
-
const stage = await preparePrivateArtifactStage(context.artifactsDir, relPath);
|
|
326
|
+
let dataUrl;
|
|
289
327
|
try {
|
|
328
|
+
dataUrl = await dataUrlPromise;
|
|
329
|
+
if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
|
|
330
|
+
throw new Error(`Extension screenshot fallbacks failed: capture-helper=${captureHelperError ?? 'not attempted'}; cdp=${cdpError}; DOM raster returned no PNG.`);
|
|
331
|
+
}
|
|
290
332
|
await writeFile(
|
|
291
|
-
|
|
333
|
+
destination.stagedPath,
|
|
292
334
|
Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64'),
|
|
293
335
|
{ flag: 'wx', mode: 0o600 },
|
|
294
336
|
);
|
|
295
|
-
await publishPrivateArtifact(stage);
|
|
296
337
|
} catch (error) {
|
|
297
|
-
await
|
|
338
|
+
await cleanupEvidenceStaging(destination.stagingDir);
|
|
298
339
|
throw error;
|
|
299
340
|
}
|
|
341
|
+
await publishPrivateEvidence(destination);
|
|
300
342
|
return {
|
|
301
|
-
path:
|
|
343
|
+
path: relative,
|
|
302
344
|
type: 'screenshot',
|
|
303
345
|
nodeId: context.nodeId,
|
|
304
346
|
label: metadata?.label ?? `${context.nodeId} screenshot`,
|
|
@@ -318,59 +360,70 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
|
|
|
318
360
|
if (process.platform !== 'darwin') {
|
|
319
361
|
return captureCdpViewportSnapshot(page, context, relPath, metadata);
|
|
320
362
|
}
|
|
321
|
-
const
|
|
363
|
+
const { relative, absolute, artifactsRoot } = resolveRelativeArtifactPath(context.artifactsDir, relPath);
|
|
364
|
+
const destination = await preparePrivateEvidenceDestination(absolute, artifactsRoot);
|
|
322
365
|
|
|
366
|
+
let pid;
|
|
367
|
+
let mode;
|
|
368
|
+
let details;
|
|
323
369
|
try {
|
|
324
|
-
|
|
370
|
+
pid = await captureHelperBrowserPid(context, page.port);
|
|
325
371
|
const timeoutMs = Number(metadata?.timeoutMs ?? 30000);
|
|
326
|
-
const sessionSnapshot = await captureActiveRecipeRecordingSnapshot(
|
|
372
|
+
const sessionSnapshot = await captureActiveRecipeRecordingSnapshot(
|
|
373
|
+
pid,
|
|
374
|
+
destination.stagedPath,
|
|
375
|
+
timeoutMs,
|
|
376
|
+
);
|
|
327
377
|
if (sessionSnapshot) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
378
|
+
mode = 'record_session_snapshot';
|
|
379
|
+
details = sessionSnapshot;
|
|
380
|
+
} else if (
|
|
381
|
+
parsePositivePid(process.env.METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID) === pid
|
|
382
|
+
) {
|
|
383
|
+
await cleanupEvidenceStaging(destination.stagingDir);
|
|
384
|
+
return captureCdpViewportSnapshot(
|
|
385
|
+
page,
|
|
386
|
+
context,
|
|
387
|
+
relPath,
|
|
388
|
+
metadata,
|
|
389
|
+
'native snapshot skipped while full-run recording owns the browser window',
|
|
390
|
+
);
|
|
391
|
+
} else {
|
|
392
|
+
const result = await runProcess(
|
|
393
|
+
captureHelperPath(),
|
|
394
|
+
['snapshot', '--pid', String(pid), '--output', destination.stagedPath],
|
|
395
|
+
{
|
|
396
|
+
cwd: context.projectRoot,
|
|
397
|
+
env: process.env,
|
|
398
|
+
timeoutMs,
|
|
341
399
|
},
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
timeoutMs,
|
|
349
|
-
});
|
|
350
|
-
if (result.exitCode !== 0) {
|
|
351
|
-
throw new Error(`capture-helper snapshot failed for pid ${pid}: ${result.stderr || result.stdout}`);
|
|
400
|
+
);
|
|
401
|
+
if (result.exitCode !== 0) {
|
|
402
|
+
throw new Error(`capture-helper snapshot failed for pid ${pid}: ${result.stderr || result.stdout}`);
|
|
403
|
+
}
|
|
404
|
+
mode = 'snapshot';
|
|
405
|
+
details = parseJsonObject(result.stdout);
|
|
352
406
|
}
|
|
353
|
-
const details = parseJsonObject(result.stdout);
|
|
354
|
-
await publishPrivateArtifact(stage);
|
|
355
|
-
return {
|
|
356
|
-
path: stage.relative,
|
|
357
|
-
type: 'screenshot',
|
|
358
|
-
nodeId: context.nodeId,
|
|
359
|
-
label: metadata?.label ?? `${context.nodeId} screenshot`,
|
|
360
|
-
category: metadata?.category ?? 'evidence',
|
|
361
|
-
mimeType: 'image/png',
|
|
362
|
-
metadata: {
|
|
363
|
-
provider: 'capture-helper',
|
|
364
|
-
mode: 'snapshot',
|
|
365
|
-
pid,
|
|
366
|
-
...(details ? { captureHelper: details } : {}),
|
|
367
|
-
},
|
|
368
|
-
};
|
|
369
407
|
} catch (error) {
|
|
370
|
-
await
|
|
408
|
+
await cleanupEvidenceStaging(destination.stagingDir);
|
|
371
409
|
const message = error instanceof Error ? error.message : String(error);
|
|
372
410
|
return captureCdpViewportSnapshot(page, context, relPath, metadata, message);
|
|
373
411
|
}
|
|
412
|
+
await publishPrivateEvidence(destination);
|
|
413
|
+
return {
|
|
414
|
+
path: relative,
|
|
415
|
+
type: 'screenshot',
|
|
416
|
+
nodeId: context.nodeId,
|
|
417
|
+
label: metadata?.label ?? `${context.nodeId} screenshot`,
|
|
418
|
+
category: metadata?.category ?? 'evidence',
|
|
419
|
+
mimeType: 'image/png',
|
|
420
|
+
metadata: {
|
|
421
|
+
provider: 'capture-helper',
|
|
422
|
+
mode,
|
|
423
|
+
pid,
|
|
424
|
+
...(details ? { captureHelper: details } : {}),
|
|
425
|
+
},
|
|
426
|
+
};
|
|
374
427
|
}
|
|
375
428
|
|
|
376
429
|
function autolaunchEnabled(input) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
locatorsFromObservation,
|
|
4
|
+
requireVisibleObservation,
|
|
5
|
+
} from '../../shared/ui/locators.mjs';
|
|
6
|
+
|
|
7
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
8
|
+
const observed = await page.observe(['ui.visible']);
|
|
9
|
+
return {
|
|
10
|
+
action: input.action,
|
|
11
|
+
...locatorsFromObservation(requireVisibleObservation(observed)),
|
|
12
|
+
};
|
|
13
|
+
}));
|
|
@@ -7,6 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import bridgeErrors from '../../../../adapters/mobile/bridge-runtime/lib/bridge-errors.cjs';
|
|
10
|
+
import { resolveMobileToolPath } from './tool-paths.mjs';
|
|
10
11
|
|
|
11
12
|
const {
|
|
12
13
|
BRIDGE_ERROR_CODES,
|
|
@@ -72,9 +73,11 @@ function runtimeDir() {
|
|
|
72
73
|
* or the serial does not respond. Uses execFile (never shell interpolation).
|
|
73
74
|
*/
|
|
74
75
|
async function resolveAndroidModel(adbSerial) {
|
|
76
|
+
const adbPath = resolveMobileToolPath('adb');
|
|
77
|
+
if (!adbPath) return null;
|
|
75
78
|
try {
|
|
76
79
|
const { stdout } = await execFileAsync(
|
|
77
|
-
|
|
80
|
+
adbPath,
|
|
78
81
|
['-s', adbSerial, 'shell', 'getprop', 'ro.product.model'],
|
|
79
82
|
{ timeout: 5000, encoding: 'utf8' },
|
|
80
83
|
);
|
|
@@ -90,12 +93,16 @@ async function resolveAndroidModel(adbSerial) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export async function bridgeEnv(input) {
|
|
96
|
+
const adbPath = resolveMobileToolPath('adb');
|
|
97
|
+
const idbPath = resolveMobileToolPath('idb');
|
|
93
98
|
/** @type {NodeJS.ProcessEnv} */
|
|
94
99
|
const env = {
|
|
95
100
|
...process.env,
|
|
96
101
|
...(input.context?.env || {}),
|
|
97
102
|
CDP_TIMEOUT: String(input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? '10000'),
|
|
98
103
|
};
|
|
104
|
+
if (adbPath) env.MM_HARNESS_ADB_PATH = adbPath;
|
|
105
|
+
if (idbPath) env.MM_HARNESS_IDB_PATH = idbPath;
|
|
99
106
|
const target = resolveMobileTarget(input);
|
|
100
107
|
const watcherPort = target.watcherPort;
|
|
101
108
|
const simulator = target.iosSimulator;
|
|
@@ -483,13 +490,14 @@ async function androidScreenshot(input, relPath) {
|
|
|
483
490
|
try {
|
|
484
491
|
temporaryDirectory = await createScreenshotTemporaryDirectory();
|
|
485
492
|
const adbSerial = await resolveAndroidScreenshotSerial(input);
|
|
493
|
+
const adbPath = resolveMobileToolPath('adb', { required: true });
|
|
486
494
|
let result;
|
|
487
495
|
try {
|
|
488
|
-
result = await runScreenshotProcess(
|
|
496
|
+
result = await runScreenshotProcess(adbPath, ['-s', adbSerial, 'exec-out', 'screencap', '-p'], true);
|
|
489
497
|
} catch (error) {
|
|
490
498
|
throw new Error(
|
|
491
499
|
`adb screenshot could not start for ${adbSerial}: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
492
|
-
' Next: install Android SDK Platform-Tools,
|
|
500
|
+
' Next: install Android SDK Platform-Tools, then rerun the same mm-harness command.',
|
|
493
501
|
);
|
|
494
502
|
}
|
|
495
503
|
if (result.exitCode !== 0) {
|
|
@@ -541,11 +549,12 @@ async function resolveAndroidScreenshotSerial(input) {
|
|
|
541
549
|
|
|
542
550
|
let result;
|
|
543
551
|
try {
|
|
544
|
-
|
|
552
|
+
const adbPath = resolveMobileToolPath('adb', { required: true });
|
|
553
|
+
result = await runScreenshotProcess(adbPath, ['devices'], false);
|
|
545
554
|
} catch (error) {
|
|
546
555
|
throw new Error(
|
|
547
556
|
`Android screenshot could not resolve an adb serial: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
548
|
-
' Next: install Android SDK Platform-Tools,
|
|
557
|
+
' Next: install Android SDK Platform-Tools, then rerun with --device <adb-serial>.',
|
|
549
558
|
);
|
|
550
559
|
}
|
|
551
560
|
if (result.exitCode !== 0) {
|