@xenon-device-management/xenon 2.14.0 → 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 (259) hide show
  1. package/README.md +7 -4
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.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-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
  40. package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +5 -5
  77. package/lib/src/app/dashboardPluginLink.js +117 -0
  78. package/lib/src/app/index.js +13 -34
  79. package/lib/src/app/openapi/control.yaml +6 -4
  80. package/lib/src/app/openapi/grid.yaml +8 -2
  81. package/lib/src/app/openapi/identity.yaml +103 -31
  82. package/lib/src/app/openapi/platform.yaml +33 -15
  83. package/lib/src/app/openapi/sessions.yaml +10 -3
  84. package/lib/src/app/routers/apikeys.js +14 -2
  85. package/lib/src/app/routers/auth.js +29 -5
  86. package/lib/src/app/routers/control.js +13 -5
  87. package/lib/src/app/routers/dashboard.js +42 -27
  88. package/lib/src/app/routers/grid.js +5 -3
  89. package/lib/src/app/routers/profile.js +21 -0
  90. package/lib/src/app/routers/users.js +10 -5
  91. package/lib/src/app/swagger.js +2 -0
  92. package/lib/src/app/ws/h264StreamWs.js +10 -1
  93. package/lib/src/config.js +1 -0
  94. package/lib/src/dashboard/commands.js +5 -5
  95. package/lib/src/dashboard/event-manager.js +49 -61
  96. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  97. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  98. package/lib/src/data-service/device-store.js +14 -1
  99. package/lib/src/data-service/prisma-store.js +19 -7
  100. package/lib/src/data-service/web-config-service.js +7 -0
  101. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  102. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  103. package/lib/src/device-utils.js +20 -2
  104. package/lib/src/gateway/defaultGateway.js +1 -1
  105. package/lib/src/gateway/forwardToNode.js +2 -0
  106. package/lib/src/gateway/healReport.js +87 -0
  107. package/lib/src/gateway/sessionGateway.js +12 -2
  108. package/lib/src/generated/client/edge.js +4 -3
  109. package/lib/src/generated/client/index-browser.js +1 -0
  110. package/lib/src/generated/client/index.d.ts +32 -0
  111. package/lib/src/generated/client/index.js +4 -3
  112. package/lib/src/generated/client/package.json +1 -1
  113. package/lib/src/generated/client/schema.prisma +3 -0
  114. package/lib/src/generated/client/wasm.js +1 -0
  115. package/lib/src/interceptors/CommandInterceptor.js +243 -181
  116. package/lib/src/middleware/authMiddleware.js +15 -3
  117. package/lib/src/middleware/commandCaller.js +3 -3
  118. package/lib/src/middleware/verifyCredential.js +58 -1
  119. package/lib/src/plugin.js +8 -3
  120. package/lib/src/services/AICommandService.js +9 -2
  121. package/lib/src/services/AIService.js +192 -42
  122. package/lib/src/services/EmailService.js +29 -0
  123. package/lib/src/services/OrphanSweeper.js +2 -0
  124. package/lib/src/services/SelectorVerificationJob.js +15 -4
  125. package/lib/src/services/ServerManager.js +11 -1
  126. package/lib/src/services/SessionLifecycleService.js +86 -26
  127. package/lib/src/services/SocketServer.js +19 -3
  128. package/lib/src/services/bug-report/video-slice.js +4 -1
  129. package/lib/src/services/device-access/leaseOverride.js +7 -5
  130. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  131. package/lib/src/services/healing/HealEtalonService.js +3 -2
  132. package/lib/src/services/healing/HealingOrchestrator.js +55 -72
  133. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  134. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  135. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  136. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  137. package/lib/src/services/healing/resilioPath.js +274 -0
  138. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  139. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  140. package/lib/src/services/ocr/ocrData.js +118 -0
  141. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  142. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  143. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  144. package/lib/src/services/passwordResetLink.js +50 -9
  145. package/lib/src/services/passwordResetMode.js +3 -1
  146. package/lib/src/services/publicUrl.js +45 -0
  147. package/lib/src/services/session/sessionIdentity.js +5 -3
  148. package/lib/src/services/session/shutdownReason.js +26 -0
  149. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  150. package/lib/src/services/token/mintedLifetime.js +51 -0
  151. package/lib/src/sessions/SessionManager.js +8 -2
  152. package/lib/src/sessions/sessionMemory.js +25 -0
  153. package/lib/test/helpers/fake-ai-provider.js +188 -0
  154. package/lib/test/helpers/loki-stores.js +33 -0
  155. package/lib/test/helpers/page-sources.js +164 -0
  156. package/lib/test/helpers/scratch-database.js +57 -13
  157. package/lib/test/integration/audit-ingest.spec.js +10 -10
  158. package/lib/test/integration/auth-flow.spec.js +9 -14
  159. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  160. package/lib/test/integration/bug-report-route.spec.js +22 -2
  161. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  162. package/lib/test/integration/forgot-password.spec.js +17 -18
  163. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  164. package/lib/test/integration/migration.spec.js +6 -1
  165. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  166. package/lib/test/integration/ports-router.spec.js +4 -4
  167. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  168. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  169. package/lib/test/integration/profile-tokens.spec.js +10 -16
  170. package/lib/test/integration/recordings-router.spec.js +16 -16
  171. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  172. package/lib/test/integration/role-matrix.spec.js +4 -9
  173. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  174. package/lib/test/integration/sdk-version.spec.js +2 -2
  175. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  176. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  177. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  178. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  179. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  180. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  181. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  182. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  183. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  184. package/lib/test/integration/users-flow.spec.js +7 -12
  185. package/lib/test/integration/users-lockout.spec.js +5 -3
  186. package/lib/test/unit/EmailService.test.js +66 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  188. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  189. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  190. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  191. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  192. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  193. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  194. package/lib/test/unit/apikeys-router.spec.js +84 -0
  195. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  196. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  197. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  198. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  199. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  200. package/lib/test/unit/device-log-book.spec.js +180 -0
  201. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  202. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  203. package/lib/test/unit/failure-analysis.spec.js +182 -0
  204. package/lib/test/unit/failure-categories.spec.js +128 -0
  205. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  206. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  207. package/lib/test/unit/heal-report.spec.js +71 -0
  208. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  209. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  210. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  211. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
  212. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  213. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  214. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  215. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  216. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  217. package/lib/test/unit/ocr-offline.spec.js +213 -0
  218. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  219. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  220. package/lib/test/unit/omni-vision.spec.js +5 -7
  221. package/lib/test/unit/package-engines.spec.js +27 -0
  222. package/lib/test/unit/passwordResetMode.test.js +10 -2
  223. package/lib/test/unit/profile-router.test.js +122 -1
  224. package/lib/test/unit/queue-requester.spec.js +1 -1
  225. package/lib/test/unit/reset-link-address.spec.js +266 -0
  226. package/lib/test/unit/reset-link.test.js +10 -4
  227. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  228. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  229. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  230. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  231. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  232. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  233. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  234. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  235. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  236. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  237. package/lib/test/unit/session-device-logs.spec.js +347 -0
  238. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  239. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  240. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  241. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  242. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  243. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  244. package/lib/test/unit/verify-credential.spec.js +33 -0
  245. package/lib/test/unit/vision-assertion.spec.js +3 -2
  246. package/lib/test/unit/web-config-service.spec.js +19 -0
  247. package/lib/tsconfig.tsbuildinfo +1 -1
  248. package/package.json +5 -5
  249. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  250. package/prisma/schema.prisma +3 -0
  251. package/schema.json +5 -5
  252. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  253. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  254. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  255. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  256. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  257. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  258. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  259. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "prisma-client-ec1ae6fb328d1bc102086acd1f77e77477081d17b653d7f5092f083d0c8a4d34",
2
+ "name": "prisma-client-51d1df0b46f05c51a020cdf82ebc5860c67366c00fe846037d4445f0f8195b4a",
3
3
  "main": "index.js",
4
4
  "types": "index.d.ts",
5
5
  "browser": "index-browser.js",
@@ -307,6 +307,9 @@ model LocatorEtalon {
307
307
  strategy String
308
308
  attributes String
309
309
  nodeName String
310
+ /// The element's path through the page source when the selector last
311
+ /// worked (JSON, resilioPath.ts), for the Resilio healing tier.
312
+ path String?
310
313
  lastSeen DateTime @default(now())
311
314
  createdAt DateTime @default(now())
312
315
  updatedAt DateTime @updatedAt
@@ -353,6 +353,7 @@ exports.Prisma.LocatorEtalonScalarFieldEnum = {
353
353
  strategy: 'strategy',
354
354
  attributes: 'attributes',
355
355
  nodeName: 'nodeName',
356
+ path: 'path',
356
357
  lastSeen: 'lastSeen',
357
358
  createdAt: 'createdAt',
358
359
  updatedAt: 'updatedAt'
@@ -58,6 +58,8 @@ const event_manager_1 = require("../dashboard/event-manager");
58
58
  const SessionManager_1 = require("../sessions/SessionManager");
59
59
  const HealingOrchestrator_1 = require("../services/healing/HealingOrchestrator");
60
60
  const HealEtalonService_1 = require("../services/healing/HealEtalonService");
61
+ const resilioPath_1 = require("../services/healing/resilioPath");
62
+ const fingerprintIdentity_1 = require("../services/healing/fingerprintIdentity");
61
63
  const OmniVisionService_1 = require("../services/omni-vision/OmniVisionService");
62
64
  const AICommandService_1 = require("../services/AICommandService");
63
65
  const logger_1 = __importDefault(require("../logger"));
@@ -67,10 +69,46 @@ const AutowaitService_1 = require("../services/autowait/AutowaitService");
67
69
  const waitFor_1 = require("../services/autowait/waitFor");
68
70
  const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
69
71
  const xenonScripts_1 = require("./xenonScripts");
72
+ const healReport_1 = require("../gateway/healReport");
73
+ const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
74
+ /**
75
+ * The commands the interceptor answers itself for an element Xenon found in a
76
+ * screenshot (Omni-Vision's `-custom:ai-*` finds, self-healing's OCR and
77
+ * Visual AI tiers). Any other command naming one is refused.
78
+ */
79
+ const VIRTUAL_ELEMENT_COMMANDS = [
80
+ 'click',
81
+ 'getElementRect',
82
+ 'getElementLocation',
83
+ 'getElementSize',
84
+ 'getText',
85
+ 'setValue',
86
+ 'elementDisplayed',
87
+ 'elementEnabled',
88
+ ];
89
+ /** An id Xenon gave an element it found in a screenshot. */
90
+ function isVirtualElementId(id) {
91
+ return (typeof id === 'string' &&
92
+ (id.startsWith('omni_') || id.startsWith('healed_ocr') || id.startsWith('healed_visual')));
93
+ }
94
+ /**
95
+ * The element an element command is about. Appium passes a command's body
96
+ * first and the ids from its path after it: setValue is (text, elementId),
97
+ * the other commands Xenon looks at are (elementId, ...). Through 2.14 the
98
+ * text of a setValue was read as its element.
99
+ */
100
+ function elementIdOf(commandName, args) {
101
+ return commandName === 'setValue' ? args[1] : args[0];
102
+ }
70
103
  let CommandInterceptor = class CommandInterceptor {
71
104
  constructor() {
72
105
  this.log = logger_1.default.scope('CommandInterceptor');
106
+ // The sessions already told about their healingTiers, by driver: a driver
107
+ // lives as long as its session, so nothing has to forget it.
108
+ this.warnedHealingTiers = new WeakSet();
73
109
  this.learningSessions = new Set();
110
+ /** Selectors whose fingerprint was learnt again for its path in this process (bounded). */
111
+ this.relearnt = new Set();
74
112
  }
75
113
  handle(next, driver, commandName, args, pluginArgs, isHub) {
76
114
  return __awaiter(this, void 0, void 0, function* () {
@@ -112,7 +150,6 @@ let CommandInterceptor = class CommandInterceptor {
112
150
  }
113
151
  handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId, span, tracingService) {
114
152
  return __awaiter(this, void 0, void 0, function* () {
115
- var _a;
116
153
  if (commandName === 'createSession' || commandName === 'deleteSession') {
117
154
  try {
118
155
  return yield next();
@@ -254,37 +291,23 @@ let CommandInterceptor = class CommandInterceptor {
254
291
  return yield this.handleOmniVisionSearch(sessionId, driver, commandName, strategy, selector);
255
292
  }
256
293
  // --- OMNI-VISION: VIRTUAL ELEMENT INTERACTION ---
257
- const elementCommands = [
258
- 'click',
259
- 'getElementRect',
260
- 'getElementLocation',
261
- 'getElementSize',
262
- 'getText',
263
- 'setValue',
264
- 'elementDisplayed',
265
- 'elementEnabled',
266
- ];
267
- if (elementCommands.includes(commandName)) {
268
- const elementId = args[0];
269
- if (typeof elementId === 'string' &&
270
- (elementId.startsWith('omni_') ||
271
- elementId.startsWith('healed_ocr') ||
272
- elementId.startsWith('healed_visual'))) {
273
- return yield this.handleVirtualElementCommand(sessionId, driver, commandName, elementId, args[1]);
274
- }
294
+ // The driver doesn't know these elements, so nothing about one reaches it.
295
+ const virtualId = this.virtualElementIn(commandName, args, sessionId);
296
+ if (virtualId !== null) {
297
+ return yield this.handleVirtualElementCommand(sessionId, driver, commandName, virtualId, args[0]);
275
298
  }
276
299
  // --- AUTOWAIT: pre-action elementEnabled check for click/setValue/clear ---
277
300
  // Mirrors appium-wait-plugin behavior; runs before native command so
278
301
  // a NotEnabled state surfaces as a wait-then-retry rather than an
279
302
  // immediate failure. Skips elements managed by other Xenon subsystems.
280
303
  const autowait = typedi_1.Container.get(AutowaitService_1.AutowaitService).getProps(sessionId, pluginArgs);
304
+ const actedOn = elementIdOf(commandName, args);
281
305
  if (autowait.enabled &&
282
306
  ['click', 'setValue', 'clear'].includes(commandName) &&
283
307
  !autowait.excludeEnabledCheck.includes(commandName) &&
284
- typeof args[0] === 'string' &&
285
- !args[0].startsWith('omni_') &&
286
- !args[0].startsWith('healed_')) {
287
- yield this.waitForElementEnabled(driver, args[0], autowait);
308
+ typeof actedOn === 'string' &&
309
+ !isVirtualElementId(actedOn)) {
310
+ yield this.waitForElementEnabled(driver, actedOn, autowait);
288
311
  }
289
312
  // --- AUTOWAIT: polling find for findElement/findElements ---
290
313
  // Wraps next() in a poll loop so transient NoSuchElement errors get
@@ -296,91 +319,58 @@ let CommandInterceptor = class CommandInterceptor {
296
319
  !this.isVisualStrategy(strategy)) {
297
320
  const response = yield this.runFindWithAutowait(next, commandName, autowait);
298
321
  if (isHub && !!pluginArgs.enableDashboard && SessionManager_1.SESSION_MANAGER.isValidSession(sessionId)) {
299
- yield this.runPostCommandHooks(sessionId, commandName, driver, args, response, pluginArgs);
322
+ yield this.runPostCommandHooks(sessionId, commandName, driver, args, response);
300
323
  }
324
+ this.learnFromFind(sessionId, commandName, driver, args, response, pluginArgs);
301
325
  return response;
302
326
  }
303
327
  const response = yield next();
304
328
  if (isHub && !!pluginArgs.enableDashboard && SessionManager_1.SESSION_MANAGER.isValidSession(sessionId)) {
305
- yield this.runPostCommandHooks(sessionId, commandName, driver, args, response, pluginArgs);
329
+ yield this.runPostCommandHooks(sessionId, commandName, driver, args, response);
306
330
  }
331
+ this.learnFromFind(sessionId, commandName, driver, args, response, pluginArgs);
307
332
  return response;
308
333
  }
309
334
  catch (error) {
310
335
  if (this.isNoSuchElementError(error) &&
311
336
  ['findElement', 'findElements'].includes(commandName) &&
337
+ // A -custom:ai-* find already looked at the screen; the tiers would
338
+ // only look again for the words of its description.
339
+ !this.isVisualStrategy(args[0]) &&
312
340
  typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
313
341
  // §2.7 healing-tier capability gate: a session created with
314
342
  // xe:options.healingTiers (or the xenon:options alias) restricts
315
343
  // self-healing to those tier indices (1=Resilio, 2=Fuzzy XML, 3=OCR,
316
- // 4=Visual AI, 5=LLM).
317
- // Every optional hop is guarded — a missing/unrecoverable session,
318
- // capability, or malformed value all fall back to "run all tiers"
319
- // (fail open). coerceHealingTiersCap enforces that: an all-non-numeric
320
- // array (e.g. ["1","2"]) or an empty [] coerces to undefined instead
321
- // of [] — pre-fix, [] silently disabled healing entirely.
322
- const allowedHealingTiers = (0, HealingOrchestrator_1.healingTiersFromCaps)((_a = SessionManager_1.SESSION_MANAGER.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.getCapabilities());
323
- const healed = yield typedi_1.Container.get(HealingOrchestrator_1.HealingOrchestrator).attemptHealing(sessionId, driver, args[0], args[1], allowedHealingTiers);
344
+ // 4=Visual AI, 5=LLM), and so keeps its screen from the AI provider.
345
+ // Read from the session's own driver, which Appium hands the plugin
346
+ // with every command. SESSION_MANAGER holds a local session only with
347
+ // the dashboard on or a video recorded, and through 2.14 every other
348
+ // session ran every tier. A value that isn't a list of tier numbers
349
+ // runs only the tiers that stay on this server (coerceHealingTiersCap).
350
+ const asked = (0, HealingOrchestrator_1.healingTiersFromCaps)(driver === null || driver === void 0 ? void 0 : driver.caps);
351
+ if (asked.unreadable !== undefined) {
352
+ this.warnUnreadableHealingTiers(driver, sessionId, asked.unreadable);
353
+ }
354
+ const healed = yield typedi_1.Container.get(HealingOrchestrator_1.HealingOrchestrator).attemptHealing(sessionId, driver, args[0], args[1], asked.tiers);
324
355
  if (healed) {
325
- yield this.logHealingEvent(sessionId, commandName, driver, args, healed);
326
- let finalId = healed.id;
327
- // OCR/Visual AI tiers return virtual IDs with rect coordinates
328
- // We need to resolve a REAL element that Appium can interact with
356
+ yield this.recordHeal(sessionId, commandName, driver, args, healed);
357
+ // The OCR and Visual AI tiers can find only a position. The test gets
358
+ // a virtual element there, which its own click taps; nothing acts on
359
+ // the screen during the find. Through 2.14 the interceptor tapped
360
+ // the spot here, so the test's click tapped it a second time. On an
361
+ // iPhone it first asked for the first element covering the spot in
362
+ // tree order, which is an outer container such as the window.
329
363
  if (healed.id.startsWith('healed_') && healed.rect) {
330
- // REGISTER the virtual element for subsequent state checks
331
- typedi_1.Container.get(OmniVisionService_1.OmniVisionService).addVirtualElement({
364
+ typedi_1.Container.get(OmniVisionService_1.OmniVisionService).remember(sessionId, {
332
365
  id: healed.id,
333
366
  rect: healed.rect,
334
367
  confidence: healed.confidence,
335
- text: healed.message,
368
+ text: healed.text,
336
369
  });
337
- this.log.info(`[Interceptor] Visual healing returned coordinates. Resolving real element at (${healed.rect.x}, ${healed.rect.y})...`);
338
- let resolved = false;
339
- // Strategy 1: Try to find element at the center of the detected area
340
- try {
341
- const cx = Math.round(healed.rect.x + healed.rect.width / 2);
342
- const cy = Math.round(healed.rect.y + healed.rect.height / 2);
343
- const touchEl = yield driver.findElement('-ios class chain', `**/XCUIElementTypeAny[\`rect.x <= ${cx} AND rect.x + rect.width >= ${cx} AND rect.y <= ${cy} AND rect.y + rect.height >= ${cy}\`]`);
344
- if (touchEl) {
345
- finalId =
346
- touchEl.ELEMENT || touchEl['element-6066-11e4-a52e-4f735466cecf'] || finalId;
347
- resolved = !!finalId && !finalId.startsWith('healed_');
348
- }
349
- }
350
- catch (e) {
351
- // Strategy 1 failed
352
- }
353
- // Strategy 2: Use coordinate tap action (W3C Actions API)
354
- if (!resolved) {
355
- this.log.info('[Interceptor] Falling back to coordinate-based tap for visual healing');
356
- try {
357
- const cx = Math.round(healed.rect.x + healed.rect.width / 2);
358
- const cy = Math.round(healed.rect.y + healed.rect.height / 2);
359
- yield driver.performActions([
360
- {
361
- type: 'pointer',
362
- id: 'xenon-heal-tap',
363
- parameters: { pointerType: 'touch' },
364
- actions: [
365
- { type: 'pointerMove', duration: 0, x: cx, y: cy },
366
- { type: 'pointerDown', button: 0 },
367
- { type: 'pause', duration: 100 },
368
- { type: 'pointerUp', button: 0 },
369
- ],
370
- },
371
- ]);
372
- yield driver.releaseActions();
373
- this.log.info(`[Interceptor] ✅ Visual healing: tapped at (${cx}, ${cy})`);
374
- // Return the virtual ID — the tap already happened
375
- }
376
- catch (tapErr) {
377
- this.log.error(`[Interceptor] Coordinate tap failed: ${tapErr.message}`);
378
- }
379
- }
380
370
  }
381
371
  const elementResponse = {
382
- ELEMENT: finalId,
383
- 'element-6066-11e4-a52e-4f735466cecf': finalId,
372
+ ELEMENT: healed.id,
373
+ [W3C_ELEMENT_KEY]: healed.id,
384
374
  };
385
375
  return commandName === 'findElement' ? elementResponse : [elementResponse];
386
376
  }
@@ -401,6 +391,16 @@ let CommandInterceptor = class CommandInterceptor {
401
391
  }
402
392
  });
403
393
  }
394
+ warnUnreadableHealingTiers(driver, sessionId, unreadable) {
395
+ if (driver && typeof driver === 'object') {
396
+ if (this.warnedHealingTiers.has(driver))
397
+ return;
398
+ this.warnedHealingTiers.add(driver);
399
+ }
400
+ this.log.warn(`Session ${sessionId}: ${unreadable}. It heals with tiers 1, 2 and 3 only (Resilio, ` +
401
+ 'Fuzzy XML, OCR), which run on this server: healing never sends its screen to the AI ' +
402
+ 'provider.');
403
+ }
404
404
  isNoSuchElementError(error) {
405
405
  var _a;
406
406
  return (error.name === 'NoSuchElementError' ||
@@ -466,7 +466,8 @@ let CommandInterceptor = class CommandInterceptor {
466
466
  }
467
467
  });
468
468
  }
469
- runPostCommandHooks(sessionId, commandName, driver, args, response, pluginArgs) {
469
+ /** The dashboard's record of a command: only where enableDashboard records the session. */
470
+ runPostCommandHooks(sessionId, commandName, driver, args, response) {
470
471
  return __awaiter(this, void 0, void 0, function* () {
471
472
  try {
472
473
  yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, commandName, driver, {
@@ -475,17 +476,38 @@ let CommandInterceptor = class CommandInterceptor {
475
476
  path: `/${commandName}`,
476
477
  originalUrl: `/${commandName}`,
477
478
  }, {}, JSON.stringify({ value: response, sessionId }));
478
- if (commandName === 'findElement' &&
479
- response &&
480
- typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
481
- this.triggerLearning(driver, args, response, sessionId);
482
- }
483
479
  }
484
480
  catch (postCommandErr) {
485
481
  this.log.warn(`[Interceptor] Post-command hooks failed: ${postCommandErr.message}`);
486
482
  }
487
483
  });
488
484
  }
485
+ /**
486
+ * Learn the fingerprint of a selector a findElement found, which the Resilio
487
+ * and Fuzzy XML tiers heal it with later. On every session this server
488
+ * drives while self-healing is on, whatever enableDashboard says, nodes
489
+ * included. Through 2.14 it ran only behind the dashboard's record above,
490
+ * so a server with enableDashboard off, and every node, learnt nothing.
491
+ * A session that turned its own healing off (`healingTiers: []`) isn't
492
+ * learnt from, as the switch stops learning for every session.
493
+ * It reads the element in the background; the find has already answered.
494
+ */
495
+ learnFromFind(sessionId, commandName, driver, args, response, pluginArgs) {
496
+ var _a, _b;
497
+ // The find has its element: nothing here may fail it.
498
+ try {
499
+ if (commandName !== 'findElement' ||
500
+ !response ||
501
+ !typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs) ||
502
+ ((_a = (0, HealingOrchestrator_1.healingTiersFromCaps)(driver === null || driver === void 0 ? void 0 : driver.caps).tiers) === null || _a === void 0 ? void 0 : _a.length) === 0) {
503
+ return;
504
+ }
505
+ this.triggerLearning(driver, args, response, sessionId).catch((err) => { var _a; return this.log.debug(`[Learning] Failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
506
+ }
507
+ catch (err) {
508
+ this.log.debug(`[Learning] Skipped: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
509
+ }
510
+ }
489
511
  handleOmniVisionSearch(sessionId, driver, commandName, strategy, selector) {
490
512
  return __awaiter(this, void 0, void 0, function* () {
491
513
  const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
@@ -497,41 +519,70 @@ let CommandInterceptor = class CommandInterceptor {
497
519
  if (match)
498
520
  results = [match];
499
521
  }
522
+ // The session's test gets these ids, so the session keeps the elements.
523
+ for (const element of results)
524
+ omniService.remember(sessionId, element);
500
525
  const appiumResults = results.map((r) => ({
501
526
  ELEMENT: r.id,
502
527
  'element-6066-11e4-a52e-4f735466cecf': r.id,
503
528
  }));
504
529
  if (commandName === 'findElement') {
505
- if (appiumResults.length === 0)
506
- throw new Error('NoSuchElement: AI Vision failed to find matching element');
530
+ if (appiumResults.length === 0) {
531
+ const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
532
+ throw new errors.NoSuchElementError(`Xenon found nothing on the screen matching ${strategy} "${selector}".`);
533
+ }
507
534
  return appiumResults[0];
508
535
  }
509
536
  return appiumResults;
510
537
  });
511
538
  }
539
+ /**
540
+ * The virtual element a command is about, or null. A command the
541
+ * interceptor answers for one is recognised by its element id; any other
542
+ * command naming a virtual element Xenon holds is caught too, so it is
543
+ * refused rather than sent to a driver that doesn't know the id.
544
+ */
545
+ virtualElementIn(commandName, args, sessionId) {
546
+ if (VIRTUAL_ELEMENT_COMMANDS.includes(commandName)) {
547
+ const id = elementIdOf(commandName, args);
548
+ return isVirtualElementId(id) ? id : null;
549
+ }
550
+ const omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
551
+ const named = args.find((a) => isVirtualElementId(a) && omni.getVirtualElement(a, sessionId));
552
+ return named !== null && named !== void 0 ? named : null;
553
+ }
554
+ tapAt(driver, x, y) {
555
+ return __awaiter(this, void 0, void 0, function* () {
556
+ yield driver.performActions([
557
+ {
558
+ type: 'pointer',
559
+ id: 'finger1',
560
+ parameters: { pointerType: 'touch' },
561
+ actions: [
562
+ { type: 'pointerMove', duration: 0, x, y },
563
+ { type: 'pointerDown', button: 0 },
564
+ { type: 'pause', duration: 100 },
565
+ { type: 'pointerUp', button: 0 },
566
+ ],
567
+ },
568
+ ]);
569
+ });
570
+ }
512
571
  handleVirtualElementCommand(sessionId, driver, commandName, elementId, value) {
513
572
  return __awaiter(this, void 0, void 0, function* () {
573
+ var _a;
574
+ const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
514
575
  const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
515
- const element = omniService.getVirtualElement(elementId);
516
- if (!element)
517
- throw new Error(`NoSuchElement: Virtual element ${elementId} not found`);
576
+ // Another session's element is unknown here, as one that has gone is.
577
+ const element = omniService.getVirtualElement(elementId, sessionId);
578
+ if (!element) {
579
+ throw new errors.NoSuchElementError(`Xenon has no element ${elementId}.`);
580
+ }
518
581
  const centerX = Math.round(element.rect.x + element.rect.width / 2);
519
582
  const centerY = Math.round(element.rect.y + element.rect.height / 2);
520
583
  switch (commandName) {
521
584
  case 'click':
522
- yield driver.performActions([
523
- {
524
- type: 'pointer',
525
- id: 'finger1',
526
- parameters: { pointerType: 'touch' },
527
- actions: [
528
- { type: 'pointerMove', duration: 0, x: centerX, y: centerY },
529
- { type: 'pointerDown', button: 0 },
530
- { type: 'pause', duration: 100 },
531
- { type: 'pointerUp', button: 0 },
532
- ],
533
- },
534
- ]);
585
+ yield this.tapAt(driver, centerX, centerY);
535
586
  return null;
536
587
  case 'getElementRect':
537
588
  return element.rect;
@@ -543,33 +594,50 @@ let CommandInterceptor = class CommandInterceptor {
543
594
  case 'elementEnabled':
544
595
  return true;
545
596
  case 'getText':
546
- return element.text || '';
547
- case 'setValue':
548
- yield driver.performActions([
549
- {
550
- type: 'pointer',
551
- id: 'finger1',
552
- parameters: { pointerType: 'touch' },
553
- actions: [
554
- { type: 'pointerMove', duration: 0, x: centerX, y: centerY },
555
- { type: 'pointerDown', button: 0 },
556
- { type: 'pause', duration: 100 },
557
- { type: 'pointerUp', button: 0 },
558
- ],
559
- },
560
- ]);
597
+ // The text OCR read there. An element Visual AI found has none: it
598
+ // used to answer '' (or, healed, Xenon's note about the match).
599
+ if (typeof element.text === 'string')
600
+ return element.text;
601
+ throw new errors.UnsupportedOperationError(`Element ${elementId} was found by AI vision, which reads no text, so Xenon has no text for it.`);
602
+ case 'setValue': {
603
+ // The driver doesn't know this element, so the text goes to the field
604
+ // the tap gives the keyboard focus. A driver that can't say which
605
+ // field that is gets no tap at all.
606
+ if (typeof driver.active !== 'function') {
607
+ throw new errors.UnsupportedOperationError(`Xenon can't type into ${elementId}, an element it found in a screenshot: ` +
608
+ "this driver can't tell which field has the keyboard focus. Tap the element, then type with key actions.");
609
+ }
610
+ yield this.tapAt(driver, centerX, centerY);
611
+ let focused = null;
561
612
  try {
562
- return yield driver.setValue(elementId, value);
613
+ focused = yield driver.active();
614
+ }
615
+ catch (_b) {
616
+ focused = null;
617
+ }
618
+ const focusedId = (_a = focused === null || focused === void 0 ? void 0 : focused[W3C_ELEMENT_KEY]) !== null && _a !== void 0 ? _a : focused === null || focused === void 0 ? void 0 : focused.ELEMENT;
619
+ if (typeof focusedId !== 'string' || !focusedId) {
620
+ throw new errors.ElementNotInteractableError(`Xenon tapped ${elementId} at (${centerX}, ${centerY}), but no field took the keyboard focus, so there was nowhere to type.`);
621
+ }
622
+ try {
623
+ return yield driver.setValue(value, focusedId);
563
624
  }
564
625
  catch (e) {
565
626
  this.log.error(`setValue failed for virtual element ${elementId} on session ${sessionId}: ${e.message}`);
566
627
  throw e;
567
628
  }
629
+ }
568
630
  default:
569
- throw new Error(`Command ${commandName} not supported for visual elements`);
631
+ throw new errors.UnsupportedOperationError(`${commandName} isn't available on ${elementId}: Xenon found it in a screenshot, and ` +
632
+ `it is only a position on the screen. It answers ${VIRTUAL_ELEMENT_COMMANDS.join(', ')}.`);
570
633
  }
571
634
  });
572
635
  }
636
+ rememberRelearnt(selector) {
637
+ if (this.relearnt.size >= 10000)
638
+ this.relearnt.clear();
639
+ this.relearnt.add(selector);
640
+ }
573
641
  triggerLearning(driver, args, response, sessionId) {
574
642
  return __awaiter(this, void 0, void 0, function* () {
575
643
  if (this.learningSessions.has(sessionId))
@@ -583,31 +651,34 @@ let CommandInterceptor = class CommandInterceptor {
583
651
  return;
584
652
  }
585
653
  (() => __awaiter(this, void 0, void 0, function* () {
654
+ var _a;
586
655
  try {
587
656
  const etalonService = typedi_1.Container.get(HealEtalonService_1.HealEtalonService);
588
657
  // CRITICAL PERFORMANCE OPTIMIZATION:
589
658
  // Only trigger the heavy metadata collection if we don't already have an etalon for this selector.
590
659
  // Collecting page source and element rects for every single action is too CPU-intensive.
660
+ // A fingerprint stored without a usable path, or without anything
661
+ // that says which element it is (all learnt ones through 2.14), is
662
+ // learnt again, once per selector per process, so the Resilio tier
663
+ // gets its path and Fuzzy XML can use it.
591
664
  const existing = yield etalonService.getSignature(selector);
592
- if (existing) {
665
+ const complete = existing && (0, resilioPath_1.isResilioPath)(existing.path) && (0, fingerprintIdentity_1.hasIdentity)(existing.attributes);
666
+ if (existing && (complete || this.relearnt.has(selector))) {
593
667
  this.log.debug(`[Learning] Etalon already exists for selector: ${selector}. Skipping...`);
594
668
  return;
595
669
  }
596
- const anchors = [
597
- 'content-desc',
598
- 'resource-id',
599
- 'text',
600
- 'name',
601
- 'id',
602
- 'hint',
603
- 'label',
604
- 'value', // iOS-specific identity attributes
605
- ];
670
+ if (existing)
671
+ this.rememberRelearnt(selector);
606
672
  const nodeAttrs = [];
607
- for (const attr of anchors) {
673
+ // The WebDriver command is getAttribute(name, elementId). Through
674
+ // 2.14 this asked for getElementAttribute, which neither UiAutomator2
675
+ // nor XCUITest has, so no attribute was ever read. Only the attributes
676
+ // the driver has are asked for, and "null" (UiAutomator2's answer for
677
+ // one the element hasn't set) isn't kept.
678
+ for (const attr of (0, fingerprintIdentity_1.attributesToLearn)((_a = driver === null || driver === void 0 ? void 0 : driver.caps) === null || _a === void 0 ? void 0 : _a.platformName)) {
608
679
  try {
609
- const val = yield driver.getElementAttribute(elementId, attr);
610
- if (val)
680
+ const val = yield driver.getAttribute(attr, elementId);
681
+ if ((0, fingerprintIdentity_1.meaningful)(val))
611
682
  nodeAttrs.push({ name: attr, value: val });
612
683
  }
613
684
  catch (e) {
@@ -652,22 +723,14 @@ let CommandInterceptor = class CommandInterceptor {
652
723
  // Final safety check to ensure nodeName is a valid string
653
724
  if (!nodeName)
654
725
  nodeName = 'Unknown';
655
- // Path capture logic
726
+ // The element's path through the page source, for the Resilio tier:
727
+ // the element there that has the most of the attributes just read,
728
+ // and none when two have as many.
656
729
  let resiliotreePathJson = null;
657
730
  try {
658
- const { JSDOMParser, Path } = yield Promise.resolve().then(() => __importStar(require('resiliotree')));
659
731
  const pageSource = yield driver.getPageSource();
660
- const rootNode = new JSDOMParser().parse(pageSource);
661
- const foundNode = this.findMatchingNode(rootNode, nodeName, nodeAttrs);
662
- if (foundNode) {
663
- const pathNodes = [];
664
- let curr = foundNode;
665
- while (curr) {
666
- pathNodes.unshift(curr);
667
- curr = curr.parent;
668
- }
669
- resiliotreePathJson = new Path(pathNodes).toJSON();
670
- }
732
+ const element = (0, resilioPath_1.findLearntElement)(pageSource, nodeName, nodeAttrs);
733
+ resiliotreePathJson = element ? (0, resilioPath_1.resilioPathOf)(element, pageSource) : null;
671
734
  }
672
735
  catch (e) {
673
736
  // Silently ignore: path capture is optional for learning
@@ -683,43 +746,42 @@ let CommandInterceptor = class CommandInterceptor {
683
746
  }))();
684
747
  });
685
748
  }
686
- findMatchingNode(root, tag, attributes) {
687
- const attrMap = new Map(attributes.map((a) => [a.name.toLowerCase(), a.value]));
688
- const queue = [root];
689
- while (queue.length > 0) {
690
- const node = queue.shift();
691
- if (node.tag.toLowerCase() === tag.toLowerCase()) {
692
- let matchCount = 0;
693
- for (const [name, value] of attrMap) {
694
- if (node.otherAttributes.get(name) === value ||
695
- node.id === value ||
696
- node.classes.has(value))
697
- matchCount++;
698
- }
699
- if (matchCount > 0)
700
- return node;
749
+ /**
750
+ * A heal of a command a hub sent goes back to the hub, which owns the
751
+ * session's record (healReport.ts); this server records any other.
752
+ */
753
+ recordHeal(sessionId, commandName, driver, args, healed) {
754
+ return __awaiter(this, void 0, void 0, function* () {
755
+ const reported = (0, healReport_1.reportHeal)(this.healReportOf(args, healed));
756
+ if (reported === 'reported')
757
+ return;
758
+ if (reported === 'too-long') {
759
+ // The session is the hub's: this server has no record to write it to.
760
+ this.log.warn(`[Interceptor] The heal of ${args[0]}=${args[1]} is too long to send to the hub; it isn't recorded.`);
761
+ return;
701
762
  }
702
- if (node.children)
703
- queue.push(...node.children);
704
- }
705
- return null;
763
+ yield this.logHealingEvent(sessionId, commandName, driver, args, healed);
764
+ });
765
+ }
766
+ healReportOf(args, healed) {
767
+ var _a;
768
+ return {
769
+ originalSelector: args[1],
770
+ originalStrategy: args[0],
771
+ healedSelector: healed.recommendedSelector,
772
+ healedStrategy: (_a = healed.recommendedStrategy) !== null && _a !== void 0 ? _a : args[0],
773
+ confidence: healed.confidence,
774
+ tier: healed.tier,
775
+ };
706
776
  }
707
777
  logHealingEvent(sessionId, commandName, driver, args, healed) {
708
778
  return __awaiter(this, void 0, void 0, function* () {
709
- var _a;
710
779
  yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, commandName, driver, {
711
780
  body: args,
712
781
  method: 'POST',
713
782
  path: `/${commandName}`,
714
783
  originalUrl: `/${commandName}`,
715
- }, {}, JSON.stringify({ value: { ELEMENT: healed.id }, sessionId }), {
716
- originalSelector: args[1],
717
- originalStrategy: args[0],
718
- healedSelector: healed.recommendedSelector,
719
- healedStrategy: (_a = healed.recommendedStrategy) !== null && _a !== void 0 ? _a : args[0],
720
- confidence: healed.confidence,
721
- tier: healed.tier,
722
- });
784
+ }, {}, JSON.stringify({ value: { ELEMENT: healed.id }, sessionId }), this.healReportOf(args, healed));
723
785
  });
724
786
  }
725
787
  };