@xenon-device-management/xenon 2.15.0 → 2.16.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 +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +26 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +14 -6
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +32 -18
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +5 -8
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +11 -2
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +1 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.SESSION_MANAGER = exports.SessionManager =
|
|
54
|
+
exports.SESSION_MANAGER = exports.SessionManager = void 0;
|
|
55
55
|
const os_1 = __importDefault(require("os"));
|
|
56
56
|
const typedi_1 = require("typedi");
|
|
57
57
|
const prisma_1 = require("../prisma");
|
|
@@ -62,12 +62,9 @@ const device_store_1 = require("../data-service/device-store");
|
|
|
62
62
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
63
63
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
64
64
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* other category (`ANALYSIS_CATEGORIES`).
|
|
69
|
-
*/
|
|
70
|
-
exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
|
|
65
|
+
// A session still running at boot that can't be picked up again is filed
|
|
66
|
+
// under it; the failure analysis also writes it for a shutdown's drain.
|
|
67
|
+
const failureCategories_1 = require("../dashboard/services/failureCategories");
|
|
71
68
|
/**
|
|
72
69
|
* SessionManager with persistence and recovery capabilities.
|
|
73
70
|
*
|
|
@@ -312,7 +309,7 @@ let SessionManager = class SessionManager {
|
|
|
312
309
|
status: 'failed',
|
|
313
310
|
endTime: new Date(),
|
|
314
311
|
failure_reason: reason,
|
|
315
|
-
failure_category:
|
|
312
|
+
failure_category: failureCategories_1.HUB_RESTART_CATEGORY,
|
|
316
313
|
},
|
|
317
314
|
});
|
|
318
315
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TestDriverLog = void 0;
|
|
13
|
+
const line_consuming_log_1 = require("appium-xcuitest-driver/build/lib/device/log/line-consuming-log");
|
|
14
|
+
const quietLog = { debug() { }, info() { }, warn() { }, error() { } };
|
|
15
|
+
/**
|
|
16
|
+
* The XCUITest driver's own log class, as an iPhone's (`IOSDeviceLog`) and a
|
|
17
|
+
* simulator's (`IOSSimulatorLog`) `logs.syslog` extend it, with `line()` in
|
|
18
|
+
* place of a device: what Xenon reads is what the driver keeps.
|
|
19
|
+
*/
|
|
20
|
+
class TestDriverLog extends line_consuming_log_1.LineConsumingLog {
|
|
21
|
+
constructor() {
|
|
22
|
+
super({ log: quietLog });
|
|
23
|
+
}
|
|
24
|
+
startCapture() {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () { });
|
|
26
|
+
}
|
|
27
|
+
stopCapture() {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () { });
|
|
29
|
+
}
|
|
30
|
+
get isCapturing() {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** A line from the device, as the driver's capture hands it on. */
|
|
34
|
+
line(message) {
|
|
35
|
+
this.broadcast(message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.TestDriverLog = TestDriverLog;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.startFakeProxy = startFakeProxy;
|
|
16
|
+
const http_1 = __importDefault(require("http"));
|
|
17
|
+
const net_1 = __importDefault(require("net"));
|
|
18
|
+
/**
|
|
19
|
+
* An HTTP proxy on 127.0.0.1 that forwards what it is sent and notes it: a
|
|
20
|
+
* request in absolute form (http-proxy-agent, axios) is passed on to its
|
|
21
|
+
* target, a CONNECT (https-proxy-agent, so WebSockets) becomes a tunnel, or
|
|
22
|
+
* is refused with `refuseConnect`. A spec can then tell a call that went
|
|
23
|
+
* through the proxy from one that went straight to the target.
|
|
24
|
+
*/
|
|
25
|
+
function startFakeProxy() {
|
|
26
|
+
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
27
|
+
const seen = [];
|
|
28
|
+
const authorizations = [];
|
|
29
|
+
const sockets = new Set();
|
|
30
|
+
const resolve = (host) => { var _a, _b; return (_b = (_a = opts.hosts) === null || _a === void 0 ? void 0 : _a[host]) !== null && _b !== void 0 ? _b : host; };
|
|
31
|
+
const server = http_1.default.createServer((req, res) => {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
seen.push(`${req.method} ${req.url}`);
|
|
34
|
+
authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
|
|
35
|
+
let target;
|
|
36
|
+
try {
|
|
37
|
+
target = new URL((_b = req.url) !== null && _b !== void 0 ? _b : '');
|
|
38
|
+
}
|
|
39
|
+
catch (_c) {
|
|
40
|
+
res.writeHead(400).end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const upstream = http_1.default.request(Object.assign(Object.assign({}, urlParts(target)), { hostname: resolve(target.hostname), method: req.method, headers: req.headers }), (answer) => {
|
|
44
|
+
var _a;
|
|
45
|
+
res.writeHead((_a = answer.statusCode) !== null && _a !== void 0 ? _a : 502, answer.headers);
|
|
46
|
+
answer.pipe(res);
|
|
47
|
+
});
|
|
48
|
+
upstream.on('error', () => res.writeHead(502).end());
|
|
49
|
+
req.pipe(upstream);
|
|
50
|
+
});
|
|
51
|
+
server.on('connect', (req, client, head) => {
|
|
52
|
+
var _a;
|
|
53
|
+
seen.push(`CONNECT ${req.url}`);
|
|
54
|
+
authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
|
|
55
|
+
if (opts.ignoreConnect) {
|
|
56
|
+
sockets.add(client);
|
|
57
|
+
client.on('error', () => client.destroy());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (opts.refuseConnect) {
|
|
61
|
+
client.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const [host, port] = String(req.url).split(':');
|
|
65
|
+
const upstream = net_1.default.connect(Number(port), resolve(host), () => {
|
|
66
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
67
|
+
if (head.length)
|
|
68
|
+
upstream.write(head);
|
|
69
|
+
upstream.pipe(client);
|
|
70
|
+
client.pipe(upstream);
|
|
71
|
+
});
|
|
72
|
+
sockets.add(upstream).add(client);
|
|
73
|
+
const drop = () => {
|
|
74
|
+
upstream.destroy();
|
|
75
|
+
client.destroy();
|
|
76
|
+
};
|
|
77
|
+
upstream.on('error', drop);
|
|
78
|
+
client.on('error', drop);
|
|
79
|
+
});
|
|
80
|
+
yield new Promise((resolve, reject) => {
|
|
81
|
+
server.once('error', reject);
|
|
82
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
83
|
+
});
|
|
84
|
+
const { port } = server.address();
|
|
85
|
+
return {
|
|
86
|
+
url: `http://127.0.0.1:${port}`,
|
|
87
|
+
seen,
|
|
88
|
+
authorizations,
|
|
89
|
+
close: () => new Promise((resolve) => {
|
|
90
|
+
var _a;
|
|
91
|
+
for (const socket of sockets)
|
|
92
|
+
socket.destroy();
|
|
93
|
+
(_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
|
|
94
|
+
server.close(() => resolve());
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function urlParts(url) {
|
|
100
|
+
return {
|
|
101
|
+
protocol: url.protocol,
|
|
102
|
+
hostname: url.hostname,
|
|
103
|
+
port: url.port,
|
|
104
|
+
path: `${url.pathname}${url.search}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.startOtlpProbe = startOtlpProbe;
|
|
16
|
+
exports.attributeOf = attributeOf;
|
|
17
|
+
exports.resetOtelGlobals = resetOtelGlobals;
|
|
18
|
+
exports.flushOtelGlobals = flushOtelGlobals;
|
|
19
|
+
const http_1 = __importDefault(require("http"));
|
|
20
|
+
const api_1 = require("@opentelemetry/api");
|
|
21
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
22
|
+
/**
|
|
23
|
+
* A stand-in OTLP/HTTP collector on 127.0.0.1. It answers every request 200
|
|
24
|
+
* and keeps what it was sent, so a spec can see where Xenon's telemetry went
|
|
25
|
+
* and in which encoding (Xenon's own exporters send OTLP/JSON; the ones the
|
|
26
|
+
* OTel SDK builds from OTEL_* variables send protobuf).
|
|
27
|
+
*/
|
|
28
|
+
function startOtlpProbe() {
|
|
29
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30
|
+
const requests = [];
|
|
31
|
+
const server = http_1.default.createServer((req, res) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
34
|
+
req.on('end', () => {
|
|
35
|
+
var _a, _b;
|
|
36
|
+
const body = Buffer.concat(chunks);
|
|
37
|
+
const contentType = String((_a = req.headers['content-type']) !== null && _a !== void 0 ? _a : '');
|
|
38
|
+
let json;
|
|
39
|
+
if (contentType.includes('json')) {
|
|
40
|
+
try {
|
|
41
|
+
json = JSON.parse(body.toString('utf8'));
|
|
42
|
+
}
|
|
43
|
+
catch (_c) {
|
|
44
|
+
json = undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
requests.push({ path: (_b = req.url) !== null && _b !== void 0 ? _b : '', contentType, body, json });
|
|
48
|
+
if (contentType.includes('json')) {
|
|
49
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
50
|
+
res.end('{}');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
res.writeHead(200, { 'content-type': 'application/x-protobuf' });
|
|
54
|
+
res.end();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
yield new Promise((resolve, reject) => {
|
|
59
|
+
server.once('error', reject);
|
|
60
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
61
|
+
});
|
|
62
|
+
const { port } = server.address();
|
|
63
|
+
return {
|
|
64
|
+
url: (path) => `http://127.0.0.1:${port}${path}`,
|
|
65
|
+
requests,
|
|
66
|
+
spans: () => requests.flatMap((r) => {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
return ((_b = (_a = r.json) === null || _a === void 0 ? void 0 : _a.resourceSpans) !== null && _b !== void 0 ? _b : []).flatMap((rs) => { var _a; return ((_a = rs.scopeSpans) !== null && _a !== void 0 ? _a : []).flatMap((ss) => { var _a; return ((_a = ss.spans) !== null && _a !== void 0 ? _a : []); }); });
|
|
69
|
+
}),
|
|
70
|
+
close: () => new Promise((resolve) => {
|
|
71
|
+
var _a;
|
|
72
|
+
(_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
|
|
73
|
+
server.close(() => resolve());
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** A span attribute's value, whatever its OTLP/JSON type. */
|
|
79
|
+
function attributeOf(attributes, key) {
|
|
80
|
+
const found = attributes === null || attributes === void 0 ? void 0 : attributes.find((a) => a.key === key);
|
|
81
|
+
if (!found)
|
|
82
|
+
return undefined;
|
|
83
|
+
return Object.values(found.value)[0];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Takes every OTel global (tracer, meter and logger providers, context
|
|
87
|
+
* manager, propagator) off the API, so the next registration wins. The API
|
|
88
|
+
* keeps the first registration for the life of the process and refuses the
|
|
89
|
+
* rest: a spec that leaves one behind decides what every later spec exports.
|
|
90
|
+
*/
|
|
91
|
+
function resetOtelGlobals() {
|
|
92
|
+
api_1.trace.disable();
|
|
93
|
+
api_1.metrics.disable();
|
|
94
|
+
api_logs_1.logs.disable();
|
|
95
|
+
api_1.context.disable();
|
|
96
|
+
api_1.propagation.disable();
|
|
97
|
+
}
|
|
98
|
+
/** Exports whatever the registered meter and logger providers hold now. */
|
|
99
|
+
function flushOtelGlobals() {
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
const meterProvider = api_1.metrics.getMeterProvider();
|
|
103
|
+
yield ((_a = meterProvider.forceFlush) === null || _a === void 0 ? void 0 : _a.call(meterProvider));
|
|
104
|
+
const loggerProvider = api_logs_1.logs.getLoggerProvider();
|
|
105
|
+
yield ((_b = loggerProvider.forceFlush) === null || _b === void 0 ? void 0 : _b.call(loggerProvider));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -133,6 +133,6 @@ describe('Android Test', () => {
|
|
|
133
133
|
};
|
|
134
134
|
yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 1000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
|
|
135
135
|
.to.be.an('error')
|
|
136
|
-
.with.property('message', 'Device is busy or blocked
|
|
136
|
+
.with.property('message', 'Device is busy or blocked. Device request: {"platform":"android"}'));
|
|
137
137
|
}));
|
|
138
138
|
});
|
|
@@ -145,7 +145,7 @@ describe('Max sessions CLI argument test', () => {
|
|
|
145
145
|
};
|
|
146
146
|
yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 6000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
|
|
147
147
|
.to.be.an('error')
|
|
148
|
-
.with.property('message', 'Device is busy or blocked
|
|
148
|
+
.with.property('message', 'Device is busy or blocked. Device request: {"platform":"ios","name":"iPhone","deviceType":"simulator"}'));
|
|
149
149
|
}));
|
|
150
150
|
});
|
|
151
151
|
describe('IOS Simulator Test', () => {
|
|
@@ -53,7 +53,7 @@ describe('IOS Test', () => {
|
|
|
53
53
|
};
|
|
54
54
|
yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 6000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
|
|
55
55
|
.to.be.an('error')
|
|
56
|
-
.with.property('message', 'No device matching request
|
|
56
|
+
.with.property('message', 'No device matching request. Device request: {"platform":"ios","name":"iPhone","deviceType":"real"}'));
|
|
57
57
|
}));
|
|
58
58
|
it('Should throw error if the IPA does not match with device type real', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
59
|
yield (0, device_utils_1.initializeStorage)();
|
|
@@ -105,7 +105,7 @@ describe('Selector state full flow (integration)', function () {
|
|
|
105
105
|
stdio: 'pipe',
|
|
106
106
|
});
|
|
107
107
|
prisma = new client_1.PrismaClient({ datasources: { db: { url } } });
|
|
108
|
-
socketStub = {
|
|
108
|
+
socketStub = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
|
|
109
109
|
stateService = new SelectorStateService_1.SelectorStateService(prisma, socketStub);
|
|
110
110
|
verifier = new SelectorVerificationJob_1.SelectorVerificationJob(prisma, socketStub);
|
|
111
111
|
}));
|
|
@@ -116,7 +116,7 @@ describe('Selector state full flow (integration)', function () {
|
|
|
116
116
|
fs.unlinkSync(dbPath);
|
|
117
117
|
}));
|
|
118
118
|
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
119
|
-
socketStub.
|
|
119
|
+
socketStub.emitToDashboardForSelector.resetHistory();
|
|
120
120
|
// SessionLog → Session → Build — delete children before parents.
|
|
121
121
|
yield prisma.sessionLog.deleteMany({});
|
|
122
122
|
yield prisma.session.deleteMany({});
|
|
@@ -156,7 +156,7 @@ describe('Selector state full flow (integration)', function () {
|
|
|
156
156
|
});
|
|
157
157
|
(0, chai_1.expect)(fixed.status).to.equal('pending');
|
|
158
158
|
(0, chai_1.expect)(fixed.fixed_at).to.be.instanceOf(Date);
|
|
159
|
-
(0, chai_1.expect)(socketStub.
|
|
159
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
|
|
160
160
|
// Three distinct CI builds, each with one un-healed findElement on the tuple.
|
|
161
161
|
for (const buildId of ['b-1', 'b-2', 'b-3']) {
|
|
162
162
|
const sid = `s-${buildId}`;
|
|
@@ -190,7 +190,7 @@ describe('Selector state full flow (integration)', function () {
|
|
|
190
190
|
(0, chai_1.expect)(final.status).to.equal('resolved');
|
|
191
191
|
(0, chai_1.expect)(final.clean_builds_count).to.equal(3);
|
|
192
192
|
(0, chai_1.expect)(final.resolved_at).to.be.instanceOf(Date);
|
|
193
|
-
const resolvedEmits = socketStub.
|
|
193
|
+
const resolvedEmits = socketStub.emitToDashboardForSelector
|
|
194
194
|
.getCalls()
|
|
195
195
|
.filter((c) => c.args[0] === SocketEvents_1.SocketEvents.SELECTOR_RESOLVED);
|
|
196
196
|
(0, chai_1.expect)(resolvedEmits.length).to.equal(1);
|
|
@@ -226,6 +226,6 @@ describe('Selector state full flow (integration)', function () {
|
|
|
226
226
|
(0, chai_1.expect)(after.regression_count).to.equal(1);
|
|
227
227
|
(0, chai_1.expect)(after.resolved_at).to.be.null;
|
|
228
228
|
(0, chai_1.expect)(after.fixed_at).to.be.null;
|
|
229
|
-
(0, chai_1.expect)(socketStub.
|
|
229
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
|
|
230
230
|
}));
|
|
231
231
|
});
|
|
@@ -60,3 +60,26 @@ describe('XenonCapabilityManager.getXenonCapabilities — interceptor activation
|
|
|
60
60
|
expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(true);
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
/**
|
|
64
|
+
* `xe:save_device_logs` is a switch like `xe:record_video`: on by default,
|
|
65
|
+
* on only for `true` or "true" once set. Through 2.15 it was read nowhere.
|
|
66
|
+
*/
|
|
67
|
+
describe('XenonCapabilityManager.getXenonCapabilities — xe:save_device_logs', () => {
|
|
68
|
+
const saved = (always) => (0, XenonCapabilityManager_1.getXenonCapabilities)(caps(always))[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS];
|
|
69
|
+
it('is on when the session says nothing', () => {
|
|
70
|
+
expect(saved({})).to.equal(true);
|
|
71
|
+
});
|
|
72
|
+
it('is on for true or "true"', () => {
|
|
73
|
+
expect(saved({ 'xe:save_device_logs': true })).to.equal(true);
|
|
74
|
+
expect(saved({ 'xe:saveDeviceLogs': 'true' })).to.equal(true);
|
|
75
|
+
});
|
|
76
|
+
it('is off for false, and for any other value, as every switch is', () => {
|
|
77
|
+
for (const value of [false, 'false', 'yes', 1]) {
|
|
78
|
+
expect(saved({ 'xe:save_device_logs': value }), String(value)).to.equal(false);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it('is read from xe:options and its xenon:options alias too', () => {
|
|
82
|
+
expect(saved({ 'xe:options': { save_device_logs: false } })).to.equal(false);
|
|
83
|
+
expect(saved({ 'xenon:options': { saveDeviceLogs: false } })).to.equal(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const adbkit_1 = __importDefault(require("@devicefarmer/adbkit"));
|
|
19
|
+
const appium_adb_1 = require("appium-adb");
|
|
20
|
+
const test_container_1 = require("../helpers/test-container");
|
|
21
|
+
const RETRY_MS = 60000;
|
|
22
|
+
/**
|
|
23
|
+
* When adb couldn't be started, or its device tracking couldn't, the server
|
|
24
|
+
* logged "Could not find ADB" without the reason and never looked again: every
|
|
25
|
+
* later discovery answered no phones until a restart, even once adb worked
|
|
26
|
+
* (an SDK installed, a stuck adb server killed). It now says why, and tries
|
|
27
|
+
* again a minute later.
|
|
28
|
+
*/
|
|
29
|
+
describe('Android discovery when adb is unavailable', () => {
|
|
30
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
31
|
+
let manager;
|
|
32
|
+
let createADB;
|
|
33
|
+
let trackDevices;
|
|
34
|
+
let errors;
|
|
35
|
+
let clock;
|
|
36
|
+
let savedNodeEnv;
|
|
37
|
+
const workingAdb = () => ({
|
|
38
|
+
adbRemoteHost: null,
|
|
39
|
+
getConnectedDevices: sandbox.stub().resolves([]),
|
|
40
|
+
});
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
savedNodeEnv = process.env.NODE_ENV;
|
|
43
|
+
clock = sandbox.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
|
|
44
|
+
createADB = sandbox.stub(appium_adb_1.ADB, 'createADB');
|
|
45
|
+
trackDevices = sandbox.stub().resolves({ on: sandbox.stub() });
|
|
46
|
+
sandbox.stub(adbkit_1.default, 'createClient').returns({ trackDevices });
|
|
47
|
+
manager = (0, test_container_1.createTestAndroidManager)({ platform: 'android' });
|
|
48
|
+
sandbox.stub(manager, 'requireSdkRoot').resolves();
|
|
49
|
+
errors = sandbox.stub(manager.log, 'error');
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
sandbox.restore();
|
|
53
|
+
if (savedNodeEnv === undefined)
|
|
54
|
+
delete process.env.NODE_ENV;
|
|
55
|
+
else
|
|
56
|
+
process.env.NODE_ENV = savedNodeEnv;
|
|
57
|
+
yield (0, test_container_1.resetTestContainer)();
|
|
58
|
+
}));
|
|
59
|
+
const discover = () => manager.getDevices({ androidDeviceType: 'both' }, []);
|
|
60
|
+
it('says why adb could not be started', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
createADB.rejects(new Error('Could not find adb. spawn adb ENOENT'));
|
|
62
|
+
(0, chai_1.expect)(yield discover()).to.deep.equal([]);
|
|
63
|
+
const logged = errors.getCalls().map((c) => String(c.args[0]));
|
|
64
|
+
(0, chai_1.expect)(logged.some((m) => m.includes('spawn adb ENOENT')), logged.join('\n')).to.equal(true);
|
|
65
|
+
}));
|
|
66
|
+
it('tries adb again a minute later, and not before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
+
const adb = workingAdb();
|
|
68
|
+
createADB.onFirstCall().rejects(new Error('spawn adb ENOENT'));
|
|
69
|
+
createADB.onSecondCall().resolves(adb);
|
|
70
|
+
yield discover();
|
|
71
|
+
clock.tick(RETRY_MS - 1000);
|
|
72
|
+
yield discover();
|
|
73
|
+
(0, chai_1.expect)(createADB.callCount, 'within the minute').to.equal(1);
|
|
74
|
+
clock.tick(2000);
|
|
75
|
+
yield discover();
|
|
76
|
+
(0, chai_1.expect)(createADB.callCount, 'after the minute').to.equal(2);
|
|
77
|
+
(0, chai_1.expect)(adb.getConnectedDevices.called, 'discovery ran').to.equal(true);
|
|
78
|
+
}));
|
|
79
|
+
it('says why device tracking failed, keeps listing phones, and tries it again', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
|
+
process.env.NODE_ENV = 'development';
|
|
81
|
+
const adb = workingAdb();
|
|
82
|
+
createADB.resolves(adb);
|
|
83
|
+
trackDevices.onFirstCall().rejects(new Error('connect ECONNREFUSED 127.0.0.1:5037'));
|
|
84
|
+
yield discover();
|
|
85
|
+
const logged = errors.getCalls().map((c) => String(c.args[0]));
|
|
86
|
+
(0, chai_1.expect)(logged.some((m) => m.includes('ECONNREFUSED')), logged.join('\n')).to.equal(true);
|
|
87
|
+
yield discover();
|
|
88
|
+
(0, chai_1.expect)(adb.getConnectedDevices.callCount, 'phones still listed').to.equal(2);
|
|
89
|
+
(0, chai_1.expect)(trackDevices.callCount, 'within the minute').to.equal(1);
|
|
90
|
+
clock.tick(RETRY_MS + 1000);
|
|
91
|
+
yield discover();
|
|
92
|
+
(0, chai_1.expect)(trackDevices.callCount, 'after the minute').to.equal(2);
|
|
93
|
+
}));
|
|
94
|
+
it('starts one device tracker when two discoveries start adb at once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
// Each tracker runs the whole plug-in handling again for every phone
|
|
96
|
+
// plugged in, and nothing could end the extra one.
|
|
97
|
+
process.env.NODE_ENV = 'development';
|
|
98
|
+
createADB.resolves(workingAdb());
|
|
99
|
+
const pending = [];
|
|
100
|
+
trackDevices.callsFake(() => new Promise((resolve) => pending.push(() => resolve({ on: sandbox.stub() }))));
|
|
101
|
+
const both = Promise.all([discover(), discover()]);
|
|
102
|
+
while (pending.length === 0)
|
|
103
|
+
yield new Promise((r) => setImmediate(r));
|
|
104
|
+
// Time for a second discovery to reach the tracker too, if it would.
|
|
105
|
+
for (let i = 0; i < 20; i++)
|
|
106
|
+
yield new Promise((r) => setImmediate(r));
|
|
107
|
+
pending.forEach((start) => start());
|
|
108
|
+
yield both;
|
|
109
|
+
(0, chai_1.expect)(createADB.callCount, 'adb started').to.equal(1);
|
|
110
|
+
(0, chai_1.expect)(trackDevices.callCount, 'trackers started').to.equal(1);
|
|
111
|
+
}));
|
|
112
|
+
});
|