@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
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const scratch_database_1 = require("../helpers/scratch-database");
20
+ const loki_stores_1 = require("../helpers/loki-stores");
21
+ const device_store_1 = require("../../src/data-service/device-store");
22
+ const page_sources_1 = require("../helpers/page-sources");
23
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
24
+ const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
25
+ const ResilioTreeHealingProvider_1 = require("../../src/services/healing/ResilioTreeHealingProvider");
26
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
27
+ const resilioPath_1 = require("../../src/services/healing/resilioPath");
28
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
29
+ /**
30
+ * Self-healing's first tier, Resilio, finds an element by the path it had
31
+ * through the screen's element tree when its selector last worked. Through
32
+ * 2.14 it never found anything: the database dropped the path when it saved
33
+ * the selector's fingerprint, and the provider gives up without one. Its
34
+ * locators were also lowercase (an HTML parse of the page source), which the
35
+ * drivers' XPath doesn't match, and it answered with the nearest element
36
+ * however far it was, so with a path it would have healed to the wrong
37
+ * button whenever the right one was gone.
38
+ *
39
+ * Here the path is learnt from a found element, stored in a scratch
40
+ * database, and used on a changed screen. resiliotree runs for real; the
41
+ * driver is a fake that runs each XPath on the page source.
42
+ */
43
+ const FORM_A = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
44
+ page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
45
+ page_sources_1.android.button(2, 'forgot', 'Forgot password?', '[40,560][1040,680]') +
46
+ page_sources_1.android.button(3, 'login', 'Log in', '[40,720][1040,840]')));
47
+ // The buttons moved into a new row, after a new "Sign up" button.
48
+ const FORM_MOVED = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
49
+ page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
50
+ page_sources_1.android.layout(2, 'actions', '[0,560][1080,1100]', page_sources_1.android.button(0, 'signup', 'Sign up', '[40,560][1040,680]') +
51
+ page_sources_1.android.button(1, 'forgot', 'Forgot password?', '[40,720][1040,840]') +
52
+ page_sources_1.android.button(2, 'login', 'Log in', '[40,880][1040,1000]'))));
53
+ // The button is another one now: new id, new text.
54
+ const FORM_RENAMED = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
55
+ page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
56
+ page_sources_1.android.button(2, 'forgot', 'Forgot password?', '[40,560][1040,680]') +
57
+ page_sources_1.android.button(3, 'sign_in', 'Sign in', '[40,720][1040,840]')));
58
+ // The button is gone.
59
+ const FORM_GONE = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
60
+ page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
61
+ page_sources_1.android.button(2, 'forgot', 'Forgot password?', '[40,560][1040,680]')));
62
+ // Two buttons the path can't tell apart.
63
+ const FORM_TWICE = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.layout(0, '', '[0,200][1080,700]', page_sources_1.android.button(0, 'login', 'Log in', '[40,720][1040,840]')) +
64
+ page_sources_1.android.layout(1, '', '[0,700][1080,1400]', page_sources_1.android.button(0, 'login', 'Log in', '[40,720][1040,840]'))));
65
+ // A positional XPath, as an inspector writes one: the second button of the form.
66
+ const POSITIONAL = "//*[@resource-id='com.example.shop:id/login_form']/android.widget.Button[2]";
67
+ describe('Resilio healing, from the path a selector had when it worked', function () {
68
+ this.timeout(90000);
69
+ (0, scratch_database_1.useScratchDatabase)();
70
+ let etalons;
71
+ let restoreStore;
72
+ beforeEach(() => {
73
+ // The etalon service on the Prisma store (the scratch database), whatever
74
+ // store this process picked.
75
+ etalons = typedi_1.Container.get(HealEtalonService_1.HealEtalonService);
76
+ const service = etalons;
77
+ const before = service.store;
78
+ service.store = new prisma_store_1.PrismaHealEtalonStore();
79
+ restoreStore = () => {
80
+ service.store = before;
81
+ };
82
+ });
83
+ afterEach(() => {
84
+ restoreStore();
85
+ sinon_1.default.restore();
86
+ });
87
+ describe('the fingerprint store', () => {
88
+ const store = () => new prisma_store_1.PrismaHealEtalonStore();
89
+ const fingerprint = (selector, path) => ({
90
+ selector,
91
+ strategy: 'xpath',
92
+ attributes: { 'resource-id': 'com.example.shop:id/login' },
93
+ nodeName: 'android.widget.Button',
94
+ path,
95
+ lastSeen: Date.now(),
96
+ });
97
+ const PATH = { format: 'page-source-1', nodes: [{ tag: 'hierarchy', id: '', index: 0 }] };
98
+ it("keeps a selector's path", () => __awaiter(this, void 0, void 0, function* () {
99
+ var _a;
100
+ yield store().saveSignature(fingerprint('//keep-path', PATH));
101
+ (0, chai_1.expect)((_a = (yield store().getSignature('//keep-path'))) === null || _a === void 0 ? void 0 : _a.path).to.deep.equal(PATH);
102
+ }));
103
+ it('keeps the path when a later fingerprint comes without one', () => __awaiter(this, void 0, void 0, function* () {
104
+ var _a;
105
+ yield store().saveSignature(fingerprint('//keep-old-path', PATH));
106
+ yield store().saveSignature(fingerprint('//keep-old-path'));
107
+ (0, chai_1.expect)((_a = (yield store().getSignature('//keep-old-path'))) === null || _a === void 0 ? void 0 : _a.path).to.deep.equal(PATH);
108
+ }));
109
+ });
110
+ /** Learn `selector` as the interceptor does after a find, and wait for it. */
111
+ function learn(driver, selector) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ var _a;
114
+ const found = yield driver.findElement('xpath', selector);
115
+ typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).triggerLearning(driver, ['xpath', selector], found, `learn-${selector}`);
116
+ for (let i = 0; i < 100; i++) {
117
+ const signature = yield etalons.getSignature(selector);
118
+ if (((_a = signature === null || signature === void 0 ? void 0 : signature.path) === null || _a === void 0 ? void 0 : _a.format) === 'page-source-1')
119
+ return signature;
120
+ yield new Promise((r) => setTimeout(r, 20));
121
+ }
122
+ throw new Error(`no path was learnt for ${selector}`);
123
+ });
124
+ }
125
+ const heal = (driver, selector, pageSource) => new ResilioTreeHealingProvider_1.ResilioTreeHealingProvider(etalons).heal({
126
+ sessionId: 'resilio-session',
127
+ driver,
128
+ strategy: 'xpath',
129
+ selector,
130
+ pageSource,
131
+ });
132
+ it('finds an Android element that moved in the tree', () => __awaiter(this, void 0, void 0, function* () {
133
+ const driver = (0, page_sources_1.pageDriver)(FORM_A);
134
+ yield learn(driver, POSITIONAL);
135
+ driver.show(FORM_MOVED);
136
+ const healed = yield heal(driver, POSITIONAL, FORM_MOVED);
137
+ (0, chai_1.expect)(healed, 'healed').to.not.equal(null);
138
+ (0, chai_1.expect)(healed.id).to.equal('el:com.example.shop:id/login');
139
+ (0, chai_1.expect)(healed.confidence).to.be.greaterThan(0.9);
140
+ // The locator it suggests finds that element on the new screen.
141
+ const again = yield driver.findElement('xpath', healed.recommendedSelector);
142
+ (0, chai_1.expect)(again.ELEMENT).to.equal('el:com.example.shop:id/login');
143
+ }));
144
+ it('finds an iPhone element that moved in the tree', () => __awaiter(this, void 0, void 0, function* () {
145
+ const before = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.field('username', 'Username', 120) +
146
+ page_sources_1.ios.button('forgot', 'Forgot password?', 200) +
147
+ page_sources_1.ios.button('login', 'Log in', 260)));
148
+ const after = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.field('username', 'Username', 120) +
149
+ page_sources_1.ios.group(page_sources_1.ios.button('signup', 'Sign up', 200) +
150
+ page_sources_1.ios.button('forgot', 'Forgot password?', 260) +
151
+ page_sources_1.ios.button('login', 'Log in', 320))));
152
+ const selector = '//XCUIElementTypeOther/XCUIElementTypeButton[2]';
153
+ const driver = (0, page_sources_1.pageDriver)(before);
154
+ yield learn(driver, selector);
155
+ driver.show(after);
156
+ const healed = yield heal(driver, selector, after);
157
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:login');
158
+ const again = yield driver.findElement('xpath', healed.recommendedSelector);
159
+ (0, chai_1.expect)(again.ELEMENT).to.equal('el:login');
160
+ }));
161
+ it('leaves a renamed element to the next tier rather than guess', () => __awaiter(this, void 0, void 0, function* () {
162
+ const driver = (0, page_sources_1.pageDriver)(FORM_A);
163
+ yield learn(driver, POSITIONAL);
164
+ driver.show(FORM_RENAMED);
165
+ (0, chai_1.expect)(yield heal(driver, POSITIONAL, FORM_RENAMED)).to.equal(null);
166
+ }));
167
+ it('does not take a neighbour when the element is gone', () => __awaiter(this, void 0, void 0, function* () {
168
+ const driver = (0, page_sources_1.pageDriver)(FORM_A);
169
+ yield learn(driver, POSITIONAL);
170
+ driver.show(FORM_GONE);
171
+ (0, chai_1.expect)(yield heal(driver, POSITIONAL, FORM_GONE)).to.equal(null);
172
+ }));
173
+ it('does not pick one of two elements it cannot tell apart', () => __awaiter(this, void 0, void 0, function* () {
174
+ const driver = (0, page_sources_1.pageDriver)(FORM_A);
175
+ yield learn(driver, POSITIONAL);
176
+ driver.show(FORM_TWICE);
177
+ (0, chai_1.expect)(yield heal(driver, POSITIONAL, FORM_TWICE)).to.equal(null);
178
+ }));
179
+ it('finds an element that holds others, such as a list row, after it moved', () => __awaiter(this, void 0, void 0, function* () {
180
+ const row = (index) => page_sources_1.android.row(index, 'logout_row', '[0,900][1080,1040]', page_sources_1.android.image(0, 'logout_icon', 'Log out icon', '[40,920][140,1020]') +
181
+ page_sources_1.android.text(1, 'logout_label', 'Log out', '[160,940][600,1000]'));
182
+ const before = page_sources_1.android.page(page_sources_1.android.layout(0, 'settings', '[0,200][1080,2000]', page_sources_1.android.text(0, 'account', 'Account', '[40,220][600,300]') + row(1)));
183
+ const after = page_sources_1.android.page(page_sources_1.android.layout(0, 'settings', '[0,200][1080,2000]', page_sources_1.android.text(0, 'account', 'Account', '[40,220][600,300]') +
184
+ page_sources_1.android.layout(1, 'danger_zone', '[0,800][1080,1200]', row(0))));
185
+ const selector = "//*[@resource-id='com.example.shop:id/settings']/android.widget.LinearLayout[1]";
186
+ const driver = (0, page_sources_1.pageDriver)(before);
187
+ yield learn(driver, selector);
188
+ driver.show(after);
189
+ const healed = yield heal(driver, selector, after);
190
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:com.example.shop:id/logout_row');
191
+ }));
192
+ it('answers with the element it matched, not another one its locator also finds', () => __awaiter(this, void 0, void 0, function* () {
193
+ const cart = page_sources_1.android.button(1, 'cart', 'Cart', '[800,40][1040,140]');
194
+ const before = page_sources_1.android.page(page_sources_1.android.layout(0, 'toolbar', '[0,0][1080,200]', page_sources_1.android.text(0, 'title', 'Shop', '[40,40][600,140]') + cart));
195
+ // A badge whose description contains "Cart" now comes before the button.
196
+ const after = page_sources_1.android.page(page_sources_1.android.layout(0, 'toolbar', '[0,0][1080,200]', page_sources_1.android.text(0, 'title', 'Shop', '[40,40][600,140]') +
197
+ page_sources_1.android.image(1, 'badge', 'Cart badge, 3 items', '[700,40][780,140]') +
198
+ page_sources_1.android.layout(2, 'actions', '[800,0][1080,200]', page_sources_1.android.button(0, 'cart', 'Cart', '[800,40][1040,140]'))));
199
+ const selector = "//*[@resource-id='com.example.shop:id/toolbar']/android.widget.Button[1]";
200
+ const driver = (0, page_sources_1.pageDriver)(before);
201
+ yield learn(driver, selector);
202
+ driver.show(after);
203
+ const healed = yield heal(driver, selector, after);
204
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:com.example.shop:id/cart');
205
+ const matches = yield driver.findElements('xpath', healed.recommendedSelector);
206
+ (0, chai_1.expect)(matches.map((m) => m.ELEMENT)).to.deep.equal(['el:com.example.shop:id/cart']);
207
+ }));
208
+ it('does not heal to an element whose text contradicts the selector', () => __awaiter(this, void 0, void 0, function* () {
209
+ const dialog = (label) => page_sources_1.android.page(page_sources_1.android.layout(0, 'dialog', '[0,600][1080,1400]', page_sources_1.android.text(0, 'message', 'Discard this draft?', '[40,640][1040,800]') +
210
+ page_sources_1.android.button(1, 'button1', label, '[600,1200][1040,1340]')));
211
+ const selector = "//android.widget.Button[@text='OK']";
212
+ const driver = (0, page_sources_1.pageDriver)(dialog('OK'));
213
+ yield learn(driver, selector);
214
+ driver.show(dialog('Delete'));
215
+ (0, chai_1.expect)(yield heal(driver, selector, dialog('Delete'))).to.equal(null);
216
+ }));
217
+ it('learns no path from an element that shares only its size with the one found', () => {
218
+ // The screen changed between the find and the learning: the button found
219
+ // (name "gone") isn't on it, and another button only has its height.
220
+ const page = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.button('next', 'Next', 300)));
221
+ const learnt = (0, resilioPath_1.findLearntElement)(page, 'XCUIElementTypeButton', [
222
+ { name: 'name', value: 'gone' },
223
+ { name: 'label', value: 'Gone' },
224
+ { name: 'height', value: '44' },
225
+ ]);
226
+ (0, chai_1.expect)(learnt).to.equal(null);
227
+ });
228
+ it('learns the element that has the identity the driver read', () => {
229
+ const page = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.button('next', 'Next', 300)));
230
+ const learnt = (0, resilioPath_1.findLearntElement)(page, 'XCUIElementTypeButton', [
231
+ { name: 'name', value: 'next' },
232
+ { name: 'height', value: '44' },
233
+ ]);
234
+ (0, chai_1.expect)(learnt === null || learnt === void 0 ? void 0 : learnt.getAttribute('name')).to.equal('next');
235
+ });
236
+ it('ignores a path an older Xenon stored, and learns it again', () => __awaiter(this, void 0, void 0, function* () {
237
+ const selector = "//*[@resource-id='com.example.shop:id/login']";
238
+ yield etalons.saveSignature('xpath', selector, { nodeName: 'android.widget.Button' }, {
239
+ nodes: [{ tag: 'html', id: '', index: 0 }],
240
+ });
241
+ const driver = (0, page_sources_1.pageDriver)(FORM_A);
242
+ (0, chai_1.expect)(yield heal(driver, selector, FORM_MOVED)).to.equal(null);
243
+ const learnt = yield learn(driver, selector);
244
+ (0, chai_1.expect)(learnt.path.format).to.equal('page-source-1');
245
+ }));
246
+ it('learns the healed element’s path after a Fuzzy XML heal', () => __awaiter(this, void 0, void 0, function* () {
247
+ const driver = (0, page_sources_1.pageDriver)(FORM_MOVED);
248
+ const save = sinon_1.default.spy(etalons, 'saveSignature');
249
+ const orchestrator = new HealingOrchestrator_1.HealingOrchestrator(etalons);
250
+ // Only Fuzzy XML (tier 2), which finds the button by the words in the selector.
251
+ const healed = yield orchestrator.attemptHealing('fuzzy-session', Object.assign(driver, { getScreenshot: () => __awaiter(this, void 0, void 0, function* () { return 'aGVsbG8='; }) }), 'xpath', "//android.widget.Button[@text='Log in' and @resource-id='com.example.shop:id/log_in']", [2]);
252
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:com.example.shop:id/login');
253
+ (0, chai_1.expect)(save.called, 'fingerprint saved').to.equal(true);
254
+ const path = save.lastCall.args[3];
255
+ (0, chai_1.expect)(path === null || path === void 0 ? void 0 : path.format).to.equal('page-source-1');
256
+ (0, chai_1.expect)(path.nodes[path.nodes.length - 1].id).to.equal('com.example.shop:id/login');
257
+ }));
258
+ });
259
+ describe('The in-memory fingerprint store', () => {
260
+ (0, loki_stores_1.useLokiStores)();
261
+ it('keeps the path when a later fingerprint comes without one', () => __awaiter(void 0, void 0, void 0, function* () {
262
+ var _a;
263
+ const store = device_store_1.DeviceStoreFactory.getHealEtalonStore();
264
+ const PATH = { format: 'page-source-1', nodes: [{ tag: 'hierarchy', id: '', index: 0 }] };
265
+ const fingerprint = { selector: '//loki-path', strategy: 'xpath', attributes: {} };
266
+ yield store.saveSignature(Object.assign(Object.assign({}, fingerprint), { nodeName: 'android.widget.Button', path: PATH }));
267
+ yield store.saveSignature(Object.assign(Object.assign({}, fingerprint), { nodeName: 'android.widget.Button', path: undefined }));
268
+ (0, chai_1.expect)((_a = (yield store.getSignature('//loki-path'))) === null || _a === void 0 ? void 0 : _a.path).to.deep.equal(PATH);
269
+ }));
270
+ });
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
20
+ const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
21
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
22
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
23
+ const event_manager_1 = require("../../src/dashboard/event-manager");
24
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
25
+ const waitFor_1 = require("../../src/services/autowait/waitFor");
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ /**
29
+ * After a findElement finds its element, Xenon learns the selector's
30
+ * fingerprint (the element's attributes, position and path), which the
31
+ * Resilio and Fuzzy XML tiers heal it with later, on the server, without
32
+ * calling the AI provider.
33
+ *
34
+ * Through 2.14 it learnt only behind the dashboard's per-command record
35
+ * (`isHub && enableDashboard && SESSION_MANAGER.isValidSession`): never with
36
+ * `enableDashboard` off (the default), and never on a node. Those servers
37
+ * healed without fingerprints, so a broken selector went on to OCR, Visual AI
38
+ * and the LLM. Learning now follows the self-healing switch alone, on every
39
+ * session this server drives. The record itself is unchanged.
40
+ */
41
+ describe('selector learning, on every session this server drives', () => {
42
+ (0, scratch_database_1.useScratchDatabase)();
43
+ let restoreContainer;
44
+ let etalons;
45
+ let learn;
46
+ let record;
47
+ beforeEach(() => {
48
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealEtalonService_1.HealEtalonService, SelfHealingSwitch_1.SelfHealingSwitch);
49
+ // The switch as at boot with nothing saved: it follows the options.
50
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
51
+ etalons = {
52
+ getSignature: sinon_1.default.stub().resolves(null),
53
+ saveSignature: sinon_1.default.stub().resolves(),
54
+ };
55
+ typedi_1.Container.set(HealEtalonService_1.HealEtalonService, etalons);
56
+ learn = sinon_1.default.spy(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
57
+ record = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
58
+ });
59
+ afterEach(() => {
60
+ sinon_1.default.restore();
61
+ restoreContainer();
62
+ });
63
+ const options = (over = {}) => (Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false }), over));
64
+ let sessions = 0;
65
+ /** A session's driver, with what learning reads from a found element. */
66
+ function sessionDriver(caps = {}) {
67
+ sessions += 1;
68
+ return {
69
+ sessionId: `learn-${sessions}-${Date.now()}`,
70
+ caps: Object.assign({ platformName: 'Android' }, caps),
71
+ // As UiAutomator2 answers: String(value), so "null" for an unset one.
72
+ getAttribute: (name, _id) => __awaiter(this, void 0, void 0, function* () { var _a; return String((_a = { 'resource-id': 'com.shop:id/pay', text: 'Pay now' }[name]) !== null && _a !== void 0 ? _a : null); }),
73
+ getElementRect: () => __awaiter(this, void 0, void 0, function* () { return ({ x: 40, y: 900, width: 300, height: 96 }); }),
74
+ getName: () => __awaiter(this, void 0, void 0, function* () { return 'android.widget.Button'; }),
75
+ getPageSource: () => __awaiter(this, void 0, void 0, function* () { return '<hierarchy><android.widget.Button text="Pay now" resource-id="com.shop:id/pay"/></hierarchy>'; }),
76
+ };
77
+ }
78
+ const PAY = ['id', 'com.shop:id/pay'];
79
+ const FOUND = { ELEMENT: 'el-1', 'element-6066-11e4-a52e-4f735466cecf': 'el-1' };
80
+ /** A findElement the driver answers with its element. */
81
+ function find(driver_1, pluginArgs_1, _a) {
82
+ return __awaiter(this, arguments, void 0, function* (driver, pluginArgs, { isHub, command = 'findElement' }) {
83
+ const response = command === 'findElements' ? [FOUND] : FOUND;
84
+ return typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () { return response; }), driver, command, [...PAY, driver.sessionId], pluginArgs, isHub);
85
+ });
86
+ }
87
+ /** Learning runs in the background, after the find has answered. */
88
+ function learnt() {
89
+ return __awaiter(this, void 0, void 0, function* () {
90
+ const result = yield (0, waitFor_1.waitFor)(() => __awaiter(this, void 0, void 0, function* () { return (etalons.saveSignature.called ? true : null); }), {
91
+ timeoutMs: 2000,
92
+ intervalMs: 5,
93
+ });
94
+ return 'value' in result;
95
+ });
96
+ }
97
+ for (const [where, isHub] of [
98
+ ['a standalone server', true],
99
+ ["a node running its hub's session", false],
100
+ ]) {
101
+ it(`learns a found selector on ${where} with enableDashboard off`, () => __awaiter(void 0, void 0, void 0, function* () {
102
+ const driver = sessionDriver();
103
+ // The case: nothing registers it, as nothing registered it before.
104
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.isValidSession(driver.sessionId)).to.equal(false);
105
+ const answer = yield find(driver, options(), { isHub });
106
+ (0, chai_1.expect)(answer).to.deep.equal(FOUND);
107
+ (0, chai_1.expect)(learn.calledOnce, 'learning started').to.equal(true);
108
+ (0, chai_1.expect)(yield learnt(), 'a fingerprint was saved').to.equal(true);
109
+ (0, chai_1.expect)(etalons.saveSignature.firstCall.args.slice(0, 2)).to.deep.equal(PAY);
110
+ // A fingerprint that says which element it is, which Fuzzy XML can use.
111
+ (0, chai_1.expect)(etalons.saveSignature.firstCall.args[2].attributes).to.deep.include({
112
+ name: 'resource-id',
113
+ value: 'com.shop:id/pay',
114
+ });
115
+ // The dashboard's record is still the dashboard's: nothing was recorded.
116
+ (0, chai_1.expect)(record.called).to.equal(false);
117
+ }));
118
+ }
119
+ it('learns a selector autowait found', () => __awaiter(void 0, void 0, void 0, function* () {
120
+ const driver = sessionDriver();
121
+ const pluginArgs = options({
122
+ autowait: {
123
+ enabled: true,
124
+ timeoutMs: 500,
125
+ intervalBetweenAttemptsMs: 10,
126
+ excludeEnabledCheck: [],
127
+ },
128
+ });
129
+ yield find(driver, pluginArgs, { isHub: true });
130
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
131
+ (0, chai_1.expect)(yield learnt()).to.equal(true);
132
+ }));
133
+ it('learns once, and still records, where the dashboard records the session', () => __awaiter(void 0, void 0, void 0, function* () {
134
+ const driver = sessionDriver();
135
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'isValidSession').returns(true);
136
+ yield find(driver, options({ enableDashboard: true }), { isHub: true });
137
+ (0, chai_1.expect)(record.calledOnce).to.equal(true);
138
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
139
+ (0, chai_1.expect)(yield learnt()).to.equal(true);
140
+ }));
141
+ it('learns nothing with self-healing off', () => __awaiter(void 0, void 0, void 0, function* () {
142
+ yield find(sessionDriver(), options({ enableSelfHealing: false }), { isHub: true });
143
+ (0, chai_1.expect)(learn.called).to.equal(false);
144
+ }));
145
+ it('learns nothing from a session that turned its own healing off', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ const driver = sessionDriver({ 'xe:options': { healingTiers: [] } });
147
+ yield find(driver, options(), { isHub: true });
148
+ (0, chai_1.expect)(learn.called).to.equal(false);
149
+ }));
150
+ it('still learns from a session that limits its tiers', () => __awaiter(void 0, void 0, void 0, function* () {
151
+ const driver = sessionDriver({ 'xe:options': { healingTiers: [3] } });
152
+ yield find(driver, options(), { isHub: true });
153
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
154
+ }));
155
+ it('learns nothing from findElements, as before', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ yield find(sessionDriver(), options(), { isHub: true, command: 'findElements' });
157
+ (0, chai_1.expect)(learn.called).to.equal(false);
158
+ }));
159
+ it('answers the find even when deciding whether to learn throws', () => __awaiter(void 0, void 0, void 0, function* () {
160
+ sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'isEnabled').throws(new Error('switch unreadable'));
161
+ const answer = yield find(sessionDriver(), options(), { isHub: true });
162
+ (0, chai_1.expect)(answer).to.deep.equal(FOUND);
163
+ (0, chai_1.expect)(learn.called).to.equal(false);
164
+ }));
165
+ it('answers the find even when learning fails', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ etalons.getSignature.rejects(new Error('database is locked'));
167
+ const driver = sessionDriver();
168
+ const answer = yield find(driver, options(), { isHub: true });
169
+ (0, chai_1.expect)(answer).to.deep.equal(FOUND);
170
+ // The background pass ends, and the session may learn again.
171
+ const settled = yield (0, waitFor_1.waitFor)(() => __awaiter(void 0, void 0, void 0, function* () {
172
+ return typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).learningSessions.has(driver.sessionId)
173
+ ? null
174
+ : true;
175
+ }), { timeoutMs: 2000, intervalMs: 5 });
176
+ (0, chai_1.expect)('value' in settled).to.equal(true);
177
+ (0, chai_1.expect)(etalons.saveSignature.called).to.equal(false);
178
+ }));
179
+ });
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const scratch_database_1 = require("../helpers/scratch-database");
19
+ const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
20
+ const HOUR = 60 * 60 * 1000;
21
+ const ELEMENT = '{"value":{"element-6066-11e4-a52e-4f735466cecf":"e-1"},"sessionId":"s"}';
22
+ const ELEMENTS = '{"value":[{"element-6066-11e4-a52e-4f735466cecf":"e-1"}],"sessionId":"s"}';
23
+ const EMPTY_LIST = '{"value":[],"sessionId":"s"}';
24
+ const NOT_FOUND = '{"value":{"error":"An element could not be located on the page"},"sessionId":"s"}';
25
+ /**
26
+ * A clean build is one where the selector was found without healing. Through
27
+ * 2.14 it was any build with a find of the selector that didn't heal, so a
28
+ * selector whose find failed outright in three builds was verified as fixed.
29
+ * The rows are the ones the dashboard writes (event-manager.afterSessionCommand):
30
+ * a failed find has `is_error` and an error body, an empty findElements has
31
+ * `{"value":[]}`, and the real SQL runs against a scratch database.
32
+ */
33
+ describe('SelectorVerificationJob counts only builds where the selector was found', function () {
34
+ this.timeout(90000);
35
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
36
+ let socket;
37
+ const now = Date.now();
38
+ const fixedAt = new Date(now - 48 * HOUR);
39
+ const builds = ['cb-b1', 'cb-b2', 'cb-b3'];
40
+ before(() => __awaiter(this, void 0, void 0, function* () {
41
+ for (const b of builds) {
42
+ yield scratch.db.build.create({ data: { id: b, name: b } });
43
+ yield scratch.db.session.create({
44
+ data: {
45
+ id: `cb-s-${b}`,
46
+ build_id: b,
47
+ device_udid: 'cb-phone',
48
+ device_platform: 'android',
49
+ device_version: '14',
50
+ desired_capabilities: '{}',
51
+ session_capabilities: '{}',
52
+ node_id: 'localhost',
53
+ has_live_video: false,
54
+ },
55
+ });
56
+ }
57
+ }));
58
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
59
+ yield scratch.db.selectorEvent.deleteMany();
60
+ yield scratch.db.selectorState.deleteMany();
61
+ yield scratch.db.sessionLog.deleteMany();
62
+ socket = { emitToDashboard: sinon_1.default.stub() };
63
+ yield scratch.db.selectorState.create({
64
+ data: {
65
+ original_strategy: 'xpath',
66
+ original_selector: '//login',
67
+ status: 'pending',
68
+ fixed_at: fixedAt,
69
+ clean_builds_count: 0,
70
+ },
71
+ });
72
+ }));
73
+ const find = (build, command, response, isError = false) => scratch.db.sessionLog.create({
74
+ data: {
75
+ session_id: `cb-s-${build}`,
76
+ command_name: command,
77
+ url: command === 'findElement' ? '/findElement' : '/findElements',
78
+ method: 'POST',
79
+ title: 'Find element',
80
+ response,
81
+ is_success: !isError,
82
+ is_error: isError,
83
+ is_healed: false,
84
+ original_strategy: 'xpath',
85
+ original_selector: '//login',
86
+ createdAt: new Date(now - HOUR),
87
+ },
88
+ });
89
+ const run = () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run();
90
+ const row = () => scratch.db.selectorState.findUniqueOrThrow({
91
+ where: {
92
+ original_strategy_original_selector: {
93
+ original_strategy: 'xpath',
94
+ original_selector: '//login',
95
+ },
96
+ },
97
+ });
98
+ it('does not count builds where the find failed outright', () => __awaiter(this, void 0, void 0, function* () {
99
+ for (const b of builds)
100
+ yield find(b, 'findElement', NOT_FOUND, true);
101
+ yield run();
102
+ const state = yield row();
103
+ (0, chai_1.expect)(state.status).to.equal('pending');
104
+ (0, chai_1.expect)(state.clean_builds_count).to.equal(0);
105
+ }));
106
+ it('does not count builds where findElements found nothing', () => __awaiter(this, void 0, void 0, function* () {
107
+ for (const b of builds)
108
+ yield find(b, 'findElements', EMPTY_LIST);
109
+ yield run();
110
+ const state = yield row();
111
+ (0, chai_1.expect)(state.status).to.equal('pending');
112
+ (0, chai_1.expect)(state.clean_builds_count).to.equal(0);
113
+ }));
114
+ it('verifies the selector after three builds that found it', () => __awaiter(this, void 0, void 0, function* () {
115
+ yield find('cb-b1', 'findElement', ELEMENT);
116
+ yield find('cb-b2', 'findElements', ELEMENTS);
117
+ yield find('cb-b3', 'findElement', ELEMENT);
118
+ yield run();
119
+ (0, chai_1.expect)((yield row()).status).to.equal('resolved');
120
+ }));
121
+ it('counts a build that found it once, even if another find of it failed', () => __awaiter(this, void 0, void 0, function* () {
122
+ yield find('cb-b1', 'findElement', NOT_FOUND, true);
123
+ yield find('cb-b1', 'findElement', ELEMENT);
124
+ yield find('cb-b2', 'findElement', NOT_FOUND, true);
125
+ yield run();
126
+ const state = yield row();
127
+ (0, chai_1.expect)(state.status).to.equal('pending');
128
+ (0, chai_1.expect)(state.clean_builds_count).to.equal(1);
129
+ }));
130
+ });
@@ -65,9 +65,9 @@ describe('SelectorVerificationJob.run', () => {
65
65
  },
66
66
  ]);
67
67
  prismaStub.$queryRaw.resolves([
68
- { build_id: 'b-1', healed: 0 },
69
- { build_id: 'b-2', healed: 0 },
70
- { build_id: 'b-3', healed: 0 },
68
+ { build_id: 'b-1', healed: 0, found: 1 },
69
+ { build_id: 'b-2', healed: 0, found: 1 },
70
+ { build_id: 'b-3', healed: 0, found: 1 },
71
71
  ]);
72
72
  prismaStub.selectorState.update.resolves({
73
73
  id: 'r-1',
@@ -99,8 +99,8 @@ describe('SelectorVerificationJob.run', () => {
99
99
  },
100
100
  ]);
101
101
  prismaStub.$queryRaw.resolves([
102
- { build_id: 'b-1', healed: 0 },
103
- { build_id: 'b-2', healed: 1 }, // not clean
102
+ { build_id: 'b-1', healed: 0, found: 1 },
103
+ { build_id: 'b-2', healed: 1, found: 1 }, // not clean
104
104
  ]);
105
105
  prismaStub.selectorState.update.resolves({
106
106
  id: 'r-1',
@@ -125,8 +125,8 @@ describe('SelectorVerificationJob.run', () => {
125
125
  },
126
126
  ]);
127
127
  prismaStub.$queryRaw.resolves([
128
- { build_id: 'b-1', healed: 0 },
129
- { build_id: 'b-2', healed: 1 },
128
+ { build_id: 'b-1', healed: 0, found: 1 },
129
+ { build_id: 'b-2', healed: 1, found: 1 },
130
130
  ]);
131
131
  yield job.run();
132
132
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.be.false;
@@ -183,9 +183,9 @@ describe('SelectorVerificationJob.run', () => {
183
183
  ]);
184
184
  prismaStub.$queryRaw.onFirstCall().rejects(new Error('boom'));
185
185
  prismaStub.$queryRaw.onSecondCall().resolves([
186
- { build_id: 'b-1', healed: 0 },
187
- { build_id: 'b-2', healed: 0 },
188
- { build_id: 'b-3', healed: 0 },
186
+ { build_id: 'b-1', healed: 0, found: 1 },
187
+ { build_id: 'b-2', healed: 0, found: 1 },
188
+ { build_id: 'b-3', healed: 0, found: 1 },
189
189
  ]);
190
190
  prismaStub.selectorState.update.resolves({
191
191
  id: 'r-2',
@@ -222,9 +222,9 @@ describe('SelectorVerificationJob.run', () => {
222
222
  },
223
223
  ]);
224
224
  prismaStub.$queryRaw.resolves([
225
- { build_id: 'b-1', healed: 0 },
226
- { build_id: 'b-2', healed: 0 },
227
- { build_id: 'b-3', healed: 0 },
225
+ { build_id: 'b-1', healed: 0, found: 1 },
226
+ { build_id: 'b-2', healed: 0, found: 1 },
227
+ { build_id: 'b-3', healed: 0, found: 1 },
228
228
  ]);
229
229
  prismaStub.selectorState.update.resolves({
230
230
  id: 'r-1',