@xenon-device-management/xenon 2.15.0 → 2.17.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-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.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/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- 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 +23 -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 +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- 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 +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- 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 +52 -23
- 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/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- 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 +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -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 +74 -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/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- 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/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -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 +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -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-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
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 express_1 = __importDefault(require("express"));
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
21
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
22
|
+
const commandLogFields_1 = require("../../src/dashboard/commandLogFields");
|
|
23
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
24
|
+
const SelectorStateService_1 = require("../../src/services/SelectorStateService");
|
|
25
|
+
const SocketServer_1 = require("../../src/services/SocketServer");
|
|
26
|
+
const typedi_1 = require("typedi");
|
|
27
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
28
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
29
|
+
/**
|
|
30
|
+
* A session's network capture (its requests, its HAR, its mocks) is for admins
|
|
31
|
+
* only: the /interceptor routes, the Network panel and the live events. The
|
|
32
|
+
* command log is read by anyone who can see the session (`session_log`, bug
|
|
33
|
+
* reports). A hub records every command it forwards to a node, so a test's own
|
|
34
|
+
* `xenon: exportHar` put the whole capture, sign-in headers included, in front
|
|
35
|
+
* of every member of the phone's team.
|
|
36
|
+
*/
|
|
37
|
+
describe('The command log keeps no network capture', () => {
|
|
38
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
39
|
+
const SESSION = 'cl-session-1';
|
|
40
|
+
const SECRET = 'eyJ-app-access-token';
|
|
41
|
+
const HAR = {
|
|
42
|
+
log: {
|
|
43
|
+
entries: [{ request: { headers: [{ name: 'authorization', value: `Bearer ${SECRET}` }] } }],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
let restore;
|
|
47
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
restore = (0, container_registration_1.saveRegistrations)(SocketServer_1.SocketServer);
|
|
49
|
+
typedi_1.Container.set(SocketServer_1.SocketServer, {
|
|
50
|
+
emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
51
|
+
hasScopedDashboard: () => false,
|
|
52
|
+
});
|
|
53
|
+
yield scratch.db.sessionLog.deleteMany({});
|
|
54
|
+
yield scratch.db.session.deleteMany({});
|
|
55
|
+
yield scratch.db.device.deleteMany({});
|
|
56
|
+
yield scratch.db.team.deleteMany({});
|
|
57
|
+
yield scratch.db.team.create({ data: { id: 'team-a', name: 'Team A' } });
|
|
58
|
+
yield scratch.db.device.create({
|
|
59
|
+
data: {
|
|
60
|
+
udid: 'cl-phone-a',
|
|
61
|
+
host: 'http://node:4723',
|
|
62
|
+
platform: 'android',
|
|
63
|
+
teamId: 'team-a',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
yield scratch.db.session.create({
|
|
67
|
+
data: {
|
|
68
|
+
id: SESSION,
|
|
69
|
+
desired_capabilities: '{}',
|
|
70
|
+
session_capabilities: '{}',
|
|
71
|
+
node_id: 'node-1',
|
|
72
|
+
has_live_video: false,
|
|
73
|
+
device_udid: 'cl-phone-a',
|
|
74
|
+
device_platform: 'android',
|
|
75
|
+
device_version: '14',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns({
|
|
79
|
+
getId: () => SESSION,
|
|
80
|
+
getDevice: () => ({ udid: 'cl-phone-a', name: 'P', platform: 'android' }),
|
|
81
|
+
getXenonOption: () => undefined,
|
|
82
|
+
getScreenShot: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }),
|
|
83
|
+
});
|
|
84
|
+
sinon_1.default.stub(SelectorStateService_1.SelectorStateService.prototype, 'onHealRecorded').resolves(null);
|
|
85
|
+
}));
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
sinon_1.default.restore();
|
|
88
|
+
restore();
|
|
89
|
+
});
|
|
90
|
+
/** A command as the hub's gateway hands it over: the client's W3C body and the node's answer. */
|
|
91
|
+
function forwarded(script, args, answer) {
|
|
92
|
+
return event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(SESSION, 'execute', null, {
|
|
93
|
+
body: { script, args },
|
|
94
|
+
method: 'POST',
|
|
95
|
+
originalUrl: `/wd/hub/session/${SESSION}/execute/sync`,
|
|
96
|
+
}, {}, JSON.stringify(answer));
|
|
97
|
+
}
|
|
98
|
+
/** What a member of the phone's team reads. */
|
|
99
|
+
function sessionLogAsMember() {
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
const app = (0, express_1.default)();
|
|
102
|
+
app.use((req, _res, next) => {
|
|
103
|
+
req.auth = {
|
|
104
|
+
kind: 'user-session',
|
|
105
|
+
userId: 'cl-u-member',
|
|
106
|
+
role: 'MEMBER',
|
|
107
|
+
scopes: 'devices,sessions,read',
|
|
108
|
+
teamIds: ['team-a'],
|
|
109
|
+
};
|
|
110
|
+
next();
|
|
111
|
+
});
|
|
112
|
+
dashboard_1.default.register(app);
|
|
113
|
+
const res = yield (0, loopbackRequest_1.default)(app).get(`/session/${SESSION}/session_log`);
|
|
114
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
115
|
+
return res.body;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
it('records which network script ran and whether it worked, never its arguments or its answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
var _a, _b;
|
|
120
|
+
yield forwarded('xenon: exportHar', [], { value: HAR });
|
|
121
|
+
yield forwarded('xe:getRequests', [], { value: [HAR] });
|
|
122
|
+
yield forwarded('getMocks', [], { value: [{ respond: { body: { token: SECRET } } }] });
|
|
123
|
+
yield forwarded('xenon: addMock', [{ respond: { body: { token: SECRET } } }], {
|
|
124
|
+
value: { id: 'm1' },
|
|
125
|
+
});
|
|
126
|
+
yield forwarded('xenon: removeMock', [{ id: 'm1' }], { value: { removed: true } });
|
|
127
|
+
yield forwarded('xenon: clearMocks', [], { value: { ok: true } });
|
|
128
|
+
const rows = yield sessionLogAsMember();
|
|
129
|
+
(0, chai_1.expect)(rows).to.have.length(6);
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
(0, chai_1.expect)(`${row.body}${row.response}`).to.not.include(SECRET);
|
|
132
|
+
(0, chai_1.expect)(row.is_success).to.equal(true);
|
|
133
|
+
// Every one, removeMock and clearMocks included, whose answers hold no secret.
|
|
134
|
+
(0, chai_1.expect)(JSON.parse(row.response), (_a = row.body) !== null && _a !== void 0 ? _a : '').to.deep.equal({
|
|
135
|
+
value: commandLogFields_1.NETWORK_CAPTURE_NOT_KEPT,
|
|
136
|
+
});
|
|
137
|
+
(0, chai_1.expect)(JSON.parse((_b = row.body) !== null && _b !== void 0 ? _b : '{}').args).to.equal(commandLogFields_1.NETWORK_CAPTURE_NOT_KEPT);
|
|
138
|
+
}
|
|
139
|
+
(0, chai_1.expect)(rows.map((r) => { var _a; return JSON.parse((_a = r.body) !== null && _a !== void 0 ? _a : '{}').script; })).to.have.members([
|
|
140
|
+
'xenon: exportHar',
|
|
141
|
+
'xe:getRequests',
|
|
142
|
+
'getMocks',
|
|
143
|
+
'xenon: addMock',
|
|
144
|
+
'xenon: removeMock',
|
|
145
|
+
'xenon: clearMocks',
|
|
146
|
+
]);
|
|
147
|
+
}));
|
|
148
|
+
it('keeps a failed network script’s error, which carries no capture', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
149
|
+
const error = {
|
|
150
|
+
value: {
|
|
151
|
+
error: 'unknown error',
|
|
152
|
+
message: `Interceptor not active for session ${SESSION}`,
|
|
153
|
+
stacktrace: '',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
yield forwarded('xenon: exportHar', [], error);
|
|
157
|
+
const [row] = yield sessionLogAsMember();
|
|
158
|
+
(0, chai_1.expect)(row.is_success).to.equal(false);
|
|
159
|
+
(0, chai_1.expect)(JSON.parse(row.response)).to.deep.equal(error);
|
|
160
|
+
}));
|
|
161
|
+
it('on the server that drives the phone, a failed network script keeps its error but not its arguments', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
162
|
+
var _a;
|
|
163
|
+
// CommandInterceptor answers these scripts before its hooks; only a throw
|
|
164
|
+
// (no capture running) reaches the log, from its catch, with the
|
|
165
|
+
// command's arguments as they came: [script, args].
|
|
166
|
+
const error = {
|
|
167
|
+
value: { error: `Interceptor not active for session ${SESSION}` },
|
|
168
|
+
sessionId: SESSION,
|
|
169
|
+
};
|
|
170
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(SESSION, 'execute', null, {
|
|
171
|
+
body: ['xenon: addMock', [{ respond: { body: { token: SECRET } } }]],
|
|
172
|
+
method: 'POST',
|
|
173
|
+
originalUrl: '/execute',
|
|
174
|
+
}, {}, JSON.stringify(error));
|
|
175
|
+
const [row] = yield sessionLogAsMember();
|
|
176
|
+
(0, chai_1.expect)(row.body).to.not.include(SECRET);
|
|
177
|
+
(0, chai_1.expect)(JSON.parse((_a = row.body) !== null && _a !== void 0 ? _a : '[]')[0]).to.equal('xenon: addMock');
|
|
178
|
+
(0, chai_1.expect)(JSON.parse(row.response)).to.deep.equal(error);
|
|
179
|
+
(0, chai_1.expect)(row.is_success).to.equal(false);
|
|
180
|
+
}));
|
|
181
|
+
it('logs every other script, and a script the node’s interceptor doesn’t answer, whole', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
// `plugin:` is only the autowait commands' prefix: `plugin: exportHar` is
|
|
183
|
+
// never a network script, and its answer is the driver's.
|
|
184
|
+
yield forwarded('mobile: shell', [{ command: 'echo' }], { value: SECRET });
|
|
185
|
+
yield forwarded('plugin: exportHar', [], { value: SECRET });
|
|
186
|
+
const rows = yield sessionLogAsMember();
|
|
187
|
+
(0, chai_1.expect)(rows).to.have.length(2);
|
|
188
|
+
for (const row of rows)
|
|
189
|
+
(0, chai_1.expect)(row.response).to.include(SECRET);
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
@@ -50,6 +50,7 @@ const chai_1 = require("chai");
|
|
|
50
50
|
const sinon_1 = __importDefault(require("sinon"));
|
|
51
51
|
const typedi_1 = require("typedi");
|
|
52
52
|
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
53
|
+
const XenonCapabilityManager_1 = require("../../src/XenonCapabilityManager");
|
|
53
54
|
const assets = __importStar(require("../../src/dashboard/asset-manager"));
|
|
54
55
|
const sessionService = __importStar(require("../../src/dashboard/services/session-service"));
|
|
55
56
|
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
@@ -84,6 +85,7 @@ describe('EventManager: an Android session’s device log', () => {
|
|
|
84
85
|
deviceLogs = {
|
|
85
86
|
start: sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () { return order.push('device logs'); })),
|
|
86
87
|
stop: sinon_1.default.stub().resolves(),
|
|
88
|
+
noteOff: sinon_1.default.stub().resolves(),
|
|
87
89
|
};
|
|
88
90
|
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, deviceLogs);
|
|
89
91
|
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, {
|
|
@@ -101,32 +103,93 @@ describe('EventManager: an Android session’s device log', () => {
|
|
|
101
103
|
sinon_1.default.restore();
|
|
102
104
|
restore();
|
|
103
105
|
});
|
|
104
|
-
|
|
106
|
+
function startSession(platform, deviceLog) {
|
|
105
107
|
sinon_1.default.stub(assets, 'prepareDirectory');
|
|
106
108
|
sinon_1.default.stub(sessionService, 'getOrCreateNewBuild').resolves({ id: 'b-logs' });
|
|
107
109
|
sinon_1.default.stub(TracingService_1.TracingService.prototype, 'getTraceId').returns('t-1');
|
|
108
110
|
sinon_1.default.stub(MetricsService_1.MetricsService.prototype, 'incrementSessionStart');
|
|
109
|
-
sinon_1.default.stub(prisma_1.prisma.session, 'create').callsFake(() => __awaiter(
|
|
111
|
+
sinon_1.default.stub(prisma_1.prisma.session, 'create').callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
110
112
|
order.push('row');
|
|
111
113
|
return {};
|
|
112
114
|
}));
|
|
113
|
-
const session = {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
allocatedAt: 1700000000000,
|
|
120
|
-
};
|
|
121
|
-
const device = { udid: 'phone-l', platform: 'android', host: 'h', name: 'S9', sdk: '10' };
|
|
115
|
+
const session = Object.assign({ getId: () => 's-logs-1', getCapabilities: () => ({ platformName: platform }), getLiveVideoUrl: () => null, startPerformanceRecording: sinon_1.default.stub().resolves(), apiKeyId: null, userId: null, allocatedAt: 1700000000000 }, (deviceLog ? { deviceLog } : {}));
|
|
116
|
+
const device = { udid: 'phone-l', platform, host: 'h', name: 'phone', sdk: '10' };
|
|
117
|
+
return { session, device };
|
|
118
|
+
}
|
|
119
|
+
it("starts once the session's row is written, from when the phone was given to it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
const { session, device } = startSession('android');
|
|
122
121
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted({}, session, device);
|
|
123
122
|
(0, chai_1.expect)(order).to.deep.equal(['row', 'device logs']);
|
|
124
123
|
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0]).to.deep.equal({
|
|
125
124
|
sessionId: 's-logs-1',
|
|
126
125
|
device,
|
|
127
126
|
since: 1700000000000,
|
|
127
|
+
driverLog: undefined,
|
|
128
|
+
appUnderTest: undefined,
|
|
128
129
|
});
|
|
129
130
|
}));
|
|
131
|
+
// The switch, from the session's capabilities as the create parses them.
|
|
132
|
+
const parsed = (alwaysMatch) => (0, XenonCapabilityManager_1.getXenonCapabilities)({ alwaysMatch, firstMatch: [{}] });
|
|
133
|
+
function stubSessionStart() {
|
|
134
|
+
sinon_1.default.stub(assets, 'prepareDirectory');
|
|
135
|
+
sinon_1.default.stub(sessionService, 'getOrCreateNewBuild').resolves({ id: 'b-logs' });
|
|
136
|
+
sinon_1.default.stub(TracingService_1.TracingService.prototype, 'getTraceId').returns('t-1');
|
|
137
|
+
sinon_1.default.stub(MetricsService_1.MetricsService.prototype, 'incrementSessionStart');
|
|
138
|
+
sinon_1.default.stub(prisma_1.prisma.session, 'create').resolves({});
|
|
139
|
+
return {
|
|
140
|
+
session: {
|
|
141
|
+
getId: () => 's-logs-off',
|
|
142
|
+
getCapabilities: () => ({ platformName: 'Android' }),
|
|
143
|
+
getLiveVideoUrl: () => null,
|
|
144
|
+
apiKeyId: null,
|
|
145
|
+
userId: null,
|
|
146
|
+
},
|
|
147
|
+
device: { udid: 'phone-l', platform: 'android', host: 'h', name: 'S9', sdk: '10' },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
it('records it when the session says nothing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
const { session, device } = stubSessionStart();
|
|
152
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(parsed({}), session, device);
|
|
153
|
+
(0, chai_1.expect)(deviceLogs.start.calledOnce).to.equal(true);
|
|
154
|
+
(0, chai_1.expect)(deviceLogs.noteOff.called).to.equal(false);
|
|
155
|
+
}));
|
|
156
|
+
it('keeps none, and says so, for a session that turned it off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
|
+
const { session, device } = stubSessionStart();
|
|
158
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(parsed({ 'xe:save_device_logs': false }), session, device);
|
|
159
|
+
(0, chai_1.expect)(deviceLogs.start.called).to.equal(false);
|
|
160
|
+
(0, chai_1.expect)(deviceLogs.noteOff.calledOnceWithExactly('s-logs-off')).to.equal(true);
|
|
161
|
+
}));
|
|
162
|
+
it("doesn't listen to an iPhone's driver log when the session turned it off", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
163
|
+
const driverLog = { on: sinon_1.default.stub(), removeListener: sinon_1.default.stub() };
|
|
164
|
+
const { session, device } = startSession('ios', () => driverLog);
|
|
165
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(parsed({ 'xe:saveDeviceLogs': 'false' }), session, device);
|
|
166
|
+
(0, chai_1.expect)(deviceLogs.start.called).to.equal(false);
|
|
167
|
+
(0, chai_1.expect)(deviceLogs.noteOff.calledOnceWithExactly('s-logs-1')).to.equal(true);
|
|
168
|
+
(0, chai_1.expect)(driverLog.on.called).to.equal(false);
|
|
169
|
+
}));
|
|
170
|
+
it("hands an iPhone's session the log its driver captures, and its app", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
const driverLog = { on() { }, removeListener() { } };
|
|
172
|
+
const { session, device } = startSession('ios', () => driverLog);
|
|
173
|
+
session.appUnderTest = () => 'com.example.shop';
|
|
174
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted({}, session, device);
|
|
175
|
+
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0].driverLog).to.equal(driverLog);
|
|
176
|
+
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0].appUnderTest).to.equal('com.example.shop');
|
|
177
|
+
}));
|
|
178
|
+
it("collects a node session's from the node, through the session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
|
+
const { device } = stubSessionStart();
|
|
180
|
+
const session = {
|
|
181
|
+
getId: () => 's-logs-node',
|
|
182
|
+
getType: () => 'remote',
|
|
183
|
+
getCapabilities: () => ({ platformName: 'Android' }),
|
|
184
|
+
getLiveVideoUrl: () => null,
|
|
185
|
+
nodeOrigin: () => 'http://node:4723',
|
|
186
|
+
nodeDeviceLogs: () => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'refused' }); }),
|
|
187
|
+
apiKeyId: null,
|
|
188
|
+
userId: null,
|
|
189
|
+
};
|
|
190
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(parsed({}), session, Object.assign(Object.assign({}, device), { host: 'http://node:4723' }));
|
|
191
|
+
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0].source).to.equal(session);
|
|
192
|
+
}));
|
|
130
193
|
it('writes it to the end when the session stops, even one no longer in memory', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
194
|
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
|
|
132
195
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
|