@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -1,43 +1,10 @@
1
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
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
23
7
  };
24
- var __importStar = (this && this.__importStar) || (function () {
25
- var ownKeys = function(o) {
26
- ownKeys = Object.getOwnPropertyNames || function (o) {
27
- var ar = [];
28
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
- return ar;
30
- };
31
- return ownKeys(o);
32
- };
33
- return function (mod) {
34
- if (mod && mod.__esModule) return mod;
35
- var result = {};
36
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
- __setModuleDefault(result, mod);
38
- return result;
39
- };
40
- })();
41
8
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
9
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
10
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -52,20 +19,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
52
19
  };
53
20
  var OmniVisionService_1;
54
21
  Object.defineProperty(exports, "__esModule", { value: true });
55
- exports.OmniVisionService = void 0;
22
+ exports.OmniVisionService = exports.MAX_VIRTUAL_ELEMENTS = exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION = void 0;
56
23
  const typedi_1 = require("typedi");
57
- const tesseract_js_1 = __importStar(require("tesseract.js"));
24
+ const tesseract_js_1 = require("tesseract.js");
25
+ const ocrData_1 = require("../ocr/ocrData");
58
26
  const sharp_1 = __importDefault(require("sharp"));
59
27
  const AIService_1 = require("../AIService");
60
28
  const logger_1 = __importDefault(require("../../logger"));
61
29
  const crypto_1 = __importDefault(require("crypto"));
30
+ const ocrTextMatch_1 = require("./ocrTextMatch");
31
+ const screenScale_1 = require("./screenScale");
32
+ /**
33
+ * How many virtual elements Xenon keeps, for one session and in all. The
34
+ * oldest go first: a test acts on what it found last, and a command on an
35
+ * element that went answers "no such element".
36
+ */
37
+ exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION = 1000;
38
+ exports.MAX_VIRTUAL_ELEMENTS = 10000;
62
39
  // Mobile UIs have sparse text on complex backgrounds - reduce contrast to preserve readability
63
40
  const DEFAULT_CONTRAST = 1.2; // Slightly above 1.0 for mild enhancement (was 0.8 which was too aggressive)
64
41
  const UPSCALE_THRESHOLD = 800; // Only upscale very small images (was 1500)
65
42
  let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
66
43
  constructor() {
67
44
  this.logger = logger_1.default.scope('OmniVision');
68
- this.virtualElementStore = new Map();
45
+ /**
46
+ * The virtual elements tests may still act on, by id, each with the session
47
+ * that found it, oldest first. Through 2.14 every element ever found stayed
48
+ * here until the server restarted, and any session could act on another's.
49
+ */
50
+ this.elements = new Map();
51
+ this.bySession = new Map();
69
52
  this.sharedWorker = null;
70
53
  this.workerBusy = false;
71
54
  this.uiLensCache = new Map();
@@ -75,7 +58,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
75
58
  if (this.sharedWorker)
76
59
  return this.sharedWorker;
77
60
  this.logger.info('Initializing persistent OCR worker with explicit configuration...');
78
- const worker = yield tesseract_js_1.default.createWorker('eng');
61
+ const worker = yield (0, ocrData_1.createOcrWorker)();
79
62
  // PSM.SPARSE_TEXT is optimized for irregular text layouts like mobile UIs
80
63
  // AUTO mode assumes document structure which fails on sparse mobile screens
81
64
  yield worker.setParameters({
@@ -379,10 +362,32 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
379
362
  }
380
363
  });
381
364
  }
365
+ /** The OCR words with usable boxes, in the screenshot's pixels. */
366
+ wordBoxes(words) {
367
+ return words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
368
+ }
369
+ /** Where `text` is among OCR's words, read with over 60% confidence, in reading order. */
370
+ textMatches(words, text) {
371
+ return (0, ocrTextMatch_1.findText)(this.wordBoxes(words), String(text !== null && text !== void 0 ? text : '')).filter((m) => m.confidence > 60);
372
+ }
382
373
  /**
383
- * Proactive OCR Search: Finds elements matching text even if not in XML
374
+ * Where `text` is on a screenshot, matched as `-custom:ai-text` matches it:
375
+ * in the screenshot's pixels, in reading order. Self-healing's OCR tier
376
+ * uses it. Throws when OCR fails.
384
377
  */
378
+ findTextInScreenshot(screenshotBase64, text) {
379
+ return __awaiter(this, void 0, void 0, function* () {
380
+ const { words } = yield this.performOcr(Buffer.from(screenshotBase64, 'base64'));
381
+ return this.textMatches(words, text);
382
+ });
383
+ }
385
384
  /**
385
+ * Proactive OCR Search: Finds elements matching text even if not in XML.
386
+ *
387
+ * The text may be several words ("Sign in"); see ocrTextMatch.ts. Each
388
+ * element's rect is in the driver's coordinates (points on iOS), like a
389
+ * real element's, so tapping it lands on the text.
390
+ *
386
391
  * `throwOnError`: fail instead of answering "no match" when the screenshot
387
392
  * or OCR fails. Device control's "Test locator" asks for it; an Appium
388
393
  * findElement doesn't, and turns the empty list into NoSuchElement.
@@ -398,22 +403,16 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
398
403
  this.logger.warn(`OCR proactive search yielded no words. Text content: "${ocrText.trim().substring(0, 100)}..."`);
399
404
  return [];
400
405
  }
401
- const matches = words.filter((w) => w.text && w.text.toLowerCase().includes(text.toLowerCase()) && w.confidence > 60);
402
- return matches.map((m) => {
403
- const el = {
404
- id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
405
- text: m.text,
406
- rect: {
407
- x: m.bbox.x0,
408
- y: m.bbox.y0,
409
- width: m.bbox.x1 - m.bbox.x0,
410
- height: m.bbox.y1 - m.bbox.y0,
411
- },
412
- confidence: m.confidence / 100,
413
- };
414
- this.virtualElementStore.set(el.id, el);
415
- return el;
416
- });
406
+ const matches = this.textMatches(words, text);
407
+ if (matches.length === 0)
408
+ return [];
409
+ const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
410
+ return matches.map((m) => ({
411
+ id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
412
+ text: m.text,
413
+ rect: (0, screenScale_1.toDriverRect)({ x: m.x0, y: m.y0, width: m.x1 - m.x0, height: m.y1 - m.y0 }, scale),
414
+ confidence: m.confidence / 100,
415
+ }));
417
416
  }
418
417
  catch (err) {
419
418
  this.logger.error(`OCR proactive search failed: ${err.message}`);
@@ -433,18 +432,14 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
433
432
  const screenshot = yield driver.getScreenshot();
434
433
  const coordinates = yield AIService_1.AI_SERVICE.visualFind(screenshot, iconDescription, opts);
435
434
  if (coordinates) {
436
- const el = {
437
- id: `omni_ai_${Date.now()}`,
438
- rect: {
439
- x: coordinates.x - 20,
440
- y: coordinates.y - 20,
441
- width: 40,
442
- height: 40,
443
- },
435
+ // The AI answers in the screenshot's pixels; the rect is the driver's
436
+ // (points on iOS), like a real element's.
437
+ const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
438
+ return {
439
+ id: `omni_ai_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
440
+ rect: (0, screenScale_1.toDriverRect)({ x: coordinates.x - 20, y: coordinates.y - 20, width: 40, height: 40 }, scale),
444
441
  confidence: 0.85,
445
442
  };
446
- this.virtualElementStore.set(el.id, el);
447
- return el;
448
443
  }
449
444
  }
450
445
  catch (err) {
@@ -460,6 +455,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
460
455
  */
461
456
  analyzeScreen(driver) {
462
457
  return __awaiter(this, void 0, void 0, function* () {
458
+ var _a;
463
459
  this.logger.info('Performing full screen analysis...');
464
460
  try {
465
461
  const screenshot = yield driver.getScreenshot();
@@ -470,32 +466,28 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
470
466
  this.validateBuffer(buffer, 'Screen Analysis');
471
467
  // 1. Get all text via OCR using the robust worker flow
472
468
  const { text: ocrText, words } = yield this.performOcr(buffer);
473
- // 2. Ask AI for qualitative analysis (Non-blocking)
474
- let aiAnalysis = null;
469
+ // 2. Ask the AI provider about the screen, with the screenshot. Its
470
+ // failure leaves the OCR answer standing, and says why there are no
471
+ // insights. Through 2.13.2 this sent the screenshot's base64 as a file
472
+ // path, so the provider got no image and a "why did this test fail"
473
+ // prompt, and answered about no screen at all.
474
+ let aiInsights = null;
475
+ let aiError;
475
476
  try {
476
- aiAnalysis = yield AIService_1.AI_SERVICE.analyzeFailure({
477
- sessionId: driver.sessionId,
478
- failureReason: 'Screen Analysis Request',
479
- commandLogs: [],
480
- deviceLogs: [],
481
- screenshotPath: screenshot, // In a real scenario, we'd save this to a file first or pass base64
482
- });
477
+ aiInsights = yield AIService_1.AI_SERVICE.describeScreen(screenshot);
483
478
  }
484
479
  catch (aiErr) {
485
- this.logger.warn(`AI insights skipped: ${aiErr.message}`);
480
+ aiError = (_a = aiErr === null || aiErr === void 0 ? void 0 : aiErr.message) !== null && _a !== void 0 ? _a : String(aiErr);
481
+ this.logger.warn(`AI insights skipped: ${aiError}`);
486
482
  }
487
- return {
488
- timestamp: new Date().toISOString(),
489
- ocr: {
483
+ return Object.assign({ timestamp: new Date().toISOString(), ocr: {
490
484
  text: ocrText,
491
485
  words: words.map((w) => ({
492
486
  text: w.text || '',
493
487
  confidence: w.confidence || 0,
494
488
  bbox: w.bbox || { x0: 0, y0: 0, x1: 0, y1: 0 },
495
489
  })),
496
- },
497
- ai_insights: aiAnalysis,
498
- };
490
+ }, ai_insights: aiInsights }, (aiError !== undefined ? { ai_insights_error: aiError } : {}));
499
491
  }
500
492
  catch (err) {
501
493
  this.logger.error(`Screen analysis failed: ${err.message}`);
@@ -503,14 +495,56 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
503
495
  }
504
496
  });
505
497
  }
506
- addVirtualElement(element) {
507
- this.virtualElementStore.set(element.id, element);
498
+ /**
499
+ * Keep `element` for `sessionId`, whose test was handed its id (a
500
+ * `-custom:ai-*` find, a heal), until the session ends (`forgetSession`)
501
+ * or the limits push it out. A find doesn't keep what it finds: device
502
+ * control's locator test only shows the ids, and a tap uses its element at
503
+ * once.
504
+ */
505
+ remember(sessionId, element) {
506
+ this.drop(element.id);
507
+ this.elements.set(element.id, { element, sessionId });
508
+ let ids = this.bySession.get(sessionId);
509
+ if (!ids) {
510
+ ids = new Set();
511
+ this.bySession.set(sessionId, ids);
512
+ }
513
+ ids.add(element.id);
514
+ if (ids.size > exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION)
515
+ this.drop(ids.values().next().value);
516
+ if (this.elements.size > exports.MAX_VIRTUAL_ELEMENTS)
517
+ this.drop(this.elements.keys().next().value);
508
518
  }
509
- getVirtualElement(id) {
510
- return this.virtualElementStore.get(id);
519
+ /** The element, when `sessionId` found it and still has it. */
520
+ getVirtualElement(id, sessionId) {
521
+ const held = this.elements.get(id);
522
+ return held && held.sessionId === sessionId ? held.element : undefined;
523
+ }
524
+ /** Drop every element a session found: it has ended (sessionMemory.ts). */
525
+ forgetSession(sessionId) {
526
+ var _a;
527
+ for (const id of (_a = this.bySession.get(sessionId)) !== null && _a !== void 0 ? _a : [])
528
+ this.elements.delete(id);
529
+ this.bySession.delete(sessionId);
530
+ }
531
+ drop(id) {
532
+ const held = this.elements.get(id);
533
+ if (!held)
534
+ return;
535
+ this.elements.delete(id);
536
+ const ids = this.bySession.get(held.sessionId);
537
+ ids === null || ids === void 0 ? void 0 : ids.delete(id);
538
+ if (ids && ids.size === 0)
539
+ this.bySession.delete(held.sessionId);
511
540
  }
512
541
  /**
513
542
  * Omni-Click: Find a target by OCR text and click its center using W3C actions.
543
+ *
544
+ * The text may be several words ("Sign in"); see ocrTextMatch.ts. The tap
545
+ * and `target` are in the driver's coordinates (points on iOS). A failed
546
+ * screenshot or OCR, or a screen size that can't be read on iOS, fails;
547
+ * `clicked: false` means the text was looked for and not found.
514
548
  */
515
549
  omniClickByText(driver, req) {
516
550
  return __awaiter(this, void 0, void 0, function* () {
@@ -533,22 +567,21 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
533
567
  const screenshot = yield driver.getScreenshot();
534
568
  const buffer = Buffer.from(screenshot, 'base64');
535
569
  const { words } = yield this.performOcr(buffer);
536
- const normalized = words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
537
- const matches = normalized
538
- .filter((w) => w.text.toLowerCase().includes(text.toLowerCase()))
539
- .sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
570
+ const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), text).sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
540
571
  if (matches.length === 0) {
541
572
  return { clicked: false, message: `No OCR match found for text "${text}"` };
542
573
  }
543
574
  const pick = matches[Math.min(index - 1, matches.length - 1)];
575
+ const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
576
+ const found = (0, screenScale_1.toDriverRect)({ x: pick.x0, y: pick.y0, width: pick.x1 - pick.x0, height: pick.y1 - pick.y0 }, scale);
544
577
  const rect = {
545
- x: Math.max(0, Math.round(pick.x0)),
546
- y: Math.max(0, Math.round(pick.y0)),
547
- width: Math.max(1, Math.round(pick.x1 - pick.x0)),
548
- height: Math.max(1, Math.round(pick.y1 - pick.y0)),
578
+ x: Math.max(0, Math.round(found.x)),
579
+ y: Math.max(0, Math.round(found.y)),
580
+ width: Math.max(1, Math.round(found.width)),
581
+ height: Math.max(1, Math.round(found.height)),
549
582
  };
550
- const cx = Math.round(rect.x + rect.width / 2);
551
- const cy = Math.round(rect.y + rect.height / 2);
583
+ const cx = Math.round(found.x + found.width / 2);
584
+ const cy = Math.round(found.y + found.height / 2);
552
585
  // Click via touch actions
553
586
  yield driver.performActions([
554
587
  {
@@ -583,6 +616,10 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
583
616
  }
584
617
  /**
585
618
  * Omni-Click by Icon: Find a target by visual description and click its center.
619
+ *
620
+ * Fails when it can't look (no AI provider, a failed screenshot or AI call);
621
+ * `clicked: false` means the AI looked and found nothing. Through 2.13.2 every
622
+ * one of those answered `clicked: false`, "No visual match found".
586
623
  */
587
624
  omniClickByIcon(driver, req) {
588
625
  return __awaiter(this, void 0, void 0, function* () {
@@ -596,7 +633,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
596
633
  };
597
634
  }
598
635
  this.logger.info(`Omni-Click: searching for icon "${icon}" via AI...`);
599
- const element = yield this.findByIcon(driver, icon);
636
+ const element = yield this.findByIcon(driver, icon, { throwOnError: true });
600
637
  if (!element) {
601
638
  return { clicked: false, message: `No visual match found for icon "${icon}"` };
602
639
  }
@@ -20,6 +20,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.VisionAssertionService = void 0;
22
22
  const typedi_1 = require("typedi");
23
+ const base_driver_1 = require("@appium/base-driver");
23
24
  const AIService_1 = require("../AIService");
24
25
  const logger_1 = __importDefault(require("../../logger"));
25
26
  let VisionAssertionService = class VisionAssertionService {
@@ -27,38 +28,40 @@ let VisionAssertionService = class VisionAssertionService {
27
28
  this.logger = logger_1.default.scope('VisionAssertion');
28
29
  }
29
30
  /**
30
- * Asserts a visual state using AI reasoning
31
+ * `xenon: assertVisualState`: whether `instruction`, a condition in plain
32
+ * words, holds on the device's screen now, as the AI provider judges it from
33
+ * a screenshot.
34
+ *
35
+ * Answers `{ result, message }` only with the provider's own verdict.
36
+ * Whenever there is none it fails, saying the condition was not checked: no
37
+ * condition, no screenshot, no AI provider, a failed or rate-limited call,
38
+ * or an answer that isn't true or false. It never answers `false` for "could
39
+ * not check": a test asserting that something is absent would pass. Through
40
+ * 2.13.2 it answered `{ result: true, message: 'Assertion placeholder' }` in
41
+ * all of those cases.
31
42
  */
32
43
  assertState(driver, instruction) {
33
44
  return __awaiter(this, void 0, void 0, function* () {
34
- this.logger.info(`Asserting visual state: "${instruction}"`);
45
+ var _a;
46
+ const condition = typeof instruction === 'string' ? instruction.trim() : '';
47
+ if (!condition) {
48
+ throw new base_driver_1.errors.InvalidArgumentError('assertVisualState needs a condition to check, as a string or { instruction }, ' +
49
+ "for example 'The cart is empty'.");
50
+ }
51
+ this.logger.info(`Asserting visual state: "${condition}"`);
52
+ let screenshot;
35
53
  try {
36
- const screenshot = yield driver.getScreenshot();
37
- const prompt = `
38
- You are a visual testing quality agent.
39
- Your task is to verify if the following condition is true based on the provided screenshot.
40
-
41
- Condition: "${instruction}"
42
-
43
- Response Format: JSON only, strictly { "result": boolean, "message": "detailed explanation of what you see" }.
44
- `;
45
- const response = yield AIService_1.AI_SERVICE.visualFind(screenshot, prompt);
46
- // Note: We use visualFind which calls analyze. We expect a JSON response.
47
- // Since analyze returns raw text, we attempt to parse it.
48
- if (response && response.result !== undefined) {
49
- return {
50
- result: response.result,
51
- message: response.message || 'Assertion evaluated successfully.',
52
- };
53
- }
54
- // Fallback: If AI_SERVICE.visualFind was used literally, it might not return what we expect.
55
- // Let's assume a more direct path if needed, but for now we'll stick to this pattern.
56
- return { result: true, message: 'Assertion placeholder' };
54
+ screenshot = yield driver.getScreenshot();
57
55
  }
58
56
  catch (err) {
59
- this.logger.error(`Visual assertion failed: ${err.message}`);
60
- return { result: false, message: `Error: ${err.message}` };
57
+ throw new Error(`No screenshot could be taken, so the condition was not checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
58
+ }
59
+ if (!screenshot) {
60
+ throw new Error('No screenshot could be taken, so the condition was not checked.');
61
61
  }
62
+ const verdict = yield AIService_1.AI_SERVICE.assertVisual(screenshot, condition);
63
+ this.logger.info(`"${condition}": ${verdict.result} (${verdict.message})`);
64
+ return verdict;
62
65
  });
63
66
  }
64
67
  };
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Finding a text, one word or several, among the words OCR read off a
4
+ * screenshot. Tesseract gives one box per word, so a text with a space in it
5
+ * ("Sign in") is looked for across neighbouring words on one line, and its
6
+ * box is the box around the words it covers.
7
+ *
8
+ * Pure: boxes in, boxes out, in the screenshot's pixels.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.wordRuns = wordRuns;
12
+ exports.findText = findText;
13
+ /**
14
+ * Two words further apart than this many times their height are not
15
+ * neighbours: a space is about a third of a word's height, and a gap this
16
+ * wide is another column, such as "Cancel" and "Done" at either end of a bar.
17
+ */
18
+ const MAX_GAP_IN_HEIGHTS = 1.5;
19
+ const normalize = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
20
+ const heightOf = (w) => Math.max(1, w.y1 - w.y0);
21
+ /** Whether a word sits on a line: their heights overlap by at least half the shorter one. */
22
+ function onLine(line, word) {
23
+ const overlap = Math.min(line.y1, word.y1) - Math.max(line.y0, word.y0);
24
+ return overlap > 0 && overlap >= Math.min(heightOf(line), heightOf(word)) / 2;
25
+ }
26
+ /**
27
+ * The words in runs of neighbours: on one line, left to right, with no wide
28
+ * gap between them. Runs are in reading order.
29
+ */
30
+ function wordRuns(words) {
31
+ const lines = [];
32
+ for (const word of [...words].sort((a, b) => a.y0 - b.y0 || a.x0 - b.x0)) {
33
+ const line = lines.find((l) => onLine(l, word));
34
+ if (line) {
35
+ line.words.push(word);
36
+ line.y0 = Math.min(line.y0, word.y0);
37
+ line.y1 = Math.max(line.y1, word.y1);
38
+ }
39
+ else {
40
+ lines.push({ y0: word.y0, y1: word.y1, words: [word] });
41
+ }
42
+ }
43
+ const runs = [];
44
+ for (const line of lines.sort((a, b) => a.y0 - b.y0)) {
45
+ let run = [];
46
+ for (const word of line.words.sort((a, b) => a.x0 - b.x0)) {
47
+ const previous = run[run.length - 1];
48
+ const gap = previous ? word.x0 - previous.x1 : 0;
49
+ if (previous && gap > MAX_GAP_IN_HEIGHTS * Math.max(heightOf(previous), heightOf(word))) {
50
+ runs.push(run);
51
+ run = [];
52
+ }
53
+ run.push(word);
54
+ }
55
+ if (run.length)
56
+ runs.push(run);
57
+ }
58
+ return runs;
59
+ }
60
+ /**
61
+ * Every place `text` appears, ignoring case and runs of spaces, within a word
62
+ * or across neighbouring words: "Sign in" in "Sign" "in", "password" in
63
+ * "password?". Each match covers the words it touches: its box surrounds
64
+ * them, its text is theirs joined with spaces, and its confidence is the
65
+ * lowest of theirs. A one-word text matches inside one word, as it always
66
+ * did. Matches are in reading order.
67
+ */
68
+ function findText(words, text) {
69
+ const wanted = normalize(text);
70
+ if (!wanted)
71
+ return [];
72
+ const matches = [];
73
+ for (const run of wordRuns(words)) {
74
+ // The run as one string, remembering where each word sits in it.
75
+ let line = '';
76
+ const spans = [];
77
+ for (const word of run) {
78
+ if (line)
79
+ line += ' ';
80
+ const start = line.length;
81
+ line += normalize(word.text);
82
+ spans.push({ start, end: line.length });
83
+ }
84
+ const seen = new Set();
85
+ for (let at = line.indexOf(wanted); at !== -1; at = line.indexOf(wanted, at + 1)) {
86
+ const end = at + wanted.length;
87
+ const first = spans.findIndex((s) => s.end > at);
88
+ const last = spans.findIndex((s) => s.end >= end);
89
+ const key = `${first}-${last}`;
90
+ if (first === -1 || last === -1 || seen.has(key))
91
+ continue;
92
+ seen.add(key);
93
+ const covered = run.slice(first, last + 1);
94
+ matches.push({
95
+ x0: Math.min(...covered.map((w) => w.x0)),
96
+ y0: Math.min(...covered.map((w) => w.y0)),
97
+ x1: Math.max(...covered.map((w) => w.x1)),
98
+ y1: Math.max(...covered.map((w) => w.y1)),
99
+ text: covered.map((w) => w.text).join(' '),
100
+ confidence: Math.min(...covered.map((w) => w.confidence)),
101
+ });
102
+ }
103
+ }
104
+ return matches;
105
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SAME_SCALE = void 0;
16
+ exports.driverUsesPoints = driverUsesPoints;
17
+ exports.screenScaleOf = screenScaleOf;
18
+ exports.toDriverRect = toDriverRect;
19
+ const sharp_1 = __importDefault(require("sharp"));
20
+ exports.SAME_SCALE = { x: 1, y: 1 };
21
+ /**
22
+ * Whether the driver's coordinates are points rather than the screenshot's
23
+ * pixels. A driver Xenon can't place (device control's screenshot-only
24
+ * stand-in, which never taps) keeps pixels.
25
+ */
26
+ function driverUsesPoints(driver) {
27
+ var _a, _b, _c, _d;
28
+ const platform = String((_d = (_b = (_a = driver === null || driver === void 0 ? void 0 : driver.caps) === null || _a === void 0 ? void 0 : _a.platformName) !== null && _b !== void 0 ? _b : (_c = driver === null || driver === void 0 ? void 0 : driver.opts) === null || _c === void 0 ? void 0 : _c.platformName) !== null && _d !== void 0 ? _d : '');
29
+ return ['ios', 'tvos'].includes(platform.toLowerCase());
30
+ }
31
+ /**
32
+ * The scale between `screenshotBase64` and the driver's coordinates:
33
+ * 1 unless the driver uses points, then the screenshot's size over the
34
+ * window's. Throws when that can't be worked out on a driver that uses
35
+ * points, since a tap in pixels there lands in the wrong place.
36
+ */
37
+ function screenScaleOf(driver, screenshotBase64) {
38
+ return __awaiter(this, void 0, void 0, function* () {
39
+ if (!driverUsesPoints(driver))
40
+ return exports.SAME_SCALE;
41
+ const image = yield (0, sharp_1.default)(Buffer.from(screenshotBase64, 'base64'))
42
+ .metadata()
43
+ .catch((err) => {
44
+ throw new Error(`The screenshot's size could not be read: ${err.message}`);
45
+ });
46
+ const window = typeof driver.getWindowRect === 'function'
47
+ ? yield driver.getWindowRect()
48
+ : yield driver.getWindowSize();
49
+ const x = Number(image.width) / Number(window === null || window === void 0 ? void 0 : window.width);
50
+ const y = Number(image.height) / Number(window === null || window === void 0 ? void 0 : window.height);
51
+ if (!(Number.isFinite(x) && x > 0 && Number.isFinite(y) && y > 0)) {
52
+ throw new Error(`The screen size could not be worked out (screenshot ${image.width}x${image.height}, ` +
53
+ `window ${window === null || window === void 0 ? void 0 : window.width}x${window === null || window === void 0 ? void 0 : window.height}).`);
54
+ }
55
+ return { x, y };
56
+ });
57
+ }
58
+ /** A rect found in the screenshot, in the driver's coordinates. */
59
+ function toDriverRect(rect, scale) {
60
+ if (scale.x === 1 && scale.y === 1)
61
+ return rect;
62
+ return {
63
+ x: rect.x / scale.x,
64
+ y: rect.y / scale.y,
65
+ width: rect.width / scale.x,
66
+ height: rect.height / scale.y,
67
+ };
68
+ }