@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,98 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const typedi_1 = require("typedi");
|
|
15
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
16
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
17
|
+
/**
|
|
18
|
+
* WebConfig holds the dashboard's saved settings, one row each. Its `id`
|
|
19
|
+
* column is the table's primary key and defaulted to 'global', and every
|
|
20
|
+
* setting was written with that id, so the table could hold one setting:
|
|
21
|
+
* saving a second failed with "Unique constraint failed on the fields (`id`)".
|
|
22
|
+
* These run the service against a real (scratch) database.
|
|
23
|
+
*/
|
|
24
|
+
describe('WebConfigService', () => {
|
|
25
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
26
|
+
const service = () => typedi_1.Container.get(web_config_service_1.WebConfigService);
|
|
27
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
28
|
+
yield scratch.db.webConfig.deleteMany({});
|
|
29
|
+
}));
|
|
30
|
+
it('saves every setting of one update', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
+
yield service().setConfig({
|
|
32
|
+
buildCleanupDays: 7,
|
|
33
|
+
buildCleanupMaxCount: 5,
|
|
34
|
+
buildCleanupSchedule: '0 3 * * *',
|
|
35
|
+
deleteBuildAssets: false,
|
|
36
|
+
});
|
|
37
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
|
|
38
|
+
buildCleanupDays: 7,
|
|
39
|
+
buildCleanupMaxCount: 5,
|
|
40
|
+
buildCleanupSchedule: '0 3 * * *',
|
|
41
|
+
deleteBuildAssets: false,
|
|
42
|
+
});
|
|
43
|
+
}));
|
|
44
|
+
it('saves settings from separate updates, as two pages save them', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
yield service().setConfig({ healthCheckIntervalMs: 600000 });
|
|
46
|
+
yield service().setConfig({ buildCleanupDays: 7 });
|
|
47
|
+
yield service().setConfig({ healthCheckSchedule: '0 * * * *', deleteBuildAssets: true });
|
|
48
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
|
|
49
|
+
healthCheckIntervalMs: 600000,
|
|
50
|
+
buildCleanupDays: 7,
|
|
51
|
+
healthCheckSchedule: '0 * * * *',
|
|
52
|
+
deleteBuildAssets: true,
|
|
53
|
+
});
|
|
54
|
+
}));
|
|
55
|
+
it('keeps the self-healing switch, off as well as on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
yield service().setConfig({ enableSelfHealing: false });
|
|
57
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: false });
|
|
58
|
+
yield service().setConfig({ enableSelfHealing: true });
|
|
59
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: true });
|
|
60
|
+
}));
|
|
61
|
+
it("keeps the AI engine page's provider, models and base URL, an empty one included", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
|
+
yield service().setConfig({
|
|
63
|
+
aiProvider: 'ollama',
|
|
64
|
+
ollamaModel: 'llava',
|
|
65
|
+
aiBaseUrl: 'http://gpu-box.lab:11434',
|
|
66
|
+
geminiModel: '',
|
|
67
|
+
});
|
|
68
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
|
|
69
|
+
aiProvider: 'ollama',
|
|
70
|
+
ollamaModel: 'llava',
|
|
71
|
+
aiBaseUrl: 'http://gpu-box.lab:11434',
|
|
72
|
+
geminiModel: '',
|
|
73
|
+
});
|
|
74
|
+
}));
|
|
75
|
+
it('never saves an API key, whatever it is sent', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
yield service().setConfig({ aiProvider: 'gemini', geminiApiKey: 'secret' });
|
|
77
|
+
const rows = yield scratch.db.webConfig.findMany();
|
|
78
|
+
(0, chai_1.expect)(rows.map((row) => row.name)).to.deep.equal(['aiProvider']);
|
|
79
|
+
}));
|
|
80
|
+
it('changes a setting saved before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
yield service().setConfig({ buildCleanupDays: 7, buildCleanupMaxCount: 5 });
|
|
82
|
+
yield service().setConfig({ buildCleanupDays: 14 });
|
|
83
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
|
|
84
|
+
buildCleanupDays: 14,
|
|
85
|
+
buildCleanupMaxCount: 5,
|
|
86
|
+
});
|
|
87
|
+
}));
|
|
88
|
+
it('still reads the row an earlier version saved under the id "global"', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
+
yield scratch.db.webConfig.create({
|
|
90
|
+
data: { id: 'global', name: 'buildCleanupDays', value: '9' },
|
|
91
|
+
});
|
|
92
|
+
yield service().setConfig({ buildCleanupMaxCount: 5 });
|
|
93
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
|
|
94
|
+
buildCleanupDays: 9,
|
|
95
|
+
buildCleanupMaxCount: 5,
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
@@ -129,6 +129,30 @@ describe('webhook delivery', () => {
|
|
|
129
129
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
130
130
|
(0, chai_1.expect)(r.body).to.deep.equal({ success: true });
|
|
131
131
|
}));
|
|
132
|
+
it('POST /api/webhook/test sends a sample of the event it is asked for, through the template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
+
const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
|
|
134
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
135
|
+
.post('/xenon/api/webhook/test')
|
|
136
|
+
.send({
|
|
137
|
+
url: URL,
|
|
138
|
+
type: 'slack',
|
|
139
|
+
payloadTemplate: '{"text": "{{eventType}}: {{sessionId}}"}',
|
|
140
|
+
event: 'session_failed',
|
|
141
|
+
})
|
|
142
|
+
.timeout(5000);
|
|
143
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
144
|
+
(0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({ text: 'session_failed: test-session-id' });
|
|
145
|
+
}));
|
|
146
|
+
it('POST /api/webhook/test with an event it does not know answers 400, sending nothing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
|
+
const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
|
|
148
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
149
|
+
.post('/xenon/api/webhook/test')
|
|
150
|
+
.send({ url: URL, event: 'device_exploded' })
|
|
151
|
+
.timeout(5000);
|
|
152
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
153
|
+
(0, chai_1.expect)(r.body.error).to.contain('session_failed');
|
|
154
|
+
(0, chai_1.expect)(post.called).to.equal(false);
|
|
155
|
+
}));
|
|
132
156
|
it('POST /api/webhook/test without a url answers 400', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
157
|
const r = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/webhook/test').send({}).timeout(5000);
|
|
134
158
|
(0, chai_1.expect)(r.status).to.equal(400);
|
|
@@ -0,0 +1,273 @@
|
|
|
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 axios_1 = __importDefault(require("axios"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const NotificationService_1 = require("../../src/services/NotificationService");
|
|
21
|
+
const webhookEvents_1 = require("../../src/services/webhookEvents");
|
|
22
|
+
// What the dashboard offers as chips: plain JSON, so a spec can read it.
|
|
23
|
+
const webhookEventVariables_json_1 = __importDefault(require("../../web/src/components/webhook-settings/webhookEventVariables.json"));
|
|
24
|
+
/**
|
|
25
|
+
* One documented payload per event, and every way a webhook turns it into a
|
|
26
|
+
* message (the built-in Slack text, the generic JSON, a custom template) reads
|
|
27
|
+
* those same names.
|
|
28
|
+
*
|
|
29
|
+
* `session_failed` used to send the database row (`id`, `failure_reason`, ...)
|
|
30
|
+
* while the built-in Slack text read `sessionId` and `failureReason` and said
|
|
31
|
+
* "undefined", and the dashboard's template chips offered the same two names,
|
|
32
|
+
* which never substituted.
|
|
33
|
+
*/
|
|
34
|
+
const URL = 'https://hooks.example.com/T000/B000/xyz';
|
|
35
|
+
/** A Session row as Prisma returns it. */
|
|
36
|
+
const SESSION_ROW = {
|
|
37
|
+
id: 'a1b2c3d4-0000-4000-8000-000000000001',
|
|
38
|
+
name: 'Checkout flow',
|
|
39
|
+
status: 'failed',
|
|
40
|
+
failure_reason: 'Element not found: ~pay-now',
|
|
41
|
+
device_udid: 'R58M123',
|
|
42
|
+
device_name: 'Galaxy S9+',
|
|
43
|
+
device_platform: 'android',
|
|
44
|
+
device_version: '10',
|
|
45
|
+
startTime: new Date('2026-10-04T09:00:00.000Z'),
|
|
46
|
+
endTime: new Date('2026-10-04T09:02:30.000Z'),
|
|
47
|
+
desired_capabilities: '{"platformName":"android","appium:app":"/secret/app.apk"}',
|
|
48
|
+
session_capabilities: '{"deviceName":"x"}',
|
|
49
|
+
api_key_id: 'key-1',
|
|
50
|
+
user_id: 'user-1',
|
|
51
|
+
ai_analysis: '{"long":"analysis"}',
|
|
52
|
+
};
|
|
53
|
+
describe('webhook payloads', () => {
|
|
54
|
+
afterEach(() => sinon_1.default.restore());
|
|
55
|
+
describe('session_failed', () => {
|
|
56
|
+
it('is built from the session row with the documented names', () => {
|
|
57
|
+
(0, chai_1.expect)((0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW)).to.deep.equal({
|
|
58
|
+
sessionId: 'a1b2c3d4-0000-4000-8000-000000000001',
|
|
59
|
+
sessionName: 'Checkout flow',
|
|
60
|
+
failureReason: 'Element not found: ~pay-now',
|
|
61
|
+
udid: 'R58M123',
|
|
62
|
+
deviceName: 'Galaxy S9+',
|
|
63
|
+
platform: 'android',
|
|
64
|
+
osVersion: '10',
|
|
65
|
+
startTime: '2026-10-04T09:00:00.000Z',
|
|
66
|
+
endTime: '2026-10-04T09:02:30.000Z',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it('leaves out what a webhook has no business with: capabilities, keys, analysis', () => {
|
|
70
|
+
const payload = JSON.stringify((0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
|
|
71
|
+
for (const secret of ['/secret/app.apk', 'key-1', 'user-1', 'analysis', 'capabilities']) {
|
|
72
|
+
(0, chai_1.expect)(payload).not.to.contain(secret);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it('never has a missing value: unknown ones are empty text', () => {
|
|
76
|
+
const payload = (0, webhookEvents_1.sessionFailedPayload)({
|
|
77
|
+
id: 's-2',
|
|
78
|
+
status: 'failed',
|
|
79
|
+
device_udid: 'U',
|
|
80
|
+
device_platform: 'ios',
|
|
81
|
+
device_version: '17',
|
|
82
|
+
device_name: null,
|
|
83
|
+
name: null,
|
|
84
|
+
failure_reason: null,
|
|
85
|
+
startTime: new Date('2026-10-04T09:00:00.000Z'),
|
|
86
|
+
endTime: null,
|
|
87
|
+
});
|
|
88
|
+
(0, chai_1.expect)(payload).to.include({
|
|
89
|
+
sessionName: '',
|
|
90
|
+
failureReason: '',
|
|
91
|
+
deviceName: '',
|
|
92
|
+
endTime: '',
|
|
93
|
+
});
|
|
94
|
+
for (const value of Object.values(payload))
|
|
95
|
+
(0, chai_1.expect)(value).to.be.a('string');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('the built-in Slack message', () => {
|
|
99
|
+
let svc;
|
|
100
|
+
let post;
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
svc = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
103
|
+
post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
|
|
104
|
+
});
|
|
105
|
+
const slackFor = (event, payload) => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
+
sinon_1.default
|
|
107
|
+
.stub(svc, 'getConfigs')
|
|
108
|
+
.resolves([
|
|
109
|
+
{ id: 'a', url: URL, events: JSON.stringify([event]), type: 'slack', active: true },
|
|
110
|
+
]);
|
|
111
|
+
yield svc.dispatchEvent(event, payload);
|
|
112
|
+
return post.firstCall.args[1].attachments[0];
|
|
113
|
+
});
|
|
114
|
+
it('names the failed session and why, instead of "undefined"', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const attachment = yield slackFor('session_failed', (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
|
|
116
|
+
(0, chai_1.expect)(attachment.text).to.contain('Session Failed');
|
|
117
|
+
(0, chai_1.expect)(attachment.text).to.contain('a1b2c3d4-0000-4000-8000-000000000001');
|
|
118
|
+
(0, chai_1.expect)(attachment.text).to.contain('Element not found: ~pay-now');
|
|
119
|
+
(0, chai_1.expect)(attachment.text).to.contain('Galaxy S9+');
|
|
120
|
+
(0, chai_1.expect)(attachment.text).not.to.contain('undefined');
|
|
121
|
+
(0, chai_1.expect)(JSON.stringify(attachment)).not.to.contain('undefined');
|
|
122
|
+
}));
|
|
123
|
+
it('says so when no reason was recorded', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
const attachment = yield slackFor('session_failed', (0, webhookEvents_1.sessionFailedPayload)(Object.assign(Object.assign({}, SESSION_ROW), { failure_reason: null })));
|
|
125
|
+
(0, chai_1.expect)(attachment.text).to.contain('no reason recorded');
|
|
126
|
+
}));
|
|
127
|
+
it('names the device for a device event', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
const attachment = yield slackFor('device_offline', webhookEvents_1.WEBHOOK_EVENTS.device_offline.sample());
|
|
129
|
+
(0, chai_1.expect)(attachment.text).to.contain('test-device-udid');
|
|
130
|
+
(0, chai_1.expect)(attachment.text).not.to.contain('undefined');
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
133
|
+
describe('the generic webhook', () => {
|
|
134
|
+
it('sends the event name and the documented payload', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
136
|
+
const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
|
|
137
|
+
sinon_1.default
|
|
138
|
+
.stub(svc, 'getConfigs')
|
|
139
|
+
.resolves([
|
|
140
|
+
{ id: 'a', url: URL, events: '["session_failed"]', type: 'webhook', active: true },
|
|
141
|
+
]);
|
|
142
|
+
yield svc.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
|
|
143
|
+
(0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({
|
|
144
|
+
event: 'session_failed',
|
|
145
|
+
payload: (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW),
|
|
146
|
+
});
|
|
147
|
+
}));
|
|
148
|
+
});
|
|
149
|
+
describe('a custom template', () => {
|
|
150
|
+
const data = Object.assign({ eventType: 'session_failed' }, (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
|
|
151
|
+
it('fills the names the dashboard offers as chips', () => {
|
|
152
|
+
(0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"text": "{{eventType}}: {{sessionId}} on {{deviceName}}: {{failureReason}}"}', data)).to.deep.equal({
|
|
153
|
+
text: 'session_failed: a1b2c3d4-0000-4000-8000-000000000001 on Galaxy S9+: Element not found: ~pay-now',
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it('keeps the JSON valid when a value holds quotes, a backslash or a line break', () => {
|
|
157
|
+
const body = (0, webhookEvents_1.renderTemplate)('{"text": "Failed: {{failureReason}}", "n": 1}', {
|
|
158
|
+
failureReason: 'No element "~pay"\nline two \\ done',
|
|
159
|
+
});
|
|
160
|
+
(0, chai_1.expect)(body).to.deep.equal({ text: 'Failed: No element "~pay"\nline two \\ done', n: 1 });
|
|
161
|
+
});
|
|
162
|
+
it('fills a number or a list written without quotes', () => {
|
|
163
|
+
(0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"heals": {{totalHeals}}, "top": {{hotspots}}}', {
|
|
164
|
+
totalHeals: 12,
|
|
165
|
+
hotspots: [{ healCount: 7 }],
|
|
166
|
+
})).to.deep.equal({ heals: 12, top: [{ healCount: 7 }] });
|
|
167
|
+
});
|
|
168
|
+
it('sends a template that is not JSON as text', () => {
|
|
169
|
+
(0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('Session {{sessionId}} failed', data)).to.deep.equal({
|
|
170
|
+
text: 'Session a1b2c3d4-0000-4000-8000-000000000001 failed',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('leaves a name the event does not have as it was written, so a typo shows', () => {
|
|
174
|
+
(0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"text": "{{sessionID}} / {{udid}}"}', data)).to.deep.equal({
|
|
175
|
+
text: '{{sessionID}} / R58M123',
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
it('reaches into a list with dots', () => {
|
|
179
|
+
(0, chai_1.expect)((0, webhookEvents_1.renderTemplate)('{"text": "{{hotspots.0.originalSelector}}"}', {
|
|
180
|
+
hotspots: [{ originalSelector: '~login' }],
|
|
181
|
+
})).to.deep.equal({ text: '~login' });
|
|
182
|
+
});
|
|
183
|
+
it('is what a real event is sent as', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
185
|
+
const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
|
|
186
|
+
sinon_1.default.stub(svc, 'getConfigs').resolves([
|
|
187
|
+
{
|
|
188
|
+
id: 'a',
|
|
189
|
+
url: URL,
|
|
190
|
+
events: '["session_failed"]',
|
|
191
|
+
type: 'slack',
|
|
192
|
+
active: true,
|
|
193
|
+
payloadTemplate: '{"text": "{{sessionId}}: {{failureReason}}"}',
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
yield svc.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(SESSION_ROW));
|
|
197
|
+
(0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({
|
|
198
|
+
text: 'a1b2c3d4-0000-4000-8000-000000000001: Element not found: ~pay-now',
|
|
199
|
+
});
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
describe('Send test', () => {
|
|
203
|
+
let svc;
|
|
204
|
+
let post;
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
svc = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
207
|
+
post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
|
|
208
|
+
});
|
|
209
|
+
it('fills the template from a sample of the event asked for', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
210
|
+
yield svc.sendTest(URL, 'slack', '{"text": "{{eventType}}: {{failureReason}} ({{deviceName}})"}', 'session_failed');
|
|
211
|
+
const sample = webhookEvents_1.WEBHOOK_EVENTS.session_failed.sample();
|
|
212
|
+
(0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({
|
|
213
|
+
text: `session_failed: ${sample.failureReason} (${sample.deviceName})`,
|
|
214
|
+
});
|
|
215
|
+
(0, chai_1.expect)(JSON.stringify(post.firstCall.args[1])).not.to.contain('{{');
|
|
216
|
+
}));
|
|
217
|
+
it('sends the built-in session_failed message when there is no template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
+
yield svc.sendTest(URL, 'slack', null, 'session_failed');
|
|
219
|
+
const text = post.firstCall.args[1].attachments[0].text;
|
|
220
|
+
(0, chai_1.expect)(text).to.contain('Session Failed');
|
|
221
|
+
(0, chai_1.expect)(text).not.to.contain('undefined');
|
|
222
|
+
}));
|
|
223
|
+
it('sends a device_new sample when no event is named, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
yield svc.sendTest(URL, 'webhook');
|
|
225
|
+
(0, chai_1.expect)(post.firstCall.args[1]).to.include({ event: 'device_new' });
|
|
226
|
+
}));
|
|
227
|
+
it('can test every event', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
228
|
+
for (const event of webhookEvents_1.EVENT_TYPES) {
|
|
229
|
+
post.resetHistory();
|
|
230
|
+
yield svc.sendTest(URL, 'slack', null, event);
|
|
231
|
+
(0, chai_1.expect)(post.calledOnce, event).to.equal(true);
|
|
232
|
+
(0, chai_1.expect)(JSON.stringify(post.firstCall.args[1]), event).not.to.contain('undefined');
|
|
233
|
+
}
|
|
234
|
+
}));
|
|
235
|
+
});
|
|
236
|
+
describe('the documented variables', () => {
|
|
237
|
+
for (const event of webhookEvents_1.EVENT_TYPES) {
|
|
238
|
+
it(`${event}: each one is in the sample, so the test message fills it`, () => {
|
|
239
|
+
const sample = webhookEvents_1.WEBHOOK_EVENTS[event].sample();
|
|
240
|
+
for (const { name } of webhookEvents_1.WEBHOOK_EVENTS[event].variables) {
|
|
241
|
+
(0, chai_1.expect)(sample, name).to.have.property(name);
|
|
242
|
+
(0, chai_1.expect)(sample[name], name).not.to.equal(undefined);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
it(`${event}: the dashboard offers exactly these as template chips`, () => {
|
|
246
|
+
(0, chai_1.expect)(webhookEventVariables_json_1.default[event]).to.deep.equal(webhookEvents_1.WEBHOOK_EVENTS[event].variables.map((v) => v.name));
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
describe('session_failed, once per session', () => {
|
|
251
|
+
it('is sent once however many times the session ends', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
252
|
+
const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
253
|
+
const dispatch = sinon_1.default.stub(svc, 'dispatchEvent').resolves();
|
|
254
|
+
yield svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'once-1' }));
|
|
255
|
+
yield svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'once-1' }));
|
|
256
|
+
yield svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'once-2' }));
|
|
257
|
+
(0, chai_1.expect)(dispatch.callCount).to.equal(2);
|
|
258
|
+
(0, chai_1.expect)(dispatch.firstCall.args[0]).to.equal('session_failed');
|
|
259
|
+
(0, chai_1.expect)(dispatch.firstCall.args[1].sessionId).to.equal('once-1');
|
|
260
|
+
}));
|
|
261
|
+
it('is sent once when two ends race', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
262
|
+
const svc = typedi_1.Container.get(NotificationService_1.NotificationService);
|
|
263
|
+
const dispatch = sinon_1.default
|
|
264
|
+
.stub(svc, 'dispatchEvent')
|
|
265
|
+
.callsFake(() => new Promise((resolve) => setTimeout(resolve, 20)));
|
|
266
|
+
yield Promise.all([
|
|
267
|
+
svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'race-1' })),
|
|
268
|
+
svc.notifySessionFailed(Object.assign(Object.assign({}, SESSION_ROW), { id: 'race-1' })),
|
|
269
|
+
]);
|
|
270
|
+
(0, chai_1.expect)(dispatch.callCount).to.equal(1);
|
|
271
|
+
}));
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
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 typedi_1 = require("typedi");
|
|
52
|
+
const deviceService = __importStar(require("../../src/data-service/device-service"));
|
|
53
|
+
const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
|
|
54
|
+
const AICommandService_1 = require("../../src/services/AICommandService");
|
|
55
|
+
const AutowaitService_1 = require("../../src/services/autowait/AutowaitService");
|
|
56
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
57
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
58
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
59
|
+
/**
|
|
60
|
+
* `xenon:` / `xe:` execute scripts for a session on this server's own phone,
|
|
61
|
+
* through the plugin's command interceptor (as Appium calls it).
|
|
62
|
+
*/
|
|
63
|
+
describe('xenon: execute scripts on this server', () => {
|
|
64
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
65
|
+
const SESSION = 'local-session-1';
|
|
66
|
+
const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
|
|
67
|
+
let interceptor;
|
|
68
|
+
let next;
|
|
69
|
+
let restoreContainer;
|
|
70
|
+
const driver = { sessionId: SESSION, caps: { udid: 'phone-1' } };
|
|
71
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
yield scratch.db.session.deleteMany({});
|
|
73
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(AICommandService_1.AICommandService);
|
|
74
|
+
sinon_1.default.stub(deviceService, 'updateCmdExecutedTime').resolves();
|
|
75
|
+
interceptor = new CommandInterceptor_1.CommandInterceptor();
|
|
76
|
+
next = sinon_1.default.stub().resolves('from the driver');
|
|
77
|
+
}));
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(SESSION);
|
|
80
|
+
sinon_1.default.restore();
|
|
81
|
+
restoreContainer();
|
|
82
|
+
});
|
|
83
|
+
/** driver.execute(script, ...args), as Appium hands it to the plugin. */
|
|
84
|
+
const execute = (script, ...scriptArgs) => interceptor.handle(next, driver, 'execute', [script, scriptArgs, SESSION], pluginArgs, true);
|
|
85
|
+
const failureOf = (p) => p.then((value) => {
|
|
86
|
+
throw new Error(`expected a failure, got ${JSON.stringify(value)}`);
|
|
87
|
+
}, (e) => e);
|
|
88
|
+
it('assertVisualState takes its condition as a plain string, as well as { instruction }', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
+
const ai = new AICommandService_1.AICommandService();
|
|
90
|
+
typedi_1.Container.set(AICommandService_1.AICommandService, ai);
|
|
91
|
+
const assert = sinon_1.default.stub(ai, 'assertVisualState').resolves({ result: true, message: 'ok' });
|
|
92
|
+
yield execute('xenon: assertVisualState', 'The cart is empty');
|
|
93
|
+
yield execute('xe: assertVisualState', { instruction: 'The cart has 2 items' });
|
|
94
|
+
(0, chai_1.expect)(assert.firstCall.args[1]).to.equal('The cart is empty');
|
|
95
|
+
(0, chai_1.expect)(assert.secondCall.args[1]).to.equal('The cart has 2 items');
|
|
96
|
+
(0, chai_1.expect)(next.called).to.equal(false);
|
|
97
|
+
}));
|
|
98
|
+
it('a session-details command answers that nothing was recorded when the session has no record', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
// The dashboard is off: no Session row. This used to fail the command (P2025).
|
|
100
|
+
const answer = yield execute('xenon: setSessionName', 'Checkout');
|
|
101
|
+
(0, chai_1.expect)(answer).to.include({ recorded: false });
|
|
102
|
+
(0, chai_1.expect)(answer.message).to.match(/no record/i);
|
|
103
|
+
(0, chai_1.expect)(next.called).to.equal(false);
|
|
104
|
+
}));
|
|
105
|
+
it('a session-details command answers with what it did, not null', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
+
var _a;
|
|
107
|
+
yield scratch.db.session.create({
|
|
108
|
+
data: {
|
|
109
|
+
id: SESSION,
|
|
110
|
+
desired_capabilities: '{}',
|
|
111
|
+
session_capabilities: '{}',
|
|
112
|
+
node_id: 'node-1',
|
|
113
|
+
has_live_video: false,
|
|
114
|
+
device_udid: 'phone-1',
|
|
115
|
+
device_platform: 'android',
|
|
116
|
+
device_version: '14',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
(0, chai_1.expect)(yield execute('xenon: setSessionName', 'Checkout')).to.deep.equal({ recorded: true });
|
|
120
|
+
(0, chai_1.expect)((_a = (yield scratch.db.session.findUnique({ where: { id: SESSION } }))) === null || _a === void 0 ? void 0 : _a.name).to.equal('Checkout');
|
|
121
|
+
}));
|
|
122
|
+
it('a xenon: script Xenon does not have fails, instead of answering null', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
const err = yield failureOf(execute('xenon: setSesionName', 'Checkout'));
|
|
124
|
+
(0, chai_1.expect)(err.message).to.match(/Unknown Xenon command "xenon: setSesionName"/);
|
|
125
|
+
(0, chai_1.expect)(err.message).to.match(/setSessionName/);
|
|
126
|
+
(0, chai_1.expect)(err.error).to.equal('unknown command');
|
|
127
|
+
(0, chai_1.expect)(next.called).to.equal(false);
|
|
128
|
+
}));
|
|
129
|
+
it("another plugin's or the driver's script still goes to the driver", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
(0, chai_1.expect)(yield execute('mobile: shell', { command: 'echo' })).to.equal('from the driver');
|
|
131
|
+
(0, chai_1.expect)(next.calledOnce).to.equal(true);
|
|
132
|
+
}));
|
|
133
|
+
it('Xenon’s own scripts are still answered here', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
const answer = yield execute('xe: setAutowaitProperties', { timeout: 1234 });
|
|
135
|
+
(0, chai_1.expect)(answer).to.include({ timeoutMs: 1234 });
|
|
136
|
+
(0, chai_1.expect)(next.called).to.equal(false);
|
|
137
|
+
}));
|
|
138
|
+
});
|