@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
|
@@ -58,6 +58,8 @@ const event_manager_1 = require("../dashboard/event-manager");
|
|
|
58
58
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
59
59
|
const HealingOrchestrator_1 = require("../services/healing/HealingOrchestrator");
|
|
60
60
|
const HealEtalonService_1 = require("../services/healing/HealEtalonService");
|
|
61
|
+
const resilioPath_1 = require("../services/healing/resilioPath");
|
|
62
|
+
const fingerprintIdentity_1 = require("../services/healing/fingerprintIdentity");
|
|
61
63
|
const OmniVisionService_1 = require("../services/omni-vision/OmniVisionService");
|
|
62
64
|
const AICommandService_1 = require("../services/AICommandService");
|
|
63
65
|
const logger_1 = __importDefault(require("../logger"));
|
|
@@ -67,10 +69,46 @@ const AutowaitService_1 = require("../services/autowait/AutowaitService");
|
|
|
67
69
|
const waitFor_1 = require("../services/autowait/waitFor");
|
|
68
70
|
const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
|
|
69
71
|
const xenonScripts_1 = require("./xenonScripts");
|
|
72
|
+
const healReport_1 = require("../gateway/healReport");
|
|
73
|
+
const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
74
|
+
/**
|
|
75
|
+
* The commands the interceptor answers itself for an element Xenon found in a
|
|
76
|
+
* screenshot (Omni-Vision's `-custom:ai-*` finds, self-healing's OCR and
|
|
77
|
+
* Visual AI tiers). Any other command naming one is refused.
|
|
78
|
+
*/
|
|
79
|
+
const VIRTUAL_ELEMENT_COMMANDS = [
|
|
80
|
+
'click',
|
|
81
|
+
'getElementRect',
|
|
82
|
+
'getElementLocation',
|
|
83
|
+
'getElementSize',
|
|
84
|
+
'getText',
|
|
85
|
+
'setValue',
|
|
86
|
+
'elementDisplayed',
|
|
87
|
+
'elementEnabled',
|
|
88
|
+
];
|
|
89
|
+
/** An id Xenon gave an element it found in a screenshot. */
|
|
90
|
+
function isVirtualElementId(id) {
|
|
91
|
+
return (typeof id === 'string' &&
|
|
92
|
+
(id.startsWith('omni_') || id.startsWith('healed_ocr') || id.startsWith('healed_visual')));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The element an element command is about. Appium passes a command's body
|
|
96
|
+
* first and the ids from its path after it: setValue is (text, elementId),
|
|
97
|
+
* the other commands Xenon looks at are (elementId, ...). Through 2.14 the
|
|
98
|
+
* text of a setValue was read as its element.
|
|
99
|
+
*/
|
|
100
|
+
function elementIdOf(commandName, args) {
|
|
101
|
+
return commandName === 'setValue' ? args[1] : args[0];
|
|
102
|
+
}
|
|
70
103
|
let CommandInterceptor = class CommandInterceptor {
|
|
71
104
|
constructor() {
|
|
72
105
|
this.log = logger_1.default.scope('CommandInterceptor');
|
|
106
|
+
// The sessions already told about their healingTiers, by driver: a driver
|
|
107
|
+
// lives as long as its session, so nothing has to forget it.
|
|
108
|
+
this.warnedHealingTiers = new WeakSet();
|
|
73
109
|
this.learningSessions = new Set();
|
|
110
|
+
/** Selectors whose fingerprint was learnt again for its path in this process (bounded). */
|
|
111
|
+
this.relearnt = new Set();
|
|
74
112
|
}
|
|
75
113
|
handle(next, driver, commandName, args, pluginArgs, isHub) {
|
|
76
114
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -80,6 +118,7 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
80
118
|
return yield next();
|
|
81
119
|
const sessionId = driver.sessionId || args[args.length - 1];
|
|
82
120
|
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
121
|
+
const spanId = `${sessionId}:${commandName}`;
|
|
83
122
|
let span;
|
|
84
123
|
if (isHub && sessionId) {
|
|
85
124
|
span = tracingService.startCommandSpan(sessionId, commandName, {
|
|
@@ -92,6 +131,7 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
92
131
|
// those services having to accept a context parameter.
|
|
93
132
|
const spanCtx = span === null || span === void 0 ? void 0 : span.spanContext();
|
|
94
133
|
const cmdStart = Date.now();
|
|
134
|
+
let failure;
|
|
95
135
|
try {
|
|
96
136
|
return yield sessionContext_1.sessionContext.run({
|
|
97
137
|
sessionId: sessionId || undefined,
|
|
@@ -99,9 +139,16 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
99
139
|
commandName,
|
|
100
140
|
traceId: spanCtx === null || spanCtx === void 0 ? void 0 : spanCtx.traceId,
|
|
101
141
|
spanId: spanCtx === null || spanCtx === void 0 ? void 0 : spanCtx.spanId,
|
|
102
|
-
}, () => this.handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId
|
|
142
|
+
}, () => this.handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// What the client gets: a healed find returned an element instead.
|
|
146
|
+
failure = { error };
|
|
147
|
+
throw error;
|
|
103
148
|
}
|
|
104
149
|
finally {
|
|
150
|
+
if (span)
|
|
151
|
+
tracingService.endCommandSpan(spanId, span, failure);
|
|
105
152
|
// Aggregate counter — no per-session label, just fleet-wide throughput
|
|
106
153
|
// so Prom rate() gives commands/sec and the duration sum gives avg
|
|
107
154
|
// latency. Errors still count: a 429-hit dashboard poll is still hub
|
|
@@ -110,9 +157,8 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
110
157
|
}
|
|
111
158
|
});
|
|
112
159
|
}
|
|
113
|
-
handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId
|
|
160
|
+
handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId) {
|
|
114
161
|
return __awaiter(this, void 0, void 0, function* () {
|
|
115
|
-
var _a;
|
|
116
162
|
if (commandName === 'createSession' || commandName === 'deleteSession') {
|
|
117
163
|
try {
|
|
118
164
|
return yield next();
|
|
@@ -121,8 +167,6 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
121
167
|
if (commandName === 'deleteSession' && sessionId) {
|
|
122
168
|
typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(sessionId);
|
|
123
169
|
}
|
|
124
|
-
if (span)
|
|
125
|
-
tracingService.endSpan(`${sessionId}:${commandName}`);
|
|
126
170
|
}
|
|
127
171
|
}
|
|
128
172
|
try {
|
|
@@ -144,7 +188,7 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
144
188
|
scriptArgs = {};
|
|
145
189
|
}
|
|
146
190
|
}
|
|
147
|
-
const aiCommand =
|
|
191
|
+
const aiCommand = (0, xenonScripts_1.interceptorScriptName)(script);
|
|
148
192
|
// appium-wait-plugin compatible alias — strip the `plugin:` prefix
|
|
149
193
|
// and map the old camelCase names so existing tests Just Work.
|
|
150
194
|
const legacyWaitCommand = script.replace(/^plugin\s*:\s*/, '').trim();
|
|
@@ -195,12 +239,7 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
195
239
|
: '';
|
|
196
240
|
return yield typedi_1.Container.get(AICommandService_1.AICommandService).assertVisualState(driver, instruction);
|
|
197
241
|
}
|
|
198
|
-
if (
|
|
199
|
-
aiCommand === 'removeMock' ||
|
|
200
|
-
aiCommand === 'clearMocks' ||
|
|
201
|
-
aiCommand === 'getRequests' ||
|
|
202
|
-
aiCommand === 'getMocks' ||
|
|
203
|
-
aiCommand === 'exportHar') {
|
|
242
|
+
if ((0, xenonScripts_1.isNetworkCaptureScript)(script)) {
|
|
204
243
|
const payload = typeof scriptArgs === 'object' && scriptArgs !== null ? scriptArgs : {};
|
|
205
244
|
const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('../services/InterceptorService')));
|
|
206
245
|
const interceptor = typedi_1.Container.get(InterceptorService);
|
|
@@ -254,37 +293,23 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
254
293
|
return yield this.handleOmniVisionSearch(sessionId, driver, commandName, strategy, selector);
|
|
255
294
|
}
|
|
256
295
|
// --- OMNI-VISION: VIRTUAL ELEMENT INTERACTION ---
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
'getElementSize',
|
|
262
|
-
'getText',
|
|
263
|
-
'setValue',
|
|
264
|
-
'elementDisplayed',
|
|
265
|
-
'elementEnabled',
|
|
266
|
-
];
|
|
267
|
-
if (elementCommands.includes(commandName)) {
|
|
268
|
-
const elementId = args[0];
|
|
269
|
-
if (typeof elementId === 'string' &&
|
|
270
|
-
(elementId.startsWith('omni_') ||
|
|
271
|
-
elementId.startsWith('healed_ocr') ||
|
|
272
|
-
elementId.startsWith('healed_visual'))) {
|
|
273
|
-
return yield this.handleVirtualElementCommand(sessionId, driver, commandName, elementId, args[1]);
|
|
274
|
-
}
|
|
296
|
+
// The driver doesn't know these elements, so nothing about one reaches it.
|
|
297
|
+
const virtualId = this.virtualElementIn(commandName, args, sessionId);
|
|
298
|
+
if (virtualId !== null) {
|
|
299
|
+
return yield this.handleVirtualElementCommand(sessionId, driver, commandName, virtualId, args[0]);
|
|
275
300
|
}
|
|
276
301
|
// --- AUTOWAIT: pre-action elementEnabled check for click/setValue/clear ---
|
|
277
302
|
// Mirrors appium-wait-plugin behavior; runs before native command so
|
|
278
303
|
// a NotEnabled state surfaces as a wait-then-retry rather than an
|
|
279
304
|
// immediate failure. Skips elements managed by other Xenon subsystems.
|
|
280
305
|
const autowait = typedi_1.Container.get(AutowaitService_1.AutowaitService).getProps(sessionId, pluginArgs);
|
|
306
|
+
const actedOn = elementIdOf(commandName, args);
|
|
281
307
|
if (autowait.enabled &&
|
|
282
308
|
['click', 'setValue', 'clear'].includes(commandName) &&
|
|
283
309
|
!autowait.excludeEnabledCheck.includes(commandName) &&
|
|
284
|
-
typeof
|
|
285
|
-
!
|
|
286
|
-
|
|
287
|
-
yield this.waitForElementEnabled(driver, args[0], autowait);
|
|
310
|
+
typeof actedOn === 'string' &&
|
|
311
|
+
!isVirtualElementId(actedOn)) {
|
|
312
|
+
yield this.waitForElementEnabled(driver, actedOn, autowait);
|
|
288
313
|
}
|
|
289
314
|
// --- AUTOWAIT: polling find for findElement/findElements ---
|
|
290
315
|
// Wraps next() in a poll loop so transient NoSuchElement errors get
|
|
@@ -296,91 +321,58 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
296
321
|
!this.isVisualStrategy(strategy)) {
|
|
297
322
|
const response = yield this.runFindWithAutowait(next, commandName, autowait);
|
|
298
323
|
if (isHub && !!pluginArgs.enableDashboard && SessionManager_1.SESSION_MANAGER.isValidSession(sessionId)) {
|
|
299
|
-
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response
|
|
324
|
+
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response);
|
|
300
325
|
}
|
|
326
|
+
this.learnFromFind(sessionId, commandName, driver, args, response, pluginArgs);
|
|
301
327
|
return response;
|
|
302
328
|
}
|
|
303
329
|
const response = yield next();
|
|
304
330
|
if (isHub && !!pluginArgs.enableDashboard && SessionManager_1.SESSION_MANAGER.isValidSession(sessionId)) {
|
|
305
|
-
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response
|
|
331
|
+
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response);
|
|
306
332
|
}
|
|
333
|
+
this.learnFromFind(sessionId, commandName, driver, args, response, pluginArgs);
|
|
307
334
|
return response;
|
|
308
335
|
}
|
|
309
336
|
catch (error) {
|
|
310
337
|
if (this.isNoSuchElementError(error) &&
|
|
311
338
|
['findElement', 'findElements'].includes(commandName) &&
|
|
339
|
+
// A -custom:ai-* find already looked at the screen; the tiers would
|
|
340
|
+
// only look again for the words of its description.
|
|
341
|
+
!this.isVisualStrategy(args[0]) &&
|
|
312
342
|
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
|
|
313
343
|
// §2.7 healing-tier capability gate: a session created with
|
|
314
344
|
// xe:options.healingTiers (or the xenon:options alias) restricts
|
|
315
345
|
// self-healing to those tier indices (1=Resilio, 2=Fuzzy XML, 3=OCR,
|
|
316
|
-
// 4=Visual AI, 5=LLM).
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
const
|
|
323
|
-
|
|
346
|
+
// 4=Visual AI, 5=LLM), and so keeps its screen from the AI provider.
|
|
347
|
+
// Read from the session's own driver, which Appium hands the plugin
|
|
348
|
+
// with every command. SESSION_MANAGER holds a local session only with
|
|
349
|
+
// the dashboard on or a video recorded, and through 2.14 every other
|
|
350
|
+
// session ran every tier. A value that isn't a list of tier numbers
|
|
351
|
+
// runs only the tiers that stay on this server (coerceHealingTiersCap).
|
|
352
|
+
const asked = (0, HealingOrchestrator_1.healingTiersFromCaps)(driver === null || driver === void 0 ? void 0 : driver.caps);
|
|
353
|
+
if (asked.unreadable !== undefined) {
|
|
354
|
+
this.warnUnreadableHealingTiers(driver, sessionId, asked.unreadable);
|
|
355
|
+
}
|
|
356
|
+
const healed = yield typedi_1.Container.get(HealingOrchestrator_1.HealingOrchestrator).attemptHealing(sessionId, driver, args[0], args[1], asked.tiers);
|
|
324
357
|
if (healed) {
|
|
325
|
-
yield this.
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
//
|
|
358
|
+
yield this.recordHeal(sessionId, commandName, driver, args, healed);
|
|
359
|
+
// The OCR and Visual AI tiers can find only a position. The test gets
|
|
360
|
+
// a virtual element there, which its own click taps; nothing acts on
|
|
361
|
+
// the screen during the find. Through 2.14 the interceptor tapped
|
|
362
|
+
// the spot here, so the test's click tapped it a second time. On an
|
|
363
|
+
// iPhone it first asked for the first element covering the spot in
|
|
364
|
+
// tree order, which is an outer container such as the window.
|
|
329
365
|
if (healed.id.startsWith('healed_') && healed.rect) {
|
|
330
|
-
|
|
331
|
-
typedi_1.Container.get(OmniVisionService_1.OmniVisionService).addVirtualElement({
|
|
366
|
+
typedi_1.Container.get(OmniVisionService_1.OmniVisionService).remember(sessionId, {
|
|
332
367
|
id: healed.id,
|
|
333
368
|
rect: healed.rect,
|
|
334
369
|
confidence: healed.confidence,
|
|
335
|
-
text: healed.
|
|
370
|
+
text: healed.text,
|
|
336
371
|
});
|
|
337
|
-
this.log.info(`[Interceptor] Visual healing returned coordinates. Resolving real element at (${healed.rect.x}, ${healed.rect.y})...`);
|
|
338
|
-
let resolved = false;
|
|
339
|
-
// Strategy 1: Try to find element at the center of the detected area
|
|
340
|
-
try {
|
|
341
|
-
const cx = Math.round(healed.rect.x + healed.rect.width / 2);
|
|
342
|
-
const cy = Math.round(healed.rect.y + healed.rect.height / 2);
|
|
343
|
-
const touchEl = yield driver.findElement('-ios class chain', `**/XCUIElementTypeAny[\`rect.x <= ${cx} AND rect.x + rect.width >= ${cx} AND rect.y <= ${cy} AND rect.y + rect.height >= ${cy}\`]`);
|
|
344
|
-
if (touchEl) {
|
|
345
|
-
finalId =
|
|
346
|
-
touchEl.ELEMENT || touchEl['element-6066-11e4-a52e-4f735466cecf'] || finalId;
|
|
347
|
-
resolved = !!finalId && !finalId.startsWith('healed_');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
catch (e) {
|
|
351
|
-
// Strategy 1 failed
|
|
352
|
-
}
|
|
353
|
-
// Strategy 2: Use coordinate tap action (W3C Actions API)
|
|
354
|
-
if (!resolved) {
|
|
355
|
-
this.log.info('[Interceptor] Falling back to coordinate-based tap for visual healing');
|
|
356
|
-
try {
|
|
357
|
-
const cx = Math.round(healed.rect.x + healed.rect.width / 2);
|
|
358
|
-
const cy = Math.round(healed.rect.y + healed.rect.height / 2);
|
|
359
|
-
yield driver.performActions([
|
|
360
|
-
{
|
|
361
|
-
type: 'pointer',
|
|
362
|
-
id: 'xenon-heal-tap',
|
|
363
|
-
parameters: { pointerType: 'touch' },
|
|
364
|
-
actions: [
|
|
365
|
-
{ type: 'pointerMove', duration: 0, x: cx, y: cy },
|
|
366
|
-
{ type: 'pointerDown', button: 0 },
|
|
367
|
-
{ type: 'pause', duration: 100 },
|
|
368
|
-
{ type: 'pointerUp', button: 0 },
|
|
369
|
-
],
|
|
370
|
-
},
|
|
371
|
-
]);
|
|
372
|
-
yield driver.releaseActions();
|
|
373
|
-
this.log.info(`[Interceptor] ✅ Visual healing: tapped at (${cx}, ${cy})`);
|
|
374
|
-
// Return the virtual ID — the tap already happened
|
|
375
|
-
}
|
|
376
|
-
catch (tapErr) {
|
|
377
|
-
this.log.error(`[Interceptor] Coordinate tap failed: ${tapErr.message}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
372
|
}
|
|
381
373
|
const elementResponse = {
|
|
382
|
-
ELEMENT:
|
|
383
|
-
|
|
374
|
+
ELEMENT: healed.id,
|
|
375
|
+
[W3C_ELEMENT_KEY]: healed.id,
|
|
384
376
|
};
|
|
385
377
|
return commandName === 'findElement' ? elementResponse : [elementResponse];
|
|
386
378
|
}
|
|
@@ -395,12 +387,18 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
395
387
|
}
|
|
396
388
|
throw error;
|
|
397
389
|
}
|
|
398
|
-
finally {
|
|
399
|
-
if (isHub && sessionId && span)
|
|
400
|
-
tracingService.endSpan(`${sessionId}:${commandName}`);
|
|
401
|
-
}
|
|
402
390
|
});
|
|
403
391
|
}
|
|
392
|
+
warnUnreadableHealingTiers(driver, sessionId, unreadable) {
|
|
393
|
+
if (driver && typeof driver === 'object') {
|
|
394
|
+
if (this.warnedHealingTiers.has(driver))
|
|
395
|
+
return;
|
|
396
|
+
this.warnedHealingTiers.add(driver);
|
|
397
|
+
}
|
|
398
|
+
this.log.warn(`Session ${sessionId}: ${unreadable}. It heals with tiers 1, 2 and 3 only (Resilio, ` +
|
|
399
|
+
'Fuzzy XML, OCR), which run on this server: healing never sends its screen to the AI ' +
|
|
400
|
+
'provider.');
|
|
401
|
+
}
|
|
404
402
|
isNoSuchElementError(error) {
|
|
405
403
|
var _a;
|
|
406
404
|
return (error.name === 'NoSuchElementError' ||
|
|
@@ -466,7 +464,8 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
466
464
|
}
|
|
467
465
|
});
|
|
468
466
|
}
|
|
469
|
-
|
|
467
|
+
/** The dashboard's record of a command: only where enableDashboard records the session. */
|
|
468
|
+
runPostCommandHooks(sessionId, commandName, driver, args, response) {
|
|
470
469
|
return __awaiter(this, void 0, void 0, function* () {
|
|
471
470
|
try {
|
|
472
471
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, commandName, driver, {
|
|
@@ -475,17 +474,38 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
475
474
|
path: `/${commandName}`,
|
|
476
475
|
originalUrl: `/${commandName}`,
|
|
477
476
|
}, {}, JSON.stringify({ value: response, sessionId }));
|
|
478
|
-
if (commandName === 'findElement' &&
|
|
479
|
-
response &&
|
|
480
|
-
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
|
|
481
|
-
this.triggerLearning(driver, args, response, sessionId);
|
|
482
|
-
}
|
|
483
477
|
}
|
|
484
478
|
catch (postCommandErr) {
|
|
485
479
|
this.log.warn(`[Interceptor] Post-command hooks failed: ${postCommandErr.message}`);
|
|
486
480
|
}
|
|
487
481
|
});
|
|
488
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Learn the fingerprint of a selector a findElement found, which the Resilio
|
|
485
|
+
* and Fuzzy XML tiers heal it with later. On every session this server
|
|
486
|
+
* drives while self-healing is on, whatever enableDashboard says, nodes
|
|
487
|
+
* included. Through 2.14 it ran only behind the dashboard's record above,
|
|
488
|
+
* so a server with enableDashboard off, and every node, learnt nothing.
|
|
489
|
+
* A session that turned its own healing off (`healingTiers: []`) isn't
|
|
490
|
+
* learnt from, as the switch stops learning for every session.
|
|
491
|
+
* It reads the element in the background; the find has already answered.
|
|
492
|
+
*/
|
|
493
|
+
learnFromFind(sessionId, commandName, driver, args, response, pluginArgs) {
|
|
494
|
+
var _a, _b;
|
|
495
|
+
// The find has its element: nothing here may fail it.
|
|
496
|
+
try {
|
|
497
|
+
if (commandName !== 'findElement' ||
|
|
498
|
+
!response ||
|
|
499
|
+
!typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs) ||
|
|
500
|
+
((_a = (0, HealingOrchestrator_1.healingTiersFromCaps)(driver === null || driver === void 0 ? void 0 : driver.caps).tiers) === null || _a === void 0 ? void 0 : _a.length) === 0) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
this.triggerLearning(driver, args, response, sessionId).catch((err) => { var _a; return this.log.debug(`[Learning] Failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
this.log.debug(`[Learning] Skipped: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
489
509
|
handleOmniVisionSearch(sessionId, driver, commandName, strategy, selector) {
|
|
490
510
|
return __awaiter(this, void 0, void 0, function* () {
|
|
491
511
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
@@ -497,41 +517,70 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
497
517
|
if (match)
|
|
498
518
|
results = [match];
|
|
499
519
|
}
|
|
520
|
+
// The session's test gets these ids, so the session keeps the elements.
|
|
521
|
+
for (const element of results)
|
|
522
|
+
omniService.remember(sessionId, element);
|
|
500
523
|
const appiumResults = results.map((r) => ({
|
|
501
524
|
ELEMENT: r.id,
|
|
502
525
|
'element-6066-11e4-a52e-4f735466cecf': r.id,
|
|
503
526
|
}));
|
|
504
527
|
if (commandName === 'findElement') {
|
|
505
|
-
if (appiumResults.length === 0)
|
|
506
|
-
|
|
528
|
+
if (appiumResults.length === 0) {
|
|
529
|
+
const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
|
|
530
|
+
throw new errors.NoSuchElementError(`Xenon found nothing on the screen matching ${strategy} "${selector}".`);
|
|
531
|
+
}
|
|
507
532
|
return appiumResults[0];
|
|
508
533
|
}
|
|
509
534
|
return appiumResults;
|
|
510
535
|
});
|
|
511
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* The virtual element a command is about, or null. A command the
|
|
539
|
+
* interceptor answers for one is recognised by its element id; any other
|
|
540
|
+
* command naming a virtual element Xenon holds is caught too, so it is
|
|
541
|
+
* refused rather than sent to a driver that doesn't know the id.
|
|
542
|
+
*/
|
|
543
|
+
virtualElementIn(commandName, args, sessionId) {
|
|
544
|
+
if (VIRTUAL_ELEMENT_COMMANDS.includes(commandName)) {
|
|
545
|
+
const id = elementIdOf(commandName, args);
|
|
546
|
+
return isVirtualElementId(id) ? id : null;
|
|
547
|
+
}
|
|
548
|
+
const omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
549
|
+
const named = args.find((a) => isVirtualElementId(a) && omni.getVirtualElement(a, sessionId));
|
|
550
|
+
return named !== null && named !== void 0 ? named : null;
|
|
551
|
+
}
|
|
552
|
+
tapAt(driver, x, y) {
|
|
553
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
554
|
+
yield driver.performActions([
|
|
555
|
+
{
|
|
556
|
+
type: 'pointer',
|
|
557
|
+
id: 'finger1',
|
|
558
|
+
parameters: { pointerType: 'touch' },
|
|
559
|
+
actions: [
|
|
560
|
+
{ type: 'pointerMove', duration: 0, x, y },
|
|
561
|
+
{ type: 'pointerDown', button: 0 },
|
|
562
|
+
{ type: 'pause', duration: 100 },
|
|
563
|
+
{ type: 'pointerUp', button: 0 },
|
|
564
|
+
],
|
|
565
|
+
},
|
|
566
|
+
]);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
512
569
|
handleVirtualElementCommand(sessionId, driver, commandName, elementId, value) {
|
|
513
570
|
return __awaiter(this, void 0, void 0, function* () {
|
|
571
|
+
var _a;
|
|
572
|
+
const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
|
|
514
573
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
574
|
+
// Another session's element is unknown here, as one that has gone is.
|
|
575
|
+
const element = omniService.getVirtualElement(elementId, sessionId);
|
|
576
|
+
if (!element) {
|
|
577
|
+
throw new errors.NoSuchElementError(`Xenon has no element ${elementId}.`);
|
|
578
|
+
}
|
|
518
579
|
const centerX = Math.round(element.rect.x + element.rect.width / 2);
|
|
519
580
|
const centerY = Math.round(element.rect.y + element.rect.height / 2);
|
|
520
581
|
switch (commandName) {
|
|
521
582
|
case 'click':
|
|
522
|
-
yield
|
|
523
|
-
{
|
|
524
|
-
type: 'pointer',
|
|
525
|
-
id: 'finger1',
|
|
526
|
-
parameters: { pointerType: 'touch' },
|
|
527
|
-
actions: [
|
|
528
|
-
{ type: 'pointerMove', duration: 0, x: centerX, y: centerY },
|
|
529
|
-
{ type: 'pointerDown', button: 0 },
|
|
530
|
-
{ type: 'pause', duration: 100 },
|
|
531
|
-
{ type: 'pointerUp', button: 0 },
|
|
532
|
-
],
|
|
533
|
-
},
|
|
534
|
-
]);
|
|
583
|
+
yield this.tapAt(driver, centerX, centerY);
|
|
535
584
|
return null;
|
|
536
585
|
case 'getElementRect':
|
|
537
586
|
return element.rect;
|
|
@@ -543,33 +592,50 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
543
592
|
case 'elementEnabled':
|
|
544
593
|
return true;
|
|
545
594
|
case 'getText':
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
595
|
+
// The text OCR read there. An element Visual AI found has none: it
|
|
596
|
+
// used to answer '' (or, healed, Xenon's note about the match).
|
|
597
|
+
if (typeof element.text === 'string')
|
|
598
|
+
return element.text;
|
|
599
|
+
throw new errors.UnsupportedOperationError(`Element ${elementId} was found by AI vision, which reads no text, so Xenon has no text for it.`);
|
|
600
|
+
case 'setValue': {
|
|
601
|
+
// The driver doesn't know this element, so the text goes to the field
|
|
602
|
+
// the tap gives the keyboard focus. A driver that can't say which
|
|
603
|
+
// field that is gets no tap at all.
|
|
604
|
+
if (typeof driver.active !== 'function') {
|
|
605
|
+
throw new errors.UnsupportedOperationError(`Xenon can't type into ${elementId}, an element it found in a screenshot: ` +
|
|
606
|
+
"this driver can't tell which field has the keyboard focus. Tap the element, then type with key actions.");
|
|
607
|
+
}
|
|
608
|
+
yield this.tapAt(driver, centerX, centerY);
|
|
609
|
+
let focused = null;
|
|
561
610
|
try {
|
|
562
|
-
|
|
611
|
+
focused = yield driver.active();
|
|
612
|
+
}
|
|
613
|
+
catch (_b) {
|
|
614
|
+
focused = null;
|
|
615
|
+
}
|
|
616
|
+
const focusedId = (_a = focused === null || focused === void 0 ? void 0 : focused[W3C_ELEMENT_KEY]) !== null && _a !== void 0 ? _a : focused === null || focused === void 0 ? void 0 : focused.ELEMENT;
|
|
617
|
+
if (typeof focusedId !== 'string' || !focusedId) {
|
|
618
|
+
throw new errors.ElementNotInteractableError(`Xenon tapped ${elementId} at (${centerX}, ${centerY}), but no field took the keyboard focus, so there was nowhere to type.`);
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
return yield driver.setValue(value, focusedId);
|
|
563
622
|
}
|
|
564
623
|
catch (e) {
|
|
565
624
|
this.log.error(`setValue failed for virtual element ${elementId} on session ${sessionId}: ${e.message}`);
|
|
566
625
|
throw e;
|
|
567
626
|
}
|
|
627
|
+
}
|
|
568
628
|
default:
|
|
569
|
-
throw new
|
|
629
|
+
throw new errors.UnsupportedOperationError(`${commandName} isn't available on ${elementId}: Xenon found it in a screenshot, and ` +
|
|
630
|
+
`it is only a position on the screen. It answers ${VIRTUAL_ELEMENT_COMMANDS.join(', ')}.`);
|
|
570
631
|
}
|
|
571
632
|
});
|
|
572
633
|
}
|
|
634
|
+
rememberRelearnt(selector) {
|
|
635
|
+
if (this.relearnt.size >= 10000)
|
|
636
|
+
this.relearnt.clear();
|
|
637
|
+
this.relearnt.add(selector);
|
|
638
|
+
}
|
|
573
639
|
triggerLearning(driver, args, response, sessionId) {
|
|
574
640
|
return __awaiter(this, void 0, void 0, function* () {
|
|
575
641
|
if (this.learningSessions.has(sessionId))
|
|
@@ -583,31 +649,34 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
583
649
|
return;
|
|
584
650
|
}
|
|
585
651
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
652
|
+
var _a;
|
|
586
653
|
try {
|
|
587
654
|
const etalonService = typedi_1.Container.get(HealEtalonService_1.HealEtalonService);
|
|
588
655
|
// CRITICAL PERFORMANCE OPTIMIZATION:
|
|
589
656
|
// Only trigger the heavy metadata collection if we don't already have an etalon for this selector.
|
|
590
657
|
// Collecting page source and element rects for every single action is too CPU-intensive.
|
|
658
|
+
// A fingerprint stored without a usable path, or without anything
|
|
659
|
+
// that says which element it is (all learnt ones through 2.14), is
|
|
660
|
+
// learnt again, once per selector per process, so the Resilio tier
|
|
661
|
+
// gets its path and Fuzzy XML can use it.
|
|
591
662
|
const existing = yield etalonService.getSignature(selector);
|
|
592
|
-
|
|
663
|
+
const complete = existing && (0, resilioPath_1.isResilioPath)(existing.path) && (0, fingerprintIdentity_1.hasIdentity)(existing.attributes);
|
|
664
|
+
if (existing && (complete || this.relearnt.has(selector))) {
|
|
593
665
|
this.log.debug(`[Learning] Etalon already exists for selector: ${selector}. Skipping...`);
|
|
594
666
|
return;
|
|
595
667
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
'resource-id',
|
|
599
|
-
'text',
|
|
600
|
-
'name',
|
|
601
|
-
'id',
|
|
602
|
-
'hint',
|
|
603
|
-
'label',
|
|
604
|
-
'value', // iOS-specific identity attributes
|
|
605
|
-
];
|
|
668
|
+
if (existing)
|
|
669
|
+
this.rememberRelearnt(selector);
|
|
606
670
|
const nodeAttrs = [];
|
|
607
|
-
|
|
671
|
+
// The WebDriver command is getAttribute(name, elementId). Through
|
|
672
|
+
// 2.14 this asked for getElementAttribute, which neither UiAutomator2
|
|
673
|
+
// nor XCUITest has, so no attribute was ever read. Only the attributes
|
|
674
|
+
// the driver has are asked for, and "null" (UiAutomator2's answer for
|
|
675
|
+
// one the element hasn't set) isn't kept.
|
|
676
|
+
for (const attr of (0, fingerprintIdentity_1.attributesToLearn)((_a = driver === null || driver === void 0 ? void 0 : driver.caps) === null || _a === void 0 ? void 0 : _a.platformName)) {
|
|
608
677
|
try {
|
|
609
|
-
const val = yield driver.
|
|
610
|
-
if (val)
|
|
678
|
+
const val = yield driver.getAttribute(attr, elementId);
|
|
679
|
+
if ((0, fingerprintIdentity_1.meaningful)(val))
|
|
611
680
|
nodeAttrs.push({ name: attr, value: val });
|
|
612
681
|
}
|
|
613
682
|
catch (e) {
|
|
@@ -652,22 +721,14 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
652
721
|
// Final safety check to ensure nodeName is a valid string
|
|
653
722
|
if (!nodeName)
|
|
654
723
|
nodeName = 'Unknown';
|
|
655
|
-
//
|
|
724
|
+
// The element's path through the page source, for the Resilio tier:
|
|
725
|
+
// the element there that has the most of the attributes just read,
|
|
726
|
+
// and none when two have as many.
|
|
656
727
|
let resiliotreePathJson = null;
|
|
657
728
|
try {
|
|
658
|
-
const { JSDOMParser, Path } = yield Promise.resolve().then(() => __importStar(require('resiliotree')));
|
|
659
729
|
const pageSource = yield driver.getPageSource();
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
if (foundNode) {
|
|
663
|
-
const pathNodes = [];
|
|
664
|
-
let curr = foundNode;
|
|
665
|
-
while (curr) {
|
|
666
|
-
pathNodes.unshift(curr);
|
|
667
|
-
curr = curr.parent;
|
|
668
|
-
}
|
|
669
|
-
resiliotreePathJson = new Path(pathNodes).toJSON();
|
|
670
|
-
}
|
|
730
|
+
const element = (0, resilioPath_1.findLearntElement)(pageSource, nodeName, nodeAttrs);
|
|
731
|
+
resiliotreePathJson = element ? (0, resilioPath_1.resilioPathOf)(element, pageSource) : null;
|
|
671
732
|
}
|
|
672
733
|
catch (e) {
|
|
673
734
|
// Silently ignore: path capture is optional for learning
|
|
@@ -683,43 +744,42 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
683
744
|
}))();
|
|
684
745
|
});
|
|
685
746
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (matchCount > 0)
|
|
700
|
-
return node;
|
|
747
|
+
/**
|
|
748
|
+
* A heal of a command a hub sent goes back to the hub, which owns the
|
|
749
|
+
* session's record (healReport.ts); this server records any other.
|
|
750
|
+
*/
|
|
751
|
+
recordHeal(sessionId, commandName, driver, args, healed) {
|
|
752
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
753
|
+
const reported = (0, healReport_1.reportHeal)(this.healReportOf(args, healed));
|
|
754
|
+
if (reported === 'reported')
|
|
755
|
+
return;
|
|
756
|
+
if (reported === 'too-long') {
|
|
757
|
+
// The session is the hub's: this server has no record to write it to.
|
|
758
|
+
this.log.warn(`[Interceptor] The heal of ${args[0]}=${args[1]} is too long to send to the hub; it isn't recorded.`);
|
|
759
|
+
return;
|
|
701
760
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
761
|
+
yield this.logHealingEvent(sessionId, commandName, driver, args, healed);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
healReportOf(args, healed) {
|
|
765
|
+
var _a;
|
|
766
|
+
return {
|
|
767
|
+
originalSelector: args[1],
|
|
768
|
+
originalStrategy: args[0],
|
|
769
|
+
healedSelector: healed.recommendedSelector,
|
|
770
|
+
healedStrategy: (_a = healed.recommendedStrategy) !== null && _a !== void 0 ? _a : args[0],
|
|
771
|
+
confidence: healed.confidence,
|
|
772
|
+
tier: healed.tier,
|
|
773
|
+
};
|
|
706
774
|
}
|
|
707
775
|
logHealingEvent(sessionId, commandName, driver, args, healed) {
|
|
708
776
|
return __awaiter(this, void 0, void 0, function* () {
|
|
709
|
-
var _a;
|
|
710
777
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, commandName, driver, {
|
|
711
778
|
body: args,
|
|
712
779
|
method: 'POST',
|
|
713
780
|
path: `/${commandName}`,
|
|
714
781
|
originalUrl: `/${commandName}`,
|
|
715
|
-
}, {}, JSON.stringify({ value: { ELEMENT: healed.id }, sessionId }),
|
|
716
|
-
originalSelector: args[1],
|
|
717
|
-
originalStrategy: args[0],
|
|
718
|
-
healedSelector: healed.recommendedSelector,
|
|
719
|
-
healedStrategy: (_a = healed.recommendedStrategy) !== null && _a !== void 0 ? _a : args[0],
|
|
720
|
-
confidence: healed.confidence,
|
|
721
|
-
tier: healed.tier,
|
|
722
|
-
});
|
|
782
|
+
}, {}, JSON.stringify({ value: { ELEMENT: healed.id }, sessionId }), this.healReportOf(args, healed));
|
|
723
783
|
});
|
|
724
784
|
}
|
|
725
785
|
};
|