@autifyhq/aximo-cli 0.5.0 → 0.7.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/README.md +196 -64
- package/dist/cli.js +4 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +33 -0
- package/dist/commands/mobile/connect-core.d.ts +2 -1
- package/dist/commands/mobile/connect-core.js +127 -15
- package/dist/commands/mobile/connect.js +3 -1
- package/dist/commands/mobile/doctor.d.ts +3 -0
- package/dist/commands/mobile/doctor.js +55 -6
- package/dist/commands/mobile/doctor.test.d.ts +1 -0
- package/dist/commands/mobile/doctor.test.js +14 -0
- package/dist/commands/mobile/index.js +1 -0
- package/dist/commands/mobile/install.d.ts +50 -0
- package/dist/commands/mobile/install.js +107 -0
- package/dist/commands/mobile/install.test.d.ts +1 -0
- package/dist/commands/mobile/install.test.js +142 -0
- package/dist/commands/mobile/list.d.ts +2 -0
- package/dist/commands/mobile/list.js +17 -11
- package/dist/commands/mobile/register.d.ts +6 -1
- package/dist/commands/mobile/register.js +12 -6
- package/dist/commands/mobile/register.test.js +34 -3
- package/dist/commands/mobile/unregister.js +11 -6
- package/dist/commands/schema.js +15 -8
- package/dist/mobile-control/app-install.d.ts +34 -0
- package/dist/mobile-control/app-install.js +55 -0
- package/dist/mobile-control/app-install.test.d.ts +1 -0
- package/dist/mobile-control/app-install.test.js +113 -0
- package/dist/mobile-control/appium-controller.d.ts +11 -0
- package/dist/mobile-control/appium-controller.js +24 -4
- package/dist/mobile-control/bundle-id-map.d.ts +9 -0
- package/dist/mobile-control/bundle-id-map.js +27 -0
- package/dist/mobile-control/devices.d.ts +8 -3
- package/dist/mobile-control/devices.js +57 -5
- package/dist/mobile-control/devices.test.js +41 -1
- package/dist/mobile-control/env-flags.d.ts +5 -0
- package/dist/mobile-control/env-flags.js +10 -0
- package/dist/mobile-control/env-flags.test.d.ts +1 -0
- package/dist/mobile-control/env-flags.test.js +21 -0
- package/dist/mobile-control/ios-auto-resign.d.ts +45 -0
- package/dist/mobile-control/ios-auto-resign.js +108 -0
- package/dist/mobile-control/ios-auto-resign.test.d.ts +1 -0
- package/dist/mobile-control/ios-auto-resign.test.js +150 -0
- package/dist/mobile-control/ios-devicectl.d.ts +39 -0
- package/dist/mobile-control/ios-devicectl.js +87 -0
- package/dist/mobile-control/ios-devicectl.test.d.ts +1 -0
- package/dist/mobile-control/ios-devicectl.test.js +110 -0
- package/dist/mobile-control/ios-identity.d.ts +27 -0
- package/dist/mobile-control/ios-identity.js +49 -0
- package/dist/mobile-control/ios-identity.test.d.ts +1 -0
- package/dist/mobile-control/ios-identity.test.js +102 -0
- package/dist/mobile-control/ios-mint.d.ts +39 -0
- package/dist/mobile-control/ios-mint.js +78 -0
- package/dist/mobile-control/ios-mint.test.d.ts +1 -0
- package/dist/mobile-control/ios-mint.test.js +94 -0
- package/dist/mobile-control/ios-profiles.d.ts +46 -0
- package/dist/mobile-control/ios-profiles.js +106 -0
- package/dist/mobile-control/ios-profiles.test.d.ts +1 -0
- package/dist/mobile-control/ios-profiles.test.js +154 -0
- package/dist/mobile-control/ios-resign.d.ts +37 -0
- package/dist/mobile-control/ios-resign.js +101 -0
- package/dist/mobile-control/ios-resign.test.d.ts +1 -0
- package/dist/mobile-control/ios-resign.test.js +84 -0
- package/dist/mobile-control/ios-session-error.d.ts +10 -0
- package/dist/mobile-control/ios-session-error.js +34 -0
- package/dist/mobile-control/ios-session-error.test.d.ts +1 -0
- package/dist/mobile-control/ios-session-error.test.js +30 -0
- package/dist/mobile-control/ios-signing.d.ts +56 -0
- package/dist/mobile-control/ios-signing.js +94 -0
- package/dist/mobile-control/ios-signing.test.d.ts +1 -0
- package/dist/mobile-control/ios-signing.test.js +160 -0
- package/dist/mobile-control/ios-tunnel.d.ts +54 -0
- package/dist/mobile-control/ios-tunnel.js +130 -0
- package/dist/mobile-control/ios-tunnel.test.d.ts +1 -0
- package/dist/mobile-control/ios-tunnel.test.js +115 -0
- package/dist/mobile-control/ios-wda-cache.d.ts +39 -0
- package/dist/mobile-control/ios-wda-cache.js +157 -0
- package/dist/mobile-control/ios-wda-cache.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-cache.test.js +128 -0
- package/dist/mobile-control/ios-wda-diagnostics.d.ts +14 -0
- package/dist/mobile-control/ios-wda-diagnostics.js +138 -0
- package/dist/mobile-control/ios-wda-diagnostics.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-diagnostics.test.js +77 -0
- package/dist/mobile-control/ios-wda-preinstall.d.ts +64 -0
- package/dist/mobile-control/ios-wda-preinstall.js +162 -0
- package/dist/mobile-control/ios-wda-preinstall.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-preinstall.test.js +107 -0
- package/dist/mobile-control/mobile-errors.d.ts +12 -0
- package/dist/mobile-control/mobile-errors.js +91 -0
- package/dist/mobile-control/mobile-errors.test.d.ts +1 -0
- package/dist/mobile-control/mobile-errors.test.js +48 -0
- package/dist/mobile-control/tunnel.d.ts +5 -1
- package/dist/mobile-control/tunnel.js +60 -3
- package/dist/mobile-control/tunnel.test.js +1 -1
- package/dist/proxy.d.ts +11 -0
- package/dist/proxy.js +85 -0
- package/dist/proxy.test.d.ts +1 -0
- package/dist/proxy.test.js +72 -0
- package/package.json +4 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execBinary } from "./exec.js";
|
|
5
|
+
/**
|
|
6
|
+
* When an XCUITest session fails to start, the Appium/WebDriver error is usually
|
|
7
|
+
* generic ("xcodebuild failed with code 65"). The real reason — an untrusted
|
|
8
|
+
* developer cert, a taken WDA bundle id, a locked device — lives in the WDA test
|
|
9
|
+
* run's `.xcresult` bundle. We read the newest one and surface its error
|
|
10
|
+
* summaries so the CLI can show the user what actually went wrong.
|
|
11
|
+
*/
|
|
12
|
+
/** Newest `Test-*.xcresult` under a DerivedData dir's Logs/Test, or null. */
|
|
13
|
+
function newestXcresultIn(derivedDataPath) {
|
|
14
|
+
const testLogs = join(derivedDataPath, "Logs", "Test");
|
|
15
|
+
if (!existsSync(testLogs))
|
|
16
|
+
return null;
|
|
17
|
+
let newest = null;
|
|
18
|
+
for (const entry of safeReaddir(testLogs)) {
|
|
19
|
+
if (!entry.endsWith(".xcresult"))
|
|
20
|
+
continue;
|
|
21
|
+
const path = join(testLogs, entry);
|
|
22
|
+
const mtimeMs = safeMtime(path);
|
|
23
|
+
if (mtimeMs !== null && (!newest || mtimeMs > newest.mtimeMs)) {
|
|
24
|
+
newest = { path, mtimeMs };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return newest?.path ?? null;
|
|
28
|
+
}
|
|
29
|
+
/** Candidate WDA DerivedData dirs, newest first: the managed cache, then Xcode's default. */
|
|
30
|
+
function candidateDerivedDataDirs(managed) {
|
|
31
|
+
const dirs = [];
|
|
32
|
+
if (managed)
|
|
33
|
+
dirs.push(managed);
|
|
34
|
+
const defaultRoot = join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
35
|
+
const xcodeDirs = safeReaddir(defaultRoot)
|
|
36
|
+
.filter((entry) => entry.startsWith("WebDriverAgent-"))
|
|
37
|
+
.map((entry) => join(defaultRoot, entry))
|
|
38
|
+
.sort((a, b) => (safeMtime(b) ?? 0) - (safeMtime(a) ?? 0));
|
|
39
|
+
dirs.push(...xcodeDirs);
|
|
40
|
+
return dirs;
|
|
41
|
+
}
|
|
42
|
+
function safeReaddir(dir) {
|
|
43
|
+
try {
|
|
44
|
+
return readdirSync(dir);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function safeMtime(path) {
|
|
51
|
+
try {
|
|
52
|
+
return statSync(path).mtimeMs;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Recursively collect messages under error/testFailure summary arrays (xcresulttool JSON). */
|
|
59
|
+
function collectSummaryMessages(node, out) {
|
|
60
|
+
if (Array.isArray(node)) {
|
|
61
|
+
for (const item of node)
|
|
62
|
+
collectSummaryMessages(item, out);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!node || typeof node !== "object")
|
|
66
|
+
return;
|
|
67
|
+
const obj = node;
|
|
68
|
+
for (const key of ["errorSummaries", "testFailureSummaries"]) {
|
|
69
|
+
const summaries = obj[key];
|
|
70
|
+
for (const summary of summaries?._values ?? []) {
|
|
71
|
+
const message = extractMessageString(summary?.["message"]);
|
|
72
|
+
if (message)
|
|
73
|
+
out.push(message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const value of Object.values(obj))
|
|
77
|
+
collectSummaryMessages(value, out);
|
|
78
|
+
}
|
|
79
|
+
/** A message field is `{ _value: "..." }` in the legacy format, or a bare string in newer ones. */
|
|
80
|
+
function extractMessageString(message) {
|
|
81
|
+
if (typeof message === "string")
|
|
82
|
+
return message.trim() || null;
|
|
83
|
+
if (message && typeof message === "object") {
|
|
84
|
+
const value = message._value;
|
|
85
|
+
if (typeof value === "string")
|
|
86
|
+
return value.trim() || null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Extract the concise failure reason from the newest WDA `.xcresult`, e.g.
|
|
92
|
+
* "The application could not be launched because the Developer App Certificate
|
|
93
|
+
* is not trusted." Returns null when nothing usable is found (best-effort).
|
|
94
|
+
*/
|
|
95
|
+
export async function readWdaFailureReason(options = {}) {
|
|
96
|
+
const xcresult = (options.findXcresult ?? defaultFindXcresult)(options.derivedDataPath);
|
|
97
|
+
if (!xcresult)
|
|
98
|
+
return null;
|
|
99
|
+
const exec = options.exec ?? execBinary;
|
|
100
|
+
const json = await runXcresultTool(xcresult, exec);
|
|
101
|
+
if (!json)
|
|
102
|
+
return null;
|
|
103
|
+
const messages = [];
|
|
104
|
+
collectSummaryMessages(json, messages);
|
|
105
|
+
if (messages.length === 0)
|
|
106
|
+
return null;
|
|
107
|
+
// Prefer the shortest non-trivial message: the concise human reason rather
|
|
108
|
+
// than the giant nested "Underlying Error: …" chain that repeats it.
|
|
109
|
+
const deduped = [...new Set(messages)].filter((m) => m.length >= 15);
|
|
110
|
+
if (deduped.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
deduped.sort((a, b) => a.length - b.length);
|
|
113
|
+
return deduped[0].slice(0, 400);
|
|
114
|
+
}
|
|
115
|
+
function defaultFindXcresult(managed) {
|
|
116
|
+
for (const dir of candidateDerivedDataDirs(managed)) {
|
|
117
|
+
const found = newestXcresultIn(dir);
|
|
118
|
+
if (found)
|
|
119
|
+
return found;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** `xcresulttool get` output as JSON. `--legacy` is required on Xcode 16+; older Xcode rejects it. */
|
|
124
|
+
async function runXcresultTool(xcresult, exec) {
|
|
125
|
+
for (const args of [
|
|
126
|
+
["xcresulttool", "get", "--legacy", "--path", xcresult, "--format", "json"],
|
|
127
|
+
["xcresulttool", "get", "--path", xcresult, "--format", "json"],
|
|
128
|
+
]) {
|
|
129
|
+
try {
|
|
130
|
+
const { stdout } = await exec("xcrun", args);
|
|
131
|
+
return JSON.parse(stdout.toString("utf-8"));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// try the next arg form, then give up
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { readWdaFailureReason } from "./ios-wda-diagnostics.js";
|
|
3
|
+
// Shape mirrors `xcrun xcresulttool get --legacy --format json` for a failed WDA
|
|
4
|
+
// launch (developer cert not trusted) — the exact case seen on a real device.
|
|
5
|
+
const LEGACY_XCRESULT = {
|
|
6
|
+
actions: {
|
|
7
|
+
_values: [
|
|
8
|
+
{
|
|
9
|
+
actionResult: {
|
|
10
|
+
issues: {
|
|
11
|
+
errorSummaries: {
|
|
12
|
+
_values: [
|
|
13
|
+
{
|
|
14
|
+
issueType: { _value: "Uncategorized" },
|
|
15
|
+
message: {
|
|
16
|
+
_value: "The application could not be launched because the Developer App Certificate is not trusted.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
issueType: { _value: "Uncategorized" },
|
|
21
|
+
message: {
|
|
22
|
+
_value: "WebDriverAgentRunner-Runner encountered an error (Failed to install or launch the test runner. (Underlying Error: The application could not be launched because the Developer App Certificate is not trusted. Verify that the Developer App certificate ...))",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
function execReturning(json) {
|
|
34
|
+
return vi.fn(async () => ({ stdout: Buffer.from(JSON.stringify(json), "utf-8"), stderr: "" }));
|
|
35
|
+
}
|
|
36
|
+
describe("readWdaFailureReason", () => {
|
|
37
|
+
it("returns null when no xcresult is found", async () => {
|
|
38
|
+
const reason = await readWdaFailureReason({ findXcresult: () => null });
|
|
39
|
+
expect(reason).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
it("extracts the concise reason (shortest error summary) from the xcresult", async () => {
|
|
42
|
+
const reason = await readWdaFailureReason({
|
|
43
|
+
findXcresult: () => "/dd/Logs/Test/Test.xcresult",
|
|
44
|
+
exec: execReturning(LEGACY_XCRESULT),
|
|
45
|
+
});
|
|
46
|
+
expect(reason).toBe("The application could not be launched because the Developer App Certificate is not trusted.");
|
|
47
|
+
});
|
|
48
|
+
it("handles the newer bare-string message format", async () => {
|
|
49
|
+
const reason = await readWdaFailureReason({
|
|
50
|
+
findXcresult: () => "/dd/Logs/Test/Test.xcresult",
|
|
51
|
+
exec: execReturning({
|
|
52
|
+
issues: {
|
|
53
|
+
testFailureSummaries: { _values: [{ message: "Unable to launch WebDriverAgent" }] },
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
expect(reason).toBe("Unable to launch WebDriverAgent");
|
|
58
|
+
});
|
|
59
|
+
it("returns null when xcresulttool fails on both arg forms", async () => {
|
|
60
|
+
const exec = vi.fn(async () => {
|
|
61
|
+
throw new Error("xcresulttool: unknown option --legacy");
|
|
62
|
+
});
|
|
63
|
+
const reason = await readWdaFailureReason({
|
|
64
|
+
findXcresult: () => "/dd/Logs/Test/Test.xcresult",
|
|
65
|
+
exec,
|
|
66
|
+
});
|
|
67
|
+
expect(reason).toBeNull();
|
|
68
|
+
expect(exec).toHaveBeenCalledTimes(2); // tried --legacy, then without
|
|
69
|
+
});
|
|
70
|
+
it("ignores trivially short messages", async () => {
|
|
71
|
+
const reason = await readWdaFailureReason({
|
|
72
|
+
findXcresult: () => "/dd/Logs/Test/Test.xcresult",
|
|
73
|
+
exec: execReturning({ errorSummaries: { _values: [{ message: { _value: "oops" } }] } }),
|
|
74
|
+
});
|
|
75
|
+
expect(reason).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ExecFn } from "./exec.js";
|
|
2
|
+
import { installAppViaDevicectl } from "./ios-devicectl.js";
|
|
3
|
+
import { mintProfileViaWdaRig } from "./ios-mint.js";
|
|
4
|
+
import { listProvisioningProfiles } from "./ios-profiles.js";
|
|
5
|
+
import { resignIpa } from "./ios-resign.js";
|
|
6
|
+
import type { IosSigningConfig } from "./ios-signing.js";
|
|
7
|
+
/**
|
|
8
|
+
* Preinstalled WebDriverAgent: skip xcodebuild entirely on iOS 17+.
|
|
9
|
+
*
|
|
10
|
+
* The Appium project publishes a prebuilt real-device WDA runner for every
|
|
11
|
+
* WebDriverAgent release (built without embedded XCTest frameworks and without
|
|
12
|
+
* a provisioning profile). Downloading the version matching the bundled driver,
|
|
13
|
+
* re-signing it with the local identity, and installing it via devicectl gives
|
|
14
|
+
* the `appium:usePreinstalledWDA` session path: no Xcode build, no ~8.5 GB iOS
|
|
15
|
+
* platform download, first session in seconds. Verified on-device — see
|
|
16
|
+
* docs/ios-resign-e2e-verification.md.
|
|
17
|
+
*
|
|
18
|
+
* The downloaded release zip is cached per version under
|
|
19
|
+
* $AXIMO_HOME/ios-wda/prebuilt/<version>/; the re-sign + install run per
|
|
20
|
+
* connect (XCTest runner bundles cannot be versioned, so a fresh install is the
|
|
21
|
+
* reliable baseline, and both steps take seconds).
|
|
22
|
+
*/
|
|
23
|
+
export declare function readBundledWdaVersion(appiumHome?: string): string | null;
|
|
24
|
+
export interface PreinstallWdaOptions {
|
|
25
|
+
udid: string;
|
|
26
|
+
/** Signing identity SHA-1 hash (ios-identity). */
|
|
27
|
+
identityHash: string;
|
|
28
|
+
/** Profile covering `<wdaBundleId>.xctrunner` on this device (ios-profiles). */
|
|
29
|
+
profilePath: string;
|
|
30
|
+
/** Base WDA bundle id; the runner installs as `<wdaBundleId>.xctrunner`. */
|
|
31
|
+
wdaBundleId: string;
|
|
32
|
+
exec?: ExecFn;
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
cacheDir?: string;
|
|
35
|
+
log?: (message: string) => void;
|
|
36
|
+
/** Injectable for tests. */
|
|
37
|
+
resign?: typeof resignIpa;
|
|
38
|
+
install?: typeof installAppViaDevicectl;
|
|
39
|
+
wdaVersion?: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Download (cached), re-sign, and install the prebuilt WDA runner. */
|
|
42
|
+
export declare function preinstallWda(options: PreinstallWdaOptions): Promise<{
|
|
43
|
+
runnerBundleId: string;
|
|
44
|
+
wdaVersion: string;
|
|
45
|
+
}>;
|
|
46
|
+
export interface PreinstalledWdaStrategyOptions {
|
|
47
|
+
udid: string;
|
|
48
|
+
/** Device iOS major version; the driver only supports this path on 17+. */
|
|
49
|
+
iosMajor: number | null;
|
|
50
|
+
env?: NodeJS.ProcessEnv;
|
|
51
|
+
log?: (message: string) => void;
|
|
52
|
+
now?: number;
|
|
53
|
+
listProfiles?: typeof listProvisioningProfiles;
|
|
54
|
+
mint?: typeof mintProfileViaWdaRig;
|
|
55
|
+
preinstall?: typeof preinstallWda;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Try to set up the preinstalled-WDA session path: find (or mint) a runner
|
|
59
|
+
* profile, download+re-sign+install the prebuilt runner, and return the signing
|
|
60
|
+
* config flipped to `usePreinstalledWDA`. Returns null — with the reason logged —
|
|
61
|
+
* whenever any ingredient is missing, so the caller falls back to the classic
|
|
62
|
+
* xcodebuild flow. Opt out entirely with AXIMO_IOS_NO_PREINSTALLED_WDA.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolvePreinstalledWdaStrategy(signing: IosSigningConfig, options: PreinstalledWdaStrategyOptions): Promise<IosSigningConfig | null>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAximoDir } from "../commands/device/paths.js";
|
|
5
|
+
import { getAppiumHome } from "./appium-server.js";
|
|
6
|
+
import { envFlag } from "./env-flags.js";
|
|
7
|
+
import { execBinary } from "./exec.js";
|
|
8
|
+
import { installAppViaDevicectl } from "./ios-devicectl.js";
|
|
9
|
+
import { mintProfileViaWdaRig } from "./ios-mint.js";
|
|
10
|
+
import { listProvisioningProfiles, selectProfile } from "./ios-profiles.js";
|
|
11
|
+
import { resignIpa } from "./ios-resign.js";
|
|
12
|
+
/**
|
|
13
|
+
* Preinstalled WebDriverAgent: skip xcodebuild entirely on iOS 17+.
|
|
14
|
+
*
|
|
15
|
+
* The Appium project publishes a prebuilt real-device WDA runner for every
|
|
16
|
+
* WebDriverAgent release (built without embedded XCTest frameworks and without
|
|
17
|
+
* a provisioning profile). Downloading the version matching the bundled driver,
|
|
18
|
+
* re-signing it with the local identity, and installing it via devicectl gives
|
|
19
|
+
* the `appium:usePreinstalledWDA` session path: no Xcode build, no ~8.5 GB iOS
|
|
20
|
+
* platform download, first session in seconds. Verified on-device — see
|
|
21
|
+
* docs/ios-resign-e2e-verification.md.
|
|
22
|
+
*
|
|
23
|
+
* The downloaded release zip is cached per version under
|
|
24
|
+
* $AXIMO_HOME/ios-wda/prebuilt/<version>/; the re-sign + install run per
|
|
25
|
+
* connect (XCTest runner bundles cannot be versioned, so a fresh install is the
|
|
26
|
+
* reliable baseline, and both steps take seconds).
|
|
27
|
+
*/
|
|
28
|
+
export function readBundledWdaVersion(appiumHome = getAppiumHome()) {
|
|
29
|
+
const candidates = [
|
|
30
|
+
join(appiumHome, "node_modules", "appium-xcuitest-driver", "node_modules", "appium-webdriveragent", "package.json"),
|
|
31
|
+
join(appiumHome, "node_modules", "appium-webdriveragent", "package.json"),
|
|
32
|
+
];
|
|
33
|
+
for (const pkgPath of candidates) {
|
|
34
|
+
try {
|
|
35
|
+
const version = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
36
|
+
if (version)
|
|
37
|
+
return version;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// try the next location
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function releaseUrl(version) {
|
|
46
|
+
return `https://github.com/appium/WebDriverAgent/releases/download/v${version}/WebDriverAgentRunner-Runner.zip`;
|
|
47
|
+
}
|
|
48
|
+
const RUNNER_APP = "WebDriverAgentRunner-Runner.app";
|
|
49
|
+
/** Download (cached), re-sign, and install the prebuilt WDA runner. */
|
|
50
|
+
export async function preinstallWda(options) {
|
|
51
|
+
const exec = options.exec ?? execBinary;
|
|
52
|
+
const log = options.log ?? (() => { });
|
|
53
|
+
const version = options.wdaVersion !== undefined ? options.wdaVersion : readBundledWdaVersion();
|
|
54
|
+
if (!version) {
|
|
55
|
+
throw new Error("Could not determine the bundled WebDriverAgent version. Run `aximo mobile setup --platform ios`.");
|
|
56
|
+
}
|
|
57
|
+
const cacheDir = options.cacheDir ?? join(getAximoDir(), "ios-wda", "prebuilt", version);
|
|
58
|
+
const zipPath = join(cacheDir, "WebDriverAgentRunner-Runner.zip");
|
|
59
|
+
if (!existsSync(zipPath)) {
|
|
60
|
+
log(`Downloading prebuilt WebDriverAgent v${version}…`);
|
|
61
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
62
|
+
const response = await fetchImpl(releaseUrl(version));
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`No prebuilt WebDriverAgent for v${version} (HTTP ${response.status} from GitHub releases).`);
|
|
65
|
+
}
|
|
66
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
67
|
+
writeFileSync(zipPath, Buffer.from(await response.arrayBuffer()));
|
|
68
|
+
}
|
|
69
|
+
const scratch = mkdtempSync(join(tmpdir(), "aximo-wda-preinstall-"));
|
|
70
|
+
try {
|
|
71
|
+
// Wrap the bare .app into Payload/ so it can go through the same resign path
|
|
72
|
+
// as any other IPA (ditto ships with macOS and handles both directions).
|
|
73
|
+
const payloadRoot = join(scratch, "wrap");
|
|
74
|
+
const extractDir = join(payloadRoot, "Payload");
|
|
75
|
+
mkdirSync(extractDir, { recursive: true });
|
|
76
|
+
await exec("ditto", ["-x", "-k", zipPath, extractDir]);
|
|
77
|
+
if (!existsSync(join(extractDir, RUNNER_APP))) {
|
|
78
|
+
throw new Error(`Prebuilt WDA zip did not contain ${RUNNER_APP} — re-download or report.`);
|
|
79
|
+
}
|
|
80
|
+
const wrappedIpa = join(scratch, "wda.ipa");
|
|
81
|
+
await exec("ditto", ["-c", "-k", payloadRoot, wrappedIpa]);
|
|
82
|
+
const runnerBundleId = `${options.wdaBundleId}.xctrunner`;
|
|
83
|
+
const resignedIpa = join(scratch, "wda-resigned.ipa");
|
|
84
|
+
log(`Signing WebDriverAgent as ${runnerBundleId}…`);
|
|
85
|
+
await (options.resign ?? resignIpa)({
|
|
86
|
+
ipaPath: wrappedIpa,
|
|
87
|
+
outPath: resignedIpa,
|
|
88
|
+
identityHash: options.identityHash,
|
|
89
|
+
profilePath: options.profilePath,
|
|
90
|
+
bundleId: runnerBundleId,
|
|
91
|
+
exec,
|
|
92
|
+
});
|
|
93
|
+
log("Installing WebDriverAgent on the device…");
|
|
94
|
+
await (options.install ?? installAppViaDevicectl)(options.udid, resignedIpa, exec);
|
|
95
|
+
return { runnerBundleId, wdaVersion: version };
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Try to set up the preinstalled-WDA session path: find (or mint) a runner
|
|
103
|
+
* profile, download+re-sign+install the prebuilt runner, and return the signing
|
|
104
|
+
* config flipped to `usePreinstalledWDA`. Returns null — with the reason logged —
|
|
105
|
+
* whenever any ingredient is missing, so the caller falls back to the classic
|
|
106
|
+
* xcodebuild flow. Opt out entirely with AXIMO_IOS_NO_PREINSTALLED_WDA.
|
|
107
|
+
*/
|
|
108
|
+
export async function resolvePreinstalledWdaStrategy(signing, options) {
|
|
109
|
+
const env = options.env ?? process.env;
|
|
110
|
+
const log = options.log ?? (() => { });
|
|
111
|
+
if (envFlag(env, "AXIMO_IOS_NO_PREINSTALLED_WDA"))
|
|
112
|
+
return null;
|
|
113
|
+
if (options.iosMajor === null || options.iosMajor < 17)
|
|
114
|
+
return null;
|
|
115
|
+
if (!signing.teamId || !signing.identityHash)
|
|
116
|
+
return null;
|
|
117
|
+
const runnerAppId = `${signing.wdaBundleId}.xctrunner`;
|
|
118
|
+
const listProfiles = options.listProfiles ?? listProvisioningProfiles;
|
|
119
|
+
const criteria = {
|
|
120
|
+
bundleId: runnerAppId,
|
|
121
|
+
udid: options.udid,
|
|
122
|
+
teamId: signing.teamId,
|
|
123
|
+
...(options.now !== undefined ? { now: options.now } : {}),
|
|
124
|
+
};
|
|
125
|
+
let profile = selectProfile(await listProfiles(), criteria);
|
|
126
|
+
if (!profile) {
|
|
127
|
+
log("No WebDriverAgent provisioning profile for this device — minting one via Xcode " +
|
|
128
|
+
"automatic signing (first run; takes a minute or two)…");
|
|
129
|
+
try {
|
|
130
|
+
await (options.mint ?? mintProfileViaWdaRig)({
|
|
131
|
+
teamId: signing.teamId,
|
|
132
|
+
bundleId: signing.wdaBundleId,
|
|
133
|
+
udid: options.udid,
|
|
134
|
+
signingId: signing.signingId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
log(`Profile minting failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
profile = selectProfile(await listProfiles(), criteria);
|
|
142
|
+
if (!profile) {
|
|
143
|
+
log("Minted profile not found on disk — falling back to the Xcode build flow.");
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await (options.preinstall ?? preinstallWda)({
|
|
149
|
+
udid: options.udid,
|
|
150
|
+
identityHash: signing.identityHash,
|
|
151
|
+
profilePath: profile.path,
|
|
152
|
+
wdaBundleId: signing.wdaBundleId,
|
|
153
|
+
log,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log(`Preinstalled WebDriverAgent unavailable (${err instanceof Error ? err.message : String(err)}); ` +
|
|
158
|
+
"falling back to the Xcode build flow.");
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return { ...signing, usePreinstalledWDA: true };
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { preinstallWda, readBundledWdaVersion } from "./ios-wda-preinstall.js";
|
|
6
|
+
describe("readBundledWdaVersion", () => {
|
|
7
|
+
let home;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
home = mkdtempSync(join(tmpdir(), "aximo-wda-home-"));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
rmSync(home, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it("reads the nested driver copy first, then the hoisted one", () => {
|
|
15
|
+
const hoisted = join(home, "node_modules/appium-webdriveragent");
|
|
16
|
+
mkdirSync(hoisted, { recursive: true });
|
|
17
|
+
writeFileSync(join(hoisted, "package.json"), JSON.stringify({ version: "14.0.0" }));
|
|
18
|
+
expect(readBundledWdaVersion(home)).toBe("14.0.0");
|
|
19
|
+
const nested = join(home, "node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent");
|
|
20
|
+
mkdirSync(nested, { recursive: true });
|
|
21
|
+
writeFileSync(join(nested, "package.json"), JSON.stringify({ version: "15.0.0" }));
|
|
22
|
+
expect(readBundledWdaVersion(home)).toBe("15.0.0");
|
|
23
|
+
});
|
|
24
|
+
it("returns null when no package is present", () => {
|
|
25
|
+
expect(readBundledWdaVersion(home)).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("preinstallWda", () => {
|
|
29
|
+
let cacheDir;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
cacheDir = mkdtempSync(join(tmpdir(), "aximo-wda-cache-"));
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
/** ditto fake: -x -k extract creates the runner app; -c -k creates the archive. */
|
|
37
|
+
const dittoExec = async (file, args) => {
|
|
38
|
+
if (file === "ditto" && args[0] === "-x") {
|
|
39
|
+
const dest = args[3];
|
|
40
|
+
if (dest)
|
|
41
|
+
mkdirSync(join(dest, "WebDriverAgentRunner-Runner.app"), { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
if (file === "ditto" && args[0] === "-c") {
|
|
44
|
+
const out = args[3];
|
|
45
|
+
if (out)
|
|
46
|
+
writeFileSync(out, "zip");
|
|
47
|
+
}
|
|
48
|
+
return { stdout: Buffer.from(""), stderr: "" };
|
|
49
|
+
};
|
|
50
|
+
function makeOptions() {
|
|
51
|
+
return {
|
|
52
|
+
udid: "UDID-1",
|
|
53
|
+
identityHash: "A".repeat(40),
|
|
54
|
+
profilePath: "/profiles/wda.mobileprovision",
|
|
55
|
+
wdaBundleId: "com.aximo.wda.testteam99",
|
|
56
|
+
exec: dittoExec,
|
|
57
|
+
cacheDir,
|
|
58
|
+
wdaVersion: "15.0.0",
|
|
59
|
+
fetchImpl: vi.fn(async (_input) => new Response(new ArrayBuffer(8), { status: 200 })),
|
|
60
|
+
resign: vi.fn(async () => { }),
|
|
61
|
+
install: vi.fn(async () => ""),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
it("downloads once, re-signs with the .xctrunner id, and installs", async () => {
|
|
65
|
+
const opts = makeOptions();
|
|
66
|
+
const result = await preinstallWda(opts);
|
|
67
|
+
expect(result).toEqual({
|
|
68
|
+
runnerBundleId: "com.aximo.wda.testteam99.xctrunner",
|
|
69
|
+
wdaVersion: "15.0.0",
|
|
70
|
+
});
|
|
71
|
+
expect(opts.fetchImpl).toHaveBeenCalledOnce();
|
|
72
|
+
expect(vi.mocked(opts.fetchImpl).mock.calls[0]?.[0]).toContain("/releases/download/v15.0.0/WebDriverAgentRunner-Runner.zip");
|
|
73
|
+
expect(existsSync(join(cacheDir, "WebDriverAgentRunner-Runner.zip"))).toBe(true);
|
|
74
|
+
expect(opts.resign).toHaveBeenCalledWith(expect.objectContaining({
|
|
75
|
+
identityHash: "A".repeat(40),
|
|
76
|
+
profilePath: "/profiles/wda.mobileprovision",
|
|
77
|
+
bundleId: "com.aximo.wda.testteam99.xctrunner",
|
|
78
|
+
}));
|
|
79
|
+
expect(opts.install).toHaveBeenCalledWith("UDID-1", expect.stringContaining("wda-resigned"), dittoExec);
|
|
80
|
+
// Second run: cache hit, no new download.
|
|
81
|
+
await preinstallWda(opts);
|
|
82
|
+
expect(opts.fetchImpl).toHaveBeenCalledOnce();
|
|
83
|
+
});
|
|
84
|
+
it("throws a clear error when the release asset is missing (404)", async () => {
|
|
85
|
+
const opts = {
|
|
86
|
+
...makeOptions(),
|
|
87
|
+
fetchImpl: vi.fn(async () => new Response("nope", { status: 404 })),
|
|
88
|
+
};
|
|
89
|
+
await expect(preinstallWda(opts)).rejects.toThrow(/No prebuilt WebDriverAgent for v15\.0\.0/);
|
|
90
|
+
});
|
|
91
|
+
it("throws when the WDA version cannot be determined", async () => {
|
|
92
|
+
const opts = { ...makeOptions(), wdaVersion: null };
|
|
93
|
+
await expect(preinstallWda(opts)).rejects.toThrow(/aximo mobile setup/);
|
|
94
|
+
});
|
|
95
|
+
it("fails loudly when the zip has an unexpected layout", async () => {
|
|
96
|
+
const badDitto = async (file, args) => {
|
|
97
|
+
if (file === "ditto" && args[0] === "-x") {
|
|
98
|
+
const dest = args[3];
|
|
99
|
+
if (dest)
|
|
100
|
+
mkdirSync(join(dest, "SomethingElse.app"), { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
return { stdout: Buffer.from(""), stderr: "" };
|
|
103
|
+
};
|
|
104
|
+
const opts = { ...makeOptions(), exec: badDitto };
|
|
105
|
+
await expect(preinstallWda(opts)).rejects.toThrow(/did not contain/);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map raw device-control / install failure signatures (from Appium's XCUITest
|
|
3
|
+
* driver, xcodebuild, idb, and adb) to actionable, human-readable hints. These
|
|
4
|
+
* cover the failures a real BYOD setup actually hits — untrusted developer certs,
|
|
5
|
+
* taken WDA bundle ids, missing Xcode platforms, unsigned .ipas, a downed tunnel,
|
|
6
|
+
* and the common adb install errors — so the JSON error envelope tells the user
|
|
7
|
+
* what to do next instead of surfacing an opaque toolchain error.
|
|
8
|
+
*/
|
|
9
|
+
/** Return an actionable hint for a known failure signature, or undefined. */
|
|
10
|
+
export declare function mobileErrorHint(message: string, platform: "ios" | "android"): string | undefined;
|
|
11
|
+
/** Append the matched hint to a raw error message (for contexts without an envelope). */
|
|
12
|
+
export declare function withMobileHint(message: string, platform: "ios" | "android"): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map raw device-control / install failure signatures (from Appium's XCUITest
|
|
3
|
+
* driver, xcodebuild, idb, and adb) to actionable, human-readable hints. These
|
|
4
|
+
* cover the failures a real BYOD setup actually hits — untrusted developer certs,
|
|
5
|
+
* taken WDA bundle ids, missing Xcode platforms, unsigned .ipas, a downed tunnel,
|
|
6
|
+
* and the common adb install errors — so the JSON error envelope tells the user
|
|
7
|
+
* what to do next instead of surfacing an opaque toolchain error.
|
|
8
|
+
*/
|
|
9
|
+
const IOS_RULES = [
|
|
10
|
+
{
|
|
11
|
+
test: /not been explicitly trusted|Developer App Certificate is not trusted/i,
|
|
12
|
+
hint: "Trust the developer certificate on the device: Settings → General → VPN & Device Management → tap your 'Apple Development' profile → Trust (the phone needs internet). Note: re-minting a provisioning profile resets this trust, so it can recur weekly on free Apple teams.",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
test: /cannot be registered to your development team|Failed Registering Bundle Identifier/i,
|
|
16
|
+
hint: "This WebDriverAgent bundle id is already taken. Set AXIMO_IOS_WDA_BUNDLE_ID to a unique id you own, e.g. com.<you>.WebDriverAgentRunner.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
test: /No profiles for|requires a development team|Signing for .* requires a development team|No signing certificate/i,
|
|
20
|
+
hint: "Add your Apple ID to Xcode (Settings → Accounts) so a development identity and profiles can be created; the CLI discovers them automatically. AXIMO_IOS_TEAM_ID is only needed to pick between multiple teams. Verify with `aximo mobile doctor`.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
test: /no accounts|No Apple ID accounts/i,
|
|
24
|
+
hint: "Sign into Xcode with your Apple ID (Xcode → Settings → Accounts) — profile minting requires an Xcode-managed account.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
test: /FairPlay-encrypted/i,
|
|
28
|
+
hint: "App Store downloads can't be re-signed. Use a development/ad-hoc build — the same .ipa you upload to a device farm works.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
test: /is not installed\. Please download and install the platform|iOS [\d.]+ is not installed|Unable to find a destination matching/i,
|
|
32
|
+
hint: "Install the matching iOS platform components: `xcodebuild -downloadPlatform iOS` (or Xcode → Settings → Components).",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
test: /Developer Mode disabled|Developer Mode is not enabled/i,
|
|
36
|
+
hint: "Enable Developer Mode on the device: Settings → Privacy & Security → Developer Mode → On, then restart.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
// WDA now runs on a dynamic wdaLocalPort, so match any refused port, plus the
|
|
40
|
+
// driver's RemoteXPC/tunnel-registry error family.
|
|
41
|
+
test: /Connection refused to port \d+|Tunnel registry port not found|RemoteXPC|tunnel is not available/i,
|
|
42
|
+
hint: "The iOS device tunnel isn't reachable. `aximo mobile connect` starts it (needs admin); confirm with `aximo mobile doctor`. Set AXIMO_IOS_SKIP_TUNNEL only if you manage it yourself.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
// "is locked" (not "is unlocked") plus XCUITest's "could not be, unlocked" phrasing.
|
|
46
|
+
test: /(device|screen).{0,30}\bis locked|could not be,? unlocked|passcode/i,
|
|
47
|
+
hint: "Unlock the iPhone and keep it unlocked while connecting.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
// App-under-test install failures (signature/provisioning mismatch).
|
|
51
|
+
test: /ApplicationVerificationFailed|code signature.*no longer supported|MismatchedApplicationIdentifierEntitlement|valid provisioning profile|0xe800801c|0xe8008015|0xe8008029/i,
|
|
52
|
+
hint: "The .ipa isn't signed for THIS device. The CLI re-signs automatically when a signing identity is available — run `aximo mobile doctor` to check, and make sure AXIMO_IOS_NO_RESIGN isn't set. App Store apps can't be sideloaded.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
// Generic WDA build/launch failure — used only when nothing more specific matched.
|
|
56
|
+
test: /Unable to launch WebDriverAgent|xcodebuild failed with code|WebDriverAgentRunner-Runner encountered an error/i,
|
|
57
|
+
hint: "WebDriverAgent failed to build/launch. Usual causes: the developer cert isn't trusted on the device, the WDA bundle id is taken (set AXIMO_IOS_WDA_BUNDLE_ID), or no Team ID is set. Run `aximo mobile doctor` and check Settings → VPN & Device Management on the phone.",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
const ANDROID_RULES = [
|
|
61
|
+
{
|
|
62
|
+
test: /INSTALL_FAILED_UPDATE_INCOMPATIBLE|signatures do not match/i,
|
|
63
|
+
hint: "A version signed with a different key is already installed. Uninstall it first: `adb uninstall <package>`.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
test: /INSTALL_FAILED_INSUFFICIENT_STORAGE/i,
|
|
67
|
+
hint: "The device is out of storage. Free up space and retry.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
test: /INSTALL_FAILED_OLDER_SDK|requires a newer sdk/i,
|
|
71
|
+
hint: "The app targets a newer Android version than the device runs. Use a compatible build or device.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
test: /no devices\/emulators found|device (unauthorized|offline)/i,
|
|
75
|
+
hint: "Reconnect the phone over USB, unlock it, and accept the RSA debugging prompt. Verify with `adb devices`.",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** Return an actionable hint for a known failure signature, or undefined. */
|
|
79
|
+
export function mobileErrorHint(message, platform) {
|
|
80
|
+
const rules = platform === "ios" ? IOS_RULES : ANDROID_RULES;
|
|
81
|
+
for (const rule of rules) {
|
|
82
|
+
if (rule.test.test(message))
|
|
83
|
+
return rule.hint;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Append the matched hint to a raw error message (for contexts without an envelope). */
|
|
88
|
+
export function withMobileHint(message, platform) {
|
|
89
|
+
const hint = mobileErrorHint(message, platform);
|
|
90
|
+
return hint ? `${message} — ${hint}` : message;
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|