@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,433 @@
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 HealedLocatorGenerator_1 = require("../../src/services/healing/HealedLocatorGenerator");
22
+ const FuzzyXmlHealingProvider_1 = require("../../src/services/healing/FuzzyXmlHealingProvider");
23
+ const resilioPath_1 = require("../../src/services/healing/resilioPath");
24
+ const fingerprintIdentity_1 = require("../../src/services/healing/fingerprintIdentity");
25
+ const waitFor_1 = require("../../src/services/autowait/waitFor");
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ /**
28
+ * A fingerprint has to say which element it is, not only where it was.
29
+ *
30
+ * Through 2.14, learning asked the driver for `getElementAttribute`, which
31
+ * UiAutomator2 and XCUITest don't have (the command is `getAttribute(name,
32
+ * elementId)`). Every read failed quietly, so a learnt fingerprint held only
33
+ * the element's type and rect. Fuzzy XML weighs position at 5 of about 8, so
34
+ * any element of that type at that spot healed the selector: on an unexpected
35
+ * screen, the test tapped whatever button sat where "Pay" used to be.
36
+ */
37
+ describe('selector fingerprints identify their element', () => {
38
+ let restoreContainer;
39
+ beforeEach(() => {
40
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealEtalonService_1.HealEtalonService, HealedLocatorGenerator_1.HealedLocatorGenerator);
41
+ typedi_1.Container.set(HealedLocatorGenerator_1.HealedLocatorGenerator, new HealedLocatorGenerator_1.HealedLocatorGenerator());
42
+ });
43
+ afterEach(() => {
44
+ sinon_1.default.restore();
45
+ restoreContainer();
46
+ });
47
+ describe('learning', () => {
48
+ let saved;
49
+ let existing;
50
+ beforeEach(() => {
51
+ existing = null;
52
+ saved = sinon_1.default.stub().resolves();
53
+ typedi_1.Container.set(HealEtalonService_1.HealEtalonService, {
54
+ getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return existing; }),
55
+ saveSignature: saved,
56
+ });
57
+ });
58
+ let asked;
59
+ beforeEach(() => {
60
+ asked = [];
61
+ });
62
+ /**
63
+ * UiAutomator2 as it is: getAttribute(name, elementId) answers
64
+ * String(value), so an unset attribute is the string "null", and an
65
+ * attribute it doesn't know throws.
66
+ */
67
+ const androidDriver = () => ({
68
+ caps: { platformName: 'Android' },
69
+ getAttribute: (name, elementId) => __awaiter(void 0, void 0, void 0, function* () {
70
+ (0, chai_1.expect)(elementId).to.equal('el-1');
71
+ asked.push(name);
72
+ const known = {
73
+ 'resource-id': 'com.shop:id/pay',
74
+ text: 'Pay now',
75
+ name: 'Pay now',
76
+ 'content-desc': null,
77
+ hint: null,
78
+ };
79
+ if (!(name in known))
80
+ throw new Error(`No such attribute '${name}'`);
81
+ return String(known[name]);
82
+ }),
83
+ getElementRect: () => __awaiter(void 0, void 0, void 0, function* () { return ({ x: 40, y: 900, width: 300, height: 96 }); }),
84
+ getName: () => __awaiter(void 0, void 0, void 0, function* () { return 'android.widget.Button'; }),
85
+ getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<hierarchy><android.widget.Button text="Pay now" resource-id="com.shop:id/pay" bounds="[40,900][340,996]"/></hierarchy>'; }),
86
+ });
87
+ /** XCUITest as it is: WebDriverAgent throws for an attribute it doesn't have. */
88
+ const iosDriver = () => ({
89
+ caps: { platformName: 'iOS' },
90
+ getAttribute: (name) => __awaiter(void 0, void 0, void 0, function* () {
91
+ asked.push(name);
92
+ const known = { name: 'checkout.pay', label: 'Pay now' };
93
+ if (!(name in known))
94
+ throw new Error('FBUnknownAttributeException');
95
+ return known[name];
96
+ }),
97
+ getElementRect: () => __awaiter(void 0, void 0, void 0, function* () { return ({ x: 20, y: 800, width: 388, height: 50 }); }),
98
+ getName: () => __awaiter(void 0, void 0, void 0, function* () { return 'XCUIElementTypeButton'; }),
99
+ getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<AppiumAUT><XCUIElementTypeButton name="checkout.pay" label="Pay now" x="20" y="800" width="388" height="50"/></AppiumAUT>'; }),
100
+ });
101
+ let sessions = 0;
102
+ function learn(driver) {
103
+ return __awaiter(this, void 0, void 0, function* () {
104
+ sessions += 1;
105
+ const interceptor = typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor);
106
+ yield interceptor.triggerLearning(driver, ['id', 'com.shop:id/pay'], { ELEMENT: 'el-1' }, `fp-${sessions}`);
107
+ const done = yield (0, waitFor_1.waitFor)(() => __awaiter(this, void 0, void 0, function* () { return (saved.called ? true : null); }), {
108
+ timeoutMs: 2000,
109
+ intervalMs: 5,
110
+ });
111
+ return 'value' in done;
112
+ });
113
+ }
114
+ it("reads an Android element's attributes with the driver's getAttribute", () => __awaiter(void 0, void 0, void 0, function* () {
115
+ (0, chai_1.expect)(yield learn(androidDriver())).to.equal(true);
116
+ const [strategy, selector, node] = saved.firstCall.args;
117
+ (0, chai_1.expect)([strategy, selector]).to.deep.equal(['id', 'com.shop:id/pay']);
118
+ const identity = node.attributes.filter((a) => !['x', 'y', 'width', 'height'].includes(a.name));
119
+ // "null" is UiAutomator2's answer for an attribute the element hasn't set.
120
+ (0, chai_1.expect)(identity).to.deep.equal([
121
+ { name: 'resource-id', value: 'com.shop:id/pay' },
122
+ { name: 'text', value: 'Pay now' },
123
+ ]);
124
+ // Only what UiAutomator2 has, and never a field's value.
125
+ (0, chai_1.expect)(asked.sort()).to.deep.equal(['content-desc', 'hint', 'resource-id', 'text']);
126
+ }));
127
+ it("reads an iPhone element's identifier and label", () => __awaiter(void 0, void 0, void 0, function* () {
128
+ (0, chai_1.expect)(yield learn(iosDriver())).to.equal(true);
129
+ const node = saved.firstCall.args[2];
130
+ (0, chai_1.expect)(node.attributes).to.deep.include({ name: 'name', value: 'checkout.pay' });
131
+ (0, chai_1.expect)(node.attributes).to.deep.include({ name: 'label', value: 'Pay now' });
132
+ // WebDriverAgent throws for any other: no round trips that can only fail.
133
+ (0, chai_1.expect)(asked.sort()).to.deep.equal(['label', 'name']);
134
+ }));
135
+ it('learns again a fingerprint that has a path but nothing that says which element it is', () => __awaiter(void 0, void 0, void 0, function* () {
136
+ existing = {
137
+ nodeName: 'android.widget.Button',
138
+ attributes: { x: '40', y: '900', width: '300', height: '96' },
139
+ path: { format: resilioPath_1.RESILIO_PATH_FORMAT, nodes: [{ tag: 'hierarchy' }] },
140
+ };
141
+ (0, chai_1.expect)(yield learn(androidDriver())).to.equal(true);
142
+ }));
143
+ });
144
+ it("doesn't keep an element's value, which is what a test typed into a field", () => __awaiter(void 0, void 0, void 0, function* () {
145
+ const service = new HealEtalonService_1.HealEtalonService();
146
+ const store = { saveSignature: sinon_1.default.stub().resolves() };
147
+ service.store = store;
148
+ yield service.saveSignature('id', 'email', {
149
+ nodeName: 'XCUIElementTypeTextField',
150
+ attributes: [
151
+ { name: 'name', value: 'signup.email' },
152
+ { name: 'value', value: 'jane.doe@example.com' },
153
+ ],
154
+ });
155
+ (0, chai_1.expect)(store.saveSignature.firstCall.args[0].attributes).to.deep.equal({
156
+ name: 'signup.email',
157
+ });
158
+ }));
159
+ describe('Fuzzy XML', () => {
160
+ const ORDER_TITLE = '<android.widget.TextView text="Order #1042" resource-id="com.shop:id/title" bounds="[48,100][1032,180]"/>';
161
+ const IOS_APP_RECT = 'name="Shop" label="Shop" x="0" y="0" width="428" height="926"';
162
+ let fingerprint;
163
+ let resolved;
164
+ beforeEach(() => {
165
+ fingerprint = null;
166
+ resolved = [];
167
+ });
168
+ function heal(selector, strategy, pageSource) {
169
+ const provider = new FuzzyXmlHealingProvider_1.FuzzyXmlHealingProvider({
170
+ getSignature: () => __awaiter(this, void 0, void 0, function* () { return fingerprint; }),
171
+ });
172
+ return provider.heal({
173
+ sessionId: 's',
174
+ strategy,
175
+ selector,
176
+ pageSource,
177
+ driver: {
178
+ findElement: (_using, xpath) => __awaiter(this, void 0, void 0, function* () {
179
+ resolved.push(xpath);
180
+ return { ELEMENT: 'healed-1' };
181
+ }),
182
+ },
183
+ });
184
+ }
185
+ const android = (button) => `<hierarchy><android.widget.FrameLayout bounds="[0,0][1080,1920]">${ORDER_TITLE}` +
186
+ `${button}</android.widget.FrameLayout></hierarchy>`;
187
+ const AT_PAY = { x: '40', y: '900', width: '300', height: '96' };
188
+ const PAY = {
189
+ nodeName: 'android.widget.Button',
190
+ attributes: {
191
+ 'resource-id': 'com.shop:id/pay',
192
+ text: 'Pay now',
193
+ x: '40',
194
+ y: '900',
195
+ width: '300',
196
+ height: '96',
197
+ },
198
+ };
199
+ it("doesn't heal to a different element at the same spot", () => __awaiter(void 0, void 0, void 0, function* () {
200
+ fingerprint = PAY;
201
+ const cancel = android('<android.widget.Button text="Cancel order" resource-id="com.shop:id/cancel" bounds="[40,900][340,996]"/>');
202
+ (0, chai_1.expect)(yield heal('com.shop:id/pay', 'id', cancel)).to.equal(null);
203
+ }));
204
+ it('heals an element whose id was renamed and kept its text', () => __awaiter(void 0, void 0, void 0, function* () {
205
+ fingerprint = PAY;
206
+ const renamed = android('<android.widget.Button text="Pay now" resource-id="com.shop:id/checkout_pay" bounds="[40,900][340,996]"/>');
207
+ const healed = yield heal('com.shop:id/pay', 'id', renamed);
208
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('healed-1');
209
+ (0, chai_1.expect)(resolved[0]).to.match(/checkout_pay|Pay now/);
210
+ }));
211
+ it('heals an element that kept its id and changed its text', () => __awaiter(void 0, void 0, void 0, function* () {
212
+ var _a;
213
+ fingerprint = PAY;
214
+ const reworded = android('<android.widget.Button text="Pay" resource-id="com.shop:id/pay" bounds="[40,900][340,996]"/>');
215
+ (0, chai_1.expect)((_a = (yield heal("//android.widget.Button[@text='Pay now']", 'xpath', reworded))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
216
+ }));
217
+ const ios = (button) => `<AppiumAUT><XCUIElementTypeApplication ${IOS_APP_RECT}>` +
218
+ `${button}</XCUIElementTypeApplication></AppiumAUT>`;
219
+ const IOS_PAY = {
220
+ nodeName: 'XCUIElementTypeButton',
221
+ attributes: {
222
+ name: 'checkout.pay',
223
+ label: 'Pay now',
224
+ x: '20',
225
+ y: '800',
226
+ width: '388',
227
+ height: '50',
228
+ },
229
+ };
230
+ it('reads an iOS accessibility identifier the same way', () => __awaiter(void 0, void 0, void 0, function* () {
231
+ var _a;
232
+ fingerprint = IOS_PAY;
233
+ const cancel = ios('<XCUIElementTypeButton name="checkout.cancel" label="Cancel order" x="20" y="800" width="388" height="50"/>');
234
+ const reworded = ios('<XCUIElementTypeButton name="checkout.pay" label="Pay" x="20" y="800" width="388" height="50"/>');
235
+ (0, chai_1.expect)(yield heal('checkout.pay', 'accessibility id', cancel)).to.equal(null);
236
+ (0, chai_1.expect)((_a = (yield heal('checkout.pay', 'accessibility id', reworded))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
237
+ }));
238
+ it("doesn't heal to the opposite button", () => __awaiter(void 0, void 0, void 0, function* () {
239
+ fingerprint = {
240
+ nodeName: 'android.widget.Button',
241
+ attributes: Object.assign({ 'resource-id': 'com.shop:id/login', text: 'Log in' }, AT_PAY),
242
+ };
243
+ const logout = android('<android.widget.Button text="Log out" resource-id="com.shop:id/logout" bounds="[40,900][340,996]"/>');
244
+ (0, chai_1.expect)(yield heal('com.shop:id/login', 'id', logout)).to.equal(null);
245
+ fingerprint = {
246
+ nodeName: 'XCUIElementTypeButton',
247
+ attributes: {
248
+ name: 'auth.signIn',
249
+ label: 'Sign in',
250
+ x: '20',
251
+ y: '800',
252
+ width: '388',
253
+ height: '50',
254
+ },
255
+ };
256
+ const signUp = ios('<XCUIElementTypeButton name="auth.signUp" label="Sign up" x="20" y="800" width="388" height="50"/>');
257
+ (0, chai_1.expect)(yield heal('auth.signIn', 'accessibility id', signUp)).to.equal(null);
258
+ }));
259
+ it('heals an element whose id was renamed outright and kept its text', () => __awaiter(void 0, void 0, void 0, function* () {
260
+ var _a;
261
+ fingerprint = PAY;
262
+ const renamed = android('<android.widget.Button text="Pay now" resource-id="com.shop:id/btn_primary" bounds="[40,900][340,996]"/>');
263
+ (0, chai_1.expect)((_a = (yield heal('com.shop:id/pay', 'id', renamed))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
264
+ }));
265
+ it('heals an icon whose id was renamed and kept its description', () => __awaiter(void 0, void 0, void 0, function* () {
266
+ var _a;
267
+ fingerprint = {
268
+ nodeName: 'android.widget.ImageButton',
269
+ attributes: Object.assign({ 'resource-id': 'com.shop:id/share', 'content-desc': 'Share' }, AT_PAY),
270
+ };
271
+ const icon = android('<android.widget.ImageButton content-desc="Share" resource-id="com.shop:id/action_3" bounds="[40,900][340,996]"/>');
272
+ (0, chai_1.expect)((_a = (yield heal('com.shop:id/share', 'id', icon))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
273
+ }));
274
+ it("compares resource-ids without the app's package, as a debug build's differs", () => __awaiter(void 0, void 0, void 0, function* () {
275
+ var _a;
276
+ fingerprint = {
277
+ nodeName: 'android.widget.Button',
278
+ attributes: Object.assign({ 'resource-id': 'com.shop.debug:id/pay', text: 'Pay now' }, AT_PAY),
279
+ };
280
+ const release = android('<android.widget.Button text="Pay" resource-id="com.shop:id/pay" bounds="[40,900][340,996]"/>');
281
+ (0, chai_1.expect)((_a = (yield heal('com.shop.debug:id/pay', 'id', release))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
282
+ }));
283
+ it('reads the text again when an id gains words, as pay_later or undo_delete do', () => __awaiter(void 0, void 0, void 0, function* () {
284
+ fingerprint = PAY;
285
+ const later = android('<android.widget.Button text="Pay later" resource-id="com.shop:id/pay_later" bounds="[40,900][340,996]"/>');
286
+ (0, chai_1.expect)(yield heal('com.shop:id/pay', 'id', later)).to.equal(null);
287
+ fingerprint = {
288
+ nodeName: 'android.widget.Button',
289
+ attributes: Object.assign({ 'resource-id': 'com.mail:id/delete', text: 'Delete' }, AT_PAY),
290
+ };
291
+ const undo = android('<android.widget.Button text="Undo delete" resource-id="com.mail:id/undo_delete" bounds="[40,900][340,996]"/>');
292
+ (0, chai_1.expect)(yield heal('com.mail:id/delete', 'id', undo)).to.equal(null);
293
+ }));
294
+ it("doesn't heal an icon with no text whose id names another", () => __awaiter(void 0, void 0, void 0, function* () {
295
+ fingerprint = {
296
+ nodeName: 'android.widget.ImageButton',
297
+ attributes: Object.assign({ 'resource-id': 'com.shop:id/cart' }, AT_PAY),
298
+ };
299
+ const menu = android('<android.widget.ImageButton resource-id="com.shop:id/menu" bounds="[40,900][340,996]"/>');
300
+ (0, chai_1.expect)(yield heal('com.shop:id/cart', 'id', menu)).to.equal(null);
301
+ }));
302
+ it('tells non-Latin identifiers and labels apart', () => __awaiter(void 0, void 0, void 0, function* () {
303
+ fingerprint = {
304
+ nodeName: 'XCUIElementTypeButton',
305
+ attributes: {
306
+ name: 'checkout.pay',
307
+ label: '支払う',
308
+ x: '20',
309
+ y: '800',
310
+ width: '388',
311
+ height: '50',
312
+ },
313
+ };
314
+ const cancel = ios('<XCUIElementTypeButton name="キャンセル" label="キャンセル" x="20" y="800" width="388" height="50"/>');
315
+ (0, chai_1.expect)(yield heal('checkout.pay', 'accessibility id', cancel)).to.equal(null);
316
+ }));
317
+ it('heals a heal-written fingerprint from a debug build, which has no position', () => __awaiter(void 0, void 0, void 0, function* () {
318
+ var _a;
319
+ // A heal stores the element of the page source: on Android its rect is
320
+ // bounds, not x and y, so the id and the text are all there is.
321
+ fingerprint = {
322
+ nodeName: 'android.widget.Button',
323
+ attributes: { 'resource-id': 'com.shop.debug:id/pay', text: 'Pay now' },
324
+ };
325
+ const release = android('<android.widget.Button text="Pay" resource-id="com.shop:id/pay" bounds="[40,1200][340,1296]"/>');
326
+ (0, chai_1.expect)((_a = (yield heal('com.shop.debug:id/pay', 'id', release))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
327
+ }));
328
+ it('heals a heal-written fingerprint whose id was renamed outright and kept its text', () => __awaiter(void 0, void 0, void 0, function* () {
329
+ var _a;
330
+ // No position: the score rests on the type, the text and the id. An id
331
+ // of the same app still earns a share of its weight, as through 2.14.
332
+ fingerprint = {
333
+ nodeName: 'android.widget.Button',
334
+ attributes: { 'resource-id': 'com.shop:id/pay', text: 'Pay now' },
335
+ };
336
+ const renamed = android('<android.widget.Button text="Pay now" resource-id="com.shop:id/btn_primary" bounds="[40,1200][340,1296]"/>');
337
+ (0, chai_1.expect)((_a = (yield heal('com.shop:id/pay', 'id', renamed))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
338
+ }));
339
+ it('reads the text of an element with no id where one with an id was, as on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
340
+ var _a;
341
+ fingerprint = PAY;
342
+ const cancel = android('<android.widget.Button text="Cancel order" bounds="[40,900][340,996]"/>');
343
+ (0, chai_1.expect)(yield heal('com.shop:id/pay', 'id', cancel)).to.equal(null);
344
+ const idDropped = android('<android.widget.Button text="Pay now" bounds="[40,900][340,996]"/>');
345
+ (0, chai_1.expect)((_a = (yield heal('com.shop:id/pay', 'id', idDropped))) === null || _a === void 0 ? void 0 : _a.id).to.equal('healed-1');
346
+ }));
347
+ it('takes an id that merely contains the old one for another element', () => __awaiter(void 0, void 0, void 0, function* () {
348
+ fingerprint = {
349
+ nodeName: 'android.widget.Button',
350
+ attributes: Object.assign({ 'resource-id': 'com.social:id/follow', text: 'Follow' }, AT_PAY),
351
+ };
352
+ const unfollow = android('<android.widget.Button text="Unfollow" resource-id="com.social:id/unfollow" bounds="[40,900][340,996]"/>');
353
+ (0, chai_1.expect)(yield heal('com.social:id/follow', 'id', unfollow)).to.equal(null);
354
+ }));
355
+ it('takes "null" for nothing, as UiAutomator2 answers for an unset attribute', () => __awaiter(void 0, void 0, void 0, function* () {
356
+ fingerprint = {
357
+ nodeName: 'android.widget.ImageView',
358
+ attributes: Object.assign({ 'resource-id': 'null', 'content-desc': 'null', hint: 'null' }, AT_PAY),
359
+ };
360
+ const other = android('<android.widget.ImageView content-desc="Delete account" bounds="[40,900][340,996]"/>');
361
+ (0, chai_1.expect)(yield heal('com.shop:id/avatar', 'id', other)).to.equal(null);
362
+ }));
363
+ it("doesn't heal by position with a fingerprint that says nothing about the element", () => __awaiter(void 0, void 0, void 0, function* () {
364
+ // As every fingerprint learnt through 2.14 was.
365
+ fingerprint = {
366
+ nodeName: 'android.widget.Button',
367
+ attributes: { x: '40', y: '900', width: '300', height: '96' },
368
+ };
369
+ const cancel = android('<android.widget.Button text="Cancel order" resource-id="com.shop:id/cancel" bounds="[40,900][340,996]"/>');
370
+ (0, chai_1.expect)(yield heal('com.shop:id/pay', 'id', cancel)).to.equal(null);
371
+ }));
372
+ });
373
+ describe('comparing names', () => {
374
+ it('reads a resource-id without its package', () => {
375
+ (0, chai_1.expect)((0, fingerprintIdentity_1.resourceIdName)('com.shop:id/pay')).to.equal('pay');
376
+ (0, chai_1.expect)((0, fingerprintIdentity_1.resourceIdName)('com.shop.debug:id/pay_button')).to.equal('pay_button');
377
+ (0, chai_1.expect)((0, fingerprintIdentity_1.resourceIdName)('pay')).to.equal('pay');
378
+ });
379
+ it('takes identifiers with the same words for the same', () => {
380
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('btn-pay', 'pay_btn')).to.equal(true);
381
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('payButton', 'pay_button')).to.equal(true);
382
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('Checkout.Pay', 'checkout_pay')).to.equal(true);
383
+ // camelCase and accents in any script.
384
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('оплатаКнопка', 'оплата_кнопка')).to.equal(true);
385
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('cafe\u0301.order', 'caf\u00e9_order')).to.equal(true);
386
+ // Neither has a letter or digit: nothing to tell them apart by.
387
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('___', '..')).to.equal(true);
388
+ });
389
+ it('takes any other identifier for another element', () => {
390
+ // Words added: the text decides (pay_later, undo_delete).
391
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('pay', 'checkout_pay')).to.equal(false);
392
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('delete', 'undo_delete')).to.equal(false);
393
+ // In any script.
394
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('支払う', 'キャンセル')).to.equal(false);
395
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('oplata', 'оплата')).to.equal(false);
396
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('checkout.pay', '___')).to.equal(false);
397
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('follow', 'unfollow')).to.equal(false);
398
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('pay', 'paypal')).to.equal(false);
399
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('ok', 'book')).to.equal(false);
400
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameIdentifier)('checkout.cancel', 'checkout.pay')).to.equal(false);
401
+ });
402
+ it('takes texts as the same only when they read the same', () => {
403
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('Pay now', ' pay NOW!')).to.equal(true);
404
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('Log in', 'Log out')).to.equal(false);
405
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('Enable', 'Disable')).to.equal(false);
406
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('X', 'Exit')).to.equal(false);
407
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)(' ', 'Anything')).to.equal(false);
408
+ });
409
+ it('keeps the marks that make a word another word, and composes accents first', () => {
410
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('भुगतान दें', 'भुगतान दो')).to.equal(false);
411
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('ยกเลิก', 'ยกเล็ก')).to.equal(false);
412
+ (0, chai_1.expect)((0, fingerprintIdentity_1.sameText)('caf\u0065\u0301', 'caf\u00e9')).to.equal(true);
413
+ });
414
+ it('asks each platform for the attributes its driver has', () => {
415
+ (0, chai_1.expect)((0, fingerprintIdentity_1.attributesToLearn)('Android')).to.deep.equal([
416
+ 'resource-id',
417
+ 'content-desc',
418
+ 'text',
419
+ 'hint',
420
+ ]);
421
+ (0, chai_1.expect)((0, fingerprintIdentity_1.attributesToLearn)('iOS')).to.deep.equal(['name', 'label']);
422
+ (0, chai_1.expect)((0, fingerprintIdentity_1.attributesToLearn)('tvOS')).to.deep.equal(['name', 'label']);
423
+ (0, chai_1.expect)((0, fingerprintIdentity_1.attributesToLearn)('mac')).to.deep.equal([
424
+ 'resource-id',
425
+ 'content-desc',
426
+ 'text',
427
+ 'hint',
428
+ 'name',
429
+ 'label',
430
+ ]);
431
+ });
432
+ });
433
+ });
@@ -58,4 +58,50 @@ describe('H264Multiplexer', () => {
58
58
  (0, chai_1.expect)(m.clientCount).to.equal(0);
59
59
  (0, chai_1.expect)(got).to.deep.equal(['config']);
60
60
  });
61
+ // The H.264 service ends a capture the moment its last viewer leaves when a
62
+ // page has asked for MJPEG (AndroidH264StreamService.endWhenUnwatched).
63
+ it('tells its listener when the last client leaves, once', () => {
64
+ const m = new H264Multiplexer_1.H264Multiplexer();
65
+ let emptied = 0;
66
+ m.onEmpty(() => (emptied += 1));
67
+ const removeA = m.addClient(() => undefined);
68
+ const removeB = m.addClient(() => undefined);
69
+ removeA();
70
+ (0, chai_1.expect)(emptied, 'one viewer is still watching').to.equal(0);
71
+ removeB();
72
+ (0, chai_1.expect)(emptied).to.equal(1);
73
+ // A socket's close and error both remove the same client.
74
+ removeB();
75
+ (0, chai_1.expect)(emptied).to.equal(1);
76
+ });
77
+ // A capture stopped under its viewers (a recording starting, a stream/stop)
78
+ // left their sockets open with no frames: the picture froze on its last
79
+ // frame and the page never fell back to MJPEG.
80
+ it('close() tells every client its stream ended, and delivers nothing more', () => {
81
+ const m = new H264Multiplexer_1.H264Multiplexer();
82
+ const got = [];
83
+ const ended = [];
84
+ let emptied = 0;
85
+ m.onEmpty(() => (emptied += 1));
86
+ const removeA = m.addClient((p) => got.push(p.type), () => ended.push('a'));
87
+ m.addClient(() => undefined, () => ended.push('b'));
88
+ m.close();
89
+ m.push(P('key', 1));
90
+ removeA(); // the socket's own close, afterwards
91
+ (0, chai_1.expect)(ended).to.deep.equal(['a', 'b']);
92
+ (0, chai_1.expect)(got).to.deep.equal([]);
93
+ (0, chai_1.expect)(m.clientCount).to.equal(0);
94
+ (0, chai_1.expect)(emptied, 'a closed stream is not an unwatched one').to.equal(0);
95
+ });
96
+ it('ends a client that joins after close() at once', () => {
97
+ const m = new H264Multiplexer_1.H264Multiplexer();
98
+ m.setConfig(P('config', 0));
99
+ m.close();
100
+ const got = [];
101
+ let ended = 0;
102
+ m.addClient((p) => got.push(p.type), () => (ended += 1));
103
+ (0, chai_1.expect)(ended).to.equal(1);
104
+ (0, chai_1.expect)(got).to.deep.equal([]);
105
+ (0, chai_1.expect)(m.clientCount).to.equal(0);
106
+ });
61
107
  });
@@ -0,0 +1,71 @@
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
+ const chai_1 = require("chai");
16
+ const http_1 = __importDefault(require("http"));
17
+ const healReport_1 = require("../../src/gateway/healReport");
18
+ /**
19
+ * How a node hands a heal back to its hub on the answer to the hub's command
20
+ * (healReport.ts): what goes on the answer, and what the hub accepts off it.
21
+ */
22
+ describe('A heal reported to the hub', () => {
23
+ const answer = () => new http_1.default.ServerResponse(new http_1.default.IncomingMessage(null));
24
+ const heal = (selector) => ({
25
+ originalSelector: selector,
26
+ originalStrategy: 'xpath',
27
+ healedSelector: "//*[@text='Anmelden']",
28
+ healedStrategy: 'xpath',
29
+ confidence: 0.82,
30
+ tier: 2,
31
+ });
32
+ it('goes on the answer and comes off it whole, whatever its characters', () => {
33
+ const res = answer();
34
+ const sent = heal("//*[@text='Ünïcödé “quotes”']");
35
+ (0, chai_1.expect)((0, healReport_1.runReportingHeals)(res, () => (0, healReport_1.reportHeal)(sent))).to.equal('reported');
36
+ const headers = { [healReport_1.HEAL_REPORT_HEADER]: String(res.getHeader(healReport_1.HEAL_REPORT_HEADER)) };
37
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)(headers)).to.deep.equal(sent);
38
+ (0, chai_1.expect)(headers, 'taken off the headers').to.not.have.property(healReport_1.HEAL_REPORT_HEADER);
39
+ });
40
+ it('is not sent for a command that did not come from a hub', () => {
41
+ (0, chai_1.expect)((0, healReport_1.reportHeal)(heal('//a'))).to.equal('not-from-hub');
42
+ });
43
+ it('is not sent when too long for the hub to read, so the command still answers', () => {
44
+ const res = answer();
45
+ const reported = (0, healReport_1.runReportingHeals)(res, () => (0, healReport_1.reportHeal)(heal(`//${'x'.repeat(10000)}`)));
46
+ (0, chai_1.expect)(reported).to.equal('too-long');
47
+ (0, chai_1.expect)(res.getHeader(healReport_1.HEAL_REPORT_HEADER)).to.equal(undefined);
48
+ });
49
+ it('is not put on an answer that has closed, by work the command left running', () => __awaiter(void 0, void 0, void 0, function* () {
50
+ const res = answer();
51
+ let release;
52
+ const gate = new Promise((resolve) => (release = resolve));
53
+ // Async work started during the command keeps its context after it.
54
+ const lingering = (0, healReport_1.runReportingHeals)(res, () => __awaiter(void 0, void 0, void 0, function* () {
55
+ yield gate;
56
+ return (0, healReport_1.reportHeal)(heal('//late'));
57
+ }));
58
+ res.emit('close');
59
+ release();
60
+ (0, chai_1.expect)(yield lingering).to.equal('not-from-hub');
61
+ }));
62
+ it('is ignored when it is not one', () => {
63
+ const notJson = { [healReport_1.HEAL_REPORT_HEADER]: 'not base64 json' };
64
+ const notAHeal = {
65
+ [healReport_1.HEAL_REPORT_HEADER]: Buffer.from('{"originalSelector":1}').toString('base64url'),
66
+ };
67
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)(notJson)).to.equal(undefined);
68
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)(notAHeal)).to.equal(undefined);
69
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)({})).to.equal(undefined);
70
+ });
71
+ });
@@ -187,7 +187,11 @@ describe('HealingOrchestrator instrumentation', () => {
187
187
  (0, chai_1.expect)(mockSpan.end.calledOnce).to.equal(true);
188
188
  }));
189
189
  it('marks span ERROR and ends on context-collection failure (driver throws)', () => __awaiter(void 0, void 0, void 0, function* () {
190
- const orch = makeOrchestratorWithProviders([]);
190
+ // A tier to run, or nothing is collected at all; it never runs here.
191
+ const heal = sinon_1.default.stub().resolves(null);
192
+ const orch = makeOrchestratorWithProviders([
193
+ { name: 'p1', tier: types_1.HealingTier.TIER_1_RECOVERY, heal },
194
+ ]);
191
195
  const driver = {
192
196
  getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
193
197
  throw new Error('disconnected');
@@ -198,6 +202,7 @@ describe('HealingOrchestrator instrumentation', () => {
198
202
  (0, chai_1.expect)(result).to.equal(null);
199
203
  (0, chai_1.expect)(mockSpan.addEvent.calledWith('context_collection_failed')).to.equal(true);
200
204
  (0, chai_1.expect)(mockSpan.end.calledOnce).to.equal(true);
205
+ (0, chai_1.expect)(heal.called).to.equal(false);
201
206
  }));
202
207
  it('does not put the original selector text on span attributes (cardinality discipline)', () => __awaiter(void 0, void 0, void 0, function* () {
203
208
  const provider = {