@xenon-device-management/xenon 1.17.4 → 1.18.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/lib/package.json +1 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +13 -2
- package/lib/src/device-managers/ios/iosStreamDiagnostics.js +30 -0
- package/lib/src/services/InspectorService.js +35 -11
- package/lib/test/unit/inspector-hierarchy.spec.js +55 -0
- package/lib/test/unit/iosStreamDiagnostics.spec.js +49 -0
- package/package.json +1 -1
package/lib/package.json
CHANGED
|
@@ -692,6 +692,12 @@ let IOSStreamService = class IOSStreamService {
|
|
|
692
692
|
if (!fs_extra_1.default.existsSync(logDir))
|
|
693
693
|
fs_extra_1.default.mkdirSync(logDir);
|
|
694
694
|
const wdaRunLog = path_1.default.join(logDir, `runwda-${udid}.log`);
|
|
695
|
+
// Truncated per run, because this file is also the evidence the failure
|
|
696
|
+
// is classified from. Appending across runs meant a "Did not find test
|
|
697
|
+
// app" line from a day earlier was still matched today, so every later
|
|
698
|
+
// failure of any kind — including one where WDA was demonstrably
|
|
699
|
+
// installed — was reported as "WebDriverAgent is not installed".
|
|
700
|
+
fs_extra_1.default.writeFileSync(wdaRunLog, '');
|
|
695
701
|
(_a = session.wdaProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => fs_extra_1.default.appendFileSync(wdaRunLog, d));
|
|
696
702
|
(_b = session.wdaProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (d) => fs_extra_1.default.appendFileSync(wdaRunLog, d));
|
|
697
703
|
// 4. Wait for WDA to be ready
|
|
@@ -770,11 +776,16 @@ let IOSStreamService = class IOSStreamService {
|
|
|
770
776
|
}
|
|
771
777
|
if (((_c = session.wdaProcess) === null || _c === void 0 ? void 0 : _c.exitCode) !== null) {
|
|
772
778
|
const logContent = fs_extra_1.default.existsSync(wdaRunLog) ? fs_extra_1.default.readFileSync(wdaRunLog, 'utf8') : '';
|
|
773
|
-
//
|
|
774
|
-
//
|
|
779
|
+
// Both of these are permanent until a human acts, and they call for
|
|
780
|
+
// opposite actions — install the app, versus trust the app that is
|
|
781
|
+
// already installed. Surface which, rather than a raw exit code and
|
|
782
|
+
// a log tail.
|
|
775
783
|
if ((0, iosStreamDiagnostics_1.isMissingWdaError)(logContent)) {
|
|
776
784
|
throw new Error((0, iosStreamDiagnostics_1.missingWdaMessage)(udid));
|
|
777
785
|
}
|
|
786
|
+
if ((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(logContent)) {
|
|
787
|
+
throw new Error((0, iosStreamDiagnostics_1.wdaLaunchFailureMessage)(udid));
|
|
788
|
+
}
|
|
778
789
|
throw new Error(`WDA process exited with code ${(_d = session.wdaProcess) === null || _d === void 0 ? void 0 : _d.exitCode}. Log: ${logContent.slice(-200)}`);
|
|
779
790
|
}
|
|
780
791
|
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.isMissingWdaError = isMissingWdaError;
|
|
11
11
|
exports.missingWdaMessage = missingWdaMessage;
|
|
12
|
+
exports.isWdaLaunchFailure = isWdaLaunchFailure;
|
|
13
|
+
exports.wdaLaunchFailureMessage = wdaLaunchFailureMessage;
|
|
12
14
|
exports.classifyTunnelStderr = classifyTunnelStderr;
|
|
13
15
|
exports.isOwnStreamProcess = isOwnStreamProcess;
|
|
14
16
|
/**
|
|
@@ -27,6 +29,34 @@ function missingWdaMessage(udid) {
|
|
|
27
29
|
return (`WebDriverAgent is not installed on ${udid}. ` +
|
|
28
30
|
`Live streaming requires the WebDriverAgentRunner app — install WDA on this device to enable the mirror.`);
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* True when WDA is installed but the runner could not be *started*.
|
|
34
|
+
*
|
|
35
|
+
* go-ios reports the two conditions quite differently, and conflating them
|
|
36
|
+
* sends people to reinstall an app that is already there:
|
|
37
|
+
*
|
|
38
|
+
* missing: cannot get test app information: Did not find test app for '…'
|
|
39
|
+
* present: cannot start test runner: LaunchAppWithStdIo: failed to launch
|
|
40
|
+
* app: launchApp: failed to get PID
|
|
41
|
+
*
|
|
42
|
+
* The second is nearly always the device refusing to run the binary — an
|
|
43
|
+
* untrusted developer certificate, a locked screen, or a lapsed signature —
|
|
44
|
+
* none of which reinstalling addresses on its own.
|
|
45
|
+
*/
|
|
46
|
+
function isWdaLaunchFailure(logContent) {
|
|
47
|
+
if (!logContent)
|
|
48
|
+
return false;
|
|
49
|
+
return (/failed to launch app/i.test(logContent) ||
|
|
50
|
+
/LaunchAppWithStdIo/i.test(logContent) ||
|
|
51
|
+
/cannot start test runner/i.test(logContent));
|
|
52
|
+
}
|
|
53
|
+
/** Human-facing, actionable message for the installed-but-unlaunchable case. */
|
|
54
|
+
function wdaLaunchFailureMessage(udid) {
|
|
55
|
+
return (`WebDriverAgent is installed on ${udid} but would not start. ` +
|
|
56
|
+
`This is usually the device refusing to run it: trust the developer certificate under ` +
|
|
57
|
+
`Settings → General → VPN & Device Management, unlock the screen, and check the signing ` +
|
|
58
|
+
`profile has not expired. Launching WebDriverAgentRunner once by hand confirms it.`);
|
|
59
|
+
}
|
|
30
60
|
/**
|
|
31
61
|
* Classify a chunk of go-ios `tunnel start` stderr.
|
|
32
62
|
*
|
|
@@ -432,28 +432,46 @@ let InspectorService = class InspectorService {
|
|
|
432
432
|
score: 95,
|
|
433
433
|
});
|
|
434
434
|
}
|
|
435
|
+
// On iOS `node.text` is `label || value`, so every locator that selects on
|
|
436
|
+
// it — predicate and xpath alike — has to name whichever of the two
|
|
437
|
+
// actually supplied it. Hardcoding `label` sends a page indicator, whose
|
|
438
|
+
// text lives in `value`, to a predicate that matches nothing.
|
|
439
|
+
const iosTextAttr = asText(node.attributes.label) === node.text ? 'label' : 'value';
|
|
435
440
|
// 3. Platform Specific - Performance Tier
|
|
436
441
|
if (platform === 'ios') {
|
|
437
442
|
// iOS Predicate String - High Performance
|
|
438
|
-
const
|
|
443
|
+
const predicateValue = node.text || node.label || '';
|
|
439
444
|
if (node.text || node.label) {
|
|
440
445
|
suggestions.push({
|
|
441
446
|
strategy: '-ios predicate string',
|
|
442
|
-
value:
|
|
443
|
-
|
|
447
|
+
value: `type == "${node.type}" AND ${iosTextAttr} == "${predicateValue}"`,
|
|
448
|
+
// A compound predicate is at least as selective as its label term, so
|
|
449
|
+
// a unique label makes it unique. The converse does not hold — two
|
|
450
|
+
// elements sharing a label may differ by type — which makes this
|
|
451
|
+
// conservative rather than exact, and that is the right direction:
|
|
452
|
+
// it was previously hardcoded true, and a predicate badged unique
|
|
453
|
+
// was measured against a real device matching two elements.
|
|
454
|
+
unique: this.isUnique(uniqueness, 'text', predicateValue),
|
|
444
455
|
score: 90,
|
|
445
456
|
});
|
|
446
457
|
}
|
|
447
458
|
// iOS Class Chain - High Precision
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
459
|
+
//
|
|
460
|
+
// The predicate MUST be wrapped in backticks. Without them Appium
|
|
461
|
+
// resolves nothing at all: measured against a real iPhone,
|
|
462
|
+
// `**/XCUIElementTypePageIndicator[name == "Page control"]` found 0
|
|
463
|
+
// elements and the same chain with backticks found 1. Every class chain
|
|
464
|
+
// this service produced was unusable, and each was badged unique — 280
|
|
465
|
+
// of them in a single snapshot of a home screen.
|
|
466
|
+
const chainValue = node.attributes.name ? String(node.attributes.name) : node.text || '';
|
|
467
|
+
const chainTerm = node.attributes.name ? 'name' : 'label';
|
|
453
468
|
suggestions.push({
|
|
454
469
|
strategy: '-ios class chain',
|
|
455
|
-
value:
|
|
456
|
-
|
|
470
|
+
value: `**/${node.type}[\`${chainTerm} == "${chainValue}"\`]`,
|
|
471
|
+
// Same index the accessibility-id suggestion uses, and the same value:
|
|
472
|
+
// a node's identity there is `name || label`, which is exactly what
|
|
473
|
+
// the chain selects on.
|
|
474
|
+
unique: this.isUnique(uniqueness, 'accessibility id', chainValue),
|
|
457
475
|
score: 85,
|
|
458
476
|
});
|
|
459
477
|
}
|
|
@@ -495,9 +513,15 @@ let InspectorService = class InspectorService {
|
|
|
495
513
|
});
|
|
496
514
|
}
|
|
497
515
|
else if (node.text) {
|
|
516
|
+
// `@text` is an Android attribute. An XCUIElement has name, label and
|
|
517
|
+
// value and no text at all, so every iOS suggestion in this shape
|
|
518
|
+
// resolved to nothing: measured on a real iPhone,
|
|
519
|
+
// `//XCUIElementTypePageIndicator[@text="Page control"]` found 0 and the
|
|
520
|
+
// same path with `@label` found 1.
|
|
521
|
+
const textAttr = platform === 'ios' ? iosTextAttr : 'text';
|
|
498
522
|
suggestions.push({
|
|
499
523
|
strategy: 'xpath',
|
|
500
|
-
value: `//${tagName}[
|
|
524
|
+
value: `//${tagName}[@${textAttr}="${node.text}"]`,
|
|
501
525
|
unique: this.isUnique(uniqueness, 'text', node.text),
|
|
502
526
|
score: 60,
|
|
503
527
|
});
|
|
@@ -197,6 +197,61 @@ describe('InspectorService hierarchy parsing', () => {
|
|
|
197
197
|
(0, chai_1.expect)(root.name).to.equal('1');
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
|
+
describe('iOS locator syntax', () => {
|
|
201
|
+
// Two StaticTexts share the label "Done" and neither has a name, so both a
|
|
202
|
+
// predicate and a class chain built from that label are genuinely
|
|
203
|
+
// ambiguous. The button carries a name and is genuinely unique. The text
|
|
204
|
+
// field has a value and no label.
|
|
205
|
+
const XML = `<AppiumAUT>
|
|
206
|
+
<XCUIElementTypeApplication type="XCUIElementTypeApplication" name="MyApp" label="MyApp" x="0" y="0" width="390" height="844">
|
|
207
|
+
<XCUIElementTypeButton type="XCUIElementTypeButton" name="done-btn" label="Save" x="0" y="0" width="10" height="10" />
|
|
208
|
+
<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" label="Done" x="0" y="20" width="10" height="10" />
|
|
209
|
+
<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" label="Done" x="0" y="30" width="10" height="10" />
|
|
210
|
+
<XCUIElementTypeTextField type="XCUIElementTypeTextField" value="typed text" x="0" y="40" width="10" height="10" />
|
|
211
|
+
</XCUIElementTypeApplication>
|
|
212
|
+
</AppiumAUT>`;
|
|
213
|
+
const root = () => parse(XML, 'ios');
|
|
214
|
+
const byType = (t) => find(root(), (n) => n.type === t)[0];
|
|
215
|
+
it('wraps the class chain predicate in backticks', () => {
|
|
216
|
+
// Measured on a real iPhone: without backticks Appium resolves 0
|
|
217
|
+
// elements, with them it resolves 1. Every chain this produced was
|
|
218
|
+
// unusable — 280 of them in one home-screen snapshot.
|
|
219
|
+
(0, chai_1.expect)(locator(byType('XCUIElementTypeButton'), '-ios class chain').value).to.equal('**/XCUIElementTypeButton[`name == "done-btn"`]');
|
|
220
|
+
});
|
|
221
|
+
it('selects on label in the class chain when there is no name', () => {
|
|
222
|
+
(0, chai_1.expect)(locator(byType('XCUIElementTypeStaticText'), '-ios class chain').value).to.equal('**/XCUIElementTypeStaticText[`label == "Done"`]');
|
|
223
|
+
});
|
|
224
|
+
it('uses an attribute an XCUIElement actually has in xpath', () => {
|
|
225
|
+
// `@text` is Android's. iOS has name, label and value — and `@text`
|
|
226
|
+
// resolved to nothing on a real device.
|
|
227
|
+
const xpath = locator(byType('XCUIElementTypeStaticText'), 'xpath').value;
|
|
228
|
+
(0, chai_1.expect)(xpath).to.equal('//XCUIElementTypeStaticText[@label="Done"]');
|
|
229
|
+
(0, chai_1.expect)(xpath).to.not.contain('@text');
|
|
230
|
+
});
|
|
231
|
+
it('names value rather than label when value is what supplied the text', () => {
|
|
232
|
+
(0, chai_1.expect)(locator(byType('XCUIElementTypeTextField'), 'xpath').value).to.equal('//XCUIElementTypeTextField[@value="typed text"]');
|
|
233
|
+
});
|
|
234
|
+
it('names the same attribute in the predicate as in the xpath', () => {
|
|
235
|
+
// A page indicator carries its text in `value` and has no label at all.
|
|
236
|
+
// The predicate hardcoded `label ==`, so it matched nothing — measured
|
|
237
|
+
// against a real iPhone as the last surviving miss after the other
|
|
238
|
+
// fixes landed.
|
|
239
|
+
const field = byType('XCUIElementTypeTextField');
|
|
240
|
+
(0, chai_1.expect)(locator(field, '-ios predicate string').value).to.equal('type == "XCUIElementTypeTextField" AND value == "typed text"');
|
|
241
|
+
(0, chai_1.expect)(locator(byType('XCUIElementTypeStaticText'), '-ios predicate string').value).to.equal('type == "XCUIElementTypeStaticText" AND label == "Done"');
|
|
242
|
+
});
|
|
243
|
+
it('stops claiming every predicate and class chain is unique', () => {
|
|
244
|
+
// Both were hardcoded true. On a real device a predicate badged unique
|
|
245
|
+
// matched two elements.
|
|
246
|
+
const shared = byType('XCUIElementTypeStaticText'); // label "Done", shared
|
|
247
|
+
(0, chai_1.expect)(locator(shared, '-ios predicate string').unique).to.equal(false);
|
|
248
|
+
(0, chai_1.expect)(locator(shared, '-ios class chain').unique).to.equal(false);
|
|
249
|
+
});
|
|
250
|
+
it('still reports a genuinely unique one as unique', () => {
|
|
251
|
+
const named = byType('XCUIElementTypeButton'); // name "done-btn", unique
|
|
252
|
+
(0, chai_1.expect)(locator(named, '-ios class chain').unique).to.equal(true);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
200
255
|
describe('iOS', () => {
|
|
201
256
|
it('parses the XCUIElementType tree unchanged', () => {
|
|
202
257
|
const root = parse(IOS_XML, 'ios');
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const chai_1 = require("chai");
|
|
4
4
|
const iosStreamDiagnostics_1 = require("../../src/device-managers/ios/iosStreamDiagnostics");
|
|
5
|
+
/**
|
|
6
|
+
* Both verbatim from a real go-ios run log, and the whole point is that they
|
|
7
|
+
* mean opposite things: reinstall the app, versus trust the app that is
|
|
8
|
+
* already installed.
|
|
9
|
+
*/
|
|
10
|
+
const GO_IOS_MISSING = `{"level":"ERROR","msg":"Failed running WDA","error":"runXUITestWithBundleIdsXcode15Ctx: ` +
|
|
11
|
+
`cannot get test app information: Did not find test app for ` +
|
|
12
|
+
`'com.qasecret.WebDriverAgentRunner.xctrunner' on device. Is it installed?"}`;
|
|
13
|
+
const GO_IOS_WONT_LAUNCH = `{"level":"ERROR","msg":"Failed running WDA","error":"runXUITestWithBundleIdsXcode15Ctx: ` +
|
|
14
|
+
`cannot start test runner: LaunchAppWithStdIo: failed to launch app: launchApp: ` +
|
|
15
|
+
`failed to get PID: pidFromResponse: could not get pid"}`;
|
|
5
16
|
describe('iosStreamDiagnostics', () => {
|
|
6
17
|
describe('isMissingWdaError', () => {
|
|
7
18
|
it('detects the go-ios "Did not find test app" WDA crash', () => {
|
|
@@ -25,6 +36,44 @@ describe('iosStreamDiagnostics', () => {
|
|
|
25
36
|
(0, chai_1.expect)(msg.toLowerCase()).to.contain('install');
|
|
26
37
|
});
|
|
27
38
|
});
|
|
39
|
+
describe('telling "not installed" apart from "would not start"', () => {
|
|
40
|
+
it('reads the real missing-app log as missing', () => {
|
|
41
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(GO_IOS_MISSING)).to.equal(true);
|
|
42
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(GO_IOS_MISSING)).to.equal(false);
|
|
43
|
+
});
|
|
44
|
+
it('reads the real launch-failure log as a launch failure, not as missing', () => {
|
|
45
|
+
// This is the one that was reported as "WebDriverAgent is not installed"
|
|
46
|
+
// against a device where it demonstrably was installed, sending the
|
|
47
|
+
// reader off to reinstall an app that was already there.
|
|
48
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(GO_IOS_WONT_LAUNCH)).to.equal(true);
|
|
49
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(GO_IOS_WONT_LAUNCH)).to.equal(false);
|
|
50
|
+
});
|
|
51
|
+
it('gives the two cases opposite advice', () => {
|
|
52
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.missingWdaMessage)('udid-1')).to.contain('not installed');
|
|
53
|
+
const launch = (0, iosStreamDiagnostics_1.wdaLaunchFailureMessage)('udid-1');
|
|
54
|
+
(0, chai_1.expect)(launch).to.contain('is installed');
|
|
55
|
+
(0, chai_1.expect)(launch).to.contain('VPN & Device Management');
|
|
56
|
+
(0, chai_1.expect)(launch).to.not.contain('not installed');
|
|
57
|
+
});
|
|
58
|
+
it('is safe on empty input', () => {
|
|
59
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)('')).to.equal(false);
|
|
60
|
+
});
|
|
61
|
+
it('is why the run log must be truncated per run', () => {
|
|
62
|
+
// These classifiers read a file, and the file used to be appended to
|
|
63
|
+
// across runs. A stale "Did not find test app" from a previous day still
|
|
64
|
+
// wins over today's launch failure — which is exactly how an installed
|
|
65
|
+
// WDA came to be reported as missing. IOSStreamService now empties the
|
|
66
|
+
// log before each spawn; this pins what happens if it stops.
|
|
67
|
+
const stale = `${GO_IOS_MISSING}\n${GO_IOS_WONT_LAUNCH}`;
|
|
68
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(stale)).to.equal(true);
|
|
69
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(stale)).to.equal(true);
|
|
70
|
+
});
|
|
71
|
+
it('does not flag an unrelated crash as either', () => {
|
|
72
|
+
const crash = 'WDA crashed: signal 11 (SIGSEGV)';
|
|
73
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(crash)).to.equal(false);
|
|
74
|
+
(0, chai_1.expect)((0, iosStreamDiagnostics_1.isWdaLaunchFailure)(crash)).to.equal(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
28
77
|
describe('classifyTunnelStderr', () => {
|
|
29
78
|
it('extracts the real target udid from the go-ios JSON payload', () => {
|
|
30
79
|
const line = JSON.stringify({
|
package/package.json
CHANGED