@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
|
@@ -20,6 +20,9 @@ const xpath_1 = require("xpath");
|
|
|
20
20
|
const typedi_1 = require("typedi");
|
|
21
21
|
const logger_1 = __importDefault(require("../../logger"));
|
|
22
22
|
const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
|
|
23
|
+
const fingerprintIdentity_1 = require("./fingerprintIdentity");
|
|
24
|
+
/** What an element says in words, for telling it apart once its identifier differs. */
|
|
25
|
+
const TEXTS = ['text', 'label', 'content-desc'];
|
|
23
26
|
class FuzzyXmlHealingProvider {
|
|
24
27
|
constructor(etalonService) {
|
|
25
28
|
this.etalonService = etalonService;
|
|
@@ -41,6 +44,12 @@ class FuzzyXmlHealingProvider {
|
|
|
41
44
|
if (this.etalonService) {
|
|
42
45
|
etalon = yield this.etalonService.getSignature(context.selector);
|
|
43
46
|
}
|
|
47
|
+
// A fingerprint with only a rect and a type (every learnt one through
|
|
48
|
+
// 2.14) would heal to whatever element of that type is at that spot.
|
|
49
|
+
if (etalon && !(0, fingerprintIdentity_1.hasIdentity)(etalon.attributes)) {
|
|
50
|
+
this.logger.info('The baseline signature says nothing about which element it is; matching without it.');
|
|
51
|
+
etalon = null;
|
|
52
|
+
}
|
|
44
53
|
const keywords = this.extractKeywords(context.selector);
|
|
45
54
|
if (keywords.length === 0 && !etalon)
|
|
46
55
|
return null;
|
|
@@ -135,6 +144,9 @@ class FuzzyXmlHealingProvider {
|
|
|
135
144
|
if (etalonTag !== nodeTag && etalonTag !== 'xcuielementtypeany' && etalonTag !== 'unknown') {
|
|
136
145
|
return 0; // Immediate rejection — wrong element type
|
|
137
146
|
}
|
|
147
|
+
// Position can't heal to an element whose identifier says it's another one.
|
|
148
|
+
if (this.contradictsIdentifier(node, etalon))
|
|
149
|
+
return 0;
|
|
138
150
|
// Tag matches perfectly
|
|
139
151
|
totalWeight += 1.0;
|
|
140
152
|
totalScore += 1.0;
|
|
@@ -260,6 +272,45 @@ class FuzzyXmlHealingProvider {
|
|
|
260
272
|
}
|
|
261
273
|
return finalScore;
|
|
262
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Whether `node` is, by its identifier, another element than the
|
|
277
|
+
* fingerprint's: the fingerprint has an identifier (an Android resource-id,
|
|
278
|
+
* compared without its package, or an iOS accessibility identifier), `node`
|
|
279
|
+
* has another one (sameIdentifier), and no text of it (text, label,
|
|
280
|
+
* content-desc) reads the same as the fingerprint's (sameText). Its
|
|
281
|
+
* position then counts for nothing.
|
|
282
|
+
*
|
|
283
|
+
* A renamed id that kept its text or description still heals, and so does
|
|
284
|
+
* a renamed text that kept its id. An element without an identifier, where
|
|
285
|
+
* the fingerprint has one, is judged by its text alone: an Android element
|
|
286
|
+
* with no resource-id, or an iOS one, whose name is then its label. Only
|
|
287
|
+
* where neither has one does position decide, as through 2.14.
|
|
288
|
+
* What this stops is the button that took the old one's place: "Cancel
|
|
289
|
+
* order" (cancel) where "Pay now" (pay) was, "Log out" where "Log in" was.
|
|
290
|
+
*/
|
|
291
|
+
contradictsIdentifier(node, etalon) {
|
|
292
|
+
const identifier = (0, fingerprintIdentity_1.stableIdentifierOf)(etalon.attributes);
|
|
293
|
+
if (!identifier)
|
|
294
|
+
return false;
|
|
295
|
+
// Another identifier, or none where the fingerprint has one, and the
|
|
296
|
+
// text decides. iOS gives an element without one its label as name.
|
|
297
|
+
const theirs = this.getAttrValue(node, identifier.attribute);
|
|
298
|
+
if ((0, fingerprintIdentity_1.meaningful)(theirs)) {
|
|
299
|
+
const [mine, other] = identifier.attribute === 'resource-id'
|
|
300
|
+
? [(0, fingerprintIdentity_1.resourceIdName)(identifier.value), (0, fingerprintIdentity_1.resourceIdName)(theirs)]
|
|
301
|
+
: [identifier.value, theirs];
|
|
302
|
+
if ((0, fingerprintIdentity_1.sameIdentifier)(mine, other))
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
const texts = TEXTS.map((name) => etalon.attributes[name]).filter(fingerprintIdentity_1.meaningful);
|
|
306
|
+
if (texts.length === 0)
|
|
307
|
+
return true;
|
|
308
|
+
const nodeTexts = [
|
|
309
|
+
node.textContent,
|
|
310
|
+
...TEXTS.map((name) => this.getAttrValue(node, name)),
|
|
311
|
+
].filter(fingerprintIdentity_1.meaningful);
|
|
312
|
+
return !nodeTexts.some((t) => texts.some((w) => (0, fingerprintIdentity_1.sameText)(t, w)));
|
|
313
|
+
}
|
|
263
314
|
getAttrValue(node, name) {
|
|
264
315
|
if (typeof node.getAttribute === 'function') {
|
|
265
316
|
const val = node.getAttribute(name);
|
|
@@ -37,7 +37,9 @@ let HealEtalonService = class HealEtalonService {
|
|
|
37
37
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
38
|
try {
|
|
39
39
|
const attributes = {};
|
|
40
|
-
// Extract identifying attributes
|
|
40
|
+
// Extract identifying attributes. Not `value`: on a text field it is
|
|
41
|
+
// what the test typed, an email or a name, which says nothing about
|
|
42
|
+
// which field it is.
|
|
41
43
|
const nodeAttrs = node.attributes || [];
|
|
42
44
|
const anchorNames = [
|
|
43
45
|
'content-desc',
|
|
@@ -48,7 +50,6 @@ let HealEtalonService = class HealEtalonService {
|
|
|
48
50
|
'hint',
|
|
49
51
|
'label',
|
|
50
52
|
'accessibility-id',
|
|
51
|
-
'value',
|
|
52
53
|
'x',
|
|
53
54
|
'y',
|
|
54
55
|
'width',
|
|
@@ -1,43 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
7
|
};
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
10
|
};
|
|
@@ -54,7 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
54
21
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
55
22
|
};
|
|
56
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
-
exports.HealingOrchestrator = void 0;
|
|
24
|
+
exports.HealingOrchestrator = exports.LOCAL_HEALING_TIERS = void 0;
|
|
58
25
|
exports.filterProvidersByTier = filterProvidersByTier;
|
|
59
26
|
exports.coerceHealingTiersCap = coerceHealingTiersCap;
|
|
60
27
|
exports.healingTiersFromCaps = healingTiersFromCaps;
|
|
@@ -67,9 +34,11 @@ const VisualAiHealingProvider_1 = require("./VisualAiHealingProvider");
|
|
|
67
34
|
const LlmHealingProvider_1 = require("./LlmHealingProvider");
|
|
68
35
|
const HealEtalonService_1 = require("./HealEtalonService");
|
|
69
36
|
const ResilioTreeHealingProvider_1 = require("./ResilioTreeHealingProvider");
|
|
37
|
+
const resilioPath_1 = require("./resilioPath");
|
|
70
38
|
const HealingMetrics_1 = require("./HealingMetrics");
|
|
71
39
|
const attributes_1 = require("../telemetry/attributes");
|
|
72
40
|
const xenonOptions_1 = require("../session/xenonOptions");
|
|
41
|
+
const screenScale_1 = require("../omni-vision/screenScale");
|
|
73
42
|
// §2.7 healing-tier capability gate. The tier numbering here is the
|
|
74
43
|
// externally-facing capability contract (xe:options.healingTiers),
|
|
75
44
|
// which is array-position based: providers[i] is tier i+1 (1=Resilio,
|
|
@@ -85,24 +54,46 @@ function filterProvidersByTier(providers, allowedTiers) {
|
|
|
85
54
|
const allowed = new Set(allowedTiers);
|
|
86
55
|
return providers.filter((_, index) => allowed.has(index + 1));
|
|
87
56
|
}
|
|
57
|
+
// The tiers that run on this server: Resilio, Fuzzy XML and OCR. Visual AI (4)
|
|
58
|
+
// and the LLM (5) send the screenshot and the page source to the AI provider.
|
|
59
|
+
exports.LOCAL_HEALING_TIERS = [1, 2, 3];
|
|
60
|
+
const isHealingTierList = (raw) => Array.isArray(raw) && raw.every((t) => Number.isInteger(t) && t >= 1 && t <= 5);
|
|
88
61
|
// Coerce the raw xe:options.healingTiers capability value into the
|
|
89
|
-
// allowedTiers argument for attemptHealing.
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// opting out of healing entirely is deliberately NOT supported.
|
|
62
|
+
// allowedTiers argument for attemptHealing. The value is a privacy control (a
|
|
63
|
+
// session leaves out 4 and 5 to keep its screen away from the AI provider), so
|
|
64
|
+
// one that can't be read fails closed for the AI tiers and open for the rest:
|
|
65
|
+
// - not set (or null): every tier (undefined);
|
|
66
|
+
// - a list of tier numbers 1 to 5: exactly those, and [] none;
|
|
67
|
+
// - anything else ("1,2", ["1","2"], [1, 6], ...): LOCAL_HEALING_TIERS.
|
|
68
|
+
// Through 2.14 anything else, and [], ran every tier, the AI ones included.
|
|
97
69
|
function coerceHealingTiersCap(raw) {
|
|
98
|
-
|
|
99
|
-
|
|
70
|
+
if (raw === undefined || raw === null)
|
|
71
|
+
return undefined;
|
|
72
|
+
return isHealingTierList(raw) ? [...raw] : [...exports.LOCAL_HEALING_TIERS];
|
|
100
73
|
}
|
|
101
74
|
// The allowedTiers a session asked for, read from its capabilities (one flat
|
|
102
|
-
// map
|
|
103
|
-
// xenon:options alias's, coerced as above.
|
|
75
|
+
// map: the session's driver's caps): xe:options.healingTiers, or the
|
|
76
|
+
// xenon:options alias's, coerced as above. A namespace that isn't an object
|
|
77
|
+
// can't be read either: xenonOptionsIn skips it, which would run every tier.
|
|
78
|
+
// `unreadable` says what couldn't be read, for the log.
|
|
104
79
|
function healingTiersFromCaps(caps) {
|
|
105
|
-
|
|
80
|
+
for (const namespace of xenonOptions_1.OPTION_NAMESPACES) {
|
|
81
|
+
const bag = (0, xenonOptions_1.isOptionsObject)(caps) ? caps[namespace] : undefined;
|
|
82
|
+
if (bag !== undefined && bag !== null && !(0, xenonOptions_1.isOptionsObject)(bag)) {
|
|
83
|
+
return {
|
|
84
|
+
tiers: [...exports.LOCAL_HEALING_TIERS],
|
|
85
|
+
unreadable: `${namespace} is ${JSON.stringify(bag)}, not an object`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const raw = (0, xenonOptions_1.xenonOptionsIn)(caps).healingTiers;
|
|
90
|
+
const tiers = coerceHealingTiersCap(raw);
|
|
91
|
+
return tiers === undefined || isHealingTierList(raw)
|
|
92
|
+
? { tiers }
|
|
93
|
+
: {
|
|
94
|
+
tiers,
|
|
95
|
+
unreadable: `healingTiers is ${JSON.stringify(raw)}, not a list of tier numbers from 1 to 5`,
|
|
96
|
+
};
|
|
106
97
|
}
|
|
107
98
|
let HealingOrchestrator = class HealingOrchestrator {
|
|
108
99
|
constructor(etalonService) {
|
|
@@ -119,7 +110,15 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
119
110
|
}
|
|
120
111
|
attemptHealing(sessionId, driver, strategy, selector, allowedTiers) {
|
|
121
112
|
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
-
var _a, _b;
|
|
113
|
+
var _a, _b, _c;
|
|
114
|
+
// §2.7: when the session's xe:options.healingTiers capability is set,
|
|
115
|
+
// restrict dispatch to the allowed tier indices; absent -> unchanged.
|
|
116
|
+
const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
|
|
117
|
+
if (activeProviders.length === 0) {
|
|
118
|
+
// Nothing to run, so nothing is collected either: no screenshot taken.
|
|
119
|
+
this.logger.info(`Session ${sessionId} allows no self-healing tier; ${strategy}=${selector} is not healed.`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
123
122
|
this.logger.info(`🚨 Self-Healing triggered for session ${sessionId}. Broken locator: ${strategy}=${selector}`);
|
|
124
123
|
// Span wraps the whole attempt. Original selector text is intentionally
|
|
125
124
|
// kept off the span — high cardinality (long XPath strings) would explode
|
|
@@ -150,15 +149,15 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
150
149
|
return null;
|
|
151
150
|
}
|
|
152
151
|
// Tiered Execution: Try providers in order of cost/complexity.
|
|
153
|
-
// §2.7: when the session's xe:options.healingTiers capability is set,
|
|
154
|
-
// restrict dispatch to the allowed tier indices; absent -> unchanged.
|
|
155
|
-
const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
|
|
156
152
|
for (const provider of activeProviders) {
|
|
157
153
|
const tierStart = Date.now();
|
|
158
154
|
span.addEvent('tier_started', { tier: provider.name });
|
|
159
155
|
try {
|
|
160
156
|
this.logger.info(`Attempting Tier ${provider.tier}: ${provider.name}...`);
|
|
161
|
-
|
|
157
|
+
let result = yield provider.heal(context);
|
|
158
|
+
if (result === null || result === void 0 ? void 0 : result.rect) {
|
|
159
|
+
result = yield this.rectInDriverCoordinates(result, result.rect, context, provider);
|
|
160
|
+
}
|
|
162
161
|
HealingMetrics_1.HEALING_METRICS.record(provider.tier, provider.name, result ? 'success' : 'failure', Date.now() - tierStart);
|
|
163
162
|
if (result) {
|
|
164
163
|
this.logger.info(`✨ Provider ${provider.name} found a match! Confidence: ${(result.confidence * 100).toFixed(0)}%`);
|
|
@@ -199,23 +198,11 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
199
198
|
else if (result.confidence > 0.7 && result.node) {
|
|
200
199
|
try {
|
|
201
200
|
this.logger.info(`🧠 Learning from healing success: updating etalon for ${selector}`);
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const pathNodes = [];
|
|
208
|
-
let curr = result.node;
|
|
209
|
-
while (curr) {
|
|
210
|
-
pathNodes.unshift(curr);
|
|
211
|
-
curr = curr.parent || curr.parentNode;
|
|
212
|
-
}
|
|
213
|
-
learnedPath = new Path(pathNodes).toJSON();
|
|
214
|
-
}
|
|
215
|
-
catch (_c) {
|
|
216
|
-
// resiliotree import is best-effort; learnedPath stays null
|
|
217
|
-
}
|
|
218
|
-
}
|
|
201
|
+
// The healed element's path through the page source, for the
|
|
202
|
+
// Resilio tier. Through 2.14 this built it from the healed node
|
|
203
|
+
// with resiliotree, which an xmldom element (every node a tier
|
|
204
|
+
// returns) can't be, so it was always null.
|
|
205
|
+
const learnedPath = (0, resilioPath_1.resilioPathOf)(result.node, (_a = context.pageSource) !== null && _a !== void 0 ? _a : '');
|
|
219
206
|
yield this.etalonService.saveSignature(strategy, selector, result.node, learnedPath);
|
|
220
207
|
}
|
|
221
208
|
catch (learnErr) {
|
|
@@ -231,7 +218,7 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
231
218
|
span.setAttributes({
|
|
232
219
|
[attributes_1.ATTR.HEALING_TIER]: provider.name,
|
|
233
220
|
[attributes_1.ATTR.HEALING_CONFIDENCE]: result.confidence,
|
|
234
|
-
[attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (
|
|
221
|
+
[attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (_b = result.recommendedStrategy) !== null && _b !== void 0 ? _b : 'xpath',
|
|
235
222
|
[attributes_1.ATTR.HEALING_DURATION_MS]: Date.now() - attemptStart,
|
|
236
223
|
});
|
|
237
224
|
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
@@ -249,7 +236,7 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
249
236
|
// tier can plausibly succeed (e.g. missing prerequisites that all
|
|
250
237
|
// share). Saves an expensive LLM round-trip when upstream context
|
|
251
238
|
// collection failed.
|
|
252
|
-
if ((
|
|
239
|
+
if ((_c = provider.shouldSkipRemaining) === null || _c === void 0 ? void 0 : _c.call(provider, context)) {
|
|
253
240
|
this.logger.warn(`Tier ${provider.tier} (${provider.name}) advised skipping remaining tiers for selector=${selector}`);
|
|
254
241
|
HealingMetrics_1.HEALING_METRICS.recordSkippedRemaining(provider.tier, provider.name);
|
|
255
242
|
span.addEvent('tier_skipped_remaining', { tier: provider.name });
|
|
@@ -267,6 +254,33 @@ let HealingOrchestrator = class HealingOrchestrator {
|
|
|
267
254
|
return null;
|
|
268
255
|
});
|
|
269
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* The OCR and Visual AI tiers find the element in the screenshot, so their
|
|
259
|
+
* `rect` is in its pixels. The interceptor returns a virtual element there,
|
|
260
|
+
* which a click taps in the driver's coordinates (points on iOS), so it is
|
|
261
|
+
* converted here. If that can't be worked out on iOS, a virtual element
|
|
262
|
+
* (only a position) is no use and the tier counts as failed; a real element
|
|
263
|
+
* the tier resolved keeps its id and loses only the rect.
|
|
264
|
+
*/
|
|
265
|
+
rectInDriverCoordinates(result, rect, context, provider) {
|
|
266
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
var _a, _b;
|
|
268
|
+
try {
|
|
269
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(context.driver, (_a = context.screenshotBase64) !== null && _a !== void 0 ? _a : '');
|
|
270
|
+
return Object.assign(Object.assign({}, result), { rect: (0, screenScale_1.toDriverRect)(rect, scale) });
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
const reason = (_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err;
|
|
274
|
+
if (!result.id.startsWith('healed_')) {
|
|
275
|
+
this.logger.warn(`${provider.name}: the element's position is unknown (${reason}).`);
|
|
276
|
+
return Object.assign(Object.assign({}, result), { rect: undefined });
|
|
277
|
+
}
|
|
278
|
+
this.logger.warn(`${provider.name} found the element in the screenshot, but not where it is on the ` +
|
|
279
|
+
`screen (${reason}); trying the next tier.`);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
270
284
|
};
|
|
271
285
|
exports.HealingOrchestrator = HealingOrchestrator;
|
|
272
286
|
exports.HealingOrchestrator = HealingOrchestrator = __decorate([
|
|
@@ -14,8 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.OcrHealingProvider = void 0;
|
|
16
16
|
const types_1 = require("./types");
|
|
17
|
-
const
|
|
17
|
+
const typedi_1 = require("typedi");
|
|
18
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
const OmniVisionService_1 = require("../omni-vision/OmniVisionService");
|
|
19
20
|
class OcrHealingProvider {
|
|
20
21
|
constructor() {
|
|
21
22
|
this.name = 'OCR Text Provider';
|
|
@@ -41,71 +42,47 @@ class OcrHealingProvider {
|
|
|
41
42
|
if (!soughtText)
|
|
42
43
|
return null;
|
|
43
44
|
this.logger.info(`Attempting OCR search for text: "${soughtText}"`);
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
rect: {
|
|
78
|
-
x: bestWord.bbox.x0,
|
|
79
|
-
y: bestWord.bbox.y0,
|
|
80
|
-
width: bestWord.bbox.x1 - bestWord.bbox.x0,
|
|
81
|
-
height: bestWord.bbox.y1 - bestWord.bbox.y0,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch (predErr) {
|
|
88
|
-
this.logger.debug(`Predicate search failed: ${predErr.message}`);
|
|
45
|
+
// Read the screenshot as Omni-Vision's `-custom:ai-text` does: the text
|
|
46
|
+
// may cover several neighbouring words ("Sign in"), and a word that is
|
|
47
|
+
// only part of it ("Log" for "Login") is no match. The first match in
|
|
48
|
+
// reading order, as `findElement` with that locator returns.
|
|
49
|
+
const [match] = yield typedi_1.Container.get(OmniVisionService_1.OmniVisionService).findTextInScreenshot(context.screenshotBase64, soughtText);
|
|
50
|
+
if (!match)
|
|
51
|
+
return null;
|
|
52
|
+
this.logger.info(`✅ OCR found text "${match.text}" at ${JSON.stringify({ x0: match.x0, y0: match.y0, x1: match.x1, y1: match.y1 })}`);
|
|
53
|
+
const found = {
|
|
54
|
+
tier: this.tier,
|
|
55
|
+
confidence: match.confidence / 100,
|
|
56
|
+
originalSelector: context.selector,
|
|
57
|
+
originalStrategy: context.strategy,
|
|
58
|
+
recommendedSelector: `ocr:text="${match.text}"`,
|
|
59
|
+
recommendedStrategy: 'xenon:visual',
|
|
60
|
+
message: `Found text "${match.text}" via local OCR (${match.confidence.toFixed(0)}% confidence)`,
|
|
61
|
+
text: match.text,
|
|
62
|
+
rect: {
|
|
63
|
+
x: match.x0,
|
|
64
|
+
y: match.y0,
|
|
65
|
+
width: match.x1 - match.x0,
|
|
66
|
+
height: match.y1 - match.y0,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
// On an iPhone, the element whose label holds the text, when there is one.
|
|
70
|
+
try {
|
|
71
|
+
const element = yield context.driver
|
|
72
|
+
.findElement('-ios predicate string', `label CONTAINS[c] "${match.text.replace(/"/g, '\\"')}"`)
|
|
73
|
+
.catch(() => null);
|
|
74
|
+
const elementId = (element === null || element === void 0 ? void 0 : element.ELEMENT) || (element === null || element === void 0 ? void 0 : element['element-6066-11e4-a52e-4f735466cecf']);
|
|
75
|
+
if (elementId) {
|
|
76
|
+
this.logger.info('🎯 OCR resolved to real element via predicate search');
|
|
77
|
+
return Object.assign({ id: elementId }, found);
|
|
89
78
|
}
|
|
90
|
-
// Fallback: Return coordinate-based result with virtual ID
|
|
91
|
-
// The CommandInterceptor handles rect-based results via coordinate tap
|
|
92
|
-
return {
|
|
93
|
-
id: `healed_ocr_${Date.now()}`,
|
|
94
|
-
tier: this.tier,
|
|
95
|
-
confidence: bestWord.confidence / 100,
|
|
96
|
-
originalSelector: context.selector,
|
|
97
|
-
originalStrategy: context.strategy,
|
|
98
|
-
recommendedSelector: `ocr:text="${bestWord.text}"`,
|
|
99
|
-
recommendedStrategy: 'xenon:visual',
|
|
100
|
-
message: `Found text "${bestWord.text}" via local OCR (${bestWord.confidence.toFixed(0)}% confidence)`,
|
|
101
|
-
rect: {
|
|
102
|
-
x: bestWord.bbox.x0,
|
|
103
|
-
y: bestWord.bbox.y0,
|
|
104
|
-
width: bestWord.bbox.x1 - bestWord.bbox.x0,
|
|
105
|
-
height: bestWord.bbox.y1 - bestWord.bbox.y0,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
79
|
}
|
|
80
|
+
catch (predErr) {
|
|
81
|
+
this.logger.debug(`Predicate search failed: ${predErr.message}`);
|
|
82
|
+
}
|
|
83
|
+
// Otherwise where the text is: the interceptor returns it as a virtual
|
|
84
|
+
// element, which a click taps.
|
|
85
|
+
return Object.assign({ id: `healed_ocr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` }, found);
|
|
109
86
|
}
|
|
110
87
|
catch (err) {
|
|
111
88
|
this.logger.error(`Error during OCR healing: ${err.message}`);
|
|
@@ -14,10 +14,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.ResilioTreeHealingProvider = void 0;
|
|
16
16
|
const types_1 = require("./types");
|
|
17
|
-
const { Path, PathFinder, JSDOMParser, LCSPathDistance, HeuristicNodeDistance, } = require('resiliotree');
|
|
18
17
|
const typedi_1 = require("typedi");
|
|
19
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
20
19
|
const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
|
|
20
|
+
const resilioPath_1 = require("./resilioPath");
|
|
21
|
+
/**
|
|
22
|
+
* Tier 0: the element at the end of the path most like the one the selector's
|
|
23
|
+
* element had when the selector last worked (resilioPath.ts). It answers only
|
|
24
|
+
* when it is sure, which is when the element kept its identity (Android's
|
|
25
|
+
* resource-id, iOS's name) and moved in the tree: a positional XPath that
|
|
26
|
+
* broke when the layout changed. Anything less is the next tier's to decide.
|
|
27
|
+
*/
|
|
21
28
|
class ResilioTreeHealingProvider {
|
|
22
29
|
constructor(etalonService) {
|
|
23
30
|
this.etalonService = etalonService;
|
|
@@ -34,44 +41,45 @@ class ResilioTreeHealingProvider {
|
|
|
34
41
|
}
|
|
35
42
|
try {
|
|
36
43
|
const signature = yield this.etalonService.getSignature(context.selector);
|
|
37
|
-
if (!signature ||
|
|
44
|
+
if (!(0, resilioPath_1.isResilioPath)(signature === null || signature === void 0 ? void 0 : signature.path)) {
|
|
38
45
|
this.logger.debug(`No ResilioTree path found for selector: ${context.selector}`);
|
|
39
46
|
return null;
|
|
40
47
|
}
|
|
41
48
|
this.logger.info(`Attempting robust ResilioTree recovery for: ${context.selector}`);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.logger.info(`ResilioTree suggested recovery XPath: ${recommendedXpath}`);
|
|
49
|
+
const nearest = (0, resilioPath_1.nearestElement)(signature.path, context.pageSource);
|
|
50
|
+
if (!nearest) {
|
|
51
|
+
this.logger.info(`ResilioTree found no element it is sure of for: ${context.selector}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if ((0, resilioPath_1.contradictsSelector)(nearest.element, context.strategy, context.selector)) {
|
|
55
|
+
this.logger.info(`ResilioTree's element doesn't have the text the selector states; leaving it to the next tier: ${context.selector}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// The first of the element's locators the driver finds, among those
|
|
59
|
+
// that select that element alone in the page source.
|
|
60
|
+
const candidates = (0, resilioPath_1.locatorsSelectingOnly)(nearest.element, this.generator.generate(nearest.element));
|
|
61
|
+
for (const candidate of candidates) {
|
|
56
62
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const found = yield context.driver.findElement('xpath', candidate);
|
|
64
|
+
const id = (found === null || found === void 0 ? void 0 : found.ELEMENT) || (found === null || found === void 0 ? void 0 : found['element-6066-11e4-a52e-4f735466cecf']);
|
|
65
|
+
if (!id)
|
|
66
|
+
continue;
|
|
67
|
+
this.logger.info(`ResilioTree recovered the element (score ${nearest.score.toFixed(2)}): ${candidate}`);
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
originalSelector: context.selector,
|
|
71
|
+
originalStrategy: context.strategy,
|
|
72
|
+
recommendedSelector: candidate,
|
|
73
|
+
recommendedStrategy: 'xpath',
|
|
74
|
+
candidateSelectors: candidates,
|
|
75
|
+
confidence: nearest.score,
|
|
76
|
+
tier: this.tier,
|
|
77
|
+
node: nearest.element,
|
|
78
|
+
message: `Recovered via ResilioTree path matching. New XPath: ${candidate}`,
|
|
79
|
+
};
|
|
72
80
|
}
|
|
73
|
-
catch (
|
|
74
|
-
this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${
|
|
81
|
+
catch (_a) {
|
|
82
|
+
this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${candidate}`);
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
}
|
|
@@ -81,22 +89,5 @@ class ResilioTreeHealingProvider {
|
|
|
81
89
|
return null;
|
|
82
90
|
});
|
|
83
91
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Generates an absolute XPath for a ResilioTree Node
|
|
86
|
-
*/
|
|
87
|
-
generateXpath(node) {
|
|
88
|
-
const parts = [];
|
|
89
|
-
let curr = node;
|
|
90
|
-
while (curr) {
|
|
91
|
-
const tag = curr.tag.toLowerCase();
|
|
92
|
-
// Skip JSDOM added wrapper tags for mobile compatibility
|
|
93
|
-
if (tag !== 'html' && tag !== 'body') {
|
|
94
|
-
const index = curr.index + 1; // XPath is 1-based
|
|
95
|
-
parts.unshift(`${curr.tag}[${index}]`);
|
|
96
|
-
}
|
|
97
|
-
curr = curr.parent;
|
|
98
|
-
}
|
|
99
|
-
return `/${parts.join('/')}`;
|
|
100
|
-
}
|
|
101
92
|
}
|
|
102
93
|
exports.ResilioTreeHealingProvider = ResilioTreeHealingProvider;
|
|
@@ -42,7 +42,7 @@ class VisualAiHealingProvider {
|
|
|
42
42
|
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
|
|
43
43
|
this.logger.info(`✅ Visual AI found element at (${coordinates.x}, ${coordinates.y})`);
|
|
44
44
|
return {
|
|
45
|
-
id: `healed_visual_${Date.now()}`,
|
|
45
|
+
id: `healed_visual_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
46
46
|
tier: this.tier,
|
|
47
47
|
confidence: 0.8, // Basic vision models don't always give confidence, assuming high if found
|
|
48
48
|
originalSelector: context.selector,
|