@xenon-device-management/xenon 2.13.2 → 2.14.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 (209) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
  40. package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
  41. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
  42. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
  43. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
  44. package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
  45. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  46. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  48. package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
  49. package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
  50. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
  51. package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
  52. package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
  53. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
  54. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
  56. package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
  59. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  60. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  61. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
  62. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
  63. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
  64. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
  65. package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  68. package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
  69. package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
  70. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
  71. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  72. package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +19 -21
  75. package/lib/src/XenonCapabilityManager.js +22 -5
  76. package/lib/src/app/openapi/control.yaml +71 -20
  77. package/lib/src/app/openapi/grid.yaml +23 -5
  78. package/lib/src/app/openapi/platform.yaml +136 -28
  79. package/lib/src/app/routers/control.js +12 -2
  80. package/lib/src/app/routers/dashboard.js +29 -1
  81. package/lib/src/app/routers/grid.js +32 -10
  82. package/lib/src/app/routers/streamType.js +7 -2
  83. package/lib/src/app/routers/webhook.js +8 -4
  84. package/lib/src/config.js +50 -5
  85. package/lib/src/dashboard/commands.js +185 -141
  86. package/lib/src/dashboard/event-manager.js +39 -6
  87. package/lib/src/data-service/appleFamily.js +43 -0
  88. package/lib/src/data-service/db.js +11 -0
  89. package/lib/src/data-service/device-service.js +6 -1
  90. package/lib/src/data-service/device-store.js +62 -2
  91. package/lib/src/data-service/deviceClaims.js +37 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  93. package/lib/src/data-service/deviceSettings.js +79 -0
  94. package/lib/src/data-service/prisma-store.js +150 -8
  95. package/lib/src/data-service/web-config-service.js +43 -92
  96. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  97. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  98. package/lib/src/device-managers/shellCommands.js +96 -0
  99. package/lib/src/device-utils.js +96 -40
  100. package/lib/src/generated/client/edge.js +17 -3
  101. package/lib/src/generated/client/index-browser.js +14 -0
  102. package/lib/src/generated/client/index.d.ts +2054 -379
  103. package/lib/src/generated/client/index.js +17 -3
  104. package/lib/src/generated/client/package.json +1 -1
  105. package/lib/src/generated/client/schema.prisma +35 -7
  106. package/lib/src/generated/client/wasm.js +14 -0
  107. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  108. package/lib/src/interceptors/xenonScripts.js +55 -0
  109. package/lib/src/interfaces/IPluginArgs.js +1 -2
  110. package/lib/src/plugin.js +15 -1
  111. package/lib/src/prisma.js +1 -1
  112. package/lib/src/scripts/database-check.js +124 -0
  113. package/lib/src/services/AIService.js +99 -0
  114. package/lib/src/services/CleanupService.js +6 -1
  115. package/lib/src/services/InterceptorService.js +70 -13
  116. package/lib/src/services/NetworkConditioningService.js +85 -88
  117. package/lib/src/services/NotificationService.js +36 -29
  118. package/lib/src/services/OrphanSweeper.js +5 -0
  119. package/lib/src/services/PortAllocator.js +5 -0
  120. package/lib/src/services/ServerManager.js +94 -8
  121. package/lib/src/services/SessionLifecycleService.js +47 -49
  122. package/lib/src/services/ShutdownCoordinator.js +33 -0
  123. package/lib/src/services/TeamService.js +20 -3
  124. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  125. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  126. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  127. package/lib/src/services/network/AndroidRadios.js +71 -0
  128. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  129. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  130. package/lib/src/services/network/adbForPhone.js +60 -0
  131. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  132. package/lib/src/services/network/xenonProxy.js +107 -0
  133. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  134. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  135. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  136. package/lib/src/services/omni-vision/screenScale.js +68 -0
  137. package/lib/src/services/recording/concurrency-gate.js +12 -3
  138. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  139. package/lib/src/services/settings/labSettings.js +149 -0
  140. package/lib/src/services/webhookEvents.js +173 -0
  141. package/lib/test/helpers/fake-adb.js +58 -0
  142. package/lib/test/helpers/fake-device-table.js +63 -24
  143. package/lib/test/helpers/scratch-database.js +1 -0
  144. package/lib/test/unit/TeamService.test.js +3 -0
  145. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  146. package/lib/test/unit/apple-family.spec.js +36 -0
  147. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  148. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  149. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  150. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  151. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  152. package/lib/test/unit/database-check.spec.js +108 -0
  153. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  154. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  155. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  156. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  157. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  158. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  159. package/lib/test/unit/emulator-boot.spec.js +91 -0
  160. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  161. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  162. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  163. package/lib/test/unit/json-logging-option.spec.js +74 -0
  164. package/lib/test/unit/lab-settings.spec.js +218 -0
  165. package/lib/test/unit/max-sessions.spec.js +240 -0
  166. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  167. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  168. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  169. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  170. package/lib/test/unit/omni-vision.spec.js +1 -1
  171. package/lib/test/unit/partial-config-file.spec.js +14 -0
  172. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  173. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  174. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  175. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  176. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  177. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  178. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  179. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  180. package/lib/test/unit/plugin.spec.js +3 -3
  181. package/lib/test/unit/recording-options.spec.js +181 -0
  182. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  183. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  184. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  185. package/lib/test/unit/shell-commands.spec.js +133 -0
  186. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  187. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  188. package/lib/test/unit/stream-status-type.spec.js +4 -0
  189. package/lib/test/unit/vision-assertion.spec.js +118 -0
  190. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  191. package/lib/test/unit/web-config-service.spec.js +79 -0
  192. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  193. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  194. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  197. package/package.json +1 -1
  198. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  199. package/prisma/schema.prisma +35 -7
  200. package/schema.json +19 -21
  201. package/scripts/generate-types-from-schema.js +1 -2
  202. package/scripts/lib/always-present-plugin-args.js +4 -1
  203. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  204. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  205. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  206. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  207. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  208. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  209. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
@@ -0,0 +1,133 @@
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 fs_1 = __importDefault(require("fs"));
18
+ const os_1 = __importDefault(require("os"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const sinon_1 = __importDefault(require("sinon"));
21
+ const typedi_1 = require("typedi");
22
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
23
+ const IOSTunnels_1 = require("../../src/device-managers/ios/IOSTunnels");
24
+ const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
25
+ const device_store_1 = require("../../src/data-service/device-store");
26
+ /**
27
+ * The Shell tab on iOS: simctl commands and a few generic ones on simulators,
28
+ * go-ios commands on real iPhones. Through 2.13.2 a real iPhone's command could
29
+ * carry its own `--udid` (another phone on the Mac), `list` named every device
30
+ * attached to the Mac, simulator commands took any path on this Mac, and the
31
+ * go-ios call ran without the phone's tunnel, so iOS 17+ phones never answered.
32
+ *
33
+ * `xcrun` and `ios` here are scripts that print the arguments they were given.
34
+ */
35
+ const SIMULATOR = 'TEST-SIM-0000-AAAA';
36
+ const IPHONE = '00008110-00084CE80E51401E';
37
+ const ECHO = `#!/bin/sh
38
+ printf '%s|' "$@"
39
+ printf 'port=%s' "$GO_IOS_AGENT_PORT"
40
+ `;
41
+ describe('WDAClient.executeShell', () => {
42
+ let dir;
43
+ let savedPath;
44
+ let realDevice;
45
+ const borrowed = [];
46
+ before(() => {
47
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-fake-shell-'));
48
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'xcrun'), ECHO, { mode: 0o755 });
49
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'ios'), ECHO, { mode: 0o755 });
50
+ });
51
+ after(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
52
+ beforeEach(() => {
53
+ savedPath = process.env.PATH;
54
+ process.env.PATH = `${dir}:${process.env.PATH}`;
55
+ borrowed.length = 0;
56
+ const stream = { isGoIOSAvailable: () => __awaiter(void 0, void 0, void 0, function* () { return true; }), goIOSPath: path_1.default.join(dir, 'ios') };
57
+ const tunnels = {
58
+ borrow(udid) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ borrowed.push(udid);
61
+ return 12100;
62
+ });
63
+ },
64
+ envFor: () => (Object.assign(Object.assign({}, process.env), { GO_IOS_AGENT_PORT: '12100' })),
65
+ };
66
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
67
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
68
+ if (token === IOSStreamService_1.default)
69
+ return stream;
70
+ if (token === IOSTunnels_1.IOSTunnels)
71
+ return tunnels;
72
+ return real(token);
73
+ });
74
+ sinon_1.default
75
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
76
+ .returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ realDevice }); }) });
77
+ });
78
+ afterEach(() => {
79
+ process.env.PATH = savedPath;
80
+ sinon_1.default.restore();
81
+ });
82
+ const run = (udid, command) => new WDAClient_1.WDAClient().executeShell(udid, command).then((out) => ({ out }), (err) => ({ err: err.message }));
83
+ describe('on a simulator', () => {
84
+ beforeEach(() => {
85
+ realDevice = false;
86
+ });
87
+ it('runs a simctl command for this simulator', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ const { out } = (yield run(SIMULATOR, 'getenv HOME'));
89
+ (0, chai_1.expect)(out).to.match(new RegExp(`^simctl\\|getenv\\|${SIMULATOR}\\|HOME\\|`));
90
+ }));
91
+ it('spawns a generic command in this simulator, without arguments', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const { out } = (yield run(SIMULATOR, 'ls'));
93
+ (0, chai_1.expect)(out).to.match(new RegExp(`^simctl\\|spawn\\|${SIMULATOR}\\|ls\\|`));
94
+ (0, chai_1.expect)(yield run(SIMULATOR, 'ls /Users'))
95
+ .to.have.property('err')
96
+ .that.match(/not allowed/);
97
+ }));
98
+ it('refuses list, chaining and look-alikes', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ for (const cmd of ['list', 'ls; id', 'lsof', 'getenv $HOME']) {
100
+ (0, chai_1.expect)(yield run(SIMULATOR, cmd), cmd)
101
+ .to.have.property('err')
102
+ .that.match(/not allowed/);
103
+ }
104
+ }));
105
+ });
106
+ describe('on a real iPhone', () => {
107
+ beforeEach(() => {
108
+ realDevice = true;
109
+ });
110
+ it('runs a go-ios command for this phone, through its tunnel', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ const { out } = (yield run(IPHONE, 'apps --list'));
112
+ (0, chai_1.expect)(out).to.equal(`apps|--list|--udid|${IPHONE}|port=12100`);
113
+ (0, chai_1.expect)(borrowed).to.deep.equal([IPHONE]);
114
+ }));
115
+ it('refuses a command that names another device, and list', () => __awaiter(void 0, void 0, void 0, function* () {
116
+ for (const cmd of [
117
+ 'apps --udid 00008101-OTHER',
118
+ 'apps --udid=00008101-OTHER',
119
+ 'list',
120
+ 'info --json',
121
+ ]) {
122
+ (0, chai_1.expect)(yield run(IPHONE, cmd), cmd)
123
+ .to.have.property('err')
124
+ .that.match(/not allowed/);
125
+ }
126
+ }));
127
+ it('says a generic command needs a simulator', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ (0, chai_1.expect)(yield run(IPHONE, 'ls'))
129
+ .to.have.property('err')
130
+ .that.match(/real/i);
131
+ }));
132
+ });
133
+ });
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
13
+ const chai_1 = require("chai");
14
+ const typedi_1 = require("typedi");
15
+ const web_config_service_1 = require("../../src/data-service/web-config-service");
16
+ const scratch_database_1 = require("../helpers/scratch-database");
17
+ /**
18
+ * WebConfig holds the dashboard's saved settings, one row each. Its `id`
19
+ * column is the table's primary key and defaulted to 'global', and every
20
+ * setting was written with that id, so the table could hold one setting:
21
+ * saving a second failed with "Unique constraint failed on the fields (`id`)".
22
+ * These run the service against a real (scratch) database.
23
+ */
24
+ describe('WebConfigService', () => {
25
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
26
+ const service = () => typedi_1.Container.get(web_config_service_1.WebConfigService);
27
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
28
+ yield scratch.db.webConfig.deleteMany({});
29
+ }));
30
+ it('saves every setting of one update', () => __awaiter(void 0, void 0, void 0, function* () {
31
+ yield service().setConfig({
32
+ buildCleanupDays: 7,
33
+ buildCleanupMaxCount: 5,
34
+ buildCleanupSchedule: '0 3 * * *',
35
+ deleteBuildAssets: false,
36
+ });
37
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
38
+ buildCleanupDays: 7,
39
+ buildCleanupMaxCount: 5,
40
+ buildCleanupSchedule: '0 3 * * *',
41
+ deleteBuildAssets: false,
42
+ });
43
+ }));
44
+ it('saves settings from separate updates, as two pages save them', () => __awaiter(void 0, void 0, void 0, function* () {
45
+ yield service().setConfig({ healthCheckIntervalMs: 600000 });
46
+ yield service().setConfig({ buildCleanupDays: 7 });
47
+ yield service().setConfig({ healthCheckSchedule: '0 * * * *', deleteBuildAssets: true });
48
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
49
+ healthCheckIntervalMs: 600000,
50
+ buildCleanupDays: 7,
51
+ healthCheckSchedule: '0 * * * *',
52
+ deleteBuildAssets: true,
53
+ });
54
+ }));
55
+ it('keeps the self-healing switch, off as well as on', () => __awaiter(void 0, void 0, void 0, function* () {
56
+ yield service().setConfig({ enableSelfHealing: false });
57
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: false });
58
+ yield service().setConfig({ enableSelfHealing: true });
59
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: true });
60
+ }));
61
+ it('changes a setting saved before', () => __awaiter(void 0, void 0, void 0, function* () {
62
+ yield service().setConfig({ buildCleanupDays: 7, buildCleanupMaxCount: 5 });
63
+ yield service().setConfig({ buildCleanupDays: 14 });
64
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
65
+ buildCleanupDays: 14,
66
+ buildCleanupMaxCount: 5,
67
+ });
68
+ }));
69
+ it('still reads the row an earlier version saved under the id "global"', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ yield scratch.db.webConfig.create({
71
+ data: { id: 'global', name: 'buildCleanupDays', value: '9' },
72
+ });
73
+ yield service().setConfig({ buildCleanupMaxCount: 5 });
74
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
75
+ buildCleanupDays: 9,
76
+ buildCleanupMaxCount: 5,
77
+ });
78
+ }));
79
+ });
@@ -129,6 +129,30 @@ describe('webhook delivery', () => {
129
129
  (0, chai_1.expect)(r.status).to.equal(200);
130
130
  (0, chai_1.expect)(r.body).to.deep.equal({ success: true });
131
131
  }));
132
+ it('POST /api/webhook/test sends a sample of the event it is asked for, through the template', () => __awaiter(void 0, void 0, void 0, function* () {
133
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
134
+ const r = yield (0, loopbackRequest_1.default)(app)
135
+ .post('/xenon/api/webhook/test')
136
+ .send({
137
+ url: URL,
138
+ type: 'slack',
139
+ payloadTemplate: '{"text": "{{eventType}}: {{sessionId}}"}',
140
+ event: 'session_failed',
141
+ })
142
+ .timeout(5000);
143
+ (0, chai_1.expect)(r.status).to.equal(200);
144
+ (0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({ text: 'session_failed: test-session-id' });
145
+ }));
146
+ it('POST /api/webhook/test with an event it does not know answers 400, sending nothing', () => __awaiter(void 0, void 0, void 0, function* () {
147
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
148
+ const r = yield (0, loopbackRequest_1.default)(app)
149
+ .post('/xenon/api/webhook/test')
150
+ .send({ url: URL, event: 'device_exploded' })
151
+ .timeout(5000);
152
+ (0, chai_1.expect)(r.status).to.equal(400);
153
+ (0, chai_1.expect)(r.body.error).to.contain('session_failed');
154
+ (0, chai_1.expect)(post.called).to.equal(false);
155
+ }));
132
156
  it('POST /api/webhook/test without a url answers 400', () => __awaiter(void 0, void 0, void 0, function* () {
133
157
  const r = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/webhook/test').send({}).timeout(5000);
134
158
  (0, chai_1.expect)(r.status).to.equal(400);
@@ -0,0 +1,273 @@
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 axios_1 = __importDefault(require("axios"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const typedi_1 = require("typedi");
20
+ const NotificationService_1 = require("../../src/services/NotificationService");
21
+ const webhookEvents_1 = require("../../src/services/webhookEvents");
22
+ // What the dashboard offers as chips: plain JSON, so a spec can read it.
23
+ const webhookEventVariables_json_1 = __importDefault(require("../../web/src/components/webhook-settings/webhookEventVariables.json"));
24
+ /**
25
+ * One documented payload per event, and every way a webhook turns it into a
26
+ * message (the built-in Slack text, the generic JSON, a custom template) reads
27
+ * those same names.
28
+ *
29
+ * `session_failed` used to send the database row (`id`, `failure_reason`, ...)
30
+ * while the built-in Slack text read `sessionId` and `failureReason` and said
31
+ * "undefined", and the dashboard's template chips offered the same two names,
32
+ * which never substituted.
33
+ */
34
+ const URL = 'https://hooks.example.com/T000/B000/xyz';
35
+ /** A Session row as Prisma returns it. */
36
+ const SESSION_ROW = {
37
+ id: 'a1b2c3d4-0000-4000-8000-000000000001',
38
+ name: 'Checkout flow',
39
+ status: 'failed',
40
+ failure_reason: 'Element not found: ~pay-now',
41
+ device_udid: 'R58M123',
42
+ device_name: 'Galaxy S9+',
43
+ device_platform: 'android',
44
+ device_version: '10',
45
+ startTime: new Date('2026-10-04T09:00:00.000Z'),
46
+ endTime: new Date('2026-10-04T09:02:30.000Z'),
47
+ desired_capabilities: '{"platformName":"android","appium:app":"/secret/app.apk"}',
48
+ session_capabilities: '{"deviceName":"x"}',
49
+ api_key_id: 'key-1',
50
+ user_id: 'user-1',
51
+ ai_analysis: '{"long":"analysis"}',
52
+ };
53
+ describe('webhook payloads', () => {
54
+ afterEach(() => sinon_1.default.restore());
55
+ describe('session_failed', () => {
56
+ it('is built from the session row with the documented names', () => {
57
+ (0, chai_1.expect)((0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW)).to.deep.equal({
58
+ sessionId: 'a1b2c3d4-0000-4000-8000-000000000001',
59
+ sessionName: 'Checkout flow',
60
+ failureReason: 'Element not found: ~pay-now',
61
+ udid: 'R58M123',
62
+ deviceName: 'Galaxy S9+',
63
+ platform: 'android',
64
+ osVersion: '10',
65
+ startTime: '2026-10-04T09:00:00.000Z',
66
+ endTime: '2026-10-04T09:02:30.000Z',
67
+ });
68
+ });
69
+ it('leaves out what a webhook has no business with: capabilities, keys, analysis', () => {
70
+ const payload = JSON.stringify((0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
71
+ for (const secret of ['/secret/app.apk', 'key-1', 'user-1', 'analysis', 'capabilities']) {
72
+ (0, chai_1.expect)(payload).not.to.contain(secret);
73
+ }
74
+ });
75
+ it('never has a missing value: unknown ones are empty text', () => {
76
+ const payload = (0, webhookEvents_1.sessionFailedPayload)({
77
+ id: 's-2',
78
+ status: 'failed',
79
+ device_udid: 'U',
80
+ device_platform: 'ios',
81
+ device_version: '17',
82
+ device_name: null,
83
+ name: null,
84
+ failure_reason: null,
85
+ startTime: new Date('2026-10-04T09:00:00.000Z'),
86
+ endTime: null,
87
+ });
88
+ (0, chai_1.expect)(payload).to.include({
89
+ sessionName: '',
90
+ failureReason: '',
91
+ deviceName: '',
92
+ endTime: '',
93
+ });
94
+ for (const value of Object.values(payload))
95
+ (0, chai_1.expect)(value).to.be.a('string');
96
+ });
97
+ });
98
+ describe('the built-in Slack message', () => {
99
+ let svc;
100
+ let post;
101
+ beforeEach(() => {
102
+ svc = typedi_1.Container.get(NotificationService_1.NotificationService);
103
+ post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
104
+ });
105
+ const slackFor = (event, payload) => __awaiter(void 0, void 0, void 0, function* () {
106
+ sinon_1.default
107
+ .stub(svc, 'getConfigs')
108
+ .resolves([
109
+ { id: 'a', url: URL, events: JSON.stringify([event]), type: 'slack', active: true },
110
+ ]);
111
+ yield svc.dispatchEvent(event, payload);
112
+ return post.firstCall.args[1].attachments[0];
113
+ });
114
+ it('names the failed session and why, instead of "undefined"', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ const attachment = yield slackFor('session_failed', (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
116
+ (0, chai_1.expect)(attachment.text).to.contain('Session Failed');
117
+ (0, chai_1.expect)(attachment.text).to.contain('a1b2c3d4-0000-4000-8000-000000000001');
118
+ (0, chai_1.expect)(attachment.text).to.contain('Element not found: ~pay-now');
119
+ (0, chai_1.expect)(attachment.text).to.contain('Galaxy S9+');
120
+ (0, chai_1.expect)(attachment.text).not.to.contain('undefined');
121
+ (0, chai_1.expect)(JSON.stringify(attachment)).not.to.contain('undefined');
122
+ }));
123
+ it('says so when no reason was recorded', () => __awaiter(void 0, void 0, void 0, function* () {
124
+ const attachment = yield slackFor('session_failed', (0, webhookEvents_1.sessionFailedPayload)(Object.assign(Object.assign({}, SESSION_ROW), { failure_reason: null })));
125
+ (0, chai_1.expect)(attachment.text).to.contain('no reason recorded');
126
+ }));
127
+ it('names the device for a device event', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ const attachment = yield slackFor('device_offline', webhookEvents_1.WEBHOOK_EVENTS.device_offline.sample());
129
+ (0, chai_1.expect)(attachment.text).to.contain('test-device-udid');
130
+ (0, chai_1.expect)(attachment.text).not.to.contain('undefined');
131
+ }));
132
+ });
133
+ describe('the generic webhook', () => {
134
+ it('sends the event name and the documented payload', () => __awaiter(void 0, void 0, void 0, function* () {
135
+ const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
136
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
137
+ sinon_1.default
138
+ .stub(svc, 'getConfigs')
139
+ .resolves([
140
+ { id: 'a', url: URL, events: '["session_failed"]', type: 'webhook', active: true },
141
+ ]);
142
+ yield svc.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
143
+ (0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({
144
+ event: 'session_failed',
145
+ payload: (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW),
146
+ });
147
+ }));
148
+ });
149
+ describe('a custom template', () => {
150
+ const data = Object.assign({ eventType: 'session_failed' }, (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
151
+ it('fills the names the dashboard offers as chips', () => {
152
+ (0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"text": "{{eventType}}: {{sessionId}} on {{deviceName}}: {{failureReason}}"}', data)).to.deep.equal({
153
+ text: 'session_failed: a1b2c3d4-0000-4000-8000-000000000001 on Galaxy S9+: Element not found: ~pay-now',
154
+ });
155
+ });
156
+ it('keeps the JSON valid when a value holds quotes, a backslash or a line break', () => {
157
+ const body = (0, webhookEvents_1.renderTemplate)('{"text": "Failed: {{failureReason}}", "n": 1}', {
158
+ failureReason: 'No element "~pay"\nline two \\ done',
159
+ });
160
+ (0, chai_1.expect)(body).to.deep.equal({ text: 'Failed: No element "~pay"\nline two \\ done', n: 1 });
161
+ });
162
+ it('fills a number or a list written without quotes', () => {
163
+ (0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"heals": {{totalHeals}}, "top": {{hotspots}}}', {
164
+ totalHeals: 12,
165
+ hotspots: [{ healCount: 7 }],
166
+ })).to.deep.equal({ heals: 12, top: [{ healCount: 7 }] });
167
+ });
168
+ it('sends a template that is not JSON as text', () => {
169
+ (0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('Session {{sessionId}} failed', data)).to.deep.equal({
170
+ text: 'Session a1b2c3d4-0000-4000-8000-000000000001 failed',
171
+ });
172
+ });
173
+ it('leaves a name the event does not have as it was written, so a typo shows', () => {
174
+ (0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"text": "{{sessionID}} / {{udid}}"}', data)).to.deep.equal({
175
+ text: '{{sessionID}} / R58M123',
176
+ });
177
+ });
178
+ it('reaches into a list with dots', () => {
179
+ (0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"text": "{{hotspots.0.originalSelector}}"}', {
180
+ hotspots: [{ originalSelector: '~login' }],
181
+ })).to.deep.equal({ text: '~login' });
182
+ });
183
+ it('is what a real event is sent as', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
185
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
186
+ sinon_1.default.stub(svc, 'getConfigs').resolves([
187
+ {
188
+ id: 'a',
189
+ url: URL,
190
+ events: '["session_failed"]',
191
+ type: 'slack',
192
+ active: true,
193
+ payloadTemplate: '{"text": "{{sessionId}}: {{failureReason}}"}',
194
+ },
195
+ ]);
196
+ yield svc.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
197
+ (0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({
198
+ text: 'a1b2c3d4-0000-4000-8000-000000000001: Element not found: ~pay-now',
199
+ });
200
+ }));
201
+ });
202
+ describe('Send test', () => {
203
+ let svc;
204
+ let post;
205
+ beforeEach(() => {
206
+ svc = typedi_1.Container.get(NotificationService_1.NotificationService);
207
+ post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
208
+ });
209
+ it('fills the template from a sample of the event asked for', () => __awaiter(void 0, void 0, void 0, function* () {
210
+ yield svc.sendTest(URL, 'slack', '{"text": "{{eventType}}: {{failureReason}} ({{deviceName}})"}', 'session_failed');
211
+ const sample = webhookEvents_1.WEBHOOK_EVENTS.session_failed.sample();
212
+ (0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({
213
+ text: `session_failed: ${sample.failureReason} (${sample.deviceName})`,
214
+ });
215
+ (0, chai_1.expect)(JSON.stringify(post.firstCall.args[1])).not.to.contain('{{');
216
+ }));
217
+ it('sends the built-in session_failed message when there is no template', () => __awaiter(void 0, void 0, void 0, function* () {
218
+ yield svc.sendTest(URL, 'slack', null, 'session_failed');
219
+ const text = post.firstCall.args[1].attachments[0].text;
220
+ (0, chai_1.expect)(text).to.contain('Session Failed');
221
+ (0, chai_1.expect)(text).not.to.contain('undefined');
222
+ }));
223
+ it('sends a device_new sample when no event is named, as before', () => __awaiter(void 0, void 0, void 0, function* () {
224
+ yield svc.sendTest(URL, 'webhook');
225
+ (0, chai_1.expect)(post.firstCall.args[1]).to.include({ event: 'device_new' });
226
+ }));
227
+ it('can test every event', () => __awaiter(void 0, void 0, void 0, function* () {
228
+ for (const event of webhookEvents_1.EVENT_TYPES) {
229
+ post.resetHistory();
230
+ yield svc.sendTest(URL, 'slack', null, event);
231
+ (0, chai_1.expect)(post.calledOnce, event).to.equal(true);
232
+ (0, chai_1.expect)(JSON.stringify(post.firstCall.args[1]), event).not.to.contain('undefined');
233
+ }
234
+ }));
235
+ });
236
+ describe('the documented variables', () => {
237
+ for (const event of webhookEvents_1.EVENT_TYPES) {
238
+ it(`${event}: each one is in the sample, so the test message fills it`, () => {
239
+ const sample = webhookEvents_1.WEBHOOK_EVENTS[event].sample();
240
+ for (const { name } of webhookEvents_1.WEBHOOK_EVENTS[event].variables) {
241
+ (0, chai_1.expect)(sample, name).to.have.property(name);
242
+ (0, chai_1.expect)(sample[name], name).not.to.equal(undefined);
243
+ }
244
+ });
245
+ it(`${event}: the dashboard offers exactly these as template chips`, () => {
246
+ (0, chai_1.expect)(webhookEventVariables_json_1.default[event]).to.deep.equal(webhookEvents_1.WEBHOOK_EVENTS[event].variables.map((v) => v.name));
247
+ });
248
+ }
249
+ });
250
+ describe('session_failed, once per session', () => {
251
+ it('is sent once however many times the session ends', () => __awaiter(void 0, void 0, void 0, function* () {
252
+ const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
253
+ const dispatch = sinon_1.default.stub(svc, 'dispatchEvent').resolves();
254
+ yield svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'once-1' }));
255
+ yield svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'once-1' }));
256
+ yield svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'once-2' }));
257
+ (0, chai_1.expect)(dispatch.callCount).to.equal(2);
258
+ (0, chai_1.expect)(dispatch.firstCall.args[0]).to.equal('session_failed');
259
+ (0, chai_1.expect)(dispatch.firstCall.args[1].sessionId).to.equal('once-1');
260
+ }));
261
+ it('is sent once when two ends race', () => __awaiter(void 0, void 0, void 0, function* () {
262
+ const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
263
+ const dispatch = sinon_1.default
264
+ .stub(svc, 'dispatchEvent')
265
+ .callsFake(() => new Promise((resolve) => setTimeout(resolve, 20)));
266
+ yield Promise.all([
267
+ svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'race-1' })),
268
+ svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'race-1' })),
269
+ ]);
270
+ (0, chai_1.expect)(dispatch.callCount).to.equal(1);
271
+ }));
272
+ });
273
+ });
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const deviceService = __importStar(require("../../src/data-service/device-service"));
53
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
54
+ const AICommandService_1 = require("../../src/services/AICommandService");
55
+ const AutowaitService_1 = require("../../src/services/autowait/AutowaitService");
56
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
57
+ const container_registration_1 = require("../helpers/container-registration");
58
+ const scratch_database_1 = require("../helpers/scratch-database");
59
+ /**
60
+ * `xenon:` / `xe:` execute scripts for a session on this server's own phone,
61
+ * through the plugin's command interceptor (as Appium calls it).
62
+ */
63
+ describe('xenon: execute scripts on this server', () => {
64
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
65
+ const SESSION = 'local-session-1';
66
+ const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
67
+ let interceptor;
68
+ let next;
69
+ let restoreContainer;
70
+ const driver = { sessionId: SESSION, caps: { udid: 'phone-1' } };
71
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
72
+ yield scratch.db.session.deleteMany({});
73
+ restoreContainer = (0, container_registration_1.saveRegistrations)(AICommandService_1.AICommandService);
74
+ sinon_1.default.stub(deviceService, 'updateCmdExecutedTime').resolves();
75
+ interceptor = new CommandInterceptor_1.CommandInterceptor();
76
+ next = sinon_1.default.stub().resolves('from the driver');
77
+ }));
78
+ afterEach(() => {
79
+ typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(SESSION);
80
+ sinon_1.default.restore();
81
+ restoreContainer();
82
+ });
83
+ /** driver.execute(script, ...args), as Appium hands it to the plugin. */
84
+ const execute = (script, ...scriptArgs) => interceptor.handle(next, driver, 'execute', [script, scriptArgs, SESSION], pluginArgs, true);
85
+ const failureOf = (p) => p.then((value) => {
86
+ throw new Error(`expected a failure, got ${JSON.stringify(value)}`);
87
+ }, (e) => e);
88
+ it('assertVisualState takes its condition as a plain string, as well as { instruction }', () => __awaiter(void 0, void 0, void 0, function* () {
89
+ const ai = new AICommandService_1.AICommandService();
90
+ typedi_1.Container.set(AICommandService_1.AICommandService, ai);
91
+ const assert = sinon_1.default.stub(ai, 'assertVisualState').resolves({ result: true, message: 'ok' });
92
+ yield execute('xenon: assertVisualState', 'The cart is empty');
93
+ yield execute('xe: assertVisualState', { instruction: 'The cart has 2 items' });
94
+ (0, chai_1.expect)(assert.firstCall.args[1]).to.equal('The cart is empty');
95
+ (0, chai_1.expect)(assert.secondCall.args[1]).to.equal('The cart has 2 items');
96
+ (0, chai_1.expect)(next.called).to.equal(false);
97
+ }));
98
+ it('a session-details command answers that nothing was recorded when the session has no record', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ // The dashboard is off: no Session row. This used to fail the command (P2025).
100
+ const answer = yield execute('xenon: setSessionName', 'Checkout');
101
+ (0, chai_1.expect)(answer).to.include({ recorded: false });
102
+ (0, chai_1.expect)(answer.message).to.match(/no record/i);
103
+ (0, chai_1.expect)(next.called).to.equal(false);
104
+ }));
105
+ it('a session-details command answers with what it did, not null', () => __awaiter(void 0, void 0, void 0, function* () {
106
+ var _a;
107
+ yield scratch.db.session.create({
108
+ data: {
109
+ id: SESSION,
110
+ desired_capabilities: '{}',
111
+ session_capabilities: '{}',
112
+ node_id: 'node-1',
113
+ has_live_video: false,
114
+ device_udid: 'phone-1',
115
+ device_platform: 'android',
116
+ device_version: '14',
117
+ },
118
+ });
119
+ (0, chai_1.expect)(yield execute('xenon: setSessionName', 'Checkout')).to.deep.equal({ recorded: true });
120
+ (0, chai_1.expect)((_a = (yield scratch.db.session.findUnique({ where: { id: SESSION } }))) === null || _a === void 0 ? void 0 : _a.name).to.equal('Checkout');
121
+ }));
122
+ it('a xenon: script Xenon does not have fails, instead of answering null', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ const err = yield failureOf(execute('xenon: setSesionName', 'Checkout'));
124
+ (0, chai_1.expect)(err.message).to.match(/Unknown Xenon command "xenon: setSesionName"/);
125
+ (0, chai_1.expect)(err.message).to.match(/setSessionName/);
126
+ (0, chai_1.expect)(err.error).to.equal('unknown command');
127
+ (0, chai_1.expect)(next.called).to.equal(false);
128
+ }));
129
+ it("another plugin's or the driver's script still goes to the driver", () => __awaiter(void 0, void 0, void 0, function* () {
130
+ (0, chai_1.expect)(yield execute('mobile: shell', { command: 'echo' })).to.equal('from the driver');
131
+ (0, chai_1.expect)(next.calledOnce).to.equal(true);
132
+ }));
133
+ it('Xenon’s own scripts are still answered here', () => __awaiter(void 0, void 0, void 0, function* () {
134
+ const answer = yield execute('xe: setAutowaitProperties', { timeout: 1234 });
135
+ (0, chai_1.expect)(answer).to.include({ timeoutMs: 1234 });
136
+ (0, chai_1.expect)(next.called).to.equal(false);
137
+ }));
138
+ });