@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
|
@@ -17,6 +17,7 @@ const chai_1 = require("chai");
|
|
|
17
17
|
const supertest_1 = __importDefault(require("supertest"));
|
|
18
18
|
const typedi_1 = require("typedi");
|
|
19
19
|
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
20
|
+
const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
|
|
20
21
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
21
22
|
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
22
23
|
/** The SQL statements `run` makes. */
|
|
@@ -30,16 +31,24 @@ function sqlOf(db, run) {
|
|
|
30
31
|
/** How SQLite reads `table` in these statements: one line per step. */
|
|
31
32
|
function stepsOn(db, table, queries) {
|
|
32
33
|
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
-
var _a;
|
|
34
|
+
var _a, _b;
|
|
34
35
|
const steps = [];
|
|
35
36
|
for (const q of queries) {
|
|
36
|
-
|
|
37
|
+
// Prisma's own SQL quotes names with backticks; a raw query's, with ".
|
|
38
|
+
if (!q.query.includes(`\`${table}\``) && !q.query.includes(`"${table}"`))
|
|
37
39
|
continue;
|
|
38
40
|
// SQLite plans without looking at the values, and Prisma's logged
|
|
39
41
|
// parameters aren't JSON, so every placeholder gets null.
|
|
40
42
|
const placeholders = ((_a = q.query.match(/\?/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
41
43
|
const plan = yield db.db.$queryRawUnsafe(`EXPLAIN QUERY PLAN ${q.query}`, ...new Array(placeholders).fill(null));
|
|
42
|
-
|
|
44
|
+
// A raw query may name the table by an alias (`"SessionLog" sl`), which
|
|
45
|
+
// is what SQLite's plan then says.
|
|
46
|
+
const alias = (_b = q.query.match(new RegExp(`"${table}"\\s+(?:AS\\s+)?(\\w+)`, 'i'))) === null || _b === void 0 ? void 0 : _b[1];
|
|
47
|
+
for (const { detail } of plan) {
|
|
48
|
+
const named = alias ? detail.replace(new RegExp(`\\b${alias}\\b`), table) : detail;
|
|
49
|
+
if (new RegExp(`\\b${table}\\b`).test(named))
|
|
50
|
+
steps.push(named);
|
|
51
|
+
}
|
|
43
52
|
}
|
|
44
53
|
return steps;
|
|
45
54
|
});
|
|
@@ -145,3 +154,55 @@ describe("A session's logs and profiling are read through an index", function ()
|
|
|
145
154
|
}
|
|
146
155
|
}));
|
|
147
156
|
});
|
|
157
|
+
/**
|
|
158
|
+
* The selector verification reads every find of a selector being verified
|
|
159
|
+
* since it was marked fixed (SelectorVerificationJob), every 15 minutes, by
|
|
160
|
+
* the selector's own index.
|
|
161
|
+
*/
|
|
162
|
+
describe('The selector verification reads SessionLog through an index', function () {
|
|
163
|
+
this.timeout(30000);
|
|
164
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)({ captureQueries: true });
|
|
165
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
166
|
+
yield scratch.db.build.create({ data: { id: 'sv-build', name: 'sv' } });
|
|
167
|
+
yield scratch.db.session.create({
|
|
168
|
+
data: {
|
|
169
|
+
id: 'sv-session',
|
|
170
|
+
build_id: 'sv-build',
|
|
171
|
+
device_udid: 'sv-phone',
|
|
172
|
+
device_platform: 'android',
|
|
173
|
+
device_version: '14',
|
|
174
|
+
desired_capabilities: '{}',
|
|
175
|
+
session_capabilities: '{}',
|
|
176
|
+
node_id: 'localhost',
|
|
177
|
+
has_live_video: false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
yield scratch.db.sessionLog.create({
|
|
181
|
+
data: {
|
|
182
|
+
session_id: 'sv-session',
|
|
183
|
+
command_name: 'findElement',
|
|
184
|
+
url: '/findElement',
|
|
185
|
+
method: 'POST',
|
|
186
|
+
title: 'Find element',
|
|
187
|
+
response: '{"value":{"ELEMENT":"e-1"}}',
|
|
188
|
+
original_strategy: 'xpath',
|
|
189
|
+
original_selector: '//sv',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
yield scratch.db.selectorState.create({
|
|
193
|
+
data: {
|
|
194
|
+
original_strategy: 'xpath',
|
|
195
|
+
original_selector: '//sv',
|
|
196
|
+
status: 'pending',
|
|
197
|
+
fixed_at: new Date(Date.now() - 60 * 60 * 1000),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}));
|
|
201
|
+
it("reads a selector's finds without a scan", () => __awaiter(this, void 0, void 0, function* () {
|
|
202
|
+
const socket = { emitToDashboard: () => undefined };
|
|
203
|
+
const ran = yield sqlOf(scratch, () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run());
|
|
204
|
+
const steps = yield stepsOn(scratch, 'SessionLog', ran);
|
|
205
|
+
(0, chai_1.expect)(steps, 'no SessionLog statement was captured').to.not.be.empty;
|
|
206
|
+
(0, chai_1.expect)(scansOf('SessionLog', steps), steps.join('\n')).to.deep.equal([]);
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
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 shellCommands_1 = require("../../src/device-managers/shellCommands");
|
|
19
|
+
const AndroidDeviceManager_1 = __importDefault(require("../../src/device-managers/AndroidDeviceManager"));
|
|
20
|
+
// The device control Shell tab's allow-list was a `startsWith` test, and the
|
|
21
|
+
// command then reached the phone's `sh`: `ls; reboot`, `ls && pm uninstall …`
|
|
22
|
+
// and `lsof` all ran. A command now has to be an allowed command word for word,
|
|
23
|
+
// and its arguments may only use characters a shell gives no meaning to.
|
|
24
|
+
describe('parseShellCommand', () => {
|
|
25
|
+
const allowed = (cmd) => (0, shellCommands_1.parseShellCommand)(cmd, shellCommands_1.ANDROID_SHELL_COMMANDS);
|
|
26
|
+
it('accepts an allowed command and its plain arguments', () => {
|
|
27
|
+
(0, chai_1.expect)(allowed('ls -la /sdcard/Download')).to.deep.equal({
|
|
28
|
+
argv: ['ls', '-la', '/sdcard/Download'],
|
|
29
|
+
});
|
|
30
|
+
(0, chai_1.expect)(allowed(' getprop ro.product.model ')).to.deep.equal({
|
|
31
|
+
argv: ['getprop', 'ro.product.model'],
|
|
32
|
+
});
|
|
33
|
+
(0, chai_1.expect)(allowed('pm list packages -3')).to.deep.equal({
|
|
34
|
+
argv: ['pm', 'list', 'packages', '-3'],
|
|
35
|
+
});
|
|
36
|
+
(0, chai_1.expect)(allowed('dumpsys battery')).to.deep.equal({ argv: ['dumpsys', 'battery'] });
|
|
37
|
+
(0, chai_1.expect)(allowed('date +%s')).to.deep.equal({ argv: ['date', '+%s'] });
|
|
38
|
+
});
|
|
39
|
+
for (const cmd of [
|
|
40
|
+
'ls; reboot',
|
|
41
|
+
'ls;reboot',
|
|
42
|
+
'ls && pm uninstall com.example.app',
|
|
43
|
+
'ls || reboot',
|
|
44
|
+
'ls | sh',
|
|
45
|
+
'ls `reboot`',
|
|
46
|
+
'getprop $(reboot)',
|
|
47
|
+
'ls > /sdcard/x',
|
|
48
|
+
'ls < /sdcard/x',
|
|
49
|
+
'ls &',
|
|
50
|
+
'ls\nreboot',
|
|
51
|
+
"ls 'a b'",
|
|
52
|
+
'ls "a b"',
|
|
53
|
+
'ls \\; reboot',
|
|
54
|
+
'getprop ${PATH}',
|
|
55
|
+
'ls *',
|
|
56
|
+
'ls ~',
|
|
57
|
+
]) {
|
|
58
|
+
it(`refuses a shell metacharacter: ${JSON.stringify(cmd)}`, () => {
|
|
59
|
+
(0, chai_1.expect)(allowed(cmd)).to.have.property('refused');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
it('refuses a command that only starts like an allowed one', () => {
|
|
63
|
+
(0, chai_1.expect)(allowed('lsof')).to.have.property('refused');
|
|
64
|
+
(0, chai_1.expect)(allowed('pstree')).to.have.property('refused');
|
|
65
|
+
(0, chai_1.expect)(allowed('dumpsys batteryproperties')).to.have.property('refused');
|
|
66
|
+
(0, chai_1.expect)(allowed('pm list packagesx')).to.have.property('refused');
|
|
67
|
+
});
|
|
68
|
+
it('refuses arguments to a command that takes none', () => {
|
|
69
|
+
(0, chai_1.expect)(allowed('dumpsys battery set level 5')).to.have.property('refused');
|
|
70
|
+
(0, chai_1.expect)(allowed('cat /proc/meminfo /data/system/users/0.xml')).to.have.property('refused');
|
|
71
|
+
(0, chai_1.expect)(allowed('whoami --help')).to.have.property('refused');
|
|
72
|
+
});
|
|
73
|
+
it('refuses an empty command and anything not listed', () => {
|
|
74
|
+
(0, chai_1.expect)(allowed(' ')).to.have.property('refused');
|
|
75
|
+
(0, chai_1.expect)(allowed('reboot')).to.have.property('refused');
|
|
76
|
+
(0, chai_1.expect)(allowed('pm uninstall com.example.app')).to.have.property('refused');
|
|
77
|
+
(0, chai_1.expect)(allowed('cat /data/system/users/0.xml')).to.have.property('refused');
|
|
78
|
+
});
|
|
79
|
+
it('keeps every command the Android allow-list offered usable', () => {
|
|
80
|
+
for (const cmd of [
|
|
81
|
+
'ls',
|
|
82
|
+
'ps -A',
|
|
83
|
+
'top -n 1',
|
|
84
|
+
'dumpsys battery',
|
|
85
|
+
'dumpsys wifi',
|
|
86
|
+
'dumpsys power',
|
|
87
|
+
'whoami',
|
|
88
|
+
'getprop',
|
|
89
|
+
'pm list packages',
|
|
90
|
+
'ip addr',
|
|
91
|
+
'cat /proc/meminfo',
|
|
92
|
+
'cat /proc/cpuinfo',
|
|
93
|
+
'date',
|
|
94
|
+
'uptime',
|
|
95
|
+
'netstat -tn',
|
|
96
|
+
]) {
|
|
97
|
+
(0, chai_1.expect)(allowed(cmd), cmd).to.have.property('argv');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it("lets a simulator's generic commands take no arguments", () => {
|
|
101
|
+
(0, chai_1.expect)((0, shellCommands_1.parseShellCommand)('ls', shellCommands_1.IOS_SIMULATOR_COMMANDS)).to.have.property('argv');
|
|
102
|
+
(0, chai_1.expect)((0, shellCommands_1.parseShellCommand)('ls /Users', shellCommands_1.IOS_SIMULATOR_COMMANDS)).to.have.property('refused');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('AndroidDeviceManager.executeShell', () => {
|
|
106
|
+
afterEach(() => sinon_1.default.restore());
|
|
107
|
+
function managerWithAdb() {
|
|
108
|
+
const manager = new AndroidDeviceManager_1.default({});
|
|
109
|
+
const adbExec = sinon_1.default.stub().resolves('ok');
|
|
110
|
+
sinon_1.default.stub(manager, 'getAdb').resolves({ adbInstance: { adbExec } });
|
|
111
|
+
return { manager, adbExec };
|
|
112
|
+
}
|
|
113
|
+
it('passes an allowed command to adb shell word by word', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
const { manager, adbExec } = managerWithAdb();
|
|
115
|
+
(0, chai_1.expect)(yield manager.executeShell('R5CT32ABCDE', 'getprop ro.product.model')).to.equal('ok');
|
|
116
|
+
(0, chai_1.expect)(adbExec.firstCall.args[0]).to.deep.equal([
|
|
117
|
+
'-s',
|
|
118
|
+
'R5CT32ABCDE',
|
|
119
|
+
'shell',
|
|
120
|
+
'getprop',
|
|
121
|
+
'ro.product.model',
|
|
122
|
+
]);
|
|
123
|
+
}));
|
|
124
|
+
it('never reaches adb for a refused command', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
125
|
+
const { manager, adbExec } = managerWithAdb();
|
|
126
|
+
for (const cmd of ['ls; reboot', 'lsof', 'dumpsys battery set level 5']) {
|
|
127
|
+
const err = yield manager.executeShell('R5CT32ABCDE', cmd).then(() => null, (e) => e);
|
|
128
|
+
(0, chai_1.expect)(err, cmd).to.be.an('Error');
|
|
129
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message, cmd).to.match(/not allowed/);
|
|
130
|
+
}
|
|
131
|
+
(0, chai_1.expect)(adbExec.called).to.equal(false);
|
|
132
|
+
}));
|
|
133
|
+
});
|
|
@@ -353,6 +353,8 @@ describe('Auth disabled: the lab path looks nothing up', () => {
|
|
|
353
353
|
const row = { udid: 'phone-a', host: 'h', teamId: 'team-a' };
|
|
354
354
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
355
355
|
removeDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
356
|
+
// removeDevice reads the row for the device_offline webhook's name and platform.
|
|
357
|
+
findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
356
358
|
addDevices: () => __awaiter(void 0, void 0, void 0, function* () { return [row]; }),
|
|
357
359
|
updateDevice: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
358
360
|
getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return [Object.assign(Object.assign({}, row), { sessionStartTime: 0, totalUtilizationTimeMilliSec: 0 })]; }),
|
|
@@ -59,7 +59,8 @@ describe('SocketServer — a signed-in dashboard session cookie', () => {
|
|
|
59
59
|
restore = (0, container_registration_1.saveRegistrations)(UserSessionService_1.UserSessionService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, EventLogService_1.EventLogService);
|
|
60
60
|
resolve = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = SESSIONS[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
61
61
|
verify = sinon_1.default.stub().resolves(null);
|
|
62
|
-
|
|
62
|
+
// ttlMs: authMiddleware renews the cookie for the session's lifetime.
|
|
63
|
+
typedi_1.Container.set(UserSessionService_1.UserSessionService, { resolve, ttlMs: () => 24 * 3600000 });
|
|
63
64
|
typedi_1.Container.set(UserService_1.UserService, { findById: (id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = USERS[id]) !== null && _a !== void 0 ? _a : null; }) });
|
|
64
65
|
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, { verify, verifyPair: sinon_1.default.stub().resolves(null) });
|
|
65
66
|
typedi_1.Container.set(EventLogService_1.EventLogService, { appendSafe: () => undefined });
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const express_1 = __importDefault(require("express"));
|
|
52
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
53
|
+
const typedi_1 = require("typedi");
|
|
54
|
+
const control_1 = __importDefault(require("../../src/app/routers/control"));
|
|
55
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
56
|
+
const deviceService = __importStar(require("../../src/data-service/device-service"));
|
|
57
|
+
const recording_store_1 = require("../../src/services/recording/recording-store");
|
|
58
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
59
|
+
const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
|
|
60
|
+
const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
|
|
61
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
62
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
63
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
64
|
+
const own_node_id_1 = require("../helpers/own-node-id");
|
|
65
|
+
/**
|
|
66
|
+
* Which capture pipelines POST /control/:udid/stream/start runs.
|
|
67
|
+
*
|
|
68
|
+
* With `streaming.androidH264` on, the start ran the H.264 service (scrcpy),
|
|
69
|
+
* and device control's `<img>` then asked GET /stream, which ran the screencap
|
|
70
|
+
* MJPEG loop beside it: two captures of one phone, the very thing the comment
|
|
71
|
+
* in the handler says must not happen. Device control, and any browser that
|
|
72
|
+
* cannot decode H.264 (WebCodecs exists only in a secure context, so plain
|
|
73
|
+
* http://hub:4723 has none), shows MJPEG, so it says so when it starts the
|
|
74
|
+
* stream (`player: 'mjpeg'`), and the server then runs MJPEG only.
|
|
75
|
+
*
|
|
76
|
+
* Only once nobody plays the H.264 capture, though. A Live devices tile, or a
|
|
77
|
+
* second tab, may still be playing it when one page falls back to MJPEG; the
|
|
78
|
+
* capture then ends when its last viewer leaves, not under them.
|
|
79
|
+
*/
|
|
80
|
+
const UDID = 'DEV-1';
|
|
81
|
+
function buildApp() {
|
|
82
|
+
const app = (0, express_1.default)();
|
|
83
|
+
app.use(express_1.default.json());
|
|
84
|
+
const apiRouter = express_1.default.Router();
|
|
85
|
+
apiRouter.use((req, _res, next) => {
|
|
86
|
+
req.auth = {
|
|
87
|
+
kind: 'user-session',
|
|
88
|
+
userId: 'usr_alice',
|
|
89
|
+
role: 'ADMIN',
|
|
90
|
+
scopes: 'admin,devices,sessions,read',
|
|
91
|
+
rateLimit: 100,
|
|
92
|
+
};
|
|
93
|
+
next();
|
|
94
|
+
});
|
|
95
|
+
control_1.default.register(apiRouter);
|
|
96
|
+
app.use('/xenon/api', apiRouter);
|
|
97
|
+
return app;
|
|
98
|
+
}
|
|
99
|
+
describe('POST /control/:udid/stream/start: which capture runs', () => {
|
|
100
|
+
(0, own_node_id_1.useOwnNodeId)();
|
|
101
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
102
|
+
let restoreContainer;
|
|
103
|
+
let savedArgs;
|
|
104
|
+
let mjpegStart;
|
|
105
|
+
let h264;
|
|
106
|
+
let h264Start;
|
|
107
|
+
let h264Stop;
|
|
108
|
+
let captureKills;
|
|
109
|
+
let pushPacket;
|
|
110
|
+
let row;
|
|
111
|
+
/** The real H.264 service with its capture (scrcpy) stubbed out. */
|
|
112
|
+
const h264Service = () => {
|
|
113
|
+
const svc = Object.create(AndroidH264StreamService_1.default.prototype);
|
|
114
|
+
svc.sessions = new Map();
|
|
115
|
+
svc.startPromises = new Map();
|
|
116
|
+
svc.scrcpyIncompatible = new Set();
|
|
117
|
+
svc.openCapture = (_udid, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
pushPacket = onPacket;
|
|
119
|
+
onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
|
|
120
|
+
return { kill: () => (captureKills += 1) };
|
|
121
|
+
});
|
|
122
|
+
return svc;
|
|
123
|
+
};
|
|
124
|
+
/** Another page playing the phone's H.264 preview: what it got, and its leave. */
|
|
125
|
+
const h264Viewer = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const got = [];
|
|
127
|
+
const leave = (yield h264.start(UDID)).addClient((p) => got.push(p.type));
|
|
128
|
+
return { got, leave };
|
|
129
|
+
});
|
|
130
|
+
const flag = (androidH264) => {
|
|
131
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = Object.assign(Object.assign({}, savedArgs), { streaming: { androidH264 } });
|
|
132
|
+
};
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(AndroidStreamService_1.default, AndroidH264StreamService_1.default, IOSStreamService_1.default, recording_store_1.RecordingStore);
|
|
135
|
+
savedArgs = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs;
|
|
136
|
+
row = {
|
|
137
|
+
udid: UDID,
|
|
138
|
+
host: '127.0.0.1',
|
|
139
|
+
nodeId: own_node_id_1.OWN_NODE_ID,
|
|
140
|
+
platform: 'android',
|
|
141
|
+
busy: false,
|
|
142
|
+
session_id: null,
|
|
143
|
+
screenWidth: '1080',
|
|
144
|
+
screenHeight: '2220',
|
|
145
|
+
};
|
|
146
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return row; }) });
|
|
147
|
+
sinon_1.default.stub(deviceService, 'blockDevice').resolves();
|
|
148
|
+
captureKills = 0;
|
|
149
|
+
mjpegStart = sinon_1.default.stub().resolves({ mjpegPort: 9100 });
|
|
150
|
+
h264 = h264Service();
|
|
151
|
+
typedi_1.Container.set(AndroidStreamService_1.default, {
|
|
152
|
+
startStream: mjpegStart,
|
|
153
|
+
getStreamStatus: () => undefined,
|
|
154
|
+
});
|
|
155
|
+
typedi_1.Container.set(IOSStreamService_1.default, { getStreamStatus: () => undefined });
|
|
156
|
+
typedi_1.Container.set(AndroidH264StreamService_1.default, h264);
|
|
157
|
+
typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return false; }) });
|
|
158
|
+
// Spied after any setup start, so they count only what the route does.
|
|
159
|
+
h264Start = sinon_1.default.spy(h264, 'start');
|
|
160
|
+
h264Stop = sinon_1.default.spy(h264, 'stop');
|
|
161
|
+
});
|
|
162
|
+
/** An H.264 capture already running for the phone, as a tile's start leaves it. */
|
|
163
|
+
const h264AlreadyRunning = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
|
+
h264Start.restore();
|
|
165
|
+
yield h264.start(UDID);
|
|
166
|
+
h264Start = sinon_1.default.spy(h264, 'start');
|
|
167
|
+
});
|
|
168
|
+
const h264Running = () => h264.getMultiplexer(UDID) !== undefined;
|
|
169
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
|
+
sinon_1.default.restore();
|
|
171
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = savedArgs;
|
|
172
|
+
restoreContainer();
|
|
173
|
+
yield loopback.closeAll();
|
|
174
|
+
}));
|
|
175
|
+
const start = (body) => __awaiter(void 0, void 0, void 0, function* () {
|
|
176
|
+
return (0, loopbackRequest_1.default)(yield loopback.serve(buildApp()))
|
|
177
|
+
.post(`/xenon/api/control/${UDID}/stream/start`)
|
|
178
|
+
.send(body !== null && body !== void 0 ? body : {});
|
|
179
|
+
});
|
|
180
|
+
it('runs the H.264 service alone when the flag is on and the page can play it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
181
|
+
flag(true);
|
|
182
|
+
const res = yield start();
|
|
183
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
184
|
+
(0, chai_1.expect)(res.body.type).to.equal('h264');
|
|
185
|
+
(0, chai_1.expect)(res.body.h264Path).to.equal(`/xenon/api/control/${UDID}/stream/h264`);
|
|
186
|
+
(0, chai_1.expect)(h264Start.calledOnce).to.equal(true);
|
|
187
|
+
(0, chai_1.expect)(mjpegStart.called).to.equal(false);
|
|
188
|
+
}));
|
|
189
|
+
it('runs the MJPEG capture alone when the page says it shows MJPEG', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
190
|
+
flag(true);
|
|
191
|
+
const res = yield start({ player: 'mjpeg' });
|
|
192
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
193
|
+
(0, chai_1.expect)(res.body.type).to.equal('mjpeg');
|
|
194
|
+
(0, chai_1.expect)(res.body.streamUrl).to.equal(`/xenon/api/control/${UDID}/stream`);
|
|
195
|
+
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
196
|
+
(0, chai_1.expect)(h264Start.called).to.equal(false);
|
|
197
|
+
}));
|
|
198
|
+
it('ends an H.264 capture nobody is watching, so the two never overlap', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
flag(true);
|
|
200
|
+
yield h264AlreadyRunning();
|
|
201
|
+
yield start({ player: 'mjpeg' });
|
|
202
|
+
(0, chai_1.expect)(h264Stop.calledOnceWith(UDID)).to.equal(true);
|
|
203
|
+
(0, chai_1.expect)(captureKills).to.equal(1);
|
|
204
|
+
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
205
|
+
(0, chai_1.expect)(h264Running()).to.equal(false);
|
|
206
|
+
}));
|
|
207
|
+
it('stops the H.264 capture before it starts the MJPEG one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
208
|
+
flag(true);
|
|
209
|
+
yield h264AlreadyRunning();
|
|
210
|
+
yield start({ player: 'mjpeg' });
|
|
211
|
+
(0, chai_1.expect)(h264Stop.calledBefore(mjpegStart)).to.equal(true);
|
|
212
|
+
}));
|
|
213
|
+
it('keeps the H.264 capture for another page still playing it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
flag(true);
|
|
215
|
+
const other = yield h264Viewer();
|
|
216
|
+
const res = yield start({ player: 'mjpeg' });
|
|
217
|
+
(0, chai_1.expect)(res.body.type).to.equal('mjpeg');
|
|
218
|
+
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
219
|
+
(0, chai_1.expect)(captureKills).to.equal(0);
|
|
220
|
+
pushPacket({ type: 'key', data: Buffer.from([1]), ptsMs: 1 });
|
|
221
|
+
(0, chai_1.expect)(other.got).to.deep.equal(['config', 'key']);
|
|
222
|
+
}));
|
|
223
|
+
it('ends the H.264 capture when that last page leaves, without waiting for the idle stop', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
flag(true);
|
|
225
|
+
const other = yield h264Viewer();
|
|
226
|
+
yield start({ player: 'mjpeg' });
|
|
227
|
+
other.leave();
|
|
228
|
+
(0, chai_1.expect)(captureKills).to.equal(1);
|
|
229
|
+
(0, chai_1.expect)(h264Running()).to.equal(false);
|
|
230
|
+
}));
|
|
231
|
+
it('runs MJPEG alone, and stops nothing, when the flag is off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
232
|
+
flag(false);
|
|
233
|
+
const res = yield start();
|
|
234
|
+
(0, chai_1.expect)(res.body.type).to.equal('mjpeg');
|
|
235
|
+
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
236
|
+
(0, chai_1.expect)(h264Start.called).to.equal(false);
|
|
237
|
+
(0, chai_1.expect)(h264Stop.called).to.equal(false);
|
|
238
|
+
}));
|
|
239
|
+
it('ignores a player it does not know, and uses the server’s choice', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
240
|
+
flag(true);
|
|
241
|
+
const res = yield start({ player: 'hologram' });
|
|
242
|
+
(0, chai_1.expect)(res.body.type).to.equal('h264');
|
|
243
|
+
(0, chai_1.expect)(h264Start.calledOnce).to.equal(true);
|
|
244
|
+
(0, chai_1.expect)(mjpegStart.called).to.equal(false);
|
|
245
|
+
}));
|
|
246
|
+
it('keeps MJPEG for a phone that is being recorded, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
247
|
+
flag(true);
|
|
248
|
+
typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return true; }) });
|
|
249
|
+
const res = yield start();
|
|
250
|
+
(0, chai_1.expect)(res.body.type).to.equal('mjpeg');
|
|
251
|
+
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
252
|
+
(0, chai_1.expect)(h264Start.called).to.equal(false);
|
|
253
|
+
}));
|
|
254
|
+
it('ends H.264 at once for a phone being recorded, viewers or not, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
// A recording reads the MJPEG capture, and H.264 never runs beside it
|
|
256
|
+
// (ensureMjpegForRecording stopped it when the recording began).
|
|
257
|
+
flag(true);
|
|
258
|
+
yield h264Viewer();
|
|
259
|
+
typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return true; }) });
|
|
260
|
+
yield start();
|
|
261
|
+
(0, chai_1.expect)(captureKills).to.equal(1);
|
|
262
|
+
(0, chai_1.expect)(h264Running()).to.equal(false);
|
|
263
|
+
}));
|
|
264
|
+
});
|
|
@@ -20,4 +20,8 @@ describe('resolveStreamType', () => {
|
|
|
20
20
|
(0, chai_1.expect)((0, streamType_1.resolveStreamType)('android', true, false)).to.equal('h264');
|
|
21
21
|
(0, chai_1.expect)((0, streamType_1.resolveStreamType)('androidtv', true, false)).to.equal('h264');
|
|
22
22
|
});
|
|
23
|
+
it('a page that shows MJPEG => mjpeg, whatever the flag', () => {
|
|
24
|
+
(0, chai_1.expect)((0, streamType_1.resolveStreamType)('android', true, false, false)).to.equal('mjpeg');
|
|
25
|
+
(0, chai_1.expect)((0, streamType_1.resolveStreamType)('android', true, false, true)).to.equal('h264');
|
|
26
|
+
});
|
|
23
27
|
});
|