@xenon-device-management/xenon 2.14.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -0,0 +1,122 @@
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 OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
20
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
21
+ const VisualAiHealingProvider_1 = require("../../src/services/healing/VisualAiHealingProvider");
22
+ const AIService_1 = require("../../src/services/AIService");
23
+ /**
24
+ * Self-healing's OCR tier took the first word OCR read that contained the
25
+ * text, or was part of it: for `Sign in` that was any lone "in" or "Sign" on
26
+ * the screen, and for `Login` a "Log". It now matches as Omni-Vision's
27
+ * `-custom:ai-text` does (ocrTextMatch.ts): the text across neighbouring words
28
+ * on one line, the first match in reading order.
29
+ *
30
+ * OCR is stubbed with the words it would read, in screenshot pixels: no
31
+ * Tesseract runs. ocr-healing-tier-real-ocr.spec.ts runs the tier on a real
32
+ * screenshot, which a stub can't stand in for: tesseract.js's own output is
33
+ * what hid the tier's never healing.
34
+ */
35
+ describe('The OCR healing tier matches text as Omni-Vision does', () => {
36
+ const word = (text, x0, y0, x1, y1) => ({
37
+ text,
38
+ confidence: 90,
39
+ bbox: { x0, y0, x1, y1 },
40
+ });
41
+ // A heading with "Log" and a lone "in" read first; then "Sign in", far
42
+ // from "Help" on its line; then "Login".
43
+ const WORDS = [
44
+ word('Log', 40, 60, 90, 84),
45
+ word('in', 300, 60, 320, 84),
46
+ word('Sign', 100, 200, 150, 224),
47
+ word('in', 158, 200, 180, 224),
48
+ word('Help', 600, 200, 650, 224),
49
+ word('Login', 100, 400, 180, 424),
50
+ ];
51
+ let driver;
52
+ beforeEach(() => {
53
+ // OCR reads these words: the tier asks Omni-Vision's OCR.
54
+ sinon_1.default
55
+ .stub(typedi_1.Container.get(OmniVisionService_1.OmniVisionService), 'performOcr')
56
+ .resolves({ text: '', words: WORDS });
57
+ // Android: no element is found by an iOS predicate, so the tier answers
58
+ // with where the text is.
59
+ driver = { findElement: sinon_1.default.stub().rejects(new Error('unsupported locator strategy')) };
60
+ });
61
+ afterEach(() => sinon_1.default.restore());
62
+ const heal = (selector) => new OcrHealingProvider_1.OcrHealingProvider().heal({
63
+ sessionId: 'ocr-session',
64
+ driver,
65
+ strategy: 'xpath',
66
+ selector,
67
+ screenshotBase64: 'aGVsbG8=',
68
+ });
69
+ it('finds a two-word text across its words, not a lone word of it', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ const healed = yield heal("//*[@text='Sign in']");
71
+ (0, chai_1.expect)(healed, 'healed').to.not.equal(null);
72
+ (0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Sign in"');
73
+ (0, chai_1.expect)(healed.rect).to.deep.equal({ x: 100, y: 200, width: 80, height: 24 });
74
+ }));
75
+ it('hands on the text it read, for getText on the element it returns', () => __awaiter(void 0, void 0, void 0, function* () {
76
+ const healed = yield heal("//*[@text='sign IN']");
77
+ (0, chai_1.expect)(healed.text).to.equal('Sign in');
78
+ (0, chai_1.expect)(healed.message).to.not.equal(healed.text);
79
+ }));
80
+ it('does not take a word that is only part of the text', () => __awaiter(void 0, void 0, void 0, function* () {
81
+ const healed = yield heal("//*[@text='Login']");
82
+ (0, chai_1.expect)(healed, 'healed').to.not.equal(null);
83
+ (0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Login"');
84
+ (0, chai_1.expect)(healed.rect).to.deep.equal({ x: 100, y: 400, width: 80, height: 24 });
85
+ }));
86
+ it('finds nothing when the text is not on the screen', () => __awaiter(void 0, void 0, void 0, function* () {
87
+ (0, chai_1.expect)(yield heal("//*[@text='Sign up']")).to.equal(null);
88
+ }));
89
+ });
90
+ /**
91
+ * A healed element's id names it in the server's memory of virtual elements,
92
+ * which every session shares. `healed_ocr_<ms>` was the same for two heals in
93
+ * one millisecond, so the second overwrote the first.
94
+ */
95
+ describe('Elements healed by OCR or Visual AI', () => {
96
+ afterEach(() => sinon_1.default.restore());
97
+ const context = {
98
+ sessionId: 'ids-session',
99
+ driver: { findElement: () => __awaiter(void 0, void 0, void 0, function* () { return Promise.reject(new Error('no predicate')); }) },
100
+ strategy: 'xpath',
101
+ selector: "//*[@text='Login']",
102
+ screenshotBase64: 'aGVsbG8=',
103
+ };
104
+ it('get ids of their own when healed in the same millisecond', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ var _a, _b, _c, _d;
106
+ sinon_1.default.stub(Date, 'now').returns(1700000000000);
107
+ sinon_1.default.stub(typedi_1.Container.get(OmniVisionService_1.OmniVisionService), 'performOcr').resolves({
108
+ text: '',
109
+ words: [{ text: 'Login', confidence: 90, bbox: { x0: 1, y0: 1, x1: 50, y1: 20 } }],
110
+ });
111
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(true);
112
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 100, y: 100 });
113
+ const ids = [
114
+ (_a = (yield new OcrHealingProvider_1.OcrHealingProvider().heal(context))) === null || _a === void 0 ? void 0 : _a.id,
115
+ (_b = (yield new OcrHealingProvider_1.OcrHealingProvider().heal(context))) === null || _b === void 0 ? void 0 : _b.id,
116
+ (_c = (yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal(context))) === null || _c === void 0 ? void 0 : _c.id,
117
+ (_d = (yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal(context))) === null || _d === void 0 ? void 0 : _d.id,
118
+ ];
119
+ (0, chai_1.expect)(ids.every(Boolean), String(ids)).to.equal(true);
120
+ (0, chai_1.expect)(new Set(ids).size, String(ids)).to.equal(4);
121
+ }));
122
+ });
@@ -0,0 +1,213 @@
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 crypto_1 = __importDefault(require("crypto"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const sharp_1 = __importDefault(require("sharp"));
22
+ const sinon_1 = __importDefault(require("sinon"));
23
+ const tesseract_js_1 = __importDefault(require("tesseract.js"));
24
+ const typedi_1 = require("typedi");
25
+ const package_json_1 = __importDefault(require("../../package.json"));
26
+ const ocrData_1 = require("../../src/services/ocr/ocrData");
27
+ const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
28
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
29
+ const container_registration_1 = require("../helpers/container-registration");
30
+ /**
31
+ * tesseract.js downloads its English data (eng.traineddata) from
32
+ * cdn.jsdelivr.net the first time a worker starts, and caches it in the
33
+ * server's working directory. A server with no internet couldn't run OCR at
34
+ * all: the first OCR call never finished, and every later one waited for it.
35
+ * One with internet left a 5 MB eng.traineddata wherever it was started (the
36
+ * home directory, ~/.appium).
37
+ *
38
+ * The data now ships with the plugin, read from there and cached nowhere.
39
+ */
40
+ describe('OCR without the internet', () => {
41
+ // The upstream file, @tesseract.js-data/eng@1.0.0 4.0.0_best_int/eng.traineddata.gz:
42
+ // what tesseract.js 7 downloads by default (LSTM only).
43
+ const UPSTREAM_SHA256 = '45b4cb346724ac1774f1c36f42f182b887bcdb28ebe63e6fff90ac41f3fcff91';
44
+ const tmpDirs = [];
45
+ const tmpDir = () => {
46
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-ocr-'));
47
+ tmpDirs.push(dir);
48
+ return dir;
49
+ };
50
+ afterEach(() => {
51
+ sinon_1.default.restore();
52
+ for (const dir of tmpDirs.splice(0))
53
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
54
+ });
55
+ /** A white image with `text` on it, as a phone's screenshot would have. */
56
+ const imageOf = (text) => (0, sharp_1.default)({ create: { width: 900, height: 220, channels: 3, background: '#ffffff' } })
57
+ .composite([
58
+ {
59
+ input: Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="900" height="220">` +
60
+ `<text x="40" y="140" font-family="Helvetica, Arial, sans-serif" font-size="96" fill="#000">${text}</text>` +
61
+ `</svg>`),
62
+ },
63
+ ])
64
+ .png()
65
+ .toBuffer();
66
+ describe('the English data', () => {
67
+ it('ships with the plugin, byte for byte the file tesseract.js would download', () => {
68
+ (0, chai_1.expect)(ocrData_1.OCR_DATA_FILE).to.equal(path_1.default.join(ocrData_1.OCR_DATA_DIR, 'eng.traineddata.gz'));
69
+ const sha = crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(ocrData_1.OCR_DATA_FILE)).digest('hex');
70
+ (0, chai_1.expect)(sha).to.equal(UPSTREAM_SHA256);
71
+ (0, chai_1.expect)(ocrData_1.OCR_DATA_SHA256).to.equal(UPSTREAM_SHA256);
72
+ const readme = fs_1.default.readFileSync(path_1.default.join(ocrData_1.OCR_DATA_DIR, 'README.md'), 'utf8');
73
+ (0, chai_1.expect)(readme).to.include(UPSTREAM_SHA256);
74
+ });
75
+ it('is copied into lib/ by the build, which is what the npm package ships', () => {
76
+ const copy = package_json_1.default.scripts['build:copy'];
77
+ (0, chai_1.expect)(copy).to.include('mkdir -p lib/src/services/ocr/vendor');
78
+ (0, chai_1.expect)(copy).to.include('cp src/services/ocr/vendor/*.gz');
79
+ (0, chai_1.expect)(package_json_1.default.files).to.include('lib');
80
+ });
81
+ it('is read from there, never fetched, and cached nowhere', () => {
82
+ const options = (0, ocrData_1.ocrWorkerOptions)();
83
+ (0, chai_1.expect)(options.langPath).to.equal(ocrData_1.OCR_DATA_DIR);
84
+ (0, chai_1.expect)(String(options.langPath)).not.to.match(/^[a-z]+:\/\//i);
85
+ (0, chai_1.expect)(options.cacheMethod).to.equal('none');
86
+ (0, chai_1.expect)(options.gzip).to.equal(true);
87
+ });
88
+ });
89
+ describe('a real OCR worker', () => {
90
+ let cwd;
91
+ beforeEach(() => {
92
+ cwd = process.cwd();
93
+ });
94
+ afterEach(() => {
95
+ process.chdir(cwd);
96
+ });
97
+ it('reads text, and writes nothing into the working directory', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ const workDir = tmpDir();
99
+ process.chdir(workDir);
100
+ const worker = yield (0, ocrData_1.createOcrWorker)();
101
+ try {
102
+ const { data } = yield worker.recognize(yield imageOf('Sign in'));
103
+ (0, chai_1.expect)(data.text).to.match(/sign\s*in/i);
104
+ }
105
+ finally {
106
+ yield worker.terminate();
107
+ }
108
+ (0, chai_1.expect)(fs_1.default.readdirSync(workDir)).to.deep.equal([]);
109
+ }));
110
+ it('fails with a reason, rather than wait for ever, when the data is missing', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ const started = Date.now();
112
+ const failure = yield (0, ocrData_1.createOcrWorker)(tmpDir()).then(() => chai_1.expect.fail('a worker with no data should not start'), (err) => err);
113
+ (0, chai_1.expect)(failure.message).to.include('eng.traineddata.gz');
114
+ (0, chai_1.expect)(Date.now() - started).to.be.below(5000);
115
+ }));
116
+ // Given data it can't load, tesseract.js 7 throws from its own message
117
+ // handler, and the server exits on an uncaught exception: the data is
118
+ // checked before a worker sees it.
119
+ for (const [damage, bytes] of [
120
+ ['not tesseract data at all', () => Buffer.from('not tesseract data')],
121
+ ['cut short', () => fs_1.default.readFileSync(ocrData_1.OCR_DATA_FILE).subarray(0, 100000)],
122
+ ]) {
123
+ it(`fails with a reason, before any worker starts, when the data is ${damage}`, () => __awaiter(void 0, void 0, void 0, function* () {
124
+ const dir = tmpDir();
125
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'eng.traineddata.gz'), bytes());
126
+ const createWorker = sinon_1.default.spy(tesseract_js_1.default, 'createWorker');
127
+ const failure = yield (0, ocrData_1.createOcrWorker)(dir).then(() => chai_1.expect.fail('a worker with damaged data should not start'), (err) => err);
128
+ (0, chai_1.expect)(failure.message).to.include('damaged');
129
+ (0, chai_1.expect)(createWorker.called).to.equal(false);
130
+ }));
131
+ }
132
+ });
133
+ describe('a worker that will not start', () => {
134
+ /** A directory holding a good copy of the data, so this test's memory is its own. */
135
+ const dataDir = () => {
136
+ const dir = tmpDir();
137
+ fs_1.default.copyFileSync(ocrData_1.OCR_DATA_FILE, path_1.default.join(dir, 'eng.traineddata.gz'));
138
+ return dir;
139
+ };
140
+ it('is remembered for a minute: OCR fails at once rather than start another', () => __awaiter(void 0, void 0, void 0, function* () {
141
+ // tesseract.js hands back no worker when its start fails, so its thread
142
+ // can't be ended; starting one per OCR call would leave one per call.
143
+ const clock = sinon_1.default.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
144
+ const createWorker = sinon_1.default
145
+ .stub(tesseract_js_1.default, 'createWorker')
146
+ .callsFake((_langs, _oem, options) => {
147
+ setTimeout(() => options.errorHandler('the OCR engine failed to load'), 5);
148
+ return new Promise(() => undefined);
149
+ });
150
+ const dir = dataDir();
151
+ const reason = (p) => p.then(() => chai_1.expect.fail('the worker should not start'), (err) => err.message);
152
+ (0, chai_1.expect)(yield reason((0, ocrData_1.createOcrWorker)(dir))).to.include('the OCR engine failed to load');
153
+ (0, chai_1.expect)(yield reason((0, ocrData_1.createOcrWorker)(dir))).to.include('the OCR engine failed to load');
154
+ (0, chai_1.expect)(createWorker.callCount).to.equal(1);
155
+ clock.tick(60001);
156
+ yield reason((0, ocrData_1.createOcrWorker)(dir));
157
+ (0, chai_1.expect)(createWorker.callCount).to.equal(2);
158
+ }));
159
+ });
160
+ describe('every place that runs OCR', () => {
161
+ let restoreContainer;
162
+ beforeEach(() => {
163
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
164
+ });
165
+ afterEach(() => {
166
+ restoreContainer();
167
+ });
168
+ it("the OCR healing tier reads the shipped data, with Omni-Vision's worker", () => __awaiter(void 0, void 0, void 0, function* () {
169
+ // The tier reads the screenshot through Omni-Vision (findTextInScreenshot),
170
+ // so it shares that worker and leaves it running for the next read.
171
+ const worker = {
172
+ setParameters: sinon_1.default.stub().resolves(),
173
+ recognize: sinon_1.default.stub().resolves({ data: { text: '', words: [], hocr: '', tsv: '' } }),
174
+ terminate: sinon_1.default.stub().resolves(),
175
+ };
176
+ const createWorker = sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(worker);
177
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
178
+ yield new OcrHealingProvider_1.OcrHealingProvider().heal({
179
+ screenshotBase64: (yield imageOf('Login')).toString('base64'),
180
+ selector: "//*[@text='Login']",
181
+ strategy: 'xpath',
182
+ driver: {},
183
+ });
184
+ (0, chai_1.expect)(createWorker.calledOnce).to.equal(true);
185
+ (0, chai_1.expect)(createWorker.firstCall.args[0]).to.equal('eng');
186
+ (0, chai_1.expect)(createWorker.firstCall.args[2]).to.include({
187
+ langPath: ocrData_1.OCR_DATA_DIR,
188
+ cacheMethod: 'none',
189
+ });
190
+ (0, chai_1.expect)(worker.recognize.calledOnce).to.equal(true);
191
+ (0, chai_1.expect)(worker.terminate.called).to.equal(false);
192
+ }));
193
+ it("Omni-Vision's worker reads the shipped data", () => __awaiter(void 0, void 0, void 0, function* () {
194
+ const worker = {
195
+ setParameters: sinon_1.default.stub().resolves(),
196
+ recognize: sinon_1.default.stub().resolves({ data: { text: '', words: [], hocr: '', tsv: '' } }),
197
+ terminate: sinon_1.default.stub().resolves(),
198
+ };
199
+ const createWorker = sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(worker);
200
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
201
+ const driver = {
202
+ getScreenshot: sinon_1.default.stub().resolves((yield imageOf('Login')).toString('base64')),
203
+ };
204
+ yield typedi_1.Container.get(OmniVisionService_1.OmniVisionService).findByText(driver, 'Login');
205
+ (0, chai_1.expect)(createWorker.calledOnce).to.equal(true);
206
+ (0, chai_1.expect)(createWorker.firstCall.args[0]).to.equal('eng');
207
+ (0, chai_1.expect)(createWorker.firstCall.args[2]).to.include({
208
+ langPath: ocrData_1.OCR_DATA_DIR,
209
+ cacheMethod: 'none',
210
+ });
211
+ }));
212
+ });
213
+ });
@@ -0,0 +1,301 @@
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 plugin_1 = require("../../src/plugin");
53
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
54
+ const DeviceService = __importStar(require("../../src/data-service/device-service"));
55
+ const ActiveLeases = __importStar(require("../../src/services/lease/activeLeases"));
56
+ const device_utils_1 = require("../../src/device-utils");
57
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
58
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
59
+ const types_1 = require("../../src/services/healing/types");
60
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
61
+ const AutowaitService_1 = require("../../src/services/autowait/AutowaitService");
62
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
63
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
64
+ const OrphanSweeper_1 = require("../../src/services/OrphanSweeper");
65
+ const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
66
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
67
+ const AIService_1 = require("../../src/services/AIService");
68
+ const event_manager_1 = require("../../src/dashboard/event-manager");
69
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
70
+ const container_registration_1 = require("../helpers/container-registration");
71
+ const scratch_database_1 = require("../helpers/scratch-database");
72
+ /**
73
+ * An element Xenon finds in a screenshot (a `-custom:ai-*` find, or an OCR or
74
+ * Visual AI heal) is a box on the screen it remembers by id. Through 2.14 it
75
+ * remembered every one, from every session, until the server restarted:
76
+ * device control's locator test and an icon `smartTap` added one per call
77
+ * that nothing ever read, and any session could act on another's. Now an
78
+ * element belongs to the session that found it and goes when that session
79
+ * ends, and the store has a size limit.
80
+ *
81
+ * OCR, the AI provider and the drivers are stubbed: no Tesseract, no AI call,
82
+ * no phone.
83
+ */
84
+ const W3C = 'element-6066-11e4-a52e-4f735466cecf';
85
+ const WORDS = [
86
+ { text: 'Sign', confidence: 90, bbox: { x0: 100, y0: 200, x1: 150, y1: 224 } },
87
+ { text: 'in', confidence: 90, bbox: { x0: 158, y0: 200, x1: 180, y1: 224 } },
88
+ ];
89
+ const box = (id) => ({
90
+ id,
91
+ rect: { x: 0, y: 0, width: 10, height: 10 },
92
+ confidence: 0.9,
93
+ });
94
+ describe('Elements found in a screenshot belong to their session', () => {
95
+ (0, scratch_database_1.useScratchDatabase)();
96
+ let restoreContainer;
97
+ let omni;
98
+ const driverFor = (sessionId) => ({
99
+ sessionId,
100
+ caps: { platformName: 'Android' },
101
+ getScreenshot: sinon_1.default.stub().resolves('aGVsbG8='),
102
+ performActions: sinon_1.default.stub().resolves(),
103
+ releaseActions: sinon_1.default.stub().resolves(),
104
+ });
105
+ beforeEach(() => {
106
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService, HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
107
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
108
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
109
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
110
+ sinon_1.default.stub(omni, 'performOcr').resolves({ text: '', words: WORDS });
111
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
112
+ });
113
+ afterEach(() => {
114
+ restoreContainer();
115
+ sinon_1.default.restore();
116
+ });
117
+ const run = (driver, command, args, next = () => null) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return next(); }), driver, command, [...args, driver.sessionId], Object.assign({}, IPluginArgs_1.DefaultPluginArgs), false);
118
+ const failure = (promise) => __awaiter(void 0, void 0, void 0, function* () {
119
+ try {
120
+ yield promise;
121
+ }
122
+ catch (err) {
123
+ return err;
124
+ }
125
+ throw new Error('expected the command to fail');
126
+ });
127
+ const held = () => omni.elements.size;
128
+ it("answers another session's command on it with no such element", () => __awaiter(void 0, void 0, void 0, function* () {
129
+ const a = driverFor('sess-a');
130
+ const b = driverFor('sess-b');
131
+ const found = yield run(a, 'findElement', ['-custom:ai-text', 'Sign in']);
132
+ const err = yield failure(run(b, 'click', [found[W3C]]));
133
+ (0, chai_1.expect)(err.error).to.equal('no such element');
134
+ (0, chai_1.expect)(b.performActions.called, "tapped on the other session's phone").to.equal(false);
135
+ yield run(a, 'click', [found[W3C]]);
136
+ (0, chai_1.expect)(a.performActions.calledOnce).to.equal(true);
137
+ }));
138
+ it('keeps a healed element for the session it healed in', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ const attemptHealing = sinon_1.default.stub().resolves({
140
+ id: 'healed_ocr_1_abc',
141
+ tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
142
+ confidence: 0.9,
143
+ originalSelector: "//*[@text='Sign in']",
144
+ originalStrategy: 'xpath',
145
+ recommendedSelector: 'ocr:text="Sign in"',
146
+ recommendedStrategy: 'xenon:visual',
147
+ text: 'Sign in',
148
+ rect: { x: 100, y: 200, width: 80, height: 24 },
149
+ });
150
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
151
+ const a = driverFor('sess-a');
152
+ yield run(a, 'findElement', ['xpath', "//*[@text='Sign in']"], () => {
153
+ throw Object.assign(new Error('NoSuchElement'), { name: 'NoSuchElementError' });
154
+ });
155
+ (0, chai_1.expect)(yield run(a, 'getText', ['healed_ocr_1_abc'])).to.equal('Sign in');
156
+ const err = yield failure(run(driverFor('sess-b'), 'getText', ['healed_ocr_1_abc']));
157
+ (0, chai_1.expect)(err.error).to.equal('no such element');
158
+ }));
159
+ it("keeps nothing for device control's locator test", () => __awaiter(void 0, void 0, void 0, function* () {
160
+ // As POST /control/<udid>/test-locator calls it: the ids are only shown.
161
+ const manual = { sessionId: 'manual_R5CT', getScreenshot: sinon_1.default.stub().resolves('aGVsbG8=') };
162
+ const found = yield omni.findByText(manual, 'Sign in', { throwOnError: true });
163
+ (0, chai_1.expect)(found).to.have.length(1);
164
+ (0, chai_1.expect)(held()).to.equal(0);
165
+ }));
166
+ it('keeps nothing for an icon smartTap, which taps at once', () => __awaiter(void 0, void 0, void 0, function* () {
167
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
168
+ const result = yield omni.omniClickByIcon(driverFor('sess-a'), { icon: 'the cart' });
169
+ (0, chai_1.expect)(result.clicked).to.equal(true);
170
+ (0, chai_1.expect)(held()).to.equal(0);
171
+ }));
172
+ it('keeps the newest 1,000 for a session', () => {
173
+ for (let i = 0; i < 1001; i++)
174
+ omni.remember('sess-a', box(`omni_ocr_${i}`));
175
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_0', 'sess-a')).to.equal(undefined);
176
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1', 'sess-a')).to.not.equal(undefined);
177
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1000', 'sess-a')).to.not.equal(undefined);
178
+ });
179
+ it('keeps the newest 10,000 in all', () => {
180
+ for (let s = 0; s < 11; s++) {
181
+ for (let i = 0; i < 1000; i++)
182
+ omni.remember(`sess-${s}`, box(`omni_ocr_${s}_${i}`));
183
+ }
184
+ (0, chai_1.expect)(held()).to.equal(10000);
185
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_0_999', 'sess-0')).to.equal(undefined);
186
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1_0', 'sess-1')).to.not.equal(undefined);
187
+ });
188
+ it('gives icon matches ids of their own, as text matches have', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ sinon_1.default.stub(Date, 'now').returns(1700000000000);
190
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
191
+ const a = driverFor('sess-a');
192
+ const first = yield omni.findByIcon(a, 'the cart');
193
+ const second = yield omni.findByIcon(a, 'the cart');
194
+ (0, chai_1.expect)(first === null || first === void 0 ? void 0 : first.id).to.not.equal(second === null || second === void 0 ? void 0 : second.id);
195
+ }));
196
+ });
197
+ /**
198
+ * Everything a session leaves in this server's memory for its commands goes
199
+ * when the session ends, however it ends: its virtual elements, and its
200
+ * autowait settings (`xenon: setAutowaitProperties`). Those were meant to be
201
+ * cleared by the interceptor on `deleteSession`, but XenonPlugin answers
202
+ * `deleteSession` itself, so Appium never hands that command to the
203
+ * interceptor: they were kept for good.
204
+ */
205
+ describe("A session's virtual elements and autowait settings go when it ends", function () {
206
+ this.timeout(30000);
207
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
208
+ let restoreRegs;
209
+ let omni;
210
+ const seed = (sessionId) => {
211
+ omni.remember(sessionId, box(`omni_ocr_${sessionId}`));
212
+ typedi_1.Container.get(AutowaitService_1.AutowaitService).setProps(sessionId, { timeoutMs: 1234 });
213
+ };
214
+ const expectForgotten = (sessionId) => {
215
+ (0, chai_1.expect)(omni.getVirtualElement(`omni_ocr_${sessionId}`, sessionId), 'element').to.equal(undefined);
216
+ (0, chai_1.expect)(typedi_1.Container.get(AutowaitService_1.AutowaitService).getProps(sessionId, Object.assign({}, IPluginArgs_1.DefaultPluginArgs)).timeoutMs, 'autowait timeout').to.equal(IPluginArgs_1.DefaultPluginArgs.autowait.timeoutMs);
217
+ };
218
+ beforeEach(() => {
219
+ restoreRegs = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService, AutowaitService_1.AutowaitService, PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
220
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
221
+ typedi_1.Container.set(AutowaitService_1.AutowaitService, new AutowaitService_1.AutowaitService());
222
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
223
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, {
224
+ restoreSession: sinon_1.default.stub().resolves(),
225
+ restoreAll: sinon_1.default.stub().resolves(),
226
+ });
227
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
228
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
229
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
230
+ });
231
+ afterEach(() => {
232
+ sinon_1.default.restore();
233
+ restoreRegs();
234
+ });
235
+ it('when the test deletes the session', () => __awaiter(this, void 0, void 0, function* () {
236
+ seed('s-deleted');
237
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), 's-deleted');
238
+ expectForgotten('s-deleted');
239
+ }));
240
+ it('when Appium ends the session itself (new-command timeout)', () => __awaiter(this, void 0, void 0, function* () {
241
+ seed('s-timeout');
242
+ const plugin = { pluginArgs: {}, xenonLog: { withSession: () => ({ info() { } }) } };
243
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-timeout', caps: { udid: 'R5CT' } }, new Error('New command timeout'));
244
+ expectForgotten('s-timeout');
245
+ }));
246
+ it('when Appium ends the session on a node', () => __awaiter(this, void 0, void 0, function* () {
247
+ seed('s-node');
248
+ sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves(undefined);
249
+ const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
250
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-node', caps: {} }, null);
251
+ expectForgotten('s-node');
252
+ }));
253
+ it("when Xenon's idle check releases the phone", () => __awaiter(this, void 0, void 0, function* () {
254
+ seed('s-idle');
255
+ const idle = {
256
+ udid: 'R5CT',
257
+ host: 'http://127.0.0.1:4723',
258
+ busy: true,
259
+ userBlocked: false,
260
+ session_id: 's-idle',
261
+ claimSessionId: 's-idle',
262
+ lastCmdExecutedAt: Date.now() - 10 * 60 * 1000,
263
+ };
264
+ sinon_1.default.stub(DeviceService, 'getAllDevices').resolves([idle]);
265
+ sinon_1.default.stub(ActiveLeases, 'activeLeasesByDevice').resolves(new Map());
266
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevice').resolves(true);
267
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
268
+ expectForgotten('s-idle');
269
+ }));
270
+ it('when a graceful shutdown drains the session', () => __awaiter(this, void 0, void 0, function* () {
271
+ seed('s-down');
272
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
273
+ expectForgotten('s-down');
274
+ }));
275
+ it("when the session's heartbeat goes stale", () => __awaiter(this, void 0, void 0, function* () {
276
+ seed('s-stale');
277
+ yield scratch.db.session.deleteMany({});
278
+ yield scratch.db.session.create({
279
+ data: {
280
+ id: 's-stale',
281
+ status: 'running',
282
+ desired_capabilities: '{}',
283
+ session_capabilities: '{}',
284
+ node_id: 'n',
285
+ has_live_video: false,
286
+ device_udid: 'R5CT',
287
+ device_platform: 'android',
288
+ device_version: '14',
289
+ last_heartbeat_at: new Date(Date.now() - 60 * 60 * 1000),
290
+ },
291
+ });
292
+ yield new OrphanSweeper_1.OrphanSweeper().sweep({ heartbeatIntervalMs: 1000 });
293
+ expectForgotten('s-stale');
294
+ }));
295
+ it("leaves other sessions' elements alone", () => __awaiter(this, void 0, void 0, function* () {
296
+ seed('s-ends');
297
+ seed('s-stays');
298
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), 's-ends');
299
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_s-stays', 's-stays')).to.not.equal(undefined);
300
+ }));
301
+ });
@@ -92,10 +92,8 @@ describe('Omni-Vision taps in points on iOS', () => {
92
92
  (0, chai_1.expect)(tappedAt(ios)).to.deep.equal({ x: 5, y: 11 });
93
93
  }));
94
94
  it('gives the -custom:ai-text locator an element in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
95
- var _a;
96
95
  const [found] = yield service.findByText(ios, 'OK');
97
96
  (0, chai_1.expect)(found.rect).to.deep.equal({ x: 4, y: 10, width: 2, height: 2 });
98
- (0, chai_1.expect)((_a = service.getVirtualElement(found.id)) === null || _a === void 0 ? void 0 : _a.rect).to.deep.equal(found.rect);
99
97
  }));
100
98
  it("doesn't tap on iOS when the window size can't be read", () => __awaiter(void 0, void 0, void 0, function* () {
101
99
  ios.getWindowRect.rejects(new Error('WDA is not running'));