@xenon-device-management/xenon 2.13.2 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- 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 +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- 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 +85 -71
- 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/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -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 +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- 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/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- 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/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/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/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- 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-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/apple-family.spec.js +36 -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/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -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 +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -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/max-sessions.spec.js +240 -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/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- 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/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- 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.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -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 +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /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"));
|
|
@@ -65,10 +67,48 @@ const sessionContext_1 = require("../logging/sessionContext");
|
|
|
65
67
|
const ProcessMetricsService_1 = require("../services/ProcessMetricsService");
|
|
66
68
|
const AutowaitService_1 = require("../services/autowait/AutowaitService");
|
|
67
69
|
const waitFor_1 = require("../services/autowait/waitFor");
|
|
70
|
+
const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
|
|
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
|
+
}
|
|
68
103
|
let CommandInterceptor = class CommandInterceptor {
|
|
69
104
|
constructor() {
|
|
70
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();
|
|
71
109
|
this.learningSessions = new Set();
|
|
110
|
+
/** Selectors whose fingerprint was learnt again for its path in this process (bounded). */
|
|
111
|
+
this.relearnt = new Set();
|
|
72
112
|
}
|
|
73
113
|
handle(next, driver, commandName, args, pluginArgs, isHub) {
|
|
74
114
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -110,7 +150,6 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
110
150
|
}
|
|
111
151
|
handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId, span, tracingService) {
|
|
112
152
|
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
-
var _a;
|
|
114
153
|
if (commandName === 'createSession' || commandName === 'deleteSession') {
|
|
115
154
|
try {
|
|
116
155
|
return yield next();
|
|
@@ -182,10 +221,14 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
182
221
|
}
|
|
183
222
|
if (aiCommand === 'assertVisualState') {
|
|
184
223
|
this.log.info(`[Interceptor] Routing AI command: ${script} with payload: ${JSON.stringify(scriptArgs)}`);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
224
|
+
// The condition as a plain string (`execute(script, 'The cart is
|
|
225
|
+
// empty')`, which arrives as ['The cart is empty']) or as
|
|
226
|
+
// { instruction }. A plain string used to arrive as ''.
|
|
227
|
+
const first = Array.isArray(scriptArgs) ? scriptArgs[0] : scriptArgs;
|
|
228
|
+
const instruction = typeof first === 'string'
|
|
229
|
+
? first
|
|
230
|
+
: typeof first === 'object' && typeof (first === null || first === void 0 ? void 0 : first.instruction) === 'string'
|
|
231
|
+
? first.instruction
|
|
189
232
|
: '';
|
|
190
233
|
return yield typedi_1.Container.get(AICommandService_1.AICommandService).assertVisualState(driver, instruction);
|
|
191
234
|
}
|
|
@@ -216,13 +259,28 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
216
259
|
}
|
|
217
260
|
}
|
|
218
261
|
}
|
|
262
|
+
// A session-details command (`xenon: setSessionName`, ...) is answered
|
|
263
|
+
// by the before-hook; its answer is what the test gets.
|
|
264
|
+
let answered = null;
|
|
219
265
|
const shouldProceed = yield event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, commandName, { body: { script: args[0], args: args[1] } }, {
|
|
220
|
-
status: () => ({
|
|
266
|
+
status: () => ({
|
|
267
|
+
json: (d) => {
|
|
268
|
+
var _a;
|
|
269
|
+
answered = (_a = d === null || d === void 0 ? void 0 : d.value) !== null && _a !== void 0 ? _a : null;
|
|
270
|
+
return d;
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
221
273
|
setHeader: () => { },
|
|
222
274
|
getHeader: () => { },
|
|
223
275
|
});
|
|
224
276
|
if (shouldProceed === false) {
|
|
225
|
-
return
|
|
277
|
+
return answered;
|
|
278
|
+
}
|
|
279
|
+
// Every Xenon script this server has was answered above. Any other
|
|
280
|
+
// name used to be answered with null, as if it had worked.
|
|
281
|
+
if (commandName === 'execute' && (0, xenonScripts_1.xenonScriptName)(args[0]) !== null) {
|
|
282
|
+
const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
|
|
283
|
+
throw new errors.UnknownCommandError((0, xenonScripts_1.unknownXenonScriptMessage)(args[0]));
|
|
226
284
|
}
|
|
227
285
|
}
|
|
228
286
|
// --- OMNI-VISION: PROACTIVE SEARCH ---
|
|
@@ -233,37 +291,23 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
233
291
|
return yield this.handleOmniVisionSearch(sessionId, driver, commandName, strategy, selector);
|
|
234
292
|
}
|
|
235
293
|
// --- OMNI-VISION: VIRTUAL ELEMENT INTERACTION ---
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
'getElementSize',
|
|
241
|
-
'getText',
|
|
242
|
-
'setValue',
|
|
243
|
-
'elementDisplayed',
|
|
244
|
-
'elementEnabled',
|
|
245
|
-
];
|
|
246
|
-
if (elementCommands.includes(commandName)) {
|
|
247
|
-
const elementId = args[0];
|
|
248
|
-
if (typeof elementId === 'string' &&
|
|
249
|
-
(elementId.startsWith('omni_') ||
|
|
250
|
-
elementId.startsWith('healed_ocr') ||
|
|
251
|
-
elementId.startsWith('healed_visual'))) {
|
|
252
|
-
return yield this.handleVirtualElementCommand(sessionId, driver, commandName, elementId, args[1]);
|
|
253
|
-
}
|
|
294
|
+
// The driver doesn't know these elements, so nothing about one reaches it.
|
|
295
|
+
const virtualId = this.virtualElementIn(commandName, args, sessionId);
|
|
296
|
+
if (virtualId !== null) {
|
|
297
|
+
return yield this.handleVirtualElementCommand(sessionId, driver, commandName, virtualId, args[0]);
|
|
254
298
|
}
|
|
255
299
|
// --- AUTOWAIT: pre-action elementEnabled check for click/setValue/clear ---
|
|
256
300
|
// Mirrors appium-wait-plugin behavior; runs before native command so
|
|
257
301
|
// a NotEnabled state surfaces as a wait-then-retry rather than an
|
|
258
302
|
// immediate failure. Skips elements managed by other Xenon subsystems.
|
|
259
303
|
const autowait = typedi_1.Container.get(AutowaitService_1.AutowaitService).getProps(sessionId, pluginArgs);
|
|
304
|
+
const actedOn = elementIdOf(commandName, args);
|
|
260
305
|
if (autowait.enabled &&
|
|
261
306
|
['click', 'setValue', 'clear'].includes(commandName) &&
|
|
262
307
|
!autowait.excludeEnabledCheck.includes(commandName) &&
|
|
263
|
-
typeof
|
|
264
|
-
!
|
|
265
|
-
|
|
266
|
-
yield this.waitForElementEnabled(driver, args[0], autowait);
|
|
308
|
+
typeof actedOn === 'string' &&
|
|
309
|
+
!isVirtualElementId(actedOn)) {
|
|
310
|
+
yield this.waitForElementEnabled(driver, actedOn, autowait);
|
|
267
311
|
}
|
|
268
312
|
// --- AUTOWAIT: polling find for findElement/findElements ---
|
|
269
313
|
// Wraps next() in a poll loop so transient NoSuchElement errors get
|
|
@@ -275,91 +319,58 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
275
319
|
!this.isVisualStrategy(strategy)) {
|
|
276
320
|
const response = yield this.runFindWithAutowait(next, commandName, autowait);
|
|
277
321
|
if (isHub && !!pluginArgs.enableDashboard && SessionManager_1.SESSION_MANAGER.isValidSession(sessionId)) {
|
|
278
|
-
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response
|
|
322
|
+
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response);
|
|
279
323
|
}
|
|
324
|
+
this.learnFromFind(sessionId, commandName, driver, args, response, pluginArgs);
|
|
280
325
|
return response;
|
|
281
326
|
}
|
|
282
327
|
const response = yield next();
|
|
283
328
|
if (isHub && !!pluginArgs.enableDashboard && SessionManager_1.SESSION_MANAGER.isValidSession(sessionId)) {
|
|
284
|
-
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response
|
|
329
|
+
yield this.runPostCommandHooks(sessionId, commandName, driver, args, response);
|
|
285
330
|
}
|
|
331
|
+
this.learnFromFind(sessionId, commandName, driver, args, response, pluginArgs);
|
|
286
332
|
return response;
|
|
287
333
|
}
|
|
288
334
|
catch (error) {
|
|
289
335
|
if (this.isNoSuchElementError(error) &&
|
|
290
336
|
['findElement', 'findElements'].includes(commandName) &&
|
|
291
|
-
|
|
337
|
+
// A -custom:ai-* find already looked at the screen; the tiers would
|
|
338
|
+
// only look again for the words of its description.
|
|
339
|
+
!this.isVisualStrategy(args[0]) &&
|
|
340
|
+
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
|
|
292
341
|
// §2.7 healing-tier capability gate: a session created with
|
|
293
342
|
// xe:options.healingTiers (or the xenon:options alias) restricts
|
|
294
343
|
// self-healing to those tier indices (1=Resilio, 2=Fuzzy XML, 3=OCR,
|
|
295
|
-
// 4=Visual AI, 5=LLM).
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
const
|
|
302
|
-
|
|
344
|
+
// 4=Visual AI, 5=LLM), and so keeps its screen from the AI provider.
|
|
345
|
+
// Read from the session's own driver, which Appium hands the plugin
|
|
346
|
+
// with every command. SESSION_MANAGER holds a local session only with
|
|
347
|
+
// the dashboard on or a video recorded, and through 2.14 every other
|
|
348
|
+
// session ran every tier. A value that isn't a list of tier numbers
|
|
349
|
+
// runs only the tiers that stay on this server (coerceHealingTiersCap).
|
|
350
|
+
const asked = (0, HealingOrchestrator_1.healingTiersFromCaps)(driver === null || driver === void 0 ? void 0 : driver.caps);
|
|
351
|
+
if (asked.unreadable !== undefined) {
|
|
352
|
+
this.warnUnreadableHealingTiers(driver, sessionId, asked.unreadable);
|
|
353
|
+
}
|
|
354
|
+
const healed = yield typedi_1.Container.get(HealingOrchestrator_1.HealingOrchestrator).attemptHealing(sessionId, driver, args[0], args[1], asked.tiers);
|
|
303
355
|
if (healed) {
|
|
304
|
-
yield this.
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
//
|
|
356
|
+
yield this.recordHeal(sessionId, commandName, driver, args, healed);
|
|
357
|
+
// The OCR and Visual AI tiers can find only a position. The test gets
|
|
358
|
+
// a virtual element there, which its own click taps; nothing acts on
|
|
359
|
+
// the screen during the find. Through 2.14 the interceptor tapped
|
|
360
|
+
// the spot here, so the test's click tapped it a second time. On an
|
|
361
|
+
// iPhone it first asked for the first element covering the spot in
|
|
362
|
+
// tree order, which is an outer container such as the window.
|
|
308
363
|
if (healed.id.startsWith('healed_') && healed.rect) {
|
|
309
|
-
|
|
310
|
-
typedi_1.Container.get(OmniVisionService_1.OmniVisionService).addVirtualElement({
|
|
364
|
+
typedi_1.Container.get(OmniVisionService_1.OmniVisionService).remember(sessionId, {
|
|
311
365
|
id: healed.id,
|
|
312
366
|
rect: healed.rect,
|
|
313
367
|
confidence: healed.confidence,
|
|
314
|
-
text: healed.
|
|
368
|
+
text: healed.text,
|
|
315
369
|
});
|
|
316
|
-
this.log.info(`[Interceptor] Visual healing returned coordinates. Resolving real element at (${healed.rect.x}, ${healed.rect.y})...`);
|
|
317
|
-
let resolved = false;
|
|
318
|
-
// Strategy 1: Try to find element at the center of the detected area
|
|
319
|
-
try {
|
|
320
|
-
const cx = Math.round(healed.rect.x + healed.rect.width / 2);
|
|
321
|
-
const cy = Math.round(healed.rect.y + healed.rect.height / 2);
|
|
322
|
-
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}\`]`);
|
|
323
|
-
if (touchEl) {
|
|
324
|
-
finalId =
|
|
325
|
-
touchEl.ELEMENT || touchEl['element-6066-11e4-a52e-4f735466cecf'] || finalId;
|
|
326
|
-
resolved = !!finalId && !finalId.startsWith('healed_');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (e) {
|
|
330
|
-
// Strategy 1 failed
|
|
331
|
-
}
|
|
332
|
-
// Strategy 2: Use coordinate tap action (W3C Actions API)
|
|
333
|
-
if (!resolved) {
|
|
334
|
-
this.log.info('[Interceptor] Falling back to coordinate-based tap for visual healing');
|
|
335
|
-
try {
|
|
336
|
-
const cx = Math.round(healed.rect.x + healed.rect.width / 2);
|
|
337
|
-
const cy = Math.round(healed.rect.y + healed.rect.height / 2);
|
|
338
|
-
yield driver.performActions([
|
|
339
|
-
{
|
|
340
|
-
type: 'pointer',
|
|
341
|
-
id: 'xenon-heal-tap',
|
|
342
|
-
parameters: { pointerType: 'touch' },
|
|
343
|
-
actions: [
|
|
344
|
-
{ type: 'pointerMove', duration: 0, x: cx, y: cy },
|
|
345
|
-
{ type: 'pointerDown', button: 0 },
|
|
346
|
-
{ type: 'pause', duration: 100 },
|
|
347
|
-
{ type: 'pointerUp', button: 0 },
|
|
348
|
-
],
|
|
349
|
-
},
|
|
350
|
-
]);
|
|
351
|
-
yield driver.releaseActions();
|
|
352
|
-
this.log.info(`[Interceptor] ✅ Visual healing: tapped at (${cx}, ${cy})`);
|
|
353
|
-
// Return the virtual ID — the tap already happened
|
|
354
|
-
}
|
|
355
|
-
catch (tapErr) {
|
|
356
|
-
this.log.error(`[Interceptor] Coordinate tap failed: ${tapErr.message}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
370
|
}
|
|
360
371
|
const elementResponse = {
|
|
361
|
-
ELEMENT:
|
|
362
|
-
|
|
372
|
+
ELEMENT: healed.id,
|
|
373
|
+
[W3C_ELEMENT_KEY]: healed.id,
|
|
363
374
|
};
|
|
364
375
|
return commandName === 'findElement' ? elementResponse : [elementResponse];
|
|
365
376
|
}
|
|
@@ -380,6 +391,16 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
380
391
|
}
|
|
381
392
|
});
|
|
382
393
|
}
|
|
394
|
+
warnUnreadableHealingTiers(driver, sessionId, unreadable) {
|
|
395
|
+
if (driver && typeof driver === 'object') {
|
|
396
|
+
if (this.warnedHealingTiers.has(driver))
|
|
397
|
+
return;
|
|
398
|
+
this.warnedHealingTiers.add(driver);
|
|
399
|
+
}
|
|
400
|
+
this.log.warn(`Session ${sessionId}: ${unreadable}. It heals with tiers 1, 2 and 3 only (Resilio, ` +
|
|
401
|
+
'Fuzzy XML, OCR), which run on this server: healing never sends its screen to the AI ' +
|
|
402
|
+
'provider.');
|
|
403
|
+
}
|
|
383
404
|
isNoSuchElementError(error) {
|
|
384
405
|
var _a;
|
|
385
406
|
return (error.name === 'NoSuchElementError' ||
|
|
@@ -445,7 +466,8 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
445
466
|
}
|
|
446
467
|
});
|
|
447
468
|
}
|
|
448
|
-
|
|
469
|
+
/** The dashboard's record of a command: only where enableDashboard records the session. */
|
|
470
|
+
runPostCommandHooks(sessionId, commandName, driver, args, response) {
|
|
449
471
|
return __awaiter(this, void 0, void 0, function* () {
|
|
450
472
|
try {
|
|
451
473
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, commandName, driver, {
|
|
@@ -454,17 +476,38 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
454
476
|
path: `/${commandName}`,
|
|
455
477
|
originalUrl: `/${commandName}`,
|
|
456
478
|
}, {}, JSON.stringify({ value: response, sessionId }));
|
|
457
|
-
if (commandName === 'findElement' &&
|
|
458
|
-
response &&
|
|
459
|
-
pluginArgs.enableSelfHealing !== false) {
|
|
460
|
-
this.triggerLearning(driver, args, response, sessionId);
|
|
461
|
-
}
|
|
462
479
|
}
|
|
463
480
|
catch (postCommandErr) {
|
|
464
481
|
this.log.warn(`[Interceptor] Post-command hooks failed: ${postCommandErr.message}`);
|
|
465
482
|
}
|
|
466
483
|
});
|
|
467
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Learn the fingerprint of a selector a findElement found, which the Resilio
|
|
487
|
+
* and Fuzzy XML tiers heal it with later. On every session this server
|
|
488
|
+
* drives while self-healing is on, whatever enableDashboard says, nodes
|
|
489
|
+
* included. Through 2.14 it ran only behind the dashboard's record above,
|
|
490
|
+
* so a server with enableDashboard off, and every node, learnt nothing.
|
|
491
|
+
* A session that turned its own healing off (`healingTiers: []`) isn't
|
|
492
|
+
* learnt from, as the switch stops learning for every session.
|
|
493
|
+
* It reads the element in the background; the find has already answered.
|
|
494
|
+
*/
|
|
495
|
+
learnFromFind(sessionId, commandName, driver, args, response, pluginArgs) {
|
|
496
|
+
var _a, _b;
|
|
497
|
+
// The find has its element: nothing here may fail it.
|
|
498
|
+
try {
|
|
499
|
+
if (commandName !== 'findElement' ||
|
|
500
|
+
!response ||
|
|
501
|
+
!typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs) ||
|
|
502
|
+
((_a = (0, HealingOrchestrator_1.healingTiersFromCaps)(driver === null || driver === void 0 ? void 0 : driver.caps).tiers) === null || _a === void 0 ? void 0 : _a.length) === 0) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
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}`); });
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
this.log.debug(`[Learning] Skipped: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
468
511
|
handleOmniVisionSearch(sessionId, driver, commandName, strategy, selector) {
|
|
469
512
|
return __awaiter(this, void 0, void 0, function* () {
|
|
470
513
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
@@ -476,41 +519,70 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
476
519
|
if (match)
|
|
477
520
|
results = [match];
|
|
478
521
|
}
|
|
522
|
+
// The session's test gets these ids, so the session keeps the elements.
|
|
523
|
+
for (const element of results)
|
|
524
|
+
omniService.remember(sessionId, element);
|
|
479
525
|
const appiumResults = results.map((r) => ({
|
|
480
526
|
ELEMENT: r.id,
|
|
481
527
|
'element-6066-11e4-a52e-4f735466cecf': r.id,
|
|
482
528
|
}));
|
|
483
529
|
if (commandName === 'findElement') {
|
|
484
|
-
if (appiumResults.length === 0)
|
|
485
|
-
|
|
530
|
+
if (appiumResults.length === 0) {
|
|
531
|
+
const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
|
|
532
|
+
throw new errors.NoSuchElementError(`Xenon found nothing on the screen matching ${strategy} "${selector}".`);
|
|
533
|
+
}
|
|
486
534
|
return appiumResults[0];
|
|
487
535
|
}
|
|
488
536
|
return appiumResults;
|
|
489
537
|
});
|
|
490
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* The virtual element a command is about, or null. A command the
|
|
541
|
+
* interceptor answers for one is recognised by its element id; any other
|
|
542
|
+
* command naming a virtual element Xenon holds is caught too, so it is
|
|
543
|
+
* refused rather than sent to a driver that doesn't know the id.
|
|
544
|
+
*/
|
|
545
|
+
virtualElementIn(commandName, args, sessionId) {
|
|
546
|
+
if (VIRTUAL_ELEMENT_COMMANDS.includes(commandName)) {
|
|
547
|
+
const id = elementIdOf(commandName, args);
|
|
548
|
+
return isVirtualElementId(id) ? id : null;
|
|
549
|
+
}
|
|
550
|
+
const omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
551
|
+
const named = args.find((a) => isVirtualElementId(a) && omni.getVirtualElement(a, sessionId));
|
|
552
|
+
return named !== null && named !== void 0 ? named : null;
|
|
553
|
+
}
|
|
554
|
+
tapAt(driver, x, y) {
|
|
555
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
556
|
+
yield driver.performActions([
|
|
557
|
+
{
|
|
558
|
+
type: 'pointer',
|
|
559
|
+
id: 'finger1',
|
|
560
|
+
parameters: { pointerType: 'touch' },
|
|
561
|
+
actions: [
|
|
562
|
+
{ type: 'pointerMove', duration: 0, x, y },
|
|
563
|
+
{ type: 'pointerDown', button: 0 },
|
|
564
|
+
{ type: 'pause', duration: 100 },
|
|
565
|
+
{ type: 'pointerUp', button: 0 },
|
|
566
|
+
],
|
|
567
|
+
},
|
|
568
|
+
]);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
491
571
|
handleVirtualElementCommand(sessionId, driver, commandName, elementId, value) {
|
|
492
572
|
return __awaiter(this, void 0, void 0, function* () {
|
|
573
|
+
var _a;
|
|
574
|
+
const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
|
|
493
575
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
576
|
+
// Another session's element is unknown here, as one that has gone is.
|
|
577
|
+
const element = omniService.getVirtualElement(elementId, sessionId);
|
|
578
|
+
if (!element) {
|
|
579
|
+
throw new errors.NoSuchElementError(`Xenon has no element ${elementId}.`);
|
|
580
|
+
}
|
|
497
581
|
const centerX = Math.round(element.rect.x + element.rect.width / 2);
|
|
498
582
|
const centerY = Math.round(element.rect.y + element.rect.height / 2);
|
|
499
583
|
switch (commandName) {
|
|
500
584
|
case 'click':
|
|
501
|
-
yield
|
|
502
|
-
{
|
|
503
|
-
type: 'pointer',
|
|
504
|
-
id: 'finger1',
|
|
505
|
-
parameters: { pointerType: 'touch' },
|
|
506
|
-
actions: [
|
|
507
|
-
{ type: 'pointerMove', duration: 0, x: centerX, y: centerY },
|
|
508
|
-
{ type: 'pointerDown', button: 0 },
|
|
509
|
-
{ type: 'pause', duration: 100 },
|
|
510
|
-
{ type: 'pointerUp', button: 0 },
|
|
511
|
-
],
|
|
512
|
-
},
|
|
513
|
-
]);
|
|
585
|
+
yield this.tapAt(driver, centerX, centerY);
|
|
514
586
|
return null;
|
|
515
587
|
case 'getElementRect':
|
|
516
588
|
return element.rect;
|
|
@@ -522,33 +594,50 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
522
594
|
case 'elementEnabled':
|
|
523
595
|
return true;
|
|
524
596
|
case 'getText':
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
597
|
+
// The text OCR read there. An element Visual AI found has none: it
|
|
598
|
+
// used to answer '' (or, healed, Xenon's note about the match).
|
|
599
|
+
if (typeof element.text === 'string')
|
|
600
|
+
return element.text;
|
|
601
|
+
throw new errors.UnsupportedOperationError(`Element ${elementId} was found by AI vision, which reads no text, so Xenon has no text for it.`);
|
|
602
|
+
case 'setValue': {
|
|
603
|
+
// The driver doesn't know this element, so the text goes to the field
|
|
604
|
+
// the tap gives the keyboard focus. A driver that can't say which
|
|
605
|
+
// field that is gets no tap at all.
|
|
606
|
+
if (typeof driver.active !== 'function') {
|
|
607
|
+
throw new errors.UnsupportedOperationError(`Xenon can't type into ${elementId}, an element it found in a screenshot: ` +
|
|
608
|
+
"this driver can't tell which field has the keyboard focus. Tap the element, then type with key actions.");
|
|
609
|
+
}
|
|
610
|
+
yield this.tapAt(driver, centerX, centerY);
|
|
611
|
+
let focused = null;
|
|
540
612
|
try {
|
|
541
|
-
|
|
613
|
+
focused = yield driver.active();
|
|
614
|
+
}
|
|
615
|
+
catch (_b) {
|
|
616
|
+
focused = null;
|
|
617
|
+
}
|
|
618
|
+
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;
|
|
619
|
+
if (typeof focusedId !== 'string' || !focusedId) {
|
|
620
|
+
throw new errors.ElementNotInteractableError(`Xenon tapped ${elementId} at (${centerX}, ${centerY}), but no field took the keyboard focus, so there was nowhere to type.`);
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
return yield driver.setValue(value, focusedId);
|
|
542
624
|
}
|
|
543
625
|
catch (e) {
|
|
544
626
|
this.log.error(`setValue failed for virtual element ${elementId} on session ${sessionId}: ${e.message}`);
|
|
545
627
|
throw e;
|
|
546
628
|
}
|
|
629
|
+
}
|
|
547
630
|
default:
|
|
548
|
-
throw new
|
|
631
|
+
throw new errors.UnsupportedOperationError(`${commandName} isn't available on ${elementId}: Xenon found it in a screenshot, and ` +
|
|
632
|
+
`it is only a position on the screen. It answers ${VIRTUAL_ELEMENT_COMMANDS.join(', ')}.`);
|
|
549
633
|
}
|
|
550
634
|
});
|
|
551
635
|
}
|
|
636
|
+
rememberRelearnt(selector) {
|
|
637
|
+
if (this.relearnt.size >= 10000)
|
|
638
|
+
this.relearnt.clear();
|
|
639
|
+
this.relearnt.add(selector);
|
|
640
|
+
}
|
|
552
641
|
triggerLearning(driver, args, response, sessionId) {
|
|
553
642
|
return __awaiter(this, void 0, void 0, function* () {
|
|
554
643
|
if (this.learningSessions.has(sessionId))
|
|
@@ -562,31 +651,34 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
562
651
|
return;
|
|
563
652
|
}
|
|
564
653
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
654
|
+
var _a;
|
|
565
655
|
try {
|
|
566
656
|
const etalonService = typedi_1.Container.get(HealEtalonService_1.HealEtalonService);
|
|
567
657
|
// CRITICAL PERFORMANCE OPTIMIZATION:
|
|
568
658
|
// Only trigger the heavy metadata collection if we don't already have an etalon for this selector.
|
|
569
659
|
// Collecting page source and element rects for every single action is too CPU-intensive.
|
|
660
|
+
// A fingerprint stored without a usable path, or without anything
|
|
661
|
+
// that says which element it is (all learnt ones through 2.14), is
|
|
662
|
+
// learnt again, once per selector per process, so the Resilio tier
|
|
663
|
+
// gets its path and Fuzzy XML can use it.
|
|
570
664
|
const existing = yield etalonService.getSignature(selector);
|
|
571
|
-
|
|
665
|
+
const complete = existing && (0, resilioPath_1.isResilioPath)(existing.path) && (0, fingerprintIdentity_1.hasIdentity)(existing.attributes);
|
|
666
|
+
if (existing && (complete || this.relearnt.has(selector))) {
|
|
572
667
|
this.log.debug(`[Learning] Etalon already exists for selector: ${selector}. Skipping...`);
|
|
573
668
|
return;
|
|
574
669
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
'resource-id',
|
|
578
|
-
'text',
|
|
579
|
-
'name',
|
|
580
|
-
'id',
|
|
581
|
-
'hint',
|
|
582
|
-
'label',
|
|
583
|
-
'value', // iOS-specific identity attributes
|
|
584
|
-
];
|
|
670
|
+
if (existing)
|
|
671
|
+
this.rememberRelearnt(selector);
|
|
585
672
|
const nodeAttrs = [];
|
|
586
|
-
|
|
673
|
+
// The WebDriver command is getAttribute(name, elementId). Through
|
|
674
|
+
// 2.14 this asked for getElementAttribute, which neither UiAutomator2
|
|
675
|
+
// nor XCUITest has, so no attribute was ever read. Only the attributes
|
|
676
|
+
// the driver has are asked for, and "null" (UiAutomator2's answer for
|
|
677
|
+
// one the element hasn't set) isn't kept.
|
|
678
|
+
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)) {
|
|
587
679
|
try {
|
|
588
|
-
const val = yield driver.
|
|
589
|
-
if (val)
|
|
680
|
+
const val = yield driver.getAttribute(attr, elementId);
|
|
681
|
+
if ((0, fingerprintIdentity_1.meaningful)(val))
|
|
590
682
|
nodeAttrs.push({ name: attr, value: val });
|
|
591
683
|
}
|
|
592
684
|
catch (e) {
|
|
@@ -631,22 +723,14 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
631
723
|
// Final safety check to ensure nodeName is a valid string
|
|
632
724
|
if (!nodeName)
|
|
633
725
|
nodeName = 'Unknown';
|
|
634
|
-
//
|
|
726
|
+
// The element's path through the page source, for the Resilio tier:
|
|
727
|
+
// the element there that has the most of the attributes just read,
|
|
728
|
+
// and none when two have as many.
|
|
635
729
|
let resiliotreePathJson = null;
|
|
636
730
|
try {
|
|
637
|
-
const { JSDOMParser, Path } = yield Promise.resolve().then(() => __importStar(require('resiliotree')));
|
|
638
731
|
const pageSource = yield driver.getPageSource();
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
if (foundNode) {
|
|
642
|
-
const pathNodes = [];
|
|
643
|
-
let curr = foundNode;
|
|
644
|
-
while (curr) {
|
|
645
|
-
pathNodes.unshift(curr);
|
|
646
|
-
curr = curr.parent;
|
|
647
|
-
}
|
|
648
|
-
resiliotreePathJson = new Path(pathNodes).toJSON();
|
|
649
|
-
}
|
|
732
|
+
const element = (0, resilioPath_1.findLearntElement)(pageSource, nodeName, nodeAttrs);
|
|
733
|
+
resiliotreePathJson = element ? (0, resilioPath_1.resilioPathOf)(element, pageSource) : null;
|
|
650
734
|
}
|
|
651
735
|
catch (e) {
|
|
652
736
|
// Silently ignore: path capture is optional for learning
|
|
@@ -662,43 +746,42 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
662
746
|
}))();
|
|
663
747
|
});
|
|
664
748
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (matchCount > 0)
|
|
679
|
-
return node;
|
|
749
|
+
/**
|
|
750
|
+
* A heal of a command a hub sent goes back to the hub, which owns the
|
|
751
|
+
* session's record (healReport.ts); this server records any other.
|
|
752
|
+
*/
|
|
753
|
+
recordHeal(sessionId, commandName, driver, args, healed) {
|
|
754
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
755
|
+
const reported = (0, healReport_1.reportHeal)(this.healReportOf(args, healed));
|
|
756
|
+
if (reported === 'reported')
|
|
757
|
+
return;
|
|
758
|
+
if (reported === 'too-long') {
|
|
759
|
+
// The session is the hub's: this server has no record to write it to.
|
|
760
|
+
this.log.warn(`[Interceptor] The heal of ${args[0]}=${args[1]} is too long to send to the hub; it isn't recorded.`);
|
|
761
|
+
return;
|
|
680
762
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
763
|
+
yield this.logHealingEvent(sessionId, commandName, driver, args, healed);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
healReportOf(args, healed) {
|
|
767
|
+
var _a;
|
|
768
|
+
return {
|
|
769
|
+
originalSelector: args[1],
|
|
770
|
+
originalStrategy: args[0],
|
|
771
|
+
healedSelector: healed.recommendedSelector,
|
|
772
|
+
healedStrategy: (_a = healed.recommendedStrategy) !== null && _a !== void 0 ? _a : args[0],
|
|
773
|
+
confidence: healed.confidence,
|
|
774
|
+
tier: healed.tier,
|
|
775
|
+
};
|
|
685
776
|
}
|
|
686
777
|
logHealingEvent(sessionId, commandName, driver, args, healed) {
|
|
687
778
|
return __awaiter(this, void 0, void 0, function* () {
|
|
688
|
-
var _a;
|
|
689
779
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, commandName, driver, {
|
|
690
780
|
body: args,
|
|
691
781
|
method: 'POST',
|
|
692
782
|
path: `/${commandName}`,
|
|
693
783
|
originalUrl: `/${commandName}`,
|
|
694
|
-
}, {}, JSON.stringify({ value: { ELEMENT: healed.id }, sessionId }),
|
|
695
|
-
originalSelector: args[1],
|
|
696
|
-
originalStrategy: args[0],
|
|
697
|
-
healedSelector: healed.recommendedSelector,
|
|
698
|
-
healedStrategy: (_a = healed.recommendedStrategy) !== null && _a !== void 0 ? _a : args[0],
|
|
699
|
-
confidence: healed.confidence,
|
|
700
|
-
tier: healed.tier,
|
|
701
|
-
});
|
|
784
|
+
}, {}, JSON.stringify({ value: { ELEMENT: healed.id }, sessionId }), this.healReportOf(args, healed));
|
|
702
785
|
});
|
|
703
786
|
}
|
|
704
787
|
};
|