@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,266 @@
|
|
|
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 express_1 = __importDefault(require("express"));
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const typedi_1 = require("typedi");
|
|
21
|
+
const users_1 = require("../../src/app/routers/users");
|
|
22
|
+
const auth_1 = require("../../src/app/routers/auth");
|
|
23
|
+
const PasswordResetService_1 = require("../../src/services/PasswordResetService");
|
|
24
|
+
const EmailService_1 = require("../../src/services/EmailService");
|
|
25
|
+
const UserService_1 = require("../../src/services/UserService");
|
|
26
|
+
const prisma_1 = require("../../src/prisma");
|
|
27
|
+
const config_1 = require("../../src/config");
|
|
28
|
+
const passwordResetMode_1 = require("../../src/services/passwordResetMode");
|
|
29
|
+
const passwordResetLink_1 = require("../../src/services/passwordResetLink");
|
|
30
|
+
/**
|
|
31
|
+
* Where a password reset link that Xenon sends points.
|
|
32
|
+
*
|
|
33
|
+
* Through 2.14 it was built from the Host header of the request that asked
|
|
34
|
+
* for it. Anyone could ask for a reset of someone else's account with a Host
|
|
35
|
+
* of their own choosing (a matching Origin passes the CSRF check), and with
|
|
36
|
+
* SMTP set up Xenon emailed the victim a genuine link to the attacker's host:
|
|
37
|
+
* opening it handed the attacker the token, which opens the account.
|
|
38
|
+
*
|
|
39
|
+
* A link Xenon emails or logs is now built from XENON_PUBLIC_URL, the address
|
|
40
|
+
* people open the dashboard at. Without it Xenon sends no link at all: the
|
|
41
|
+
* sign-in page sends people to an administrator, and an administrator's
|
|
42
|
+
* "send reset link" hands the link back to them instead of emailing it.
|
|
43
|
+
*/
|
|
44
|
+
const RAW = 'RAWTOKEN_abcdefghijklmnopqrstuvwxyz0123456789';
|
|
45
|
+
const EVIL = 'attacker.example';
|
|
46
|
+
function withConfig(patch) {
|
|
47
|
+
const orig = {};
|
|
48
|
+
for (const k of Object.keys(patch)) {
|
|
49
|
+
orig[k] = config_1.config[k];
|
|
50
|
+
config_1.config[k] = patch[k];
|
|
51
|
+
}
|
|
52
|
+
return () => Object.assign(config_1.config, orig);
|
|
53
|
+
}
|
|
54
|
+
function until(pred_1) {
|
|
55
|
+
return __awaiter(this, arguments, void 0, function* (pred, ms = 3000) {
|
|
56
|
+
const t0 = Date.now();
|
|
57
|
+
while (!pred()) {
|
|
58
|
+
if (Date.now() - t0 > ms)
|
|
59
|
+
throw new Error('timed out waiting for the route to finish');
|
|
60
|
+
yield new Promise((r) => setTimeout(r, 5));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
describe('password reset links point at XENON_PUBLIC_URL, never the request', () => {
|
|
65
|
+
let restore;
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
sinon_1.default.restore();
|
|
68
|
+
restore === null || restore === void 0 ? void 0 : restore();
|
|
69
|
+
restore = undefined;
|
|
70
|
+
});
|
|
71
|
+
function publicApp() {
|
|
72
|
+
const app = (0, express_1.default)();
|
|
73
|
+
app.use(express_1.default.json());
|
|
74
|
+
app.use('/auth', (0, auth_1.authPublicRouter)());
|
|
75
|
+
return app;
|
|
76
|
+
}
|
|
77
|
+
function usersApp() {
|
|
78
|
+
const app = (0, express_1.default)();
|
|
79
|
+
app.use(express_1.default.json());
|
|
80
|
+
app.use((req, _res, next) => {
|
|
81
|
+
req.auth = {
|
|
82
|
+
userId: 'admin1',
|
|
83
|
+
role: 'ADMIN',
|
|
84
|
+
scopes: 'admin,devices,sessions,read',
|
|
85
|
+
};
|
|
86
|
+
next();
|
|
87
|
+
});
|
|
88
|
+
app.use('/users', (0, users_1.usersRouter)());
|
|
89
|
+
return app;
|
|
90
|
+
}
|
|
91
|
+
const active = { id: 'u1', email: 'u@x.local', name: 'U', role: 'MEMBER', status: 'ACTIVE' };
|
|
92
|
+
describe('POST /auth/forgot-password', () => {
|
|
93
|
+
it('emails a link to XENON_PUBLIC_URL whatever Host the request names', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
|
+
restore = withConfig({
|
|
95
|
+
smtpUrl: 'smtp://mail.example.com:587',
|
|
96
|
+
publicUrl: 'https://xenon.example.com',
|
|
97
|
+
});
|
|
98
|
+
sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
|
|
99
|
+
sinon_1.default
|
|
100
|
+
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
|
|
101
|
+
.resolves({ raw: RAW, id: 't1' });
|
|
102
|
+
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
103
|
+
yield (0, loopbackRequest_1.default)(publicApp())
|
|
104
|
+
.post('/auth/forgot-password')
|
|
105
|
+
.set('Host', EVIL)
|
|
106
|
+
.set('X-Forwarded-Proto', 'http')
|
|
107
|
+
.send({ email: 'u@x.local' });
|
|
108
|
+
yield until(() => send.called);
|
|
109
|
+
const text = send.firstCall.args[0].text;
|
|
110
|
+
(0, chai_1.expect)(text).to.include(`https://xenon.example.com/xenon/reset-password#${RAW}`);
|
|
111
|
+
(0, chai_1.expect)(text).to.not.include(EVIL);
|
|
112
|
+
}));
|
|
113
|
+
it("builds a link that opens when XENON_PUBLIC_URL is the dashboard's address", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
restore = withConfig({
|
|
115
|
+
smtpUrl: 'smtp://mail.example.com:587',
|
|
116
|
+
publicUrl: 'http://localhost:4723/xenon/',
|
|
117
|
+
});
|
|
118
|
+
sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
|
|
119
|
+
sinon_1.default
|
|
120
|
+
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
|
|
121
|
+
.resolves({ raw: RAW, id: 't1' });
|
|
122
|
+
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
123
|
+
yield (0, loopbackRequest_1.default)(publicApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
|
|
124
|
+
yield until(() => send.called);
|
|
125
|
+
(0, chai_1.expect)(send.firstCall.args[0].text).to.include(`http://localhost:4723/xenon/reset-password#${RAW}`);
|
|
126
|
+
(0, chai_1.expect)(send.firstCall.args[0].text).to.not.include('/xenon/xenon/');
|
|
127
|
+
}));
|
|
128
|
+
it('sends nothing, and mints nothing, when XENON_PUBLIC_URL is not set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587', publicUrl: undefined });
|
|
130
|
+
const find = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
|
|
131
|
+
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
132
|
+
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
133
|
+
const r = yield (0, loopbackRequest_1.default)(publicApp())
|
|
134
|
+
.post('/auth/forgot-password')
|
|
135
|
+
.set('Host', EVIL)
|
|
136
|
+
.send({ email: 'u@x.local' });
|
|
137
|
+
yield until(() => find.called);
|
|
138
|
+
yield new Promise((resolve) => setImmediate(resolve));
|
|
139
|
+
(0, chai_1.expect)(r.status).to.equal(204);
|
|
140
|
+
(0, chai_1.expect)(create.called).to.equal(false);
|
|
141
|
+
(0, chai_1.expect)(send.called).to.equal(false);
|
|
142
|
+
}));
|
|
143
|
+
it('sends nothing through the log fallback without XENON_PUBLIC_URL either', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
144
|
+
restore = withConfig({
|
|
145
|
+
smtpUrl: undefined,
|
|
146
|
+
passwordResetLogFallback: true,
|
|
147
|
+
publicUrl: undefined,
|
|
148
|
+
});
|
|
149
|
+
const find = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
|
|
150
|
+
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
151
|
+
yield (0, loopbackRequest_1.default)(publicApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
|
|
152
|
+
yield until(() => find.called);
|
|
153
|
+
yield new Promise((resolve) => setImmediate(resolve));
|
|
154
|
+
(0, chai_1.expect)(create.called).to.equal(false);
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
157
|
+
describe('GET /auth/options tells the sign-in page whether self-service reset works', () => {
|
|
158
|
+
it("is 'admin' with SMTP but no XENON_PUBLIC_URL, 'email' with both", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
159
|
+
restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587', publicUrl: undefined });
|
|
160
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(publicApp()).get('/auth/options')).body.passwordReset).to.equal('admin');
|
|
161
|
+
config_1.config.publicUrl = 'https://xenon.example.com';
|
|
162
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(publicApp()).get('/auth/options')).body.passwordReset).to.equal('email');
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
describe('POST /users/:id/reset-link', () => {
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
sinon_1.default.stub(prisma_1.prisma.user, 'findUnique').resolves(active);
|
|
168
|
+
sinon_1.default
|
|
169
|
+
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
|
|
170
|
+
.resolves({ raw: RAW, id: 't1' });
|
|
171
|
+
});
|
|
172
|
+
it('emails a link to XENON_PUBLIC_URL whatever Host the request names', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
173
|
+
restore = withConfig({
|
|
174
|
+
smtpUrl: 'smtp://mail.example.com:587',
|
|
175
|
+
publicUrl: 'https://xenon.example.com',
|
|
176
|
+
});
|
|
177
|
+
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
178
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp()).post('/users/u1/reset-link').set('Host', EVIL);
|
|
179
|
+
(0, chai_1.expect)(r.body).to.include({ emailed: true });
|
|
180
|
+
(0, chai_1.expect)(send.firstCall.args[0].text).to.include(`https://xenon.example.com/xenon/reset-password#${RAW}`);
|
|
181
|
+
(0, chai_1.expect)(send.firstCall.args[0].text).to.not.include(EVIL);
|
|
182
|
+
}));
|
|
183
|
+
it('without XENON_PUBLIC_URL, hands the link to the admin instead of emailing it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587', publicUrl: undefined });
|
|
185
|
+
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
186
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp()).post('/users/u1/reset-link');
|
|
187
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
188
|
+
(0, chai_1.expect)(r.body.emailed).to.equal(false);
|
|
189
|
+
(0, chai_1.expect)(r.body.link).to.match(new RegExp(`/xenon/reset-password#${RAW}$`));
|
|
190
|
+
(0, chai_1.expect)(send.called).to.equal(false);
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
describe('resetLinkBase (XENON_PUBLIC_URL)', () => {
|
|
194
|
+
// The variable is the server's address. The dashboard's own address,
|
|
195
|
+
// with /xenon (README gives http://localhost:4723/xenon/), is taken too:
|
|
196
|
+
// kept as given it made /xenon/xenon/reset-password, which the dashboard
|
|
197
|
+
// routes to the sign-in page, losing the token.
|
|
198
|
+
it('is the server address, with or without a trailing slash', () => {
|
|
199
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://xenon.example.com' })).to.equal('https://xenon.example.com');
|
|
200
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://xenon.example.com/' })).to.equal('https://xenon.example.com');
|
|
201
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'http://lab-mac:4723' })).to.equal('http://lab-mac:4723');
|
|
202
|
+
});
|
|
203
|
+
for (const publicUrl of [
|
|
204
|
+
'http://localhost:4723/xenon',
|
|
205
|
+
'http://localhost:4723/xenon/',
|
|
206
|
+
'http://localhost:4723/xenon//',
|
|
207
|
+
]) {
|
|
208
|
+
it(`takes the dashboard's address too: ${publicUrl}`, () => {
|
|
209
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl })).to.equal('http://localhost:4723');
|
|
210
|
+
(0, chai_1.expect)((0, passwordResetLink_1.buildResetLink)(String((0, passwordResetLink_1.resetLinkBase)({ publicUrl })), 'TOK')).to.equal('http://localhost:4723/xenon/reset-password#TOK');
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// The dashboard loads its pages, scripts and API from /xenon on the
|
|
214
|
+
// server's root, so it can't be served under another prefix: an address
|
|
215
|
+
// with any other path is a mistake, and no link is sent rather than one
|
|
216
|
+
// that can't open.
|
|
217
|
+
it('is null for an address with any other path', () => {
|
|
218
|
+
for (const publicUrl of [
|
|
219
|
+
'https://lab.example.com/qa/',
|
|
220
|
+
'https://lab.example.com/qa/xenon',
|
|
221
|
+
'http://localhost:4723/xenon/login',
|
|
222
|
+
]) {
|
|
223
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl }), publicUrl).to.equal(null);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
it('is null when unset, or not an http(s) address', () => {
|
|
227
|
+
for (const publicUrl of [
|
|
228
|
+
undefined,
|
|
229
|
+
'',
|
|
230
|
+
' ',
|
|
231
|
+
'not a url',
|
|
232
|
+
'ftp://x.example',
|
|
233
|
+
'javascript:alert(1)',
|
|
234
|
+
]) {
|
|
235
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl }), String(publicUrl)).to.equal(null);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
it('is null for an address with a query or fragment, which would swallow the token', () => {
|
|
239
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://x.example/?a=1' })).to.equal(null);
|
|
240
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://x.example/#a' })).to.equal(null);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('passwordResetMode', () => {
|
|
244
|
+
it("is 'email' only with both SMTP and XENON_PUBLIC_URL", () => {
|
|
245
|
+
(0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://m:587', publicUrl: 'https://x.example' })).to.equal('email');
|
|
246
|
+
(0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://m:587' })).to.equal('admin');
|
|
247
|
+
(0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ publicUrl: 'https://x.example' })).to.equal('admin');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
// The email said "1 hour" whatever XENON_RESET_TOKEN_TTL_MS was.
|
|
251
|
+
describe('the email says how long the link really lasts', () => {
|
|
252
|
+
const user = { email: 'u@x.local', name: 'U' };
|
|
253
|
+
for (const [ms, words] of [
|
|
254
|
+
[60 * 60 * 1000, '1 hour'],
|
|
255
|
+
[30 * 60 * 1000, '30 minutes'],
|
|
256
|
+
[2 * 60 * 60 * 1000, '2 hours'],
|
|
257
|
+
[90 * 60 * 1000, '90 minutes'],
|
|
258
|
+
[24 * 60 * 60 * 1000, '24 hours'],
|
|
259
|
+
[60 * 1000, '1 minute'],
|
|
260
|
+
]) {
|
|
261
|
+
it(`${words} for ${ms} ms`, () => {
|
|
262
|
+
(0, chai_1.expect)((0, passwordResetLink_1.resetEmail)(user, 'https://x/l', ms).text).to.include(`This link expires in ${words}.`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -82,7 +82,10 @@ describe('password-reset links', () => {
|
|
|
82
82
|
(0, chai_1.expect)(logged.join('\n')).to.not.include(RAW);
|
|
83
83
|
}));
|
|
84
84
|
it('with SMTP, emails the user and returns no link', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
-
restore = withConfig({
|
|
85
|
+
restore = withConfig({
|
|
86
|
+
smtpUrl: 'smtp://mail.example.com:587',
|
|
87
|
+
publicUrl: 'https://xenon.example.com',
|
|
88
|
+
});
|
|
86
89
|
sinon_1.default.stub(prisma_1.prisma.user, 'findUnique').resolves(member);
|
|
87
90
|
sinon_1.default
|
|
88
91
|
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
|
|
@@ -164,8 +167,11 @@ describe('password-reset links', () => {
|
|
|
164
167
|
(0, chai_1.expect)(r.status).to.equal(204);
|
|
165
168
|
(0, chai_1.expect)(create.called).to.equal(false);
|
|
166
169
|
}));
|
|
167
|
-
it('still mints and sends when SMTP
|
|
168
|
-
restore = withConfig({
|
|
170
|
+
it('still mints and sends when SMTP and XENON_PUBLIC_URL are configured', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
restore = withConfig({
|
|
172
|
+
smtpUrl: 'smtp://mail.example.com:587',
|
|
173
|
+
publicUrl: 'https://xenon.example.com',
|
|
174
|
+
});
|
|
169
175
|
sinon_1.default
|
|
170
176
|
.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail')
|
|
171
177
|
.resolves({ id: 'u1', email: 'u@x.local', name: 'U', status: 'ACTIVE' });
|
|
@@ -181,7 +187,7 @@ describe('password-reset links', () => {
|
|
|
181
187
|
});
|
|
182
188
|
describe('token placement (never in a URL the server sees)', () => {
|
|
183
189
|
it('puts the token in the fragment, which browsers never send', () => {
|
|
184
|
-
const link = (0, passwordResetLink_1.buildResetLink)(
|
|
190
|
+
const link = (0, passwordResetLink_1.buildResetLink)('http://lab:4723', RAW);
|
|
185
191
|
(0, chai_1.expect)(link).to.equal(`http://lab:4723/xenon/reset-password#${RAW}`);
|
|
186
192
|
(0, chai_1.expect)(new URL(link).pathname).to.not.include(RAW);
|
|
187
193
|
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
20
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
21
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
22
|
+
const page_sources_1 = require("../helpers/page-sources");
|
|
23
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
24
|
+
const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
|
|
25
|
+
const ResilioTreeHealingProvider_1 = require("../../src/services/healing/ResilioTreeHealingProvider");
|
|
26
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
27
|
+
const resilioPath_1 = require("../../src/services/healing/resilioPath");
|
|
28
|
+
const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
|
|
29
|
+
/**
|
|
30
|
+
* Self-healing's first tier, Resilio, finds an element by the path it had
|
|
31
|
+
* through the screen's element tree when its selector last worked. Through
|
|
32
|
+
* 2.14 it never found anything: the database dropped the path when it saved
|
|
33
|
+
* the selector's fingerprint, and the provider gives up without one. Its
|
|
34
|
+
* locators were also lowercase (an HTML parse of the page source), which the
|
|
35
|
+
* drivers' XPath doesn't match, and it answered with the nearest element
|
|
36
|
+
* however far it was, so with a path it would have healed to the wrong
|
|
37
|
+
* button whenever the right one was gone.
|
|
38
|
+
*
|
|
39
|
+
* Here the path is learnt from a found element, stored in a scratch
|
|
40
|
+
* database, and used on a changed screen. resiliotree runs for real; the
|
|
41
|
+
* driver is a fake that runs each XPath on the page source.
|
|
42
|
+
*/
|
|
43
|
+
const FORM_A = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
|
|
44
|
+
page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
|
|
45
|
+
page_sources_1.android.button(2, 'forgot', 'Forgot password?', '[40,560][1040,680]') +
|
|
46
|
+
page_sources_1.android.button(3, 'login', 'Log in', '[40,720][1040,840]')));
|
|
47
|
+
// The buttons moved into a new row, after a new "Sign up" button.
|
|
48
|
+
const FORM_MOVED = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
|
|
49
|
+
page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
|
|
50
|
+
page_sources_1.android.layout(2, 'actions', '[0,560][1080,1100]', page_sources_1.android.button(0, 'signup', 'Sign up', '[40,560][1040,680]') +
|
|
51
|
+
page_sources_1.android.button(1, 'forgot', 'Forgot password?', '[40,720][1040,840]') +
|
|
52
|
+
page_sources_1.android.button(2, 'login', 'Log in', '[40,880][1040,1000]'))));
|
|
53
|
+
// The button is another one now: new id, new text.
|
|
54
|
+
const FORM_RENAMED = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
|
|
55
|
+
page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
|
|
56
|
+
page_sources_1.android.button(2, 'forgot', 'Forgot password?', '[40,560][1040,680]') +
|
|
57
|
+
page_sources_1.android.button(3, 'sign_in', 'Sign in', '[40,720][1040,840]')));
|
|
58
|
+
// The button is gone.
|
|
59
|
+
const FORM_GONE = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.field(0, 'username', 'Username', '[40,240][1040,360]') +
|
|
60
|
+
page_sources_1.android.field(1, 'password', 'Password', '[40,400][1040,520]') +
|
|
61
|
+
page_sources_1.android.button(2, 'forgot', 'Forgot password?', '[40,560][1040,680]')));
|
|
62
|
+
// Two buttons the path can't tell apart.
|
|
63
|
+
const FORM_TWICE = page_sources_1.android.page(page_sources_1.android.layout(0, 'login_form', '[0,200][1080,1400]', page_sources_1.android.layout(0, '', '[0,200][1080,700]', page_sources_1.android.button(0, 'login', 'Log in', '[40,720][1040,840]')) +
|
|
64
|
+
page_sources_1.android.layout(1, '', '[0,700][1080,1400]', page_sources_1.android.button(0, 'login', 'Log in', '[40,720][1040,840]'))));
|
|
65
|
+
// A positional XPath, as an inspector writes one: the second button of the form.
|
|
66
|
+
const POSITIONAL = "//*[@resource-id='com.example.shop:id/login_form']/android.widget.Button[2]";
|
|
67
|
+
describe('Resilio healing, from the path a selector had when it worked', function () {
|
|
68
|
+
this.timeout(90000);
|
|
69
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
70
|
+
let etalons;
|
|
71
|
+
let restoreStore;
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
// The etalon service on the Prisma store (the scratch database), whatever
|
|
74
|
+
// store this process picked.
|
|
75
|
+
etalons = typedi_1.Container.get(HealEtalonService_1.HealEtalonService);
|
|
76
|
+
const service = etalons;
|
|
77
|
+
const before = service.store;
|
|
78
|
+
service.store = new prisma_store_1.PrismaHealEtalonStore();
|
|
79
|
+
restoreStore = () => {
|
|
80
|
+
service.store = before;
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
restoreStore();
|
|
85
|
+
sinon_1.default.restore();
|
|
86
|
+
});
|
|
87
|
+
describe('the fingerprint store', () => {
|
|
88
|
+
const store = () => new prisma_store_1.PrismaHealEtalonStore();
|
|
89
|
+
const fingerprint = (selector, path) => ({
|
|
90
|
+
selector,
|
|
91
|
+
strategy: 'xpath',
|
|
92
|
+
attributes: { 'resource-id': 'com.example.shop:id/login' },
|
|
93
|
+
nodeName: 'android.widget.Button',
|
|
94
|
+
path,
|
|
95
|
+
lastSeen: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
const PATH = { format: 'page-source-1', nodes: [{ tag: 'hierarchy', id: '', index: 0 }] };
|
|
98
|
+
it("keeps a selector's path", () => __awaiter(this, void 0, void 0, function* () {
|
|
99
|
+
var _a;
|
|
100
|
+
yield store().saveSignature(fingerprint('//keep-path', PATH));
|
|
101
|
+
(0, chai_1.expect)((_a = (yield store().getSignature('//keep-path'))) === null || _a === void 0 ? void 0 : _a.path).to.deep.equal(PATH);
|
|
102
|
+
}));
|
|
103
|
+
it('keeps the path when a later fingerprint comes without one', () => __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
var _a;
|
|
105
|
+
yield store().saveSignature(fingerprint('//keep-old-path', PATH));
|
|
106
|
+
yield store().saveSignature(fingerprint('//keep-old-path'));
|
|
107
|
+
(0, chai_1.expect)((_a = (yield store().getSignature('//keep-old-path'))) === null || _a === void 0 ? void 0 : _a.path).to.deep.equal(PATH);
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
/** Learn `selector` as the interceptor does after a find, and wait for it. */
|
|
111
|
+
function learn(driver, selector) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
var _a;
|
|
114
|
+
const found = yield driver.findElement('xpath', selector);
|
|
115
|
+
typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).triggerLearning(driver, ['xpath', selector], found, `learn-${selector}`);
|
|
116
|
+
for (let i = 0; i < 100; i++) {
|
|
117
|
+
const signature = yield etalons.getSignature(selector);
|
|
118
|
+
if (((_a = signature === null || signature === void 0 ? void 0 : signature.path) === null || _a === void 0 ? void 0 : _a.format) === 'page-source-1')
|
|
119
|
+
return signature;
|
|
120
|
+
yield new Promise((r) => setTimeout(r, 20));
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`no path was learnt for ${selector}`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const heal = (driver, selector, pageSource) => new ResilioTreeHealingProvider_1.ResilioTreeHealingProvider(etalons).heal({
|
|
126
|
+
sessionId: 'resilio-session',
|
|
127
|
+
driver,
|
|
128
|
+
strategy: 'xpath',
|
|
129
|
+
selector,
|
|
130
|
+
pageSource,
|
|
131
|
+
});
|
|
132
|
+
it('finds an Android element that moved in the tree', () => __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
const driver = (0, page_sources_1.pageDriver)(FORM_A);
|
|
134
|
+
yield learn(driver, POSITIONAL);
|
|
135
|
+
driver.show(FORM_MOVED);
|
|
136
|
+
const healed = yield heal(driver, POSITIONAL, FORM_MOVED);
|
|
137
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
138
|
+
(0, chai_1.expect)(healed.id).to.equal('el:com.example.shop:id/login');
|
|
139
|
+
(0, chai_1.expect)(healed.confidence).to.be.greaterThan(0.9);
|
|
140
|
+
// The locator it suggests finds that element on the new screen.
|
|
141
|
+
const again = yield driver.findElement('xpath', healed.recommendedSelector);
|
|
142
|
+
(0, chai_1.expect)(again.ELEMENT).to.equal('el:com.example.shop:id/login');
|
|
143
|
+
}));
|
|
144
|
+
it('finds an iPhone element that moved in the tree', () => __awaiter(this, void 0, void 0, function* () {
|
|
145
|
+
const before = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.field('username', 'Username', 120) +
|
|
146
|
+
page_sources_1.ios.button('forgot', 'Forgot password?', 200) +
|
|
147
|
+
page_sources_1.ios.button('login', 'Log in', 260)));
|
|
148
|
+
const after = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.field('username', 'Username', 120) +
|
|
149
|
+
page_sources_1.ios.group(page_sources_1.ios.button('signup', 'Sign up', 200) +
|
|
150
|
+
page_sources_1.ios.button('forgot', 'Forgot password?', 260) +
|
|
151
|
+
page_sources_1.ios.button('login', 'Log in', 320))));
|
|
152
|
+
const selector = '//XCUIElementTypeOther/XCUIElementTypeButton[2]';
|
|
153
|
+
const driver = (0, page_sources_1.pageDriver)(before);
|
|
154
|
+
yield learn(driver, selector);
|
|
155
|
+
driver.show(after);
|
|
156
|
+
const healed = yield heal(driver, selector, after);
|
|
157
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:login');
|
|
158
|
+
const again = yield driver.findElement('xpath', healed.recommendedSelector);
|
|
159
|
+
(0, chai_1.expect)(again.ELEMENT).to.equal('el:login');
|
|
160
|
+
}));
|
|
161
|
+
it('leaves a renamed element to the next tier rather than guess', () => __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
const driver = (0, page_sources_1.pageDriver)(FORM_A);
|
|
163
|
+
yield learn(driver, POSITIONAL);
|
|
164
|
+
driver.show(FORM_RENAMED);
|
|
165
|
+
(0, chai_1.expect)(yield heal(driver, POSITIONAL, FORM_RENAMED)).to.equal(null);
|
|
166
|
+
}));
|
|
167
|
+
it('does not take a neighbour when the element is gone', () => __awaiter(this, void 0, void 0, function* () {
|
|
168
|
+
const driver = (0, page_sources_1.pageDriver)(FORM_A);
|
|
169
|
+
yield learn(driver, POSITIONAL);
|
|
170
|
+
driver.show(FORM_GONE);
|
|
171
|
+
(0, chai_1.expect)(yield heal(driver, POSITIONAL, FORM_GONE)).to.equal(null);
|
|
172
|
+
}));
|
|
173
|
+
it('does not pick one of two elements it cannot tell apart', () => __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
const driver = (0, page_sources_1.pageDriver)(FORM_A);
|
|
175
|
+
yield learn(driver, POSITIONAL);
|
|
176
|
+
driver.show(FORM_TWICE);
|
|
177
|
+
(0, chai_1.expect)(yield heal(driver, POSITIONAL, FORM_TWICE)).to.equal(null);
|
|
178
|
+
}));
|
|
179
|
+
it('finds an element that holds others, such as a list row, after it moved', () => __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
const row = (index) => page_sources_1.android.row(index, 'logout_row', '[0,900][1080,1040]', page_sources_1.android.image(0, 'logout_icon', 'Log out icon', '[40,920][140,1020]') +
|
|
181
|
+
page_sources_1.android.text(1, 'logout_label', 'Log out', '[160,940][600,1000]'));
|
|
182
|
+
const before = page_sources_1.android.page(page_sources_1.android.layout(0, 'settings', '[0,200][1080,2000]', page_sources_1.android.text(0, 'account', 'Account', '[40,220][600,300]') + row(1)));
|
|
183
|
+
const after = page_sources_1.android.page(page_sources_1.android.layout(0, 'settings', '[0,200][1080,2000]', page_sources_1.android.text(0, 'account', 'Account', '[40,220][600,300]') +
|
|
184
|
+
page_sources_1.android.layout(1, 'danger_zone', '[0,800][1080,1200]', row(0))));
|
|
185
|
+
const selector = "//*[@resource-id='com.example.shop:id/settings']/android.widget.LinearLayout[1]";
|
|
186
|
+
const driver = (0, page_sources_1.pageDriver)(before);
|
|
187
|
+
yield learn(driver, selector);
|
|
188
|
+
driver.show(after);
|
|
189
|
+
const healed = yield heal(driver, selector, after);
|
|
190
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:com.example.shop:id/logout_row');
|
|
191
|
+
}));
|
|
192
|
+
it('answers with the element it matched, not another one its locator also finds', () => __awaiter(this, void 0, void 0, function* () {
|
|
193
|
+
const cart = page_sources_1.android.button(1, 'cart', 'Cart', '[800,40][1040,140]');
|
|
194
|
+
const before = page_sources_1.android.page(page_sources_1.android.layout(0, 'toolbar', '[0,0][1080,200]', page_sources_1.android.text(0, 'title', 'Shop', '[40,40][600,140]') + cart));
|
|
195
|
+
// A badge whose description contains "Cart" now comes before the button.
|
|
196
|
+
const after = page_sources_1.android.page(page_sources_1.android.layout(0, 'toolbar', '[0,0][1080,200]', page_sources_1.android.text(0, 'title', 'Shop', '[40,40][600,140]') +
|
|
197
|
+
page_sources_1.android.image(1, 'badge', 'Cart badge, 3 items', '[700,40][780,140]') +
|
|
198
|
+
page_sources_1.android.layout(2, 'actions', '[800,0][1080,200]', page_sources_1.android.button(0, 'cart', 'Cart', '[800,40][1040,140]'))));
|
|
199
|
+
const selector = "//*[@resource-id='com.example.shop:id/toolbar']/android.widget.Button[1]";
|
|
200
|
+
const driver = (0, page_sources_1.pageDriver)(before);
|
|
201
|
+
yield learn(driver, selector);
|
|
202
|
+
driver.show(after);
|
|
203
|
+
const healed = yield heal(driver, selector, after);
|
|
204
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:com.example.shop:id/cart');
|
|
205
|
+
const matches = yield driver.findElements('xpath', healed.recommendedSelector);
|
|
206
|
+
(0, chai_1.expect)(matches.map((m) => m.ELEMENT)).to.deep.equal(['el:com.example.shop:id/cart']);
|
|
207
|
+
}));
|
|
208
|
+
it('does not heal to an element whose text contradicts the selector', () => __awaiter(this, void 0, void 0, function* () {
|
|
209
|
+
const dialog = (label) => page_sources_1.android.page(page_sources_1.android.layout(0, 'dialog', '[0,600][1080,1400]', page_sources_1.android.text(0, 'message', 'Discard this draft?', '[40,640][1040,800]') +
|
|
210
|
+
page_sources_1.android.button(1, 'button1', label, '[600,1200][1040,1340]')));
|
|
211
|
+
const selector = "//android.widget.Button[@text='OK']";
|
|
212
|
+
const driver = (0, page_sources_1.pageDriver)(dialog('OK'));
|
|
213
|
+
yield learn(driver, selector);
|
|
214
|
+
driver.show(dialog('Delete'));
|
|
215
|
+
(0, chai_1.expect)(yield heal(driver, selector, dialog('Delete'))).to.equal(null);
|
|
216
|
+
}));
|
|
217
|
+
it('learns no path from an element that shares only its size with the one found', () => {
|
|
218
|
+
// The screen changed between the find and the learning: the button found
|
|
219
|
+
// (name "gone") isn't on it, and another button only has its height.
|
|
220
|
+
const page = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.button('next', 'Next', 300)));
|
|
221
|
+
const learnt = (0, resilioPath_1.findLearntElement)(page, 'XCUIElementTypeButton', [
|
|
222
|
+
{ name: 'name', value: 'gone' },
|
|
223
|
+
{ name: 'label', value: 'Gone' },
|
|
224
|
+
{ name: 'height', value: '44' },
|
|
225
|
+
]);
|
|
226
|
+
(0, chai_1.expect)(learnt).to.equal(null);
|
|
227
|
+
});
|
|
228
|
+
it('learns the element that has the identity the driver read', () => {
|
|
229
|
+
const page = page_sources_1.ios.page(page_sources_1.ios.group(page_sources_1.ios.button('next', 'Next', 300)));
|
|
230
|
+
const learnt = (0, resilioPath_1.findLearntElement)(page, 'XCUIElementTypeButton', [
|
|
231
|
+
{ name: 'name', value: 'next' },
|
|
232
|
+
{ name: 'height', value: '44' },
|
|
233
|
+
]);
|
|
234
|
+
(0, chai_1.expect)(learnt === null || learnt === void 0 ? void 0 : learnt.getAttribute('name')).to.equal('next');
|
|
235
|
+
});
|
|
236
|
+
it('ignores a path an older Xenon stored, and learns it again', () => __awaiter(this, void 0, void 0, function* () {
|
|
237
|
+
const selector = "//*[@resource-id='com.example.shop:id/login']";
|
|
238
|
+
yield etalons.saveSignature('xpath', selector, { nodeName: 'android.widget.Button' }, {
|
|
239
|
+
nodes: [{ tag: 'html', id: '', index: 0 }],
|
|
240
|
+
});
|
|
241
|
+
const driver = (0, page_sources_1.pageDriver)(FORM_A);
|
|
242
|
+
(0, chai_1.expect)(yield heal(driver, selector, FORM_MOVED)).to.equal(null);
|
|
243
|
+
const learnt = yield learn(driver, selector);
|
|
244
|
+
(0, chai_1.expect)(learnt.path.format).to.equal('page-source-1');
|
|
245
|
+
}));
|
|
246
|
+
it('learns the healed element’s path after a Fuzzy XML heal', () => __awaiter(this, void 0, void 0, function* () {
|
|
247
|
+
const driver = (0, page_sources_1.pageDriver)(FORM_MOVED);
|
|
248
|
+
const save = sinon_1.default.spy(etalons, 'saveSignature');
|
|
249
|
+
const orchestrator = new HealingOrchestrator_1.HealingOrchestrator(etalons);
|
|
250
|
+
// Only Fuzzy XML (tier 2), which finds the button by the words in the selector.
|
|
251
|
+
const healed = yield orchestrator.attemptHealing('fuzzy-session', Object.assign(driver, { getScreenshot: () => __awaiter(this, void 0, void 0, function* () { return 'aGVsbG8='; }) }), 'xpath', "//android.widget.Button[@text='Log in' and @resource-id='com.example.shop:id/log_in']", [2]);
|
|
252
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el:com.example.shop:id/login');
|
|
253
|
+
(0, chai_1.expect)(save.called, 'fingerprint saved').to.equal(true);
|
|
254
|
+
const path = save.lastCall.args[3];
|
|
255
|
+
(0, chai_1.expect)(path === null || path === void 0 ? void 0 : path.format).to.equal('page-source-1');
|
|
256
|
+
(0, chai_1.expect)(path.nodes[path.nodes.length - 1].id).to.equal('com.example.shop:id/login');
|
|
257
|
+
}));
|
|
258
|
+
});
|
|
259
|
+
describe('The in-memory fingerprint store', () => {
|
|
260
|
+
(0, loki_stores_1.useLokiStores)();
|
|
261
|
+
it('keeps the path when a later fingerprint comes without one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
262
|
+
var _a;
|
|
263
|
+
const store = device_store_1.DeviceStoreFactory.getHealEtalonStore();
|
|
264
|
+
const PATH = { format: 'page-source-1', nodes: [{ tag: 'hierarchy', id: '', index: 0 }] };
|
|
265
|
+
const fingerprint = { selector: '//loki-path', strategy: 'xpath', attributes: {} };
|
|
266
|
+
yield store.saveSignature(Object.assign(Object.assign({}, fingerprint), { nodeName: 'android.widget.Button', path: PATH }));
|
|
267
|
+
yield store.saveSignature(Object.assign(Object.assign({}, fingerprint), { nodeName: 'android.widget.Button', path: undefined }));
|
|
268
|
+
(0, chai_1.expect)((_a = (yield store.getSignature('//loki-path'))) === null || _a === void 0 ? void 0 : _a.path).to.deep.equal(PATH);
|
|
269
|
+
}));
|
|
270
|
+
});
|