@xenon-device-management/xenon 2.13.2 → 2.15.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 (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -71,6 +71,11 @@ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
71
71
  const internalCall_1 = require("../../src/gateway/internalCall");
72
72
  const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
73
73
  const defaultGateway_1 = require("../../src/gateway/defaultGateway");
74
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
75
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
76
+ const types_1 = require("../../src/services/healing/types");
77
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
78
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
74
79
  const container_registration_1 = require("../helpers/container-registration");
75
80
  const scratch_database_1 = require("../helpers/scratch-database");
76
81
  /**
@@ -104,6 +109,13 @@ const DRIVER_ROUTES = {
104
109
  POST: { command: 'stopRecordingScreen', payloadParams: { optional: ['options'] } },
105
110
  },
106
111
  };
112
+ /**
113
+ * The node's findElement as its plugin runs it, when a test sets it: the
114
+ * driver's own find goes through Xenon's CommandInterceptor.
115
+ */
116
+ let nodeFind;
117
+ /** The hub's findElement as its plugin runs it, for a session the hub runs itself. */
118
+ let hubFind;
107
119
  function fakeDriver(name) {
108
120
  const { errors } = appiumBaseDriver();
109
121
  const commands = [];
@@ -120,12 +132,18 @@ function fakeDriver(name) {
120
132
  if (command === 'getUrl')
121
133
  return `https://${name}.example/`;
122
134
  if (command === 'findElement') {
135
+ if (name === 'node' && nodeFind)
136
+ return nodeFind(args);
137
+ if (name === 'hub' && hubFind)
138
+ return hubFind(args);
123
139
  if (args[1] === 'missing')
124
140
  throw new errors.NoSuchElementError();
125
141
  return { 'element-6066-11e4-a52e-4f735466cecf': `${name}-el` };
126
142
  }
127
143
  if (command === 'stopRecordingScreen')
128
144
  return 'node-video.mp4';
145
+ if (command === 'execute')
146
+ return { ranOn: name, script: args[0] };
129
147
  if (command === 'deleteSession') {
130
148
  sessions.delete(args[0]);
131
149
  return null;
@@ -178,6 +196,9 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
178
196
  typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
179
197
  savedStore = device_store_1.DeviceStoreFactory._deviceStore;
180
198
  device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
199
+ // A dashboard-on hub logs each forwarded command against its session.
200
+ yield scratch.db.sessionLog.deleteMany({});
201
+ yield scratch.db.log.deleteMany({});
181
202
  yield scratch.db.session.deleteMany({});
182
203
  yield scratch.db.device.deleteMany({});
183
204
  nodeRequests = [];
@@ -418,6 +439,49 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
418
439
  (0, chai_1.expect)(hub.commands).to.deep.equal([]);
419
440
  }));
420
441
  });
442
+ describe('xenon: execute scripts for a node’s phone', () => {
443
+ // With the hub's dashboard on, its before-hook used to take every
444
+ // `xenon:`/`xe:` script as a dashboard command and answer `{ value: null }`
445
+ // itself, so autowait, Omni-Vision and network-capture scripts for a node's
446
+ // phone never reached the node.
447
+ const execute = (id, script, args = [{}]) => (0, loopbackRequest_1.default)(hubUrl).post(`/wd/hub/session/${id}/execute/sync`).send({ script, args });
448
+ it('forwards Xenon’s scripts other than session details to the node, which answers them', () => __awaiter(this, void 0, void 0, function* () {
449
+ yield boot({ dashboard: true });
450
+ const { id } = yield remoteSession();
451
+ const scripts = [
452
+ 'xenon: setAutowaitProperties',
453
+ 'xe: smartTap',
454
+ 'xenon: assertVisualState',
455
+ 'xenon: addMock',
456
+ 'xenon: notACommand',
457
+ ];
458
+ for (const script of scripts) {
459
+ const res = yield execute(id, script);
460
+ (0, chai_1.expect)(res.status, script).to.equal(200);
461
+ (0, chai_1.expect)(res.body.value, script).to.deep.equal({ ranOn: 'node', script });
462
+ }
463
+ (0, chai_1.expect)(node.commands).to.deep.equal(scripts.map(() => 'execute'));
464
+ (0, chai_1.expect)(hub.commands).to.deep.equal([]);
465
+ }));
466
+ it('answers a session-details command on the hub, from its own record; the node never sees it', () => __awaiter(this, void 0, void 0, function* () {
467
+ yield boot({ dashboard: true });
468
+ const { id } = yield remoteSession();
469
+ const res = yield execute(id, 'xenon: setSessionName', ['Checkout']);
470
+ (0, chai_1.expect)(res.status).to.equal(200);
471
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: { recorded: true } });
472
+ (0, chai_1.expect)(node.commands).to.deep.equal([]);
473
+ (0, chai_1.expect)(hub.commands).to.deep.equal([]);
474
+ const row = yield scratch.db.session.findUnique({ where: { id } });
475
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.name).to.equal('Checkout');
476
+ }));
477
+ it('with the hub’s dashboard off, sends a session-details command on to the node', () => __awaiter(this, void 0, void 0, function* () {
478
+ yield boot({ dashboard: false });
479
+ const { id } = yield remoteSession();
480
+ const res = yield execute(id, 'xenon: setSessionName', ['Checkout']);
481
+ (0, chai_1.expect)(res.body.value).to.deep.equal({ ranOn: 'node', script: 'xenon: setSessionName' });
482
+ (0, chai_1.expect)(node.commands).to.deep.equal(['execute']);
483
+ }));
484
+ });
421
485
  describe('DELETE of a remote session', () => {
422
486
  it('runs the session lifecycle with the forward as its driver step', () => __awaiter(this, void 0, void 0, function* () {
423
487
  yield boot({ nodeAuth: true });
@@ -591,4 +655,159 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
591
655
  yield (0, loopbackRequest_1.default)(nodeOrigin).get(`${nodeBase}/session/${id}/url`).expect(200);
592
656
  }));
593
657
  });
658
+ /**
659
+ * A find on a node's phone runs on the node, so the node's interceptor
660
+ * heals it. Through 2.14 the heal was recorded nowhere: the node keeps no
661
+ * record of a session the hub created, and the hub saw only the element the
662
+ * node answered with, so the find was logged as found (and counted toward
663
+ * the selector's verification as a clean find). The node now hands the heal
664
+ * back on its answer, and the hub, which owns the session record, records
665
+ * it as it records a local session's.
666
+ */
667
+ describe("a heal on a node's phone", () => {
668
+ const W3C = 'element-6066-11e4-a52e-4f735466cecf';
669
+ let attemptHealing;
670
+ let nodeLogged;
671
+ let restoreHealing;
672
+ beforeEach(() => {
673
+ restoreHealing = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
674
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
675
+ attemptHealing = sinon_1.default.stub().resolves({
676
+ id: 'node-healed-el',
677
+ tier: types_1.HealingTier.TIER_2_FUZZY_XML,
678
+ confidence: 0.82,
679
+ originalSelector: 'broken',
680
+ originalStrategy: 'xpath',
681
+ recommendedSelector: "//*[@resource-id='com.example:id/login']",
682
+ recommendedStrategy: 'xpath',
683
+ });
684
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
685
+ // Hub and node share this process; the node's own record of a heal is
686
+ // the node's (on a real node it has no Session row to write it to).
687
+ nodeLogged = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'logHealingEvent').resolves();
688
+ const { errors } = appiumBaseDriver();
689
+ nodeFind = hubFind = (args) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () {
690
+ if (args[1] === 'broken')
691
+ throw new errors.NoSuchElementError();
692
+ return { [W3C]: 'node-el' };
693
+ }), { sessionId: args[2] }, 'findElement', args, Object.assign({}, IPluginArgs_1.DefaultPluginArgs), false);
694
+ });
695
+ afterEach(() => {
696
+ nodeFind = hubFind = undefined;
697
+ restoreHealing();
698
+ });
699
+ /** The session's logged finds; the hub logs a command after it has answered it. */
700
+ function logsOf(id) {
701
+ return __awaiter(this, void 0, void 0, function* () {
702
+ for (let i = 0; i < 100; i++) {
703
+ const rows = yield scratch.db.sessionLog.findMany({
704
+ where: { session_id: id, command_name: 'findElement' },
705
+ });
706
+ if (rows.length)
707
+ return rows;
708
+ yield new Promise((r) => setTimeout(r, 20));
709
+ }
710
+ return [];
711
+ });
712
+ }
713
+ it('is recorded on the hub, against the session, with what it healed to', () => __awaiter(this, void 0, void 0, function* () {
714
+ yield boot({ dashboard: true });
715
+ const { id } = yield remoteSession();
716
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
717
+ .post(`/wd/hub/session/${id}/element`)
718
+ .send({ using: 'xpath', value: 'broken' });
719
+ (0, chai_1.expect)(res.status).to.equal(200);
720
+ (0, chai_1.expect)(res.body.value[W3C]).to.equal('node-healed-el');
721
+ const [row] = yield logsOf(id);
722
+ (0, chai_1.expect)(row, 'the find was logged').to.not.equal(undefined);
723
+ (0, chai_1.expect)(row).to.deep.include({
724
+ is_healed: true,
725
+ original_strategy: 'xpath',
726
+ original_selector: 'broken',
727
+ healed_strategy: 'xpath',
728
+ healed_selector: "//*[@resource-id='com.example:id/login']",
729
+ healing_confidence: 0.82,
730
+ healing_tier: 'Fuzzy XML',
731
+ });
732
+ (0, chai_1.expect)(nodeLogged.called, 'recorded on the node as well').to.equal(false);
733
+ }));
734
+ it("is Xenon's own: the client's answer carries nothing about it", () => __awaiter(this, void 0, void 0, function* () {
735
+ yield boot({ dashboard: true });
736
+ const { id } = yield remoteSession();
737
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
738
+ .post(`/wd/hub/session/${id}/element`)
739
+ .send({ using: 'xpath', value: 'broken' });
740
+ (0, chai_1.expect)(Object.keys(res.headers).filter((h) => h.startsWith('x-xenon'))).to.deep.equal([]);
741
+ }));
742
+ it("records the selector of a find on a node's phone that needed no heal", () => __awaiter(this, void 0, void 0, function* () {
743
+ yield boot({ dashboard: true });
744
+ const { id } = yield remoteSession();
745
+ yield (0, loopbackRequest_1.default)(hubUrl)
746
+ .post(`/wd/hub/session/${id}/element`)
747
+ .send({ using: 'id', value: 'login' })
748
+ .expect(200);
749
+ const [row] = yield logsOf(id);
750
+ (0, chai_1.expect)(row).to.deep.include({
751
+ is_healed: false,
752
+ original_strategy: 'id',
753
+ original_selector: 'login',
754
+ });
755
+ }));
756
+ it('takes a heal only from the node, never from the client', () => __awaiter(this, void 0, void 0, function* () {
757
+ yield boot({ dashboard: true });
758
+ const { id } = yield remoteSession();
759
+ const forged = Buffer.from(JSON.stringify({ originalSelector: 'login', healedSelector: '//x', confidence: 1 })).toString('base64url');
760
+ yield (0, loopbackRequest_1.default)(hubUrl)
761
+ .post(`/wd/hub/session/${id}/element`)
762
+ .set('x-xenon-heal', forged)
763
+ .send({ using: 'id', value: 'login' })
764
+ .expect(200);
765
+ const [row] = yield logsOf(id);
766
+ (0, chai_1.expect)(row.is_healed).to.equal(false);
767
+ (0, chai_1.expect)(nodeRequests[0].headers['x-xenon-heal']).to.equal(undefined);
768
+ }));
769
+ it('is recorded by the node itself when the find did not come from a hub', () => __awaiter(this, void 0, void 0, function* () {
770
+ yield boot();
771
+ const { id } = yield remoteSession({ registered: false });
772
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
773
+ .post(`/node/session/${id}/element`)
774
+ .send({ using: 'xpath', value: 'broken' });
775
+ (0, chai_1.expect)(res.status).to.equal(200);
776
+ (0, chai_1.expect)(res.headers['x-xenon-heal']).to.equal(undefined);
777
+ (0, chai_1.expect)(nodeLogged.calledOnce).to.equal(true);
778
+ }));
779
+ it('is recorded on the hub for its own session, whatever hub token a client sends', () => __awaiter(this, void 0, void 0, function* () {
780
+ yield boot({ dashboard: true });
781
+ const id = `hub-session-${stamp}`;
782
+ hub.sessions.add(id);
783
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
784
+ .post(`/wd/hub/session/${id}/element`)
785
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'not-a-hub')
786
+ .send({ using: 'xpath', value: 'broken' });
787
+ (0, chai_1.expect)(res.status).to.equal(200);
788
+ (0, chai_1.expect)(res.headers['x-xenon-heal']).to.equal(undefined);
789
+ (0, chai_1.expect)(nodeLogged.calledOnce, 'recorded by the server that healed it').to.equal(true);
790
+ }));
791
+ it('is neither sent nor recorded on the node when too long for the header', () => __awaiter(this, void 0, void 0, function* () {
792
+ yield boot({ dashboard: true });
793
+ const { id } = yield remoteSession();
794
+ attemptHealing.resolves({
795
+ id: 'node-healed-el',
796
+ tier: types_1.HealingTier.TIER_2_FUZZY_XML,
797
+ confidence: 0.82,
798
+ originalSelector: 'broken',
799
+ originalStrategy: 'xpath',
800
+ recommendedSelector: `//*[@text='${'x'.repeat(10000)}']`,
801
+ recommendedStrategy: 'xpath',
802
+ });
803
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
804
+ .post(`/wd/hub/session/${id}/element`)
805
+ .send({ using: 'xpath', value: 'broken' });
806
+ (0, chai_1.expect)(res.status).to.equal(200);
807
+ (0, chai_1.expect)(res.body.value[W3C]).to.equal('node-healed-el');
808
+ const [row] = yield logsOf(id);
809
+ (0, chai_1.expect)(row.is_healed).to.equal(false);
810
+ (0, chai_1.expect)(nodeLogged.called, 'recorded on the node, which has no row for it').to.equal(false);
811
+ }));
812
+ });
594
813
  });
@@ -133,6 +133,11 @@ describe('A hub and a node on one machine keep to their own phones', () => {
133
133
  for (const m of Object.keys(table.delegate)) {
134
134
  sinon_1.default.stub(prisma_1.prisma.device, m).callsFake(table.delegate[m]);
135
135
  }
136
+ // The phones' saved settings (deviceSettings.ts), so none reach a database.
137
+ const settings = (0, fake_device_table_1.fakeDeviceSettingTable)();
138
+ for (const m of Object.keys(settings.delegate)) {
139
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, m).callsFake(settings.delegate[m]);
140
+ }
136
141
  };
137
142
  beforeEach(() => {
138
143
  discover = () => __awaiter(void 0, void 0, void 0, function* () { return []; });
@@ -0,0 +1,192 @@
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 typedi_1 = require("typedi");
19
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
20
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
21
+ const types_1 = require("../../src/services/healing/types");
22
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
23
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
24
+ const event_manager_1 = require("../../src/dashboard/event-manager");
25
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ /**
29
+ * Elements Xenon found in a screenshot (Omni-Vision's `-custom:ai-*` locators,
30
+ * and self-healing's OCR and Visual AI tiers) are boxes on the screen with an
31
+ * id of Xenon's own (`omni_*`, `healed_ocr*`, `healed_visual*`). The
32
+ * interceptor answers their commands itself. Through 2.14:
33
+ *
34
+ * - a `-custom:ai-*` findElement that matched nothing answered `unknown error`
35
+ * ("NoSuchElement: AI Vision failed…"), and self-healing then ran on it;
36
+ * - a healed OCR or Visual AI element was tapped during the find, and the
37
+ * test's own click tapped it again;
38
+ * - `getText` on a healed element answered Xenon's note about the match;
39
+ * - `setValue` never reached the element: Appium passes it (text, elementId),
40
+ * and the interceptor read the text as the element id.
41
+ *
42
+ * No AI provider, OCR or phone: healing, OCR and the driver are stubbed.
43
+ */
44
+ describe('Elements Xenon found in a screenshot', () => {
45
+ (0, scratch_database_1.useScratchDatabase)();
46
+ const SESSION = 'visual-session';
47
+ let pluginArgs;
48
+ let restoreContainer;
49
+ let attemptHealing;
50
+ let driver;
51
+ let omni;
52
+ beforeEach(() => {
53
+ pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs);
54
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
55
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
56
+ attemptHealing = sinon_1.default.stub().resolves(null);
57
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
58
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
59
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
60
+ driver = {
61
+ sessionId: SESSION,
62
+ caps: { platformName: 'Android' },
63
+ performActions: sinon_1.default.stub().resolves(),
64
+ releaseActions: sinon_1.default.stub().resolves(),
65
+ findElement: sinon_1.default.stub().rejects(new Error('unexpected find')),
66
+ active: sinon_1.default.stub().resolves({ 'element-6066-11e4-a52e-4f735466cecf': 'field-1' }),
67
+ setValue: sinon_1.default.stub().resolves(null),
68
+ elementEnabled: sinon_1.default.stub().resolves(true),
69
+ };
70
+ });
71
+ afterEach(() => {
72
+ restoreContainer();
73
+ sinon_1.default.restore();
74
+ });
75
+ const run = (command, args, next = () => undefined) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return next(); }), driver, command, [...args, SESSION], pluginArgs, false);
76
+ const failure = (promise) => __awaiter(void 0, void 0, void 0, function* () {
77
+ try {
78
+ yield promise;
79
+ }
80
+ catch (err) {
81
+ return err;
82
+ }
83
+ throw new Error('expected the command to fail');
84
+ });
85
+ const tapsAt = () => driver.performActions.getCalls().map((c) => {
86
+ const move = c.args[0][0].actions[0];
87
+ return { x: move.x, y: move.y };
88
+ });
89
+ describe('a -custom:ai-* find that matches nothing', () => {
90
+ it('fails with the standard "no such element" error for ai-text', () => __awaiter(void 0, void 0, void 0, function* () {
91
+ sinon_1.default.stub(omni, 'findByText').resolves([]);
92
+ const err = yield failure(run('findElement', ['-custom:ai-text', 'Checkout']));
93
+ (0, chai_1.expect)(err.error).to.equal('no such element');
94
+ (0, chai_1.expect)(err.w3cStatus).to.equal(404);
95
+ }));
96
+ it('fails with the standard "no such element" error for ai-icon', () => __awaiter(void 0, void 0, void 0, function* () {
97
+ sinon_1.default.stub(omni, 'findByIcon').resolves(null);
98
+ const err = yield failure(run('findElement', ['-custom:ai-icon', 'a cart icon']));
99
+ (0, chai_1.expect)(err.error).to.equal('no such element');
100
+ }));
101
+ it('is not handed to self-healing', () => __awaiter(void 0, void 0, void 0, function* () {
102
+ sinon_1.default.stub(omni, 'findByText').resolves([]);
103
+ yield failure(run('findElement', ['-custom:ai-text', 'Checkout']));
104
+ (0, chai_1.expect)(attemptHealing.called).to.equal(false);
105
+ }));
106
+ it('still answers an empty list to findElements', () => __awaiter(void 0, void 0, void 0, function* () {
107
+ sinon_1.default.stub(omni, 'findByText').resolves([]);
108
+ (0, chai_1.expect)(yield run('findElements', ['-custom:ai-text', 'Checkout'])).to.deep.equal([]);
109
+ }));
110
+ });
111
+ describe('a healed OCR or Visual AI element', () => {
112
+ const healedBy = (id, tier, text) => (Object.assign({ id,
113
+ tier, confidence: 0.9, originalSelector: "//*[@text='Login']", originalStrategy: 'xpath', recommendedSelector: 'ocr:text="Login"', recommendedStrategy: 'xenon:visual', message: 'Found text "Login" via local OCR (90% confidence)', rect: { x: 100, y: 400, width: 80, height: 24 } }, (text !== undefined ? { text } : {})));
114
+ const findHealed = (healed) => __awaiter(void 0, void 0, void 0, function* () {
115
+ attemptHealing.resolves(healed);
116
+ return run('findElement', ['xpath', "//*[@text='Login']"], () => {
117
+ throw Object.assign(new Error('NoSuchElement: gone'), { name: 'NoSuchElementError' });
118
+ });
119
+ });
120
+ it('is not tapped during the find', () => __awaiter(void 0, void 0, void 0, function* () {
121
+ const found = yield findHealed(healedBy('healed_ocr_1', types_1.HealingTier.TIER_3_LOCAL_OCR, 'Login'));
122
+ (0, chai_1.expect)(found).to.deep.include({ 'element-6066-11e4-a52e-4f735466cecf': 'healed_ocr_1' });
123
+ (0, chai_1.expect)(driver.performActions.called, 'tapped during the find').to.equal(false);
124
+ }));
125
+ it("is tapped once, by the test's own click", () => __awaiter(void 0, void 0, void 0, function* () {
126
+ yield findHealed(healedBy('healed_visual_1', types_1.HealingTier.TIER_4_VISUAL_AI));
127
+ yield run('click', ['healed_visual_1']);
128
+ (0, chai_1.expect)(tapsAt()).to.deep.equal([{ x: 140, y: 412 }]);
129
+ }));
130
+ it('answers getText with the text OCR read, not a note about the match', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ yield findHealed(healedBy('healed_ocr_2', types_1.HealingTier.TIER_3_LOCAL_OCR, 'Login'));
132
+ (0, chai_1.expect)(yield run('getText', ['healed_ocr_2'])).to.equal('Login');
133
+ }));
134
+ it('refuses getText when Visual AI found it, since no text was read', () => __awaiter(void 0, void 0, void 0, function* () {
135
+ yield findHealed(healedBy('healed_visual_2', types_1.HealingTier.TIER_4_VISUAL_AI));
136
+ const err = yield failure(run('getText', ['healed_visual_2']));
137
+ (0, chai_1.expect)(err.error).to.equal('unsupported operation');
138
+ (0, chai_1.expect)(err.message).to.match(/text/i);
139
+ }));
140
+ });
141
+ describe('setValue on an element found in a screenshot', () => {
142
+ beforeEach(() => {
143
+ omni.remember(SESSION, {
144
+ id: 'omni_ocr_field',
145
+ text: 'Email',
146
+ rect: { x: 10, y: 20, width: 100, height: 40 },
147
+ confidence: 0.9,
148
+ });
149
+ });
150
+ it('taps it, then types into the field that has the keyboard focus', () => __awaiter(void 0, void 0, void 0, function* () {
151
+ yield run('setValue', ['me@example.com', 'omni_ocr_field']);
152
+ (0, chai_1.expect)(tapsAt()).to.deep.equal([{ x: 60, y: 40 }]);
153
+ (0, chai_1.expect)(driver.active.calledAfter(driver.performActions)).to.equal(true);
154
+ (0, chai_1.expect)(driver.setValue.firstCall.args.slice(0, 2)).to.deep.equal([
155
+ 'me@example.com',
156
+ 'field-1',
157
+ ]);
158
+ }));
159
+ it('refuses before tapping when the driver cannot tell which field has the focus', () => __awaiter(void 0, void 0, void 0, function* () {
160
+ delete driver.active;
161
+ const err = yield failure(run('setValue', ['me@example.com', 'omni_ocr_field']));
162
+ (0, chai_1.expect)(err.error).to.equal('unsupported operation');
163
+ (0, chai_1.expect)(driver.performActions.called, 'tapped').to.equal(false);
164
+ }));
165
+ it('says so when no field took the focus after the tap', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ driver.active.rejects(new Error('no element has focus'));
167
+ const err = yield failure(run('setValue', ['me@example.com', 'omni_ocr_field']));
168
+ (0, chai_1.expect)(err.error).to.equal('element not interactable');
169
+ (0, chai_1.expect)(driver.setValue.called).to.equal(false);
170
+ }));
171
+ });
172
+ describe('other commands on an element found in a screenshot', () => {
173
+ it("refuses them clearly instead of sending Xenon's id to the driver", () => __awaiter(void 0, void 0, void 0, function* () {
174
+ omni.remember(SESSION, {
175
+ id: 'omni_ai_cart',
176
+ rect: { x: 0, y: 0, width: 40, height: 40 },
177
+ confidence: 0.85,
178
+ });
179
+ const next = sinon_1.default.stub();
180
+ const err = yield failure(run('getAttribute', ['content-desc', 'omni_ai_cart'], next));
181
+ (0, chai_1.expect)(err.error).to.equal('unsupported operation');
182
+ (0, chai_1.expect)(next.called, 'sent to the driver').to.equal(false);
183
+ }));
184
+ });
185
+ describe('autowait before setValue on a real element', () => {
186
+ it('waits for the element, not for an element named after the text', () => __awaiter(void 0, void 0, void 0, function* () {
187
+ pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { autowait: Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs.autowait), { enabled: true }) });
188
+ yield run('setValue', ['hello', 'el-1']);
189
+ (0, chai_1.expect)(driver.elementEnabled.firstCall.args[0]).to.equal('el-1');
190
+ }));
191
+ });
192
+ });
@@ -0,0 +1,132 @@
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 prisma_store_1 = require("../../src/data-service/prisma-store");
15
+ const device_utils_1 = require("../../src/device-utils");
16
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
17
+ const scratch_database_1 = require("../helpers/scratch-database");
18
+ /**
19
+ * `appium:iPhoneOnly` / `appium:iPadOnly` become a device filter in
20
+ * getDeviceFiltersFromCapability, but the Prisma store, the one a running
21
+ * server uses, never applied it: a session asking for an iPad could be given
22
+ * an iPhone. These run the real store against a real (scratch) database, from
23
+ * the capability to the rows that come back.
24
+ *
25
+ * A real iPhone's name is whatever its owner typed ("QA-Phone-3"), so the
26
+ * family is read from what the phone reports (its model, then its form
27
+ * factor), and only from its name for a row written before those existed.
28
+ */
29
+ const HOST = 'http://127.0.0.1:4723';
30
+ const row = (udid, over) => (Object.assign({ udid, host: HOST, platform: 'ios', deviceType: 'real', realDevice: true, busy: false, userBlocked: false }, over));
31
+ const DEVICES = [
32
+ // Simulators: the name is the marketing name, and the form factor comes from it.
33
+ row('sim-iphone', {
34
+ name: 'iPhone 16 Pro',
35
+ deviceType: 'simulator',
36
+ realDevice: false,
37
+ formFactor: 'phone',
38
+ marketingName: 'iPhone 16 Pro',
39
+ }),
40
+ row('sim-ipad', {
41
+ name: 'iPad Pro 13-inch (M4)',
42
+ deviceType: 'simulator',
43
+ realDevice: false,
44
+ formFactor: 'tablet',
45
+ marketingName: 'iPad Pro 13-inch (M4)',
46
+ }),
47
+ row('sim-tv', {
48
+ name: 'Apple TV 4K',
49
+ deviceType: 'simulator',
50
+ realDevice: false,
51
+ formFactor: 'tv',
52
+ marketingName: 'Apple TV 4K',
53
+ }),
54
+ // Real phones, named by their owners.
55
+ row('real-iphone', {
56
+ name: 'QA-Phone-3',
57
+ model: 'iPhone17,3',
58
+ formFactor: 'phone',
59
+ marketingName: 'iPhone 16',
60
+ }),
61
+ row('real-ipad-named-iphone', {
62
+ name: 'iPhone spare',
63
+ model: 'iPad13,4',
64
+ formFactor: 'tablet',
65
+ marketingName: 'iPad Pro 11-inch (3rd generation)',
66
+ }),
67
+ // Rows written before the identity columns: only the name is known.
68
+ row('old-iphone', { name: 'iPhone 13' }),
69
+ row('old-ipad', { name: 'iPad (9th generation)' }),
70
+ row('old-unnamed', { name: 'unknown' }),
71
+ // Android: never an iPhone or an iPad, whatever its form factor.
72
+ row('pixel', {
73
+ name: 'Pixel 7',
74
+ platform: 'android',
75
+ formFactor: 'phone',
76
+ model: 'Pixel 7',
77
+ }),
78
+ row('pixel-tablet', {
79
+ name: 'Pixel Tablet',
80
+ platform: 'android',
81
+ formFactor: 'tablet',
82
+ model: 'Pixel Tablet',
83
+ }),
84
+ ];
85
+ describe('appium:iPhoneOnly and appium:iPadOnly', () => {
86
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
87
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
88
+ yield scratch.db.device.deleteMany({});
89
+ for (const d of DEVICES)
90
+ yield scratch.db.device.create({ data: d });
91
+ }));
92
+ /** The udids a session with these capabilities may be given. */
93
+ function allocatable(caps) {
94
+ return __awaiter(this, void 0, void 0, function* () {
95
+ const filter = (0, device_utils_1.getDeviceFiltersFromCapability)(caps, IPluginArgs_1.DefaultPluginArgs);
96
+ const found = yield new prisma_store_1.PrismaDeviceStore().getDevices(filter);
97
+ return found.map((d) => d.udid).sort();
98
+ });
99
+ }
100
+ it('iPhoneOnly gets iPhones: by model, by form factor, and by name for an old row', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ (0, chai_1.expect)(yield allocatable({ platformName: 'iOS', 'appium:iPhoneOnly': true })).to.deep.equal([
102
+ 'old-iphone',
103
+ 'real-iphone',
104
+ 'sim-iphone',
105
+ ]);
106
+ }));
107
+ it('iPadOnly gets iPads, including one named for an iPhone', () => __awaiter(void 0, void 0, void 0, function* () {
108
+ (0, chai_1.expect)(yield allocatable({ platformName: 'iOS', 'appium:iPadOnly': true })).to.deep.equal([
109
+ 'old-ipad',
110
+ 'real-ipad-named-iphone',
111
+ 'sim-ipad',
112
+ ]);
113
+ }));
114
+ it('gives an iPad when both are asked for, as before', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ (0, chai_1.expect)(yield allocatable({
116
+ platformName: 'iOS',
117
+ 'appium:iPhoneOnly': true,
118
+ 'appium:iPadOnly': true,
119
+ })).to.deep.equal(['old-ipad', 'real-ipad-named-iphone', 'sim-ipad']);
120
+ }));
121
+ it('never gives an Android phone, even when the platform is not named', () => __awaiter(void 0, void 0, void 0, function* () {
122
+ const ids = yield allocatable({ 'appium:iPadOnly': true });
123
+ (0, chai_1.expect)(ids).to.not.include.members(['pixel-tablet']);
124
+ const phones = yield allocatable({ 'appium:iPhoneOnly': true });
125
+ (0, chai_1.expect)(phones).to.not.include.members(['pixel']);
126
+ }));
127
+ it('filters nothing without either capability', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ const all = yield allocatable({ platformName: 'iOS' });
129
+ (0, chai_1.expect)(all).to.include.members(['sim-iphone', 'sim-ipad', 'real-iphone', 'old-unnamed']);
130
+ (0, chai_1.expect)(all).to.not.include.members(['pixel', 'pixel-tablet']);
131
+ }));
132
+ });