@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
|
@@ -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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
42
9
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
43
10
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -52,20 +19,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
52
19
|
};
|
|
53
20
|
var OmniVisionService_1;
|
|
54
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
-
exports.OmniVisionService = void 0;
|
|
22
|
+
exports.OmniVisionService = exports.MAX_VIRTUAL_ELEMENTS = exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION = void 0;
|
|
56
23
|
const typedi_1 = require("typedi");
|
|
57
|
-
const tesseract_js_1 =
|
|
24
|
+
const tesseract_js_1 = require("tesseract.js");
|
|
25
|
+
const ocrData_1 = require("../ocr/ocrData");
|
|
58
26
|
const sharp_1 = __importDefault(require("sharp"));
|
|
59
27
|
const AIService_1 = require("../AIService");
|
|
60
28
|
const logger_1 = __importDefault(require("../../logger"));
|
|
61
29
|
const crypto_1 = __importDefault(require("crypto"));
|
|
30
|
+
const ocrTextMatch_1 = require("./ocrTextMatch");
|
|
31
|
+
const screenScale_1 = require("./screenScale");
|
|
32
|
+
/**
|
|
33
|
+
* How many virtual elements Xenon keeps, for one session and in all. The
|
|
34
|
+
* oldest go first: a test acts on what it found last, and a command on an
|
|
35
|
+
* element that went answers "no such element".
|
|
36
|
+
*/
|
|
37
|
+
exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION = 1000;
|
|
38
|
+
exports.MAX_VIRTUAL_ELEMENTS = 10000;
|
|
62
39
|
// Mobile UIs have sparse text on complex backgrounds - reduce contrast to preserve readability
|
|
63
40
|
const DEFAULT_CONTRAST = 1.2; // Slightly above 1.0 for mild enhancement (was 0.8 which was too aggressive)
|
|
64
41
|
const UPSCALE_THRESHOLD = 800; // Only upscale very small images (was 1500)
|
|
65
42
|
let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
66
43
|
constructor() {
|
|
67
44
|
this.logger = logger_1.default.scope('OmniVision');
|
|
68
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The virtual elements tests may still act on, by id, each with the session
|
|
47
|
+
* that found it, oldest first. Through 2.14 every element ever found stayed
|
|
48
|
+
* here until the server restarted, and any session could act on another's.
|
|
49
|
+
*/
|
|
50
|
+
this.elements = new Map();
|
|
51
|
+
this.bySession = new Map();
|
|
69
52
|
this.sharedWorker = null;
|
|
70
53
|
this.workerBusy = false;
|
|
71
54
|
this.uiLensCache = new Map();
|
|
@@ -75,7 +58,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
75
58
|
if (this.sharedWorker)
|
|
76
59
|
return this.sharedWorker;
|
|
77
60
|
this.logger.info('Initializing persistent OCR worker with explicit configuration...');
|
|
78
|
-
const worker = yield
|
|
61
|
+
const worker = yield (0, ocrData_1.createOcrWorker)();
|
|
79
62
|
// PSM.SPARSE_TEXT is optimized for irregular text layouts like mobile UIs
|
|
80
63
|
// AUTO mode assumes document structure which fails on sparse mobile screens
|
|
81
64
|
yield worker.setParameters({
|
|
@@ -379,10 +362,32 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
379
362
|
}
|
|
380
363
|
});
|
|
381
364
|
}
|
|
365
|
+
/** The OCR words with usable boxes, in the screenshot's pixels. */
|
|
366
|
+
wordBoxes(words) {
|
|
367
|
+
return words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
|
|
368
|
+
}
|
|
369
|
+
/** Where `text` is among OCR's words, read with over 60% confidence, in reading order. */
|
|
370
|
+
textMatches(words, text) {
|
|
371
|
+
return (0, ocrTextMatch_1.findText)(this.wordBoxes(words), String(text !== null && text !== void 0 ? text : '')).filter((m) => m.confidence > 60);
|
|
372
|
+
}
|
|
382
373
|
/**
|
|
383
|
-
*
|
|
374
|
+
* Where `text` is on a screenshot, matched as `-custom:ai-text` matches it:
|
|
375
|
+
* in the screenshot's pixels, in reading order. Self-healing's OCR tier
|
|
376
|
+
* uses it. Throws when OCR fails.
|
|
384
377
|
*/
|
|
378
|
+
findTextInScreenshot(screenshotBase64, text) {
|
|
379
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
380
|
+
const { words } = yield this.performOcr(Buffer.from(screenshotBase64, 'base64'));
|
|
381
|
+
return this.textMatches(words, text);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
385
384
|
/**
|
|
385
|
+
* Proactive OCR Search: Finds elements matching text even if not in XML.
|
|
386
|
+
*
|
|
387
|
+
* The text may be several words ("Sign in"); see ocrTextMatch.ts. Each
|
|
388
|
+
* element's rect is in the driver's coordinates (points on iOS), like a
|
|
389
|
+
* real element's, so tapping it lands on the text.
|
|
390
|
+
*
|
|
386
391
|
* `throwOnError`: fail instead of answering "no match" when the screenshot
|
|
387
392
|
* or OCR fails. Device control's "Test locator" asks for it; an Appium
|
|
388
393
|
* findElement doesn't, and turns the empty list into NoSuchElement.
|
|
@@ -398,22 +403,16 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
398
403
|
this.logger.warn(`OCR proactive search yielded no words. Text content: "${ocrText.trim().substring(0, 100)}..."`);
|
|
399
404
|
return [];
|
|
400
405
|
}
|
|
401
|
-
const matches =
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
},
|
|
412
|
-
confidence: m.confidence / 100,
|
|
413
|
-
};
|
|
414
|
-
this.virtualElementStore.set(el.id, el);
|
|
415
|
-
return el;
|
|
416
|
-
});
|
|
406
|
+
const matches = this.textMatches(words, text);
|
|
407
|
+
if (matches.length === 0)
|
|
408
|
+
return [];
|
|
409
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
|
|
410
|
+
return matches.map((m) => ({
|
|
411
|
+
id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
|
412
|
+
text: m.text,
|
|
413
|
+
rect: (0, screenScale_1.toDriverRect)({ x: m.x0, y: m.y0, width: m.x1 - m.x0, height: m.y1 - m.y0 }, scale),
|
|
414
|
+
confidence: m.confidence / 100,
|
|
415
|
+
}));
|
|
417
416
|
}
|
|
418
417
|
catch (err) {
|
|
419
418
|
this.logger.error(`OCR proactive search failed: ${err.message}`);
|
|
@@ -433,18 +432,14 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
433
432
|
const screenshot = yield driver.getScreenshot();
|
|
434
433
|
const coordinates = yield AIService_1.AI_SERVICE.visualFind(screenshot, iconDescription, opts);
|
|
435
434
|
if (coordinates) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
height: 40,
|
|
443
|
-
},
|
|
435
|
+
// The AI answers in the screenshot's pixels; the rect is the driver's
|
|
436
|
+
// (points on iOS), like a real element's.
|
|
437
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
|
|
438
|
+
return {
|
|
439
|
+
id: `omni_ai_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
440
|
+
rect: (0, screenScale_1.toDriverRect)({ x: coordinates.x - 20, y: coordinates.y - 20, width: 40, height: 40 }, scale),
|
|
444
441
|
confidence: 0.85,
|
|
445
442
|
};
|
|
446
|
-
this.virtualElementStore.set(el.id, el);
|
|
447
|
-
return el;
|
|
448
443
|
}
|
|
449
444
|
}
|
|
450
445
|
catch (err) {
|
|
@@ -460,6 +455,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
460
455
|
*/
|
|
461
456
|
analyzeScreen(driver) {
|
|
462
457
|
return __awaiter(this, void 0, void 0, function* () {
|
|
458
|
+
var _a;
|
|
463
459
|
this.logger.info('Performing full screen analysis...');
|
|
464
460
|
try {
|
|
465
461
|
const screenshot = yield driver.getScreenshot();
|
|
@@ -470,32 +466,28 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
470
466
|
this.validateBuffer(buffer, 'Screen Analysis');
|
|
471
467
|
// 1. Get all text via OCR using the robust worker flow
|
|
472
468
|
const { text: ocrText, words } = yield this.performOcr(buffer);
|
|
473
|
-
// 2. Ask AI
|
|
474
|
-
|
|
469
|
+
// 2. Ask the AI provider about the screen, with the screenshot. Its
|
|
470
|
+
// failure leaves the OCR answer standing, and says why there are no
|
|
471
|
+
// insights. Through 2.13.2 this sent the screenshot's base64 as a file
|
|
472
|
+
// path, so the provider got no image and a "why did this test fail"
|
|
473
|
+
// prompt, and answered about no screen at all.
|
|
474
|
+
let aiInsights = null;
|
|
475
|
+
let aiError;
|
|
475
476
|
try {
|
|
476
|
-
|
|
477
|
-
sessionId: driver.sessionId,
|
|
478
|
-
failureReason: 'Screen Analysis Request',
|
|
479
|
-
commandLogs: [],
|
|
480
|
-
deviceLogs: [],
|
|
481
|
-
screenshotPath: screenshot, // In a real scenario, we'd save this to a file first or pass base64
|
|
482
|
-
});
|
|
477
|
+
aiInsights = yield AIService_1.AI_SERVICE.describeScreen(screenshot);
|
|
483
478
|
}
|
|
484
479
|
catch (aiErr) {
|
|
485
|
-
|
|
480
|
+
aiError = (_a = aiErr === null || aiErr === void 0 ? void 0 : aiErr.message) !== null && _a !== void 0 ? _a : String(aiErr);
|
|
481
|
+
this.logger.warn(`AI insights skipped: ${aiError}`);
|
|
486
482
|
}
|
|
487
|
-
return {
|
|
488
|
-
timestamp: new Date().toISOString(),
|
|
489
|
-
ocr: {
|
|
483
|
+
return Object.assign({ timestamp: new Date().toISOString(), ocr: {
|
|
490
484
|
text: ocrText,
|
|
491
485
|
words: words.map((w) => ({
|
|
492
486
|
text: w.text || '',
|
|
493
487
|
confidence: w.confidence || 0,
|
|
494
488
|
bbox: w.bbox || { x0: 0, y0: 0, x1: 0, y1: 0 },
|
|
495
489
|
})),
|
|
496
|
-
},
|
|
497
|
-
ai_insights: aiAnalysis,
|
|
498
|
-
};
|
|
490
|
+
}, ai_insights: aiInsights }, (aiError !== undefined ? { ai_insights_error: aiError } : {}));
|
|
499
491
|
}
|
|
500
492
|
catch (err) {
|
|
501
493
|
this.logger.error(`Screen analysis failed: ${err.message}`);
|
|
@@ -503,14 +495,56 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
503
495
|
}
|
|
504
496
|
});
|
|
505
497
|
}
|
|
506
|
-
|
|
507
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Keep `element` for `sessionId`, whose test was handed its id (a
|
|
500
|
+
* `-custom:ai-*` find, a heal), until the session ends (`forgetSession`)
|
|
501
|
+
* or the limits push it out. A find doesn't keep what it finds: device
|
|
502
|
+
* control's locator test only shows the ids, and a tap uses its element at
|
|
503
|
+
* once.
|
|
504
|
+
*/
|
|
505
|
+
remember(sessionId, element) {
|
|
506
|
+
this.drop(element.id);
|
|
507
|
+
this.elements.set(element.id, { element, sessionId });
|
|
508
|
+
let ids = this.bySession.get(sessionId);
|
|
509
|
+
if (!ids) {
|
|
510
|
+
ids = new Set();
|
|
511
|
+
this.bySession.set(sessionId, ids);
|
|
512
|
+
}
|
|
513
|
+
ids.add(element.id);
|
|
514
|
+
if (ids.size > exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION)
|
|
515
|
+
this.drop(ids.values().next().value);
|
|
516
|
+
if (this.elements.size > exports.MAX_VIRTUAL_ELEMENTS)
|
|
517
|
+
this.drop(this.elements.keys().next().value);
|
|
508
518
|
}
|
|
509
|
-
|
|
510
|
-
|
|
519
|
+
/** The element, when `sessionId` found it and still has it. */
|
|
520
|
+
getVirtualElement(id, sessionId) {
|
|
521
|
+
const held = this.elements.get(id);
|
|
522
|
+
return held && held.sessionId === sessionId ? held.element : undefined;
|
|
523
|
+
}
|
|
524
|
+
/** Drop every element a session found: it has ended (sessionMemory.ts). */
|
|
525
|
+
forgetSession(sessionId) {
|
|
526
|
+
var _a;
|
|
527
|
+
for (const id of (_a = this.bySession.get(sessionId)) !== null && _a !== void 0 ? _a : [])
|
|
528
|
+
this.elements.delete(id);
|
|
529
|
+
this.bySession.delete(sessionId);
|
|
530
|
+
}
|
|
531
|
+
drop(id) {
|
|
532
|
+
const held = this.elements.get(id);
|
|
533
|
+
if (!held)
|
|
534
|
+
return;
|
|
535
|
+
this.elements.delete(id);
|
|
536
|
+
const ids = this.bySession.get(held.sessionId);
|
|
537
|
+
ids === null || ids === void 0 ? void 0 : ids.delete(id);
|
|
538
|
+
if (ids && ids.size === 0)
|
|
539
|
+
this.bySession.delete(held.sessionId);
|
|
511
540
|
}
|
|
512
541
|
/**
|
|
513
542
|
* Omni-Click: Find a target by OCR text and click its center using W3C actions.
|
|
543
|
+
*
|
|
544
|
+
* The text may be several words ("Sign in"); see ocrTextMatch.ts. The tap
|
|
545
|
+
* and `target` are in the driver's coordinates (points on iOS). A failed
|
|
546
|
+
* screenshot or OCR, or a screen size that can't be read on iOS, fails;
|
|
547
|
+
* `clicked: false` means the text was looked for and not found.
|
|
514
548
|
*/
|
|
515
549
|
omniClickByText(driver, req) {
|
|
516
550
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -533,22 +567,21 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
533
567
|
const screenshot = yield driver.getScreenshot();
|
|
534
568
|
const buffer = Buffer.from(screenshot, 'base64');
|
|
535
569
|
const { words } = yield this.performOcr(buffer);
|
|
536
|
-
const
|
|
537
|
-
const matches = normalized
|
|
538
|
-
.filter((w) => w.text.toLowerCase().includes(text.toLowerCase()))
|
|
539
|
-
.sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
|
|
570
|
+
const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), text).sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
|
|
540
571
|
if (matches.length === 0) {
|
|
541
572
|
return { clicked: false, message: `No OCR match found for text "${text}"` };
|
|
542
573
|
}
|
|
543
574
|
const pick = matches[Math.min(index - 1, matches.length - 1)];
|
|
575
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
|
|
576
|
+
const found = (0, screenScale_1.toDriverRect)({ x: pick.x0, y: pick.y0, width: pick.x1 - pick.x0, height: pick.y1 - pick.y0 }, scale);
|
|
544
577
|
const rect = {
|
|
545
|
-
x: Math.max(0, Math.round(
|
|
546
|
-
y: Math.max(0, Math.round(
|
|
547
|
-
width: Math.max(1, Math.round(
|
|
548
|
-
height: Math.max(1, Math.round(
|
|
578
|
+
x: Math.max(0, Math.round(found.x)),
|
|
579
|
+
y: Math.max(0, Math.round(found.y)),
|
|
580
|
+
width: Math.max(1, Math.round(found.width)),
|
|
581
|
+
height: Math.max(1, Math.round(found.height)),
|
|
549
582
|
};
|
|
550
|
-
const cx = Math.round(
|
|
551
|
-
const cy = Math.round(
|
|
583
|
+
const cx = Math.round(found.x + found.width / 2);
|
|
584
|
+
const cy = Math.round(found.y + found.height / 2);
|
|
552
585
|
// Click via touch actions
|
|
553
586
|
yield driver.performActions([
|
|
554
587
|
{
|
|
@@ -583,6 +616,10 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
583
616
|
}
|
|
584
617
|
/**
|
|
585
618
|
* Omni-Click by Icon: Find a target by visual description and click its center.
|
|
619
|
+
*
|
|
620
|
+
* Fails when it can't look (no AI provider, a failed screenshot or AI call);
|
|
621
|
+
* `clicked: false` means the AI looked and found nothing. Through 2.13.2 every
|
|
622
|
+
* one of those answered `clicked: false`, "No visual match found".
|
|
586
623
|
*/
|
|
587
624
|
omniClickByIcon(driver, req) {
|
|
588
625
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -596,7 +633,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
596
633
|
};
|
|
597
634
|
}
|
|
598
635
|
this.logger.info(`Omni-Click: searching for icon "${icon}" via AI...`);
|
|
599
|
-
const element = yield this.findByIcon(driver, icon);
|
|
636
|
+
const element = yield this.findByIcon(driver, icon, { throwOnError: true });
|
|
600
637
|
if (!element) {
|
|
601
638
|
return { clicked: false, message: `No visual match found for icon "${icon}"` };
|
|
602
639
|
}
|
|
@@ -20,6 +20,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.VisionAssertionService = void 0;
|
|
22
22
|
const typedi_1 = require("typedi");
|
|
23
|
+
const base_driver_1 = require("@appium/base-driver");
|
|
23
24
|
const AIService_1 = require("../AIService");
|
|
24
25
|
const logger_1 = __importDefault(require("../../logger"));
|
|
25
26
|
let VisionAssertionService = class VisionAssertionService {
|
|
@@ -27,38 +28,40 @@ let VisionAssertionService = class VisionAssertionService {
|
|
|
27
28
|
this.logger = logger_1.default.scope('VisionAssertion');
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* `xenon: assertVisualState`: whether `instruction`, a condition in plain
|
|
32
|
+
* words, holds on the device's screen now, as the AI provider judges it from
|
|
33
|
+
* a screenshot.
|
|
34
|
+
*
|
|
35
|
+
* Answers `{ result, message }` only with the provider's own verdict.
|
|
36
|
+
* Whenever there is none it fails, saying the condition was not checked: no
|
|
37
|
+
* condition, no screenshot, no AI provider, a failed or rate-limited call,
|
|
38
|
+
* or an answer that isn't true or false. It never answers `false` for "could
|
|
39
|
+
* not check": a test asserting that something is absent would pass. Through
|
|
40
|
+
* 2.13.2 it answered `{ result: true, message: 'Assertion placeholder' }` in
|
|
41
|
+
* all of those cases.
|
|
31
42
|
*/
|
|
32
43
|
assertState(driver, instruction) {
|
|
33
44
|
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
-
|
|
45
|
+
var _a;
|
|
46
|
+
const condition = typeof instruction === 'string' ? instruction.trim() : '';
|
|
47
|
+
if (!condition) {
|
|
48
|
+
throw new base_driver_1.errors.InvalidArgumentError('assertVisualState needs a condition to check, as a string or { instruction }, ' +
|
|
49
|
+
"for example 'The cart is empty'.");
|
|
50
|
+
}
|
|
51
|
+
this.logger.info(`Asserting visual state: "${condition}"`);
|
|
52
|
+
let screenshot;
|
|
35
53
|
try {
|
|
36
|
-
|
|
37
|
-
const prompt = `
|
|
38
|
-
You are a visual testing quality agent.
|
|
39
|
-
Your task is to verify if the following condition is true based on the provided screenshot.
|
|
40
|
-
|
|
41
|
-
Condition: "${instruction}"
|
|
42
|
-
|
|
43
|
-
Response Format: JSON only, strictly { "result": boolean, "message": "detailed explanation of what you see" }.
|
|
44
|
-
`;
|
|
45
|
-
const response = yield AIService_1.AI_SERVICE.visualFind(screenshot, prompt);
|
|
46
|
-
// Note: We use visualFind which calls analyze. We expect a JSON response.
|
|
47
|
-
// Since analyze returns raw text, we attempt to parse it.
|
|
48
|
-
if (response && response.result !== undefined) {
|
|
49
|
-
return {
|
|
50
|
-
result: response.result,
|
|
51
|
-
message: response.message || 'Assertion evaluated successfully.',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
// Fallback: If AI_SERVICE.visualFind was used literally, it might not return what we expect.
|
|
55
|
-
// Let's assume a more direct path if needed, but for now we'll stick to this pattern.
|
|
56
|
-
return { result: true, message: 'Assertion placeholder' };
|
|
54
|
+
screenshot = yield driver.getScreenshot();
|
|
57
55
|
}
|
|
58
56
|
catch (err) {
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
throw new Error(`No screenshot could be taken, so the condition was not checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
58
|
+
}
|
|
59
|
+
if (!screenshot) {
|
|
60
|
+
throw new Error('No screenshot could be taken, so the condition was not checked.');
|
|
61
61
|
}
|
|
62
|
+
const verdict = yield AIService_1.AI_SERVICE.assertVisual(screenshot, condition);
|
|
63
|
+
this.logger.info(`"${condition}": ${verdict.result} (${verdict.message})`);
|
|
64
|
+
return verdict;
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Finding a text, one word or several, among the words OCR read off a
|
|
4
|
+
* screenshot. Tesseract gives one box per word, so a text with a space in it
|
|
5
|
+
* ("Sign in") is looked for across neighbouring words on one line, and its
|
|
6
|
+
* box is the box around the words it covers.
|
|
7
|
+
*
|
|
8
|
+
* Pure: boxes in, boxes out, in the screenshot's pixels.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.wordRuns = wordRuns;
|
|
12
|
+
exports.findText = findText;
|
|
13
|
+
/**
|
|
14
|
+
* Two words further apart than this many times their height are not
|
|
15
|
+
* neighbours: a space is about a third of a word's height, and a gap this
|
|
16
|
+
* wide is another column, such as "Cancel" and "Done" at either end of a bar.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_GAP_IN_HEIGHTS = 1.5;
|
|
19
|
+
const normalize = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
20
|
+
const heightOf = (w) => Math.max(1, w.y1 - w.y0);
|
|
21
|
+
/** Whether a word sits on a line: their heights overlap by at least half the shorter one. */
|
|
22
|
+
function onLine(line, word) {
|
|
23
|
+
const overlap = Math.min(line.y1, word.y1) - Math.max(line.y0, word.y0);
|
|
24
|
+
return overlap > 0 && overlap >= Math.min(heightOf(line), heightOf(word)) / 2;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The words in runs of neighbours: on one line, left to right, with no wide
|
|
28
|
+
* gap between them. Runs are in reading order.
|
|
29
|
+
*/
|
|
30
|
+
function wordRuns(words) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const word of [...words].sort((a, b) => a.y0 - b.y0 || a.x0 - b.x0)) {
|
|
33
|
+
const line = lines.find((l) => onLine(l, word));
|
|
34
|
+
if (line) {
|
|
35
|
+
line.words.push(word);
|
|
36
|
+
line.y0 = Math.min(line.y0, word.y0);
|
|
37
|
+
line.y1 = Math.max(line.y1, word.y1);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push({ y0: word.y0, y1: word.y1, words: [word] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const runs = [];
|
|
44
|
+
for (const line of lines.sort((a, b) => a.y0 - b.y0)) {
|
|
45
|
+
let run = [];
|
|
46
|
+
for (const word of line.words.sort((a, b) => a.x0 - b.x0)) {
|
|
47
|
+
const previous = run[run.length - 1];
|
|
48
|
+
const gap = previous ? word.x0 - previous.x1 : 0;
|
|
49
|
+
if (previous && gap > MAX_GAP_IN_HEIGHTS * Math.max(heightOf(previous), heightOf(word))) {
|
|
50
|
+
runs.push(run);
|
|
51
|
+
run = [];
|
|
52
|
+
}
|
|
53
|
+
run.push(word);
|
|
54
|
+
}
|
|
55
|
+
if (run.length)
|
|
56
|
+
runs.push(run);
|
|
57
|
+
}
|
|
58
|
+
return runs;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Every place `text` appears, ignoring case and runs of spaces, within a word
|
|
62
|
+
* or across neighbouring words: "Sign in" in "Sign" "in", "password" in
|
|
63
|
+
* "password?". Each match covers the words it touches: its box surrounds
|
|
64
|
+
* them, its text is theirs joined with spaces, and its confidence is the
|
|
65
|
+
* lowest of theirs. A one-word text matches inside one word, as it always
|
|
66
|
+
* did. Matches are in reading order.
|
|
67
|
+
*/
|
|
68
|
+
function findText(words, text) {
|
|
69
|
+
const wanted = normalize(text);
|
|
70
|
+
if (!wanted)
|
|
71
|
+
return [];
|
|
72
|
+
const matches = [];
|
|
73
|
+
for (const run of wordRuns(words)) {
|
|
74
|
+
// The run as one string, remembering where each word sits in it.
|
|
75
|
+
let line = '';
|
|
76
|
+
const spans = [];
|
|
77
|
+
for (const word of run) {
|
|
78
|
+
if (line)
|
|
79
|
+
line += ' ';
|
|
80
|
+
const start = line.length;
|
|
81
|
+
line += normalize(word.text);
|
|
82
|
+
spans.push({ start, end: line.length });
|
|
83
|
+
}
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (let at = line.indexOf(wanted); at !== -1; at = line.indexOf(wanted, at + 1)) {
|
|
86
|
+
const end = at + wanted.length;
|
|
87
|
+
const first = spans.findIndex((s) => s.end > at);
|
|
88
|
+
const last = spans.findIndex((s) => s.end >= end);
|
|
89
|
+
const key = `${first}-${last}`;
|
|
90
|
+
if (first === -1 || last === -1 || seen.has(key))
|
|
91
|
+
continue;
|
|
92
|
+
seen.add(key);
|
|
93
|
+
const covered = run.slice(first, last + 1);
|
|
94
|
+
matches.push({
|
|
95
|
+
x0: Math.min(...covered.map((w) => w.x0)),
|
|
96
|
+
y0: Math.min(...covered.map((w) => w.y0)),
|
|
97
|
+
x1: Math.max(...covered.map((w) => w.x1)),
|
|
98
|
+
y1: Math.max(...covered.map((w) => w.y1)),
|
|
99
|
+
text: covered.map((w) => w.text).join(' '),
|
|
100
|
+
confidence: Math.min(...covered.map((w) => w.confidence)),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return matches;
|
|
105
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SAME_SCALE = void 0;
|
|
16
|
+
exports.driverUsesPoints = driverUsesPoints;
|
|
17
|
+
exports.screenScaleOf = screenScaleOf;
|
|
18
|
+
exports.toDriverRect = toDriverRect;
|
|
19
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
20
|
+
exports.SAME_SCALE = { x: 1, y: 1 };
|
|
21
|
+
/**
|
|
22
|
+
* Whether the driver's coordinates are points rather than the screenshot's
|
|
23
|
+
* pixels. A driver Xenon can't place (device control's screenshot-only
|
|
24
|
+
* stand-in, which never taps) keeps pixels.
|
|
25
|
+
*/
|
|
26
|
+
function driverUsesPoints(driver) {
|
|
27
|
+
var _a, _b, _c, _d;
|
|
28
|
+
const platform = String((_d = (_b = (_a = driver === null || driver === void 0 ? void 0 : driver.caps) === null || _a === void 0 ? void 0 : _a.platformName) !== null && _b !== void 0 ? _b : (_c = driver === null || driver === void 0 ? void 0 : driver.opts) === null || _c === void 0 ? void 0 : _c.platformName) !== null && _d !== void 0 ? _d : '');
|
|
29
|
+
return ['ios', 'tvos'].includes(platform.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The scale between `screenshotBase64` and the driver's coordinates:
|
|
33
|
+
* 1 unless the driver uses points, then the screenshot's size over the
|
|
34
|
+
* window's. Throws when that can't be worked out on a driver that uses
|
|
35
|
+
* points, since a tap in pixels there lands in the wrong place.
|
|
36
|
+
*/
|
|
37
|
+
function screenScaleOf(driver, screenshotBase64) {
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
if (!driverUsesPoints(driver))
|
|
40
|
+
return exports.SAME_SCALE;
|
|
41
|
+
const image = yield (0, sharp_1.default)(Buffer.from(screenshotBase64, 'base64'))
|
|
42
|
+
.metadata()
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
throw new Error(`The screenshot's size could not be read: ${err.message}`);
|
|
45
|
+
});
|
|
46
|
+
const window = typeof driver.getWindowRect === 'function'
|
|
47
|
+
? yield driver.getWindowRect()
|
|
48
|
+
: yield driver.getWindowSize();
|
|
49
|
+
const x = Number(image.width) / Number(window === null || window === void 0 ? void 0 : window.width);
|
|
50
|
+
const y = Number(image.height) / Number(window === null || window === void 0 ? void 0 : window.height);
|
|
51
|
+
if (!(Number.isFinite(x) && x > 0 && Number.isFinite(y) && y > 0)) {
|
|
52
|
+
throw new Error(`The screen size could not be worked out (screenshot ${image.width}x${image.height}, ` +
|
|
53
|
+
`window ${window === null || window === void 0 ? void 0 : window.width}x${window === null || window === void 0 ? void 0 : window.height}).`);
|
|
54
|
+
}
|
|
55
|
+
return { x, y };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** A rect found in the screenshot, in the driver's coordinates. */
|
|
59
|
+
function toDriverRect(rect, scale) {
|
|
60
|
+
if (scale.x === 1 && scale.y === 1)
|
|
61
|
+
return rect;
|
|
62
|
+
return {
|
|
63
|
+
x: rect.x / scale.x,
|
|
64
|
+
y: rect.y / scale.y,
|
|
65
|
+
width: rect.width / scale.x,
|
|
66
|
+
height: rect.height / scale.y,
|
|
67
|
+
};
|
|
68
|
+
}
|