@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.
Files changed (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. 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.. Device request: {"platform":"android"}'));
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.. Device request: {"platform":"ios","name":"iPhone","deviceType":"simulator"}'));
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.. Device request: {"platform":"ios","name":"iPhone","deviceType":"real"}'));
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 = { emitToDashboard: sinon_1.default.stub() };
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.emitToDashboard.resetHistory();
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.emitToDashboard.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
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.emitToDashboard
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.emitToDashboard.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
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
- 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* () {
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(void 0, void 0, void 0, function* () {
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
- getId: () => 's-logs-1',
115
- getCapabilities: () => ({ platformName: 'Android' }),
116
- getLiveVideoUrl: () => null,
117
- apiKeyId: null,
118
- userId: null,
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 []; }) });