@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
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
|
|
20
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
21
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
22
|
+
/**
|
|
23
|
+
* The OCR healing tier, on a real screenshot read by a real Tesseract worker
|
|
24
|
+
* (the language data the plugin ships), with nothing stubbed between them.
|
|
25
|
+
*
|
|
26
|
+
* Through 2.14 the tier read `result.data.words` from tesseract.js, which
|
|
27
|
+
* since tesseract.js 6 returns text only unless asked: `words` was undefined,
|
|
28
|
+
* and the tier healed nothing, ever. Its tests stubbed the OCR with words, so
|
|
29
|
+
* none of them could see it. ocr-healing-tier.spec.ts covers the matching
|
|
30
|
+
* rules on stubbed words; this checks the whole tier reads a screen.
|
|
31
|
+
*/
|
|
32
|
+
describe('The OCR healing tier, reading a real screenshot', () => {
|
|
33
|
+
let restoreContainer;
|
|
34
|
+
let screenshot;
|
|
35
|
+
// A phone-like screen: four lines, 200 px apart, each at x 60. "Log out"
|
|
36
|
+
// and "Check in" come first, so a tier that took a lone "Log" for "Login",
|
|
37
|
+
// or a lone "in" for "Sign in", answers with the wrong line.
|
|
38
|
+
const WIDTH = 720;
|
|
39
|
+
const LINES = { 'Log out': 150, 'Check in': 350, 'Sign in': 550, Login: 750 };
|
|
40
|
+
const FONT_SIZE = 56;
|
|
41
|
+
/** Where a line's text is: from its cap height above the baseline to just below it. */
|
|
42
|
+
const band = (line) => ({
|
|
43
|
+
top: LINES[line] - FONT_SIZE,
|
|
44
|
+
bottom: LINES[line] + FONT_SIZE / 4,
|
|
45
|
+
});
|
|
46
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
const text = Object.entries(LINES)
|
|
48
|
+
.map(([words, y]) => `<text x="60" y="${y}" font-family="Helvetica, Arial, sans-serif" font-size="${FONT_SIZE}" fill="#111">${words}</text>`)
|
|
49
|
+
.join('');
|
|
50
|
+
const png = yield (0, sharp_1.default)({
|
|
51
|
+
create: { width: WIDTH, height: 900, channels: 3, background: '#ffffff' },
|
|
52
|
+
})
|
|
53
|
+
.composite([
|
|
54
|
+
{
|
|
55
|
+
input: Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="900">${text}</svg>`),
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
.png()
|
|
59
|
+
.toBuffer();
|
|
60
|
+
screenshot = png.toString('base64');
|
|
61
|
+
// An Omni-Vision of its own, whose worker this suite starts and ends.
|
|
62
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
|
|
63
|
+
typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
|
|
64
|
+
}));
|
|
65
|
+
after(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
const worker = typedi_1.Container.get(OmniVisionService_1.OmniVisionService).sharedWorker;
|
|
67
|
+
yield (worker === null || worker === void 0 ? void 0 : worker.terminate());
|
|
68
|
+
restoreContainer();
|
|
69
|
+
}));
|
|
70
|
+
/** An Android driver: no element answers an iOS predicate, so the tier says where the text is. */
|
|
71
|
+
const android = { findElement: () => __awaiter(void 0, void 0, void 0, function* () { return Promise.reject(new Error('unsupported locator')); }) };
|
|
72
|
+
const heal = (selector) => new OcrHealingProvider_1.OcrHealingProvider().heal({
|
|
73
|
+
sessionId: 'real-ocr-session',
|
|
74
|
+
driver: android,
|
|
75
|
+
strategy: 'xpath',
|
|
76
|
+
selector,
|
|
77
|
+
screenshotBase64: screenshot,
|
|
78
|
+
});
|
|
79
|
+
/** The healed element's box lies on `line`, starting where its text starts. */
|
|
80
|
+
const expectOnLine = (rect, line) => {
|
|
81
|
+
const { top, bottom } = band(line);
|
|
82
|
+
(0, chai_1.expect)(rect, 'rect').to.be.an('object');
|
|
83
|
+
(0, chai_1.expect)(rect.y, `top of the box, on "${line}"`).to.be.within(top, bottom);
|
|
84
|
+
(0, chai_1.expect)(rect.y + rect.height, `bottom of the box, on "${line}"`).to.be.within(top, bottom);
|
|
85
|
+
(0, chai_1.expect)(rect.x, 'left of the box').to.be.within(40, 80);
|
|
86
|
+
};
|
|
87
|
+
it('finds a two-word text on its line, not a lone word of it read earlier', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const healed = yield heal("//*[@text='Sign in']");
|
|
89
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
90
|
+
(0, chai_1.expect)(healed.id).to.match(/^healed_ocr_/);
|
|
91
|
+
(0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Sign in"');
|
|
92
|
+
(0, chai_1.expect)(healed.text).to.equal('Sign in');
|
|
93
|
+
expectOnLine(healed.rect, 'Sign in');
|
|
94
|
+
// Both words: wider than "Sign" alone at this size.
|
|
95
|
+
(0, chai_1.expect)(healed.rect.width).to.be.above(150);
|
|
96
|
+
(0, chai_1.expect)(healed.confidence).to.be.within(0.6, 1);
|
|
97
|
+
}));
|
|
98
|
+
it('does not take a word that is only part of the text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
const healed = yield heal("//*[@text='Login']");
|
|
100
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
101
|
+
(0, chai_1.expect)(healed.text).to.equal('Login');
|
|
102
|
+
expectOnLine(healed.rect, 'Login');
|
|
103
|
+
}));
|
|
104
|
+
it('reads the text from the attribute the selector names, whatever its case', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const healed = yield heal("//android.widget.Button[@content-desc='check IN']");
|
|
106
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
107
|
+
(0, chai_1.expect)(healed.text).to.equal('Check in');
|
|
108
|
+
expectOnLine(healed.rect, 'Check in');
|
|
109
|
+
}));
|
|
110
|
+
it('heals nothing when the text is not on the screen', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
(0, chai_1.expect)(yield heal("//*[@text='Sign up']")).to.equal(null);
|
|
112
|
+
}));
|
|
113
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
|
|
20
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
21
|
+
const VisualAiHealingProvider_1 = require("../../src/services/healing/VisualAiHealingProvider");
|
|
22
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
23
|
+
/**
|
|
24
|
+
* Self-healing's OCR tier took the first word OCR read that contained the
|
|
25
|
+
* text, or was part of it: for `Sign in` that was any lone "in" or "Sign" on
|
|
26
|
+
* the screen, and for `Login` a "Log". It now matches as Omni-Vision's
|
|
27
|
+
* `-custom:ai-text` does (ocrTextMatch.ts): the text across neighbouring words
|
|
28
|
+
* on one line, the first match in reading order.
|
|
29
|
+
*
|
|
30
|
+
* OCR is stubbed with the words it would read, in screenshot pixels: no
|
|
31
|
+
* Tesseract runs. ocr-healing-tier-real-ocr.spec.ts runs the tier on a real
|
|
32
|
+
* screenshot, which a stub can't stand in for: tesseract.js's own output is
|
|
33
|
+
* what hid the tier's never healing.
|
|
34
|
+
*/
|
|
35
|
+
describe('The OCR healing tier matches text as Omni-Vision does', () => {
|
|
36
|
+
const word = (text, x0, y0, x1, y1) => ({
|
|
37
|
+
text,
|
|
38
|
+
confidence: 90,
|
|
39
|
+
bbox: { x0, y0, x1, y1 },
|
|
40
|
+
});
|
|
41
|
+
// A heading with "Log" and a lone "in" read first; then "Sign in", far
|
|
42
|
+
// from "Help" on its line; then "Login".
|
|
43
|
+
const WORDS = [
|
|
44
|
+
word('Log', 40, 60, 90, 84),
|
|
45
|
+
word('in', 300, 60, 320, 84),
|
|
46
|
+
word('Sign', 100, 200, 150, 224),
|
|
47
|
+
word('in', 158, 200, 180, 224),
|
|
48
|
+
word('Help', 600, 200, 650, 224),
|
|
49
|
+
word('Login', 100, 400, 180, 424),
|
|
50
|
+
];
|
|
51
|
+
let driver;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
// OCR reads these words: the tier asks Omni-Vision's OCR.
|
|
54
|
+
sinon_1.default
|
|
55
|
+
.stub(typedi_1.Container.get(OmniVisionService_1.OmniVisionService), 'performOcr')
|
|
56
|
+
.resolves({ text: '', words: WORDS });
|
|
57
|
+
// Android: no element is found by an iOS predicate, so the tier answers
|
|
58
|
+
// with where the text is.
|
|
59
|
+
driver = { findElement: sinon_1.default.stub().rejects(new Error('unsupported locator strategy')) };
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => sinon_1.default.restore());
|
|
62
|
+
const heal = (selector) => new OcrHealingProvider_1.OcrHealingProvider().heal({
|
|
63
|
+
sessionId: 'ocr-session',
|
|
64
|
+
driver,
|
|
65
|
+
strategy: 'xpath',
|
|
66
|
+
selector,
|
|
67
|
+
screenshotBase64: 'aGVsbG8=',
|
|
68
|
+
});
|
|
69
|
+
it('finds a two-word text across its words, not a lone word of it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
const healed = yield heal("//*[@text='Sign in']");
|
|
71
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
72
|
+
(0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Sign in"');
|
|
73
|
+
(0, chai_1.expect)(healed.rect).to.deep.equal({ x: 100, y: 200, width: 80, height: 24 });
|
|
74
|
+
}));
|
|
75
|
+
it('hands on the text it read, for getText on the element it returns', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
const healed = yield heal("//*[@text='sign IN']");
|
|
77
|
+
(0, chai_1.expect)(healed.text).to.equal('Sign in');
|
|
78
|
+
(0, chai_1.expect)(healed.message).to.not.equal(healed.text);
|
|
79
|
+
}));
|
|
80
|
+
it('does not take a word that is only part of the text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
const healed = yield heal("//*[@text='Login']");
|
|
82
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
83
|
+
(0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Login"');
|
|
84
|
+
(0, chai_1.expect)(healed.rect).to.deep.equal({ x: 100, y: 400, width: 80, height: 24 });
|
|
85
|
+
}));
|
|
86
|
+
it('finds nothing when the text is not on the screen', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
87
|
+
(0, chai_1.expect)(yield heal("//*[@text='Sign up']")).to.equal(null);
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* A healed element's id names it in the server's memory of virtual elements,
|
|
92
|
+
* which every session shares. `healed_ocr_<ms>` was the same for two heals in
|
|
93
|
+
* one millisecond, so the second overwrote the first.
|
|
94
|
+
*/
|
|
95
|
+
describe('Elements healed by OCR or Visual AI', () => {
|
|
96
|
+
afterEach(() => sinon_1.default.restore());
|
|
97
|
+
const context = {
|
|
98
|
+
sessionId: 'ids-session',
|
|
99
|
+
driver: { findElement: () => __awaiter(void 0, void 0, void 0, function* () { return Promise.reject(new Error('no predicate')); }) },
|
|
100
|
+
strategy: 'xpath',
|
|
101
|
+
selector: "//*[@text='Login']",
|
|
102
|
+
screenshotBase64: 'aGVsbG8=',
|
|
103
|
+
};
|
|
104
|
+
it('get ids of their own when healed in the same millisecond', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
var _a, _b, _c, _d;
|
|
106
|
+
sinon_1.default.stub(Date, 'now').returns(1700000000000);
|
|
107
|
+
sinon_1.default.stub(typedi_1.Container.get(OmniVisionService_1.OmniVisionService), 'performOcr').resolves({
|
|
108
|
+
text: '',
|
|
109
|
+
words: [{ text: 'Login', confidence: 90, bbox: { x0: 1, y0: 1, x1: 50, y1: 20 } }],
|
|
110
|
+
});
|
|
111
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(true);
|
|
112
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 100, y: 100 });
|
|
113
|
+
const ids = [
|
|
114
|
+
(_a = (yield new OcrHealingProvider_1.OcrHealingProvider().heal(context))) === null || _a === void 0 ? void 0 : _a.id,
|
|
115
|
+
(_b = (yield new OcrHealingProvider_1.OcrHealingProvider().heal(context))) === null || _b === void 0 ? void 0 : _b.id,
|
|
116
|
+
(_c = (yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal(context))) === null || _c === void 0 ? void 0 : _c.id,
|
|
117
|
+
(_d = (yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal(context))) === null || _d === void 0 ? void 0 : _d.id,
|
|
118
|
+
];
|
|
119
|
+
(0, chai_1.expect)(ids.every(Boolean), String(ids)).to.equal(true);
|
|
120
|
+
(0, chai_1.expect)(new Set(ids).size, String(ids)).to.equal(4);
|
|
121
|
+
}));
|
|
122
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
22
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
23
|
+
const tesseract_js_1 = __importDefault(require("tesseract.js"));
|
|
24
|
+
const typedi_1 = require("typedi");
|
|
25
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
26
|
+
const ocrData_1 = require("../../src/services/ocr/ocrData");
|
|
27
|
+
const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
|
|
28
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
29
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
30
|
+
/**
|
|
31
|
+
* tesseract.js downloads its English data (eng.traineddata) from
|
|
32
|
+
* cdn.jsdelivr.net the first time a worker starts, and caches it in the
|
|
33
|
+
* server's working directory. A server with no internet couldn't run OCR at
|
|
34
|
+
* all: the first OCR call never finished, and every later one waited for it.
|
|
35
|
+
* One with internet left a 5 MB eng.traineddata wherever it was started (the
|
|
36
|
+
* home directory, ~/.appium).
|
|
37
|
+
*
|
|
38
|
+
* The data now ships with the plugin, read from there and cached nowhere.
|
|
39
|
+
*/
|
|
40
|
+
describe('OCR without the internet', () => {
|
|
41
|
+
// The upstream file, @tesseract.js-data/eng@1.0.0 4.0.0_best_int/eng.traineddata.gz:
|
|
42
|
+
// what tesseract.js 7 downloads by default (LSTM only).
|
|
43
|
+
const UPSTREAM_SHA256 = '45b4cb346724ac1774f1c36f42f182b887bcdb28ebe63e6fff90ac41f3fcff91';
|
|
44
|
+
const tmpDirs = [];
|
|
45
|
+
const tmpDir = () => {
|
|
46
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-ocr-'));
|
|
47
|
+
tmpDirs.push(dir);
|
|
48
|
+
return dir;
|
|
49
|
+
};
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
sinon_1.default.restore();
|
|
52
|
+
for (const dir of tmpDirs.splice(0))
|
|
53
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
/** A white image with `text` on it, as a phone's screenshot would have. */
|
|
56
|
+
const imageOf = (text) => (0, sharp_1.default)({ create: { width: 900, height: 220, channels: 3, background: '#ffffff' } })
|
|
57
|
+
.composite([
|
|
58
|
+
{
|
|
59
|
+
input: Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="900" height="220">` +
|
|
60
|
+
`<text x="40" y="140" font-family="Helvetica, Arial, sans-serif" font-size="96" fill="#000">${text}</text>` +
|
|
61
|
+
`</svg>`),
|
|
62
|
+
},
|
|
63
|
+
])
|
|
64
|
+
.png()
|
|
65
|
+
.toBuffer();
|
|
66
|
+
describe('the English data', () => {
|
|
67
|
+
it('ships with the plugin, byte for byte the file tesseract.js would download', () => {
|
|
68
|
+
(0, chai_1.expect)(ocrData_1.OCR_DATA_FILE).to.equal(path_1.default.join(ocrData_1.OCR_DATA_DIR, 'eng.traineddata.gz'));
|
|
69
|
+
const sha = crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(ocrData_1.OCR_DATA_FILE)).digest('hex');
|
|
70
|
+
(0, chai_1.expect)(sha).to.equal(UPSTREAM_SHA256);
|
|
71
|
+
(0, chai_1.expect)(ocrData_1.OCR_DATA_SHA256).to.equal(UPSTREAM_SHA256);
|
|
72
|
+
const readme = fs_1.default.readFileSync(path_1.default.join(ocrData_1.OCR_DATA_DIR, 'README.md'), 'utf8');
|
|
73
|
+
(0, chai_1.expect)(readme).to.include(UPSTREAM_SHA256);
|
|
74
|
+
});
|
|
75
|
+
it('is copied into lib/ by the build, which is what the npm package ships', () => {
|
|
76
|
+
const copy = package_json_1.default.scripts['build:copy'];
|
|
77
|
+
(0, chai_1.expect)(copy).to.include('mkdir -p lib/src/services/ocr/vendor');
|
|
78
|
+
(0, chai_1.expect)(copy).to.include('cp src/services/ocr/vendor/*.gz');
|
|
79
|
+
(0, chai_1.expect)(package_json_1.default.files).to.include('lib');
|
|
80
|
+
});
|
|
81
|
+
it('is read from there, never fetched, and cached nowhere', () => {
|
|
82
|
+
const options = (0, ocrData_1.ocrWorkerOptions)();
|
|
83
|
+
(0, chai_1.expect)(options.langPath).to.equal(ocrData_1.OCR_DATA_DIR);
|
|
84
|
+
(0, chai_1.expect)(String(options.langPath)).not.to.match(/^[a-z]+:\/\//i);
|
|
85
|
+
(0, chai_1.expect)(options.cacheMethod).to.equal('none');
|
|
86
|
+
(0, chai_1.expect)(options.gzip).to.equal(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('a real OCR worker', () => {
|
|
90
|
+
let cwd;
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
cwd = process.cwd();
|
|
93
|
+
});
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
process.chdir(cwd);
|
|
96
|
+
});
|
|
97
|
+
it('reads text, and writes nothing into the working directory', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
const workDir = tmpDir();
|
|
99
|
+
process.chdir(workDir);
|
|
100
|
+
const worker = yield (0, ocrData_1.createOcrWorker)();
|
|
101
|
+
try {
|
|
102
|
+
const { data } = yield worker.recognize(yield imageOf('Sign in'));
|
|
103
|
+
(0, chai_1.expect)(data.text).to.match(/sign\s*in/i);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
yield worker.terminate();
|
|
107
|
+
}
|
|
108
|
+
(0, chai_1.expect)(fs_1.default.readdirSync(workDir)).to.deep.equal([]);
|
|
109
|
+
}));
|
|
110
|
+
it('fails with a reason, rather than wait for ever, when the data is missing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
const started = Date.now();
|
|
112
|
+
const failure = yield (0, ocrData_1.createOcrWorker)(tmpDir()).then(() => chai_1.expect.fail('a worker with no data should not start'), (err) => err);
|
|
113
|
+
(0, chai_1.expect)(failure.message).to.include('eng.traineddata.gz');
|
|
114
|
+
(0, chai_1.expect)(Date.now() - started).to.be.below(5000);
|
|
115
|
+
}));
|
|
116
|
+
// Given data it can't load, tesseract.js 7 throws from its own message
|
|
117
|
+
// handler, and the server exits on an uncaught exception: the data is
|
|
118
|
+
// checked before a worker sees it.
|
|
119
|
+
for (const [damage, bytes] of [
|
|
120
|
+
['not tesseract data at all', () => Buffer.from('not tesseract data')],
|
|
121
|
+
['cut short', () => fs_1.default.readFileSync(ocrData_1.OCR_DATA_FILE).subarray(0, 100000)],
|
|
122
|
+
]) {
|
|
123
|
+
it(`fails with a reason, before any worker starts, when the data is ${damage}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
const dir = tmpDir();
|
|
125
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'eng.traineddata.gz'), bytes());
|
|
126
|
+
const createWorker = sinon_1.default.spy(tesseract_js_1.default, 'createWorker');
|
|
127
|
+
const failure = yield (0, ocrData_1.createOcrWorker)(dir).then(() => chai_1.expect.fail('a worker with damaged data should not start'), (err) => err);
|
|
128
|
+
(0, chai_1.expect)(failure.message).to.include('damaged');
|
|
129
|
+
(0, chai_1.expect)(createWorker.called).to.equal(false);
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
describe('a worker that will not start', () => {
|
|
134
|
+
/** A directory holding a good copy of the data, so this test's memory is its own. */
|
|
135
|
+
const dataDir = () => {
|
|
136
|
+
const dir = tmpDir();
|
|
137
|
+
fs_1.default.copyFileSync(ocrData_1.OCR_DATA_FILE, path_1.default.join(dir, 'eng.traineddata.gz'));
|
|
138
|
+
return dir;
|
|
139
|
+
};
|
|
140
|
+
it('is remembered for a minute: OCR fails at once rather than start another', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
141
|
+
// tesseract.js hands back no worker when its start fails, so its thread
|
|
142
|
+
// can't be ended; starting one per OCR call would leave one per call.
|
|
143
|
+
const clock = sinon_1.default.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
|
|
144
|
+
const createWorker = sinon_1.default
|
|
145
|
+
.stub(tesseract_js_1.default, 'createWorker')
|
|
146
|
+
.callsFake((_langs, _oem, options) => {
|
|
147
|
+
setTimeout(() => options.errorHandler('the OCR engine failed to load'), 5);
|
|
148
|
+
return new Promise(() => undefined);
|
|
149
|
+
});
|
|
150
|
+
const dir = dataDir();
|
|
151
|
+
const reason = (p) => p.then(() => chai_1.expect.fail('the worker should not start'), (err) => err.message);
|
|
152
|
+
(0, chai_1.expect)(yield reason((0, ocrData_1.createOcrWorker)(dir))).to.include('the OCR engine failed to load');
|
|
153
|
+
(0, chai_1.expect)(yield reason((0, ocrData_1.createOcrWorker)(dir))).to.include('the OCR engine failed to load');
|
|
154
|
+
(0, chai_1.expect)(createWorker.callCount).to.equal(1);
|
|
155
|
+
clock.tick(60001);
|
|
156
|
+
yield reason((0, ocrData_1.createOcrWorker)(dir));
|
|
157
|
+
(0, chai_1.expect)(createWorker.callCount).to.equal(2);
|
|
158
|
+
}));
|
|
159
|
+
});
|
|
160
|
+
describe('every place that runs OCR', () => {
|
|
161
|
+
let restoreContainer;
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
|
|
164
|
+
});
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
restoreContainer();
|
|
167
|
+
});
|
|
168
|
+
it("the OCR healing tier reads the shipped data, with Omni-Vision's worker", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
169
|
+
// The tier reads the screenshot through Omni-Vision (findTextInScreenshot),
|
|
170
|
+
// so it shares that worker and leaves it running for the next read.
|
|
171
|
+
const worker = {
|
|
172
|
+
setParameters: sinon_1.default.stub().resolves(),
|
|
173
|
+
recognize: sinon_1.default.stub().resolves({ data: { text: '', words: [], hocr: '', tsv: '' } }),
|
|
174
|
+
terminate: sinon_1.default.stub().resolves(),
|
|
175
|
+
};
|
|
176
|
+
const createWorker = sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(worker);
|
|
177
|
+
typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
|
|
178
|
+
yield new OcrHealingProvider_1.OcrHealingProvider().heal({
|
|
179
|
+
screenshotBase64: (yield imageOf('Login')).toString('base64'),
|
|
180
|
+
selector: "//*[@text='Login']",
|
|
181
|
+
strategy: 'xpath',
|
|
182
|
+
driver: {},
|
|
183
|
+
});
|
|
184
|
+
(0, chai_1.expect)(createWorker.calledOnce).to.equal(true);
|
|
185
|
+
(0, chai_1.expect)(createWorker.firstCall.args[0]).to.equal('eng');
|
|
186
|
+
(0, chai_1.expect)(createWorker.firstCall.args[2]).to.include({
|
|
187
|
+
langPath: ocrData_1.OCR_DATA_DIR,
|
|
188
|
+
cacheMethod: 'none',
|
|
189
|
+
});
|
|
190
|
+
(0, chai_1.expect)(worker.recognize.calledOnce).to.equal(true);
|
|
191
|
+
(0, chai_1.expect)(worker.terminate.called).to.equal(false);
|
|
192
|
+
}));
|
|
193
|
+
it("Omni-Vision's worker reads the shipped data", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
194
|
+
const worker = {
|
|
195
|
+
setParameters: sinon_1.default.stub().resolves(),
|
|
196
|
+
recognize: sinon_1.default.stub().resolves({ data: { text: '', words: [], hocr: '', tsv: '' } }),
|
|
197
|
+
terminate: sinon_1.default.stub().resolves(),
|
|
198
|
+
};
|
|
199
|
+
const createWorker = sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(worker);
|
|
200
|
+
typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
|
|
201
|
+
const driver = {
|
|
202
|
+
getScreenshot: sinon_1.default.stub().resolves((yield imageOf('Login')).toString('base64')),
|
|
203
|
+
};
|
|
204
|
+
yield typedi_1.Container.get(OmniVisionService_1.OmniVisionService).findByText(driver, 'Login');
|
|
205
|
+
(0, chai_1.expect)(createWorker.calledOnce).to.equal(true);
|
|
206
|
+
(0, chai_1.expect)(createWorker.firstCall.args[0]).to.equal('eng');
|
|
207
|
+
(0, chai_1.expect)(createWorker.firstCall.args[2]).to.include({
|
|
208
|
+
langPath: ocrData_1.OCR_DATA_DIR,
|
|
209
|
+
cacheMethod: 'none',
|
|
210
|
+
});
|
|
211
|
+
}));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const ocrTextMatch_1 = require("../../src/services/omni-vision/ocrTextMatch");
|
|
5
|
+
describe('ocrTextMatch', () => {
|
|
6
|
+
const word = (text, x0, y0, x1, y1, confidence = 90) => ({ text, x0, y0, x1, y1, confidence });
|
|
7
|
+
it('groups words into lines and runs, in reading order, despite a slightly uneven baseline', () => {
|
|
8
|
+
const runs = (0, ocrTextMatch_1.wordRuns)([
|
|
9
|
+
word('Done', 900, 40, 980, 70),
|
|
10
|
+
word('in', 158, 202, 180, 226),
|
|
11
|
+
word('Cancel', 20, 42, 120, 70),
|
|
12
|
+
word('Sign', 100, 200, 150, 224),
|
|
13
|
+
]);
|
|
14
|
+
(0, chai_1.expect)(runs.map((r) => r.map((w) => w.text))).to.deep.equal([
|
|
15
|
+
['Cancel'],
|
|
16
|
+
['Done'],
|
|
17
|
+
['Sign', 'in'],
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
it('finds every occurrence, once per set of words, with the lowest confidence of the words', () => {
|
|
21
|
+
const words = [
|
|
22
|
+
word('Add', 10, 10, 50, 30, 95),
|
|
23
|
+
word('to', 56, 10, 70, 30, 70),
|
|
24
|
+
word('cart', 76, 10, 120, 30, 88),
|
|
25
|
+
word('Add', 10, 100, 50, 120),
|
|
26
|
+
word('to', 56, 100, 70, 120),
|
|
27
|
+
word('cart', 76, 100, 120, 120),
|
|
28
|
+
];
|
|
29
|
+
const found = (0, ocrTextMatch_1.findText)(words, ' add TO cart ');
|
|
30
|
+
(0, chai_1.expect)(found.map((m) => [m.text, m.y0, m.confidence])).to.deep.equal([
|
|
31
|
+
['Add to cart', 10, 70],
|
|
32
|
+
['Add to cart', 100, 90],
|
|
33
|
+
]);
|
|
34
|
+
(0, chai_1.expect)(found[0]).to.include({ x0: 10, x1: 120, y1: 30 });
|
|
35
|
+
});
|
|
36
|
+
it('matches a one-word text inside a word once, however often it appears there', () => {
|
|
37
|
+
(0, chai_1.expect)((0, ocrTextMatch_1.findText)([word('banana', 0, 0, 60, 20)], 'an').map((m) => m.text)).to.deep.equal([
|
|
38
|
+
'banana',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
it('matches nothing for an empty text', () => {
|
|
42
|
+
(0, chai_1.expect)((0, ocrTextMatch_1.findText)([word('Sign', 0, 0, 40, 20)], ' ')).to.deep.equal([]);
|
|
43
|
+
});
|
|
44
|
+
});
|