@xenon-device-management/xenon 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +57 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -1,43 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
7
|
};
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
10
|
};
|
|
@@ -54,7 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
54
21
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
55
22
|
};
|
|
56
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
-
exports.HealingOrchestrator = void 0;
|
|
24
|
+
exports.HealingOrchestrator = exports.LOCAL_HEALING_TIERS = void 0;
|
|
58
25
|
exports.filterProvidersByTier = filterProvidersByTier;
|
|
59
26
|
exports.coerceHealingTiersCap = coerceHealingTiersCap;
|
|
60
27
|
exports.healingTiersFromCaps = healingTiersFromCaps;
|
|
@@ -67,6 +34,7 @@ const VisualAiHealingProvider_1 = require("./VisualAiHealingProvider");
|
|
|
67
34
|
const LlmHealingProvider_1 = require("./LlmHealingProvider");
|
|
68
35
|
const HealEtalonService_1 = require("./HealEtalonService");
|
|
69
36
|
const ResilioTreeHealingProvider_1 = require("./ResilioTreeHealingProvider");
|
|
37
|
+
const resilioPath_1 = require("./resilioPath");
|
|
70
38
|
const HealingMetrics_1 = require("./HealingMetrics");
|
|
71
39
|
const attributes_1 = require("../telemetry/attributes");
|
|
72
40
|
const xenonOptions_1 = require("../session/xenonOptions");
|
|
@@ -86,24 +54,48 @@ function filterProvidersByTier(providers, allowedTiers) {
|
|
|
86
54
|
const allowed = new Set(allowedTiers);
|
|
87
55
|
return providers.filter((_, index) => allowed.has(index + 1));
|
|
88
56
|
}
|
|
57
|
+
// The tiers that run on this server: Resilio, Fuzzy XML and OCR. Visual AI (4)
|
|
58
|
+
// and the LLM (5) send the screenshot and the page source to the AI provider.
|
|
59
|
+
exports.LOCAL_HEALING_TIERS = [1, 2, 3];
|
|
60
|
+
const isHealingTierList = (raw) => Array.isArray(raw) && raw.every((t) => Number.isInteger(t) && t >= 1 && t <= 5);
|
|
89
61
|
// Coerce the raw xe:options.healingTiers capability value into the
|
|
90
|
-
// allowedTiers argument for attemptHealing.
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
62
|
+
// allowedTiers argument for attemptHealing. The value is a privacy control (a
|
|
63
|
+
// session leaves out 4 and 5 to keep its screen away from the AI provider), so
|
|
64
|
+
// one that can't be read fails closed for the AI tiers and open for the rest:
|
|
65
|
+
// - not set (or null): every tier (undefined);
|
|
66
|
+
// - a list of tier numbers 1 to 5: exactly those, and [] none;
|
|
67
|
+
// - anything else ("1,2", ["1","2"], [1, 6], ...): LOCAL_HEALING_TIERS.
|
|
68
|
+
// Through 2.14 [], or a value that wasn't a list or had no numbers in it, ran
|
|
69
|
+
// every tier, the AI ones included; a list that held some numbers ran those
|
|
70
|
+
// and skipped the rest ([1, 6] ran tier 1, [6] none).
|
|
98
71
|
function coerceHealingTiersCap(raw) {
|
|
99
|
-
|
|
100
|
-
|
|
72
|
+
if (raw === undefined || raw === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
return isHealingTierList(raw) ? [...raw] : [...exports.LOCAL_HEALING_TIERS];
|
|
101
75
|
}
|
|
102
76
|
// The allowedTiers a session asked for, read from its capabilities (one flat
|
|
103
|
-
// map
|
|
104
|
-
// xenon:options alias's, coerced as above.
|
|
77
|
+
// map: the session's driver's caps): xe:options.healingTiers, or the
|
|
78
|
+
// xenon:options alias's, coerced as above. A namespace that isn't an object
|
|
79
|
+
// can't be read either: xenonOptionsIn skips it, which would run every tier.
|
|
80
|
+
// `unreadable` says what couldn't be read, for the log.
|
|
105
81
|
function healingTiersFromCaps(caps) {
|
|
106
|
-
|
|
82
|
+
for (const namespace of xenonOptions_1.OPTION_NAMESPACES) {
|
|
83
|
+
const bag = (0, xenonOptions_1.isOptionsObject)(caps) ? caps[namespace] : undefined;
|
|
84
|
+
if (bag !== undefined && bag !== null && !(0, xenonOptions_1.isOptionsObject)(bag)) {
|
|
85
|
+
return {
|
|
86
|
+
tiers: [...exports.LOCAL_HEALING_TIERS],
|
|
87
|
+
unreadable: `${namespace} is ${JSON.stringify(bag)}, not an object`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const raw = (0, xenonOptions_1.xenonOptionsIn)(caps).healingTiers;
|
|
92
|
+
const tiers = coerceHealingTiersCap(raw);
|
|
93
|
+
return tiers === undefined || isHealingTierList(raw)
|
|
94
|
+
? { tiers }
|
|
95
|
+
: {
|
|
96
|
+
tiers,
|
|
97
|
+
unreadable: `healingTiers is ${JSON.stringify(raw)}, not a list of tier numbers from 1 to 5`,
|
|
98
|
+
};
|
|
107
99
|
}
|
|
108
100
|
let HealingOrchestrator = class HealingOrchestrator {
|
|
109
101
|
constructor(etalonService) {
|
|
@@ -120,7 +112,15 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
120
112
|
}
|
|
121
113
|
attemptHealing(sessionId, driver, strategy, selector, allowedTiers) {
|
|
122
114
|
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
-
var _a, _b;
|
|
115
|
+
var _a, _b, _c;
|
|
116
|
+
// §2.7: when the session's xe:options.healingTiers capability is set,
|
|
117
|
+
// restrict dispatch to the allowed tier indices; absent -> unchanged.
|
|
118
|
+
const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
|
|
119
|
+
if (activeProviders.length === 0) {
|
|
120
|
+
// Nothing to run, so nothing is collected either: no screenshot taken.
|
|
121
|
+
this.logger.info(`Session ${sessionId} allows no self-healing tier; ${strategy}=${selector} is not healed.`);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
124
|
this.logger.info(`🚨 Self-Healing triggered for session ${sessionId}. Broken locator: ${strategy}=${selector}`);
|
|
125
125
|
// Span wraps the whole attempt. Original selector text is intentionally
|
|
126
126
|
// kept off the span — high cardinality (long XPath strings) would explode
|
|
@@ -151,9 +151,6 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
151
151
|
return null;
|
|
152
152
|
}
|
|
153
153
|
// Tiered Execution: Try providers in order of cost/complexity.
|
|
154
|
-
// §2.7: when the session's xe:options.healingTiers capability is set,
|
|
155
|
-
// restrict dispatch to the allowed tier indices; absent -> unchanged.
|
|
156
|
-
const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
|
|
157
154
|
for (const provider of activeProviders) {
|
|
158
155
|
const tierStart = Date.now();
|
|
159
156
|
span.addEvent('tier_started', { tier: provider.name });
|
|
@@ -203,23 +200,11 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
203
200
|
else if (result.confidence > 0.7 && result.node) {
|
|
204
201
|
try {
|
|
205
202
|
this.logger.info(`🧠 Learning from healing success: updating etalon for ${selector}`);
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const pathNodes = [];
|
|
212
|
-
let curr = result.node;
|
|
213
|
-
while (curr) {
|
|
214
|
-
pathNodes.unshift(curr);
|
|
215
|
-
curr = curr.parent || curr.parentNode;
|
|
216
|
-
}
|
|
217
|
-
learnedPath = new Path(pathNodes).toJSON();
|
|
218
|
-
}
|
|
219
|
-
catch (_c) {
|
|
220
|
-
// resiliotree import is best-effort; learnedPath stays null
|
|
221
|
-
}
|
|
222
|
-
}
|
|
203
|
+
// The healed element's path through the page source, for the
|
|
204
|
+
// Resilio tier. Through 2.14 this built it from the healed node
|
|
205
|
+
// with resiliotree, which an xmldom element (every node a tier
|
|
206
|
+
// returns) can't be, so it was always null.
|
|
207
|
+
const learnedPath = (0, resilioPath_1.resilioPathOf)(result.node, (_a = context.pageSource) !== null && _a !== void 0 ? _a : '');
|
|
223
208
|
yield this.etalonService.saveSignature(strategy, selector, result.node, learnedPath);
|
|
224
209
|
}
|
|
225
210
|
catch (learnErr) {
|
|
@@ -235,7 +220,7 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
235
220
|
span.setAttributes({
|
|
236
221
|
[attributes_1.ATTR.HEALING_TIER]: provider.name,
|
|
237
222
|
[attributes_1.ATTR.HEALING_CONFIDENCE]: result.confidence,
|
|
238
|
-
[attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (
|
|
223
|
+
[attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (_b = result.recommendedStrategy) !== null && _b !== void 0 ? _b : 'xpath',
|
|
239
224
|
[attributes_1.ATTR.HEALING_DURATION_MS]: Date.now() - attemptStart,
|
|
240
225
|
});
|
|
241
226
|
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
@@ -253,7 +238,7 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
253
238
|
// tier can plausibly succeed (e.g. missing prerequisites that all
|
|
254
239
|
// share). Saves an expensive LLM round-trip when upstream context
|
|
255
240
|
// collection failed.
|
|
256
|
-
if ((
|
|
241
|
+
if ((_c = provider.shouldSkipRemaining) === null || _c === void 0 ? void 0 : _c.call(provider, context)) {
|
|
257
242
|
this.logger.warn(`Tier ${provider.tier} (${provider.name}) advised skipping remaining tiers for selector=${selector}`);
|
|
258
243
|
HealingMetrics_1.HEALING_METRICS.recordSkippedRemaining(provider.tier, provider.name);
|
|
259
244
|
span.addEvent('tier_skipped_remaining', { tier: provider.name });
|
|
@@ -273,8 +258,8 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
273
258
|
}
|
|
274
259
|
/**
|
|
275
260
|
* The OCR and Visual AI tiers find the element in the screenshot, so their
|
|
276
|
-
* `rect` is in its pixels. The interceptor
|
|
277
|
-
*
|
|
261
|
+
* `rect` is in its pixels. The interceptor returns a virtual element there,
|
|
262
|
+
* which a click taps in the driver's coordinates (points on iOS), so it is
|
|
278
263
|
* converted here. If that can't be worked out on iOS, a virtual element
|
|
279
264
|
* (only a position) is no use and the tier counts as failed; a real element
|
|
280
265
|
* the tier resolved keeps its id and loses only the rect.
|
|
@@ -14,8 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.OcrHealingProvider = void 0;
|
|
16
16
|
const types_1 = require("./types");
|
|
17
|
-
const
|
|
17
|
+
const typedi_1 = require("typedi");
|
|
18
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
const OmniVisionService_1 = require("../omni-vision/OmniVisionService");
|
|
19
20
|
class OcrHealingProvider {
|
|
20
21
|
constructor() {
|
|
21
22
|
this.name = 'OCR Text Provider';
|
|
@@ -41,71 +42,47 @@ class OcrHealingProvider {
|
|
|
41
42
|
if (!soughtText)
|
|
42
43
|
return null;
|
|
43
44
|
this.logger.info(`Attempting OCR search for text: "${soughtText}"`);
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
rect: {
|
|
78
|
-
x: bestWord.bbox.x0,
|
|
79
|
-
y: bestWord.bbox.y0,
|
|
80
|
-
width: bestWord.bbox.x1 - bestWord.bbox.x0,
|
|
81
|
-
height: bestWord.bbox.y1 - bestWord.bbox.y0,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch (predErr) {
|
|
88
|
-
this.logger.debug(`Predicate search failed: ${predErr.message}`);
|
|
45
|
+
// Read the screenshot as Omni-Vision's `-custom:ai-text` does: the text
|
|
46
|
+
// may cover several neighbouring words ("Sign in"), and a word that is
|
|
47
|
+
// only part of it ("Log" for "Login") is no match. The first match in
|
|
48
|
+
// reading order, as `findElement` with that locator returns.
|
|
49
|
+
const [match] = yield typedi_1.Container.get(OmniVisionService_1.OmniVisionService).findTextInScreenshot(context.screenshotBase64, soughtText);
|
|
50
|
+
if (!match)
|
|
51
|
+
return null;
|
|
52
|
+
this.logger.info(`✅ OCR found text "${match.text}" at ${JSON.stringify({ x0: match.x0, y0: match.y0, x1: match.x1, y1: match.y1 })}`);
|
|
53
|
+
const found = {
|
|
54
|
+
tier: this.tier,
|
|
55
|
+
confidence: match.confidence / 100,
|
|
56
|
+
originalSelector: context.selector,
|
|
57
|
+
originalStrategy: context.strategy,
|
|
58
|
+
recommendedSelector: `ocr:text="${match.text}"`,
|
|
59
|
+
recommendedStrategy: 'xenon:visual',
|
|
60
|
+
message: `Found text "${match.text}" via local OCR (${match.confidence.toFixed(0)}% confidence)`,
|
|
61
|
+
text: match.text,
|
|
62
|
+
rect: {
|
|
63
|
+
x: match.x0,
|
|
64
|
+
y: match.y0,
|
|
65
|
+
width: match.x1 - match.x0,
|
|
66
|
+
height: match.y1 - match.y0,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
// On an iPhone, the element whose label holds the text, when there is one.
|
|
70
|
+
try {
|
|
71
|
+
const element = yield context.driver
|
|
72
|
+
.findElement('-ios predicate string', `label CONTAINS[c] "${match.text.replace(/"/g, '\\"')}"`)
|
|
73
|
+
.catch(() => null);
|
|
74
|
+
const elementId = (element === null || element === void 0 ? void 0 : element.ELEMENT) || (element === null || element === void 0 ? void 0 : element['element-6066-11e4-a52e-4f735466cecf']);
|
|
75
|
+
if (elementId) {
|
|
76
|
+
this.logger.info('🎯 OCR resolved to real element via predicate search');
|
|
77
|
+
return Object.assign({ id: elementId }, found);
|
|
89
78
|
}
|
|
90
|
-
// Fallback: Return coordinate-based result with virtual ID
|
|
91
|
-
// The CommandInterceptor handles rect-based results via coordinate tap
|
|
92
|
-
return {
|
|
93
|
-
id: `healed_ocr_${Date.now()}`,
|
|
94
|
-
tier: this.tier,
|
|
95
|
-
confidence: bestWord.confidence / 100,
|
|
96
|
-
originalSelector: context.selector,
|
|
97
|
-
originalStrategy: context.strategy,
|
|
98
|
-
recommendedSelector: `ocr:text="${bestWord.text}"`,
|
|
99
|
-
recommendedStrategy: 'xenon:visual',
|
|
100
|
-
message: `Found text "${bestWord.text}" via local OCR (${bestWord.confidence.toFixed(0)}% confidence)`,
|
|
101
|
-
rect: {
|
|
102
|
-
x: bestWord.bbox.x0,
|
|
103
|
-
y: bestWord.bbox.y0,
|
|
104
|
-
width: bestWord.bbox.x1 - bestWord.bbox.x0,
|
|
105
|
-
height: bestWord.bbox.y1 - bestWord.bbox.y0,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
79
|
}
|
|
80
|
+
catch (predErr) {
|
|
81
|
+
this.logger.debug(`Predicate search failed: ${predErr.message}`);
|
|
82
|
+
}
|
|
83
|
+
// Otherwise where the text is: the interceptor returns it as a virtual
|
|
84
|
+
// element, which a click taps.
|
|
85
|
+
return Object.assign({ id: `healed_ocr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` }, found);
|
|
109
86
|
}
|
|
110
87
|
catch (err) {
|
|
111
88
|
this.logger.error(`Error during OCR healing: ${err.message}`);
|
|
@@ -14,10 +14,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.ResilioTreeHealingProvider = void 0;
|
|
16
16
|
const types_1 = require("./types");
|
|
17
|
-
const { Path, PathFinder, JSDOMParser, LCSPathDistance, HeuristicNodeDistance, } = require('resiliotree');
|
|
18
17
|
const typedi_1 = require("typedi");
|
|
19
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
20
19
|
const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
|
|
20
|
+
const resilioPath_1 = require("./resilioPath");
|
|
21
|
+
/**
|
|
22
|
+
* Tier 0: the element at the end of the path most like the one the selector's
|
|
23
|
+
* element had when the selector last worked (resilioPath.ts). It answers only
|
|
24
|
+
* when it is sure, which is when the element kept its identity (Android's
|
|
25
|
+
* resource-id, iOS's name) and moved in the tree: a positional XPath that
|
|
26
|
+
* broke when the layout changed. Anything less is the next tier's to decide.
|
|
27
|
+
*/
|
|
21
28
|
class ResilioTreeHealingProvider {
|
|
22
29
|
constructor(etalonService) {
|
|
23
30
|
this.etalonService = etalonService;
|
|
@@ -34,44 +41,45 @@ class ResilioTreeHealingProvider {
|
|
|
34
41
|
}
|
|
35
42
|
try {
|
|
36
43
|
const signature = yield this.etalonService.getSignature(context.selector);
|
|
37
|
-
if (!signature ||
|
|
44
|
+
if (!(0, resilioPath_1.isResilioPath)(signature === null || signature === void 0 ? void 0 : signature.path)) {
|
|
38
45
|
this.logger.debug(`No ResilioTree path found for selector: ${context.selector}`);
|
|
39
46
|
return null;
|
|
40
47
|
}
|
|
41
48
|
this.logger.info(`Attempting robust ResilioTree recovery for: ${context.selector}`);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.logger.info(`ResilioTree suggested recovery XPath: ${recommendedXpath}`);
|
|
49
|
+
const nearest = (0, resilioPath_1.nearestElement)(signature.path, context.pageSource);
|
|
50
|
+
if (!nearest) {
|
|
51
|
+
this.logger.info(`ResilioTree found no element it is sure of for: ${context.selector}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if ((0, resilioPath_1.contradictsSelector)(nearest.element, context.strategy, context.selector)) {
|
|
55
|
+
this.logger.info(`ResilioTree's element doesn't have the text the selector states; leaving it to the next tier: ${context.selector}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// The first of the element's locators the driver finds, among those
|
|
59
|
+
// that select that element alone in the page source.
|
|
60
|
+
const candidates = (0, resilioPath_1.locatorsSelectingOnly)(nearest.element, this.generator.generate(nearest.element));
|
|
61
|
+
for (const candidate of candidates) {
|
|
56
62
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const found = yield context.driver.findElement('xpath', candidate);
|
|
64
|
+
const id = (found === null || found === void 0 ? void 0 : found.ELEMENT) || (found === null || found === void 0 ? void 0 : found['element-6066-11e4-a52e-4f735466cecf']);
|
|
65
|
+
if (!id)
|
|
66
|
+
continue;
|
|
67
|
+
this.logger.info(`ResilioTree recovered the element (score ${nearest.score.toFixed(2)}): ${candidate}`);
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
originalSelector: context.selector,
|
|
71
|
+
originalStrategy: context.strategy,
|
|
72
|
+
recommendedSelector: candidate,
|
|
73
|
+
recommendedStrategy: 'xpath',
|
|
74
|
+
candidateSelectors: candidates,
|
|
75
|
+
confidence: nearest.score,
|
|
76
|
+
tier: this.tier,
|
|
77
|
+
node: nearest.element,
|
|
78
|
+
message: `Recovered via ResilioTree path matching. New XPath: ${candidate}`,
|
|
79
|
+
};
|
|
72
80
|
}
|
|
73
|
-
catch (
|
|
74
|
-
this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${
|
|
81
|
+
catch (_a) {
|
|
82
|
+
this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${candidate}`);
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
}
|
|
@@ -81,22 +89,5 @@ class ResilioTreeHealingProvider {
|
|
|
81
89
|
return null;
|
|
82
90
|
});
|
|
83
91
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Generates an absolute XPath for a ResilioTree Node
|
|
86
|
-
*/
|
|
87
|
-
generateXpath(node) {
|
|
88
|
-
const parts = [];
|
|
89
|
-
let curr = node;
|
|
90
|
-
while (curr) {
|
|
91
|
-
const tag = curr.tag.toLowerCase();
|
|
92
|
-
// Skip JSDOM added wrapper tags for mobile compatibility
|
|
93
|
-
if (tag !== 'html' && tag !== 'body') {
|
|
94
|
-
const index = curr.index + 1; // XPath is 1-based
|
|
95
|
-
parts.unshift(`${curr.tag}[${index}]`);
|
|
96
|
-
}
|
|
97
|
-
curr = curr.parent;
|
|
98
|
-
}
|
|
99
|
-
return `/${parts.join('/')}`;
|
|
100
|
-
}
|
|
101
92
|
}
|
|
102
93
|
exports.ResilioTreeHealingProvider = ResilioTreeHealingProvider;
|
|
@@ -42,7 +42,7 @@ class VisualAiHealingProvider {
|
|
|
42
42
|
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
|
|
43
43
|
this.logger.info(`✅ Visual AI found element at (${coordinates.x}, ${coordinates.y})`);
|
|
44
44
|
return {
|
|
45
|
-
id: `healed_visual_${Date.now()}`,
|
|
45
|
+
id: `healed_visual_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
46
46
|
tier: this.tier,
|
|
47
47
|
confidence: 0.8, // Basic vision models don't always give confidence, assuming high if found
|
|
48
48
|
originalSelector: context.selector,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What a stored fingerprint (`LocatorSignature.attributes`) says about which
|
|
4
|
+
* element it is, as opposed to where the element was (x, y, width, height).
|
|
5
|
+
*
|
|
6
|
+
* Through 2.14 every learnt fingerprint held only the rect and the type: the
|
|
7
|
+
* attribute reads failed (CommandInterceptor.triggerLearning). Fuzzy XML, which
|
|
8
|
+
* weighs position above everything else, then healed a selector to whatever
|
|
9
|
+
* element of that type sat at that spot. A fingerprint with no identity isn't
|
|
10
|
+
* used for position, and is learnt again.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.FINGERPRINT_IDENTITY_ATTRIBUTES = void 0;
|
|
14
|
+
exports.meaningful = meaningful;
|
|
15
|
+
exports.hasIdentity = hasIdentity;
|
|
16
|
+
exports.attributesToLearn = attributesToLearn;
|
|
17
|
+
exports.resourceIdName = resourceIdName;
|
|
18
|
+
exports.stableIdentifierOf = stableIdentifierOf;
|
|
19
|
+
exports.sameIdentifier = sameIdentifier;
|
|
20
|
+
exports.sameText = sameText;
|
|
21
|
+
/**
|
|
22
|
+
* The attributes that say which element a fingerprint is. Not `value`: on a
|
|
23
|
+
* text field it is what the test typed, which fingerprints no longer keep.
|
|
24
|
+
*/
|
|
25
|
+
exports.FINGERPRINT_IDENTITY_ATTRIBUTES = [
|
|
26
|
+
'resource-id',
|
|
27
|
+
'content-desc',
|
|
28
|
+
'accessibility-id',
|
|
29
|
+
'text',
|
|
30
|
+
'name',
|
|
31
|
+
'label',
|
|
32
|
+
'id',
|
|
33
|
+
'hint',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* A value that says something: a string with more than spaces in it, and not
|
|
37
|
+
* "null", which is what UiAutomator2 answers for an attribute the element
|
|
38
|
+
* hasn't set (its getAttribute is String(value)).
|
|
39
|
+
*/
|
|
40
|
+
function meaningful(value) {
|
|
41
|
+
return typeof value === 'string' && value.trim() !== '' && value.trim() !== 'null';
|
|
42
|
+
}
|
|
43
|
+
/** Whether a fingerprint says anything about which element it is. */
|
|
44
|
+
function hasIdentity(attributes) {
|
|
45
|
+
return exports.FINGERPRINT_IDENTITY_ATTRIBUTES.some((name) => meaningful(attributes === null || attributes === void 0 ? void 0 : attributes[name]));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The attributes learning asks a driver for: those it has. UiAutomator2
|
|
49
|
+
* throws for `id` and `label`, and its `name` is the text again; WebDriverAgent
|
|
50
|
+
* throws for anything but its own, so five of the old list were five failed
|
|
51
|
+
* round trips per selector learnt.
|
|
52
|
+
*/
|
|
53
|
+
function attributesToLearn(platformName) {
|
|
54
|
+
const platform = String(platformName !== null && platformName !== void 0 ? platformName : '').toLowerCase();
|
|
55
|
+
if (platform === 'android')
|
|
56
|
+
return ['resource-id', 'content-desc', 'text', 'hint'];
|
|
57
|
+
if (platform === 'ios' || platform === 'tvos')
|
|
58
|
+
return ['name', 'label'];
|
|
59
|
+
return ['resource-id', 'content-desc', 'text', 'hint', 'name', 'label'];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* An Android resource-id without its package: `com.shop:id/pay` is `pay`.
|
|
63
|
+
* Fuzzy XML's veto asks whether two ids are the same this way: ids of one
|
|
64
|
+
* app share the package, and of a debug build another one. Its score still
|
|
65
|
+
* compares whole ids, where the shared package earns a share of the weight.
|
|
66
|
+
*/
|
|
67
|
+
function resourceIdName(resourceId) {
|
|
68
|
+
const at = resourceId.lastIndexOf(':id/');
|
|
69
|
+
if (at >= 0)
|
|
70
|
+
return resourceId.slice(at + 4);
|
|
71
|
+
return resourceId.slice(resourceId.lastIndexOf('/') + 1);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The identifier a developer gave the element, which stays when its text
|
|
75
|
+
* changes: an Android resource-id, or an iOS accessibility identifier (`name`
|
|
76
|
+
* when it isn't just the label; an element without one has its label as name).
|
|
77
|
+
*/
|
|
78
|
+
function stableIdentifierOf(attributes) {
|
|
79
|
+
const resourceId = attributes === null || attributes === void 0 ? void 0 : attributes['resource-id'];
|
|
80
|
+
if (meaningful(resourceId))
|
|
81
|
+
return { attribute: 'resource-id', value: resourceId };
|
|
82
|
+
const name = attributes === null || attributes === void 0 ? void 0 : attributes.name;
|
|
83
|
+
if (meaningful(name) && name !== (attributes === null || attributes === void 0 ? void 0 : attributes.label))
|
|
84
|
+
return { attribute: 'name', value: name };
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Built at run time: the compiler's target predates Unicode property escapes
|
|
88
|
+
// in regular expression literals. Letters, their combining marks and digits,
|
|
89
|
+
// in any script.
|
|
90
|
+
const NOT_A_WORD = new RegExp('[^\\p{L}\\p{M}\\p{N}]+', 'gu');
|
|
91
|
+
const CAMEL_TURN = new RegExp('([\\p{Ll}\\p{N}])(\\p{Lu})', 'gu');
|
|
92
|
+
/** An identifier's words: split where camelCase turns and at anything but a letter or digit. */
|
|
93
|
+
function identifierWords(identifier) {
|
|
94
|
+
return identifier
|
|
95
|
+
.normalize('NFC')
|
|
96
|
+
.replace(CAMEL_TURN, '$1 $2')
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.split(NOT_A_WORD)
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether two identifiers name the same element: they have the same words,
|
|
103
|
+
* however written (btn-pay, pay_btn, payBtn). Any other pair names another
|
|
104
|
+
* element, and the text decides: an id that gains words is as often another
|
|
105
|
+
* element (pay_later, undo_delete) as a rename (checkout_pay), and letters
|
|
106
|
+
* alone can't tell follow from unfollow, or checkout.cancel from
|
|
107
|
+
* checkout.pay (64% of their letter pairs are shared). Two with no letter or
|
|
108
|
+
* digit between them can't be told apart, and count as the same.
|
|
109
|
+
*/
|
|
110
|
+
function sameIdentifier(a, b) {
|
|
111
|
+
const [wordsA, wordsB] = [new Set(identifierWords(a)), new Set(identifierWords(b))];
|
|
112
|
+
if (wordsA.size !== wordsB.size)
|
|
113
|
+
return false;
|
|
114
|
+
return [...wordsA].every((w) => wordsB.has(w));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether two texts read the same: case, punctuation and spacing aside, with
|
|
118
|
+
* accents composed (NFC). Combining marks count: Hindi दें and दो differ only
|
|
119
|
+
* by theirs. Nothing looser: "Log in" and "Log out", "Enable" and "Disable"
|
|
120
|
+
* are as alike by their letters as a reworded label is.
|
|
121
|
+
*/
|
|
122
|
+
function sameText(a, b) {
|
|
123
|
+
const read = (t) => t.normalize('NFC').toLowerCase().replace(NOT_A_WORD, ' ').trim();
|
|
124
|
+
const [textA, textB] = [read(a), read(b)];
|
|
125
|
+
return textA !== '' && textA === textB;
|
|
126
|
+
}
|