@xenon-device-management/xenon 2.14.0 → 2.16.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 +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- 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 +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- 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 +57 -72
- 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/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -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 +83 -56
- 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/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- 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/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- 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/selector-state-flow.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/XenonCapabilityManager.spec.js +23 -0
- 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-adb-retry.spec.js +112 -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/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/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -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 +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -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/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -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/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- 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/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- 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-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -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 +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
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 CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
|
|
20
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
21
|
+
const types_1 = require("../../src/services/healing/types");
|
|
22
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
23
|
+
const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
|
|
24
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
25
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
26
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
27
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
28
|
+
/**
|
|
29
|
+
* Elements Xenon found in a screenshot (Omni-Vision's `-custom:ai-*` locators,
|
|
30
|
+
* and self-healing's OCR and Visual AI tiers) are boxes on the screen with an
|
|
31
|
+
* id of Xenon's own (`omni_*`, `healed_ocr*`, `healed_visual*`). The
|
|
32
|
+
* interceptor answers their commands itself. Through 2.14:
|
|
33
|
+
*
|
|
34
|
+
* - a `-custom:ai-*` findElement that matched nothing answered `unknown error`
|
|
35
|
+
* ("NoSuchElement: AI Vision failed…"), and self-healing then ran on it;
|
|
36
|
+
* - a healed OCR or Visual AI element was tapped during the find, and the
|
|
37
|
+
* test's own click tapped it again;
|
|
38
|
+
* - `getText` on a healed element answered Xenon's note about the match;
|
|
39
|
+
* - `setValue` never reached the element: Appium passes it (text, elementId),
|
|
40
|
+
* and the interceptor read the text as the element id.
|
|
41
|
+
*
|
|
42
|
+
* No AI provider, OCR or phone: healing, OCR and the driver are stubbed.
|
|
43
|
+
*/
|
|
44
|
+
describe('Elements Xenon found in a screenshot', () => {
|
|
45
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
46
|
+
const SESSION = 'visual-session';
|
|
47
|
+
let pluginArgs;
|
|
48
|
+
let restoreContainer;
|
|
49
|
+
let attemptHealing;
|
|
50
|
+
let driver;
|
|
51
|
+
let omni;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs);
|
|
54
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
|
|
55
|
+
typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
|
|
56
|
+
attemptHealing = sinon_1.default.stub().resolves(null);
|
|
57
|
+
typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
|
|
58
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
|
|
59
|
+
omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
60
|
+
driver = {
|
|
61
|
+
sessionId: SESSION,
|
|
62
|
+
caps: { platformName: 'Android' },
|
|
63
|
+
performActions: sinon_1.default.stub().resolves(),
|
|
64
|
+
releaseActions: sinon_1.default.stub().resolves(),
|
|
65
|
+
findElement: sinon_1.default.stub().rejects(new Error('unexpected find')),
|
|
66
|
+
active: sinon_1.default.stub().resolves({ 'element-6066-11e4-a52e-4f735466cecf': 'field-1' }),
|
|
67
|
+
setValue: sinon_1.default.stub().resolves(null),
|
|
68
|
+
elementEnabled: sinon_1.default.stub().resolves(true),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
restoreContainer();
|
|
73
|
+
sinon_1.default.restore();
|
|
74
|
+
});
|
|
75
|
+
const run = (command, args, next = () => undefined) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return next(); }), driver, command, [...args, SESSION], pluginArgs, false);
|
|
76
|
+
const failure = (promise) => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
try {
|
|
78
|
+
yield promise;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return err;
|
|
82
|
+
}
|
|
83
|
+
throw new Error('expected the command to fail');
|
|
84
|
+
});
|
|
85
|
+
const tapsAt = () => driver.performActions.getCalls().map((c) => {
|
|
86
|
+
const move = c.args[0][0].actions[0];
|
|
87
|
+
return { x: move.x, y: move.y };
|
|
88
|
+
});
|
|
89
|
+
describe('a -custom:ai-* find that matches nothing', () => {
|
|
90
|
+
it('fails with the standard "no such element" error for ai-text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
sinon_1.default.stub(omni, 'findByText').resolves([]);
|
|
92
|
+
const err = yield failure(run('findElement', ['-custom:ai-text', 'Checkout']));
|
|
93
|
+
(0, chai_1.expect)(err.error).to.equal('no such element');
|
|
94
|
+
(0, chai_1.expect)(err.w3cStatus).to.equal(404);
|
|
95
|
+
}));
|
|
96
|
+
it('fails with the standard "no such element" error for ai-icon', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
+
sinon_1.default.stub(omni, 'findByIcon').resolves(null);
|
|
98
|
+
const err = yield failure(run('findElement', ['-custom:ai-icon', 'a cart icon']));
|
|
99
|
+
(0, chai_1.expect)(err.error).to.equal('no such element');
|
|
100
|
+
}));
|
|
101
|
+
it('is not handed to self-healing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
+
sinon_1.default.stub(omni, 'findByText').resolves([]);
|
|
103
|
+
yield failure(run('findElement', ['-custom:ai-text', 'Checkout']));
|
|
104
|
+
(0, chai_1.expect)(attemptHealing.called).to.equal(false);
|
|
105
|
+
}));
|
|
106
|
+
it('still answers an empty list to findElements', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
|
+
sinon_1.default.stub(omni, 'findByText').resolves([]);
|
|
108
|
+
(0, chai_1.expect)(yield run('findElements', ['-custom:ai-text', 'Checkout'])).to.deep.equal([]);
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
describe('a healed OCR or Visual AI element', () => {
|
|
112
|
+
const healedBy = (id, tier, text) => (Object.assign({ id,
|
|
113
|
+
tier, confidence: 0.9, originalSelector: "//*[@text='Login']", originalStrategy: 'xpath', recommendedSelector: 'ocr:text="Login"', recommendedStrategy: 'xenon:visual', message: 'Found text "Login" via local OCR (90% confidence)', rect: { x: 100, y: 400, width: 80, height: 24 } }, (text !== undefined ? { text } : {})));
|
|
114
|
+
const findHealed = (healed) => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
attemptHealing.resolves(healed);
|
|
116
|
+
return run('findElement', ['xpath', "//*[@text='Login']"], () => {
|
|
117
|
+
throw Object.assign(new Error('NoSuchElement: gone'), { name: 'NoSuchElementError' });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it('is not tapped during the find', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
+
const found = yield findHealed(healedBy('healed_ocr_1', types_1.HealingTier.TIER_3_LOCAL_OCR, 'Login'));
|
|
122
|
+
(0, chai_1.expect)(found).to.deep.include({ 'element-6066-11e4-a52e-4f735466cecf': 'healed_ocr_1' });
|
|
123
|
+
(0, chai_1.expect)(driver.performActions.called, 'tapped during the find').to.equal(false);
|
|
124
|
+
}));
|
|
125
|
+
it("is tapped once, by the test's own click", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
yield findHealed(healedBy('healed_visual_1', types_1.HealingTier.TIER_4_VISUAL_AI));
|
|
127
|
+
yield run('click', ['healed_visual_1']);
|
|
128
|
+
(0, chai_1.expect)(tapsAt()).to.deep.equal([{ x: 140, y: 412 }]);
|
|
129
|
+
}));
|
|
130
|
+
it('answers getText with the text OCR read, not a note about the match', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
yield findHealed(healedBy('healed_ocr_2', types_1.HealingTier.TIER_3_LOCAL_OCR, 'Login'));
|
|
132
|
+
(0, chai_1.expect)(yield run('getText', ['healed_ocr_2'])).to.equal('Login');
|
|
133
|
+
}));
|
|
134
|
+
it('refuses getText when Visual AI found it, since no text was read', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
yield findHealed(healedBy('healed_visual_2', types_1.HealingTier.TIER_4_VISUAL_AI));
|
|
136
|
+
const err = yield failure(run('getText', ['healed_visual_2']));
|
|
137
|
+
(0, chai_1.expect)(err.error).to.equal('unsupported operation');
|
|
138
|
+
(0, chai_1.expect)(err.message).to.match(/text/i);
|
|
139
|
+
}));
|
|
140
|
+
});
|
|
141
|
+
describe('setValue on an element found in a screenshot', () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
omni.remember(SESSION, {
|
|
144
|
+
id: 'omni_ocr_field',
|
|
145
|
+
text: 'Email',
|
|
146
|
+
rect: { x: 10, y: 20, width: 100, height: 40 },
|
|
147
|
+
confidence: 0.9,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
it('taps it, then types into the field that has the keyboard focus', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
yield run('setValue', ['me@example.com', 'omni_ocr_field']);
|
|
152
|
+
(0, chai_1.expect)(tapsAt()).to.deep.equal([{ x: 60, y: 40 }]);
|
|
153
|
+
(0, chai_1.expect)(driver.active.calledAfter(driver.performActions)).to.equal(true);
|
|
154
|
+
(0, chai_1.expect)(driver.setValue.firstCall.args.slice(0, 2)).to.deep.equal([
|
|
155
|
+
'me@example.com',
|
|
156
|
+
'field-1',
|
|
157
|
+
]);
|
|
158
|
+
}));
|
|
159
|
+
it('refuses before tapping when the driver cannot tell which field has the focus', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
delete driver.active;
|
|
161
|
+
const err = yield failure(run('setValue', ['me@example.com', 'omni_ocr_field']));
|
|
162
|
+
(0, chai_1.expect)(err.error).to.equal('unsupported operation');
|
|
163
|
+
(0, chai_1.expect)(driver.performActions.called, 'tapped').to.equal(false);
|
|
164
|
+
}));
|
|
165
|
+
it('says so when no field took the focus after the tap', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
166
|
+
driver.active.rejects(new Error('no element has focus'));
|
|
167
|
+
const err = yield failure(run('setValue', ['me@example.com', 'omni_ocr_field']));
|
|
168
|
+
(0, chai_1.expect)(err.error).to.equal('element not interactable');
|
|
169
|
+
(0, chai_1.expect)(driver.setValue.called).to.equal(false);
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
describe('other commands on an element found in a screenshot', () => {
|
|
173
|
+
it("refuses them clearly instead of sending Xenon's id to the driver", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
174
|
+
omni.remember(SESSION, {
|
|
175
|
+
id: 'omni_ai_cart',
|
|
176
|
+
rect: { x: 0, y: 0, width: 40, height: 40 },
|
|
177
|
+
confidence: 0.85,
|
|
178
|
+
});
|
|
179
|
+
const next = sinon_1.default.stub();
|
|
180
|
+
const err = yield failure(run('getAttribute', ['content-desc', 'omni_ai_cart'], next));
|
|
181
|
+
(0, chai_1.expect)(err.error).to.equal('unsupported operation');
|
|
182
|
+
(0, chai_1.expect)(next.called, 'sent to the driver').to.equal(false);
|
|
183
|
+
}));
|
|
184
|
+
});
|
|
185
|
+
describe('autowait before setValue on a real element', () => {
|
|
186
|
+
it('waits for the element, not for an element named after the text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
|
+
pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { autowait: Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs.autowait), { enabled: true }) });
|
|
188
|
+
yield run('setValue', ['hello', 'el-1']);
|
|
189
|
+
(0, chai_1.expect)(driver.elementEnabled.firstCall.args[0]).to.equal('el-1');
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
const chai_1 = require("chai");
|
|
13
|
+
const iosDriverLog_1 = require("../../src/services/logcat/iosDriverLog");
|
|
14
|
+
// The driver's own log class (test/helpers/driver-log.ts): an iPhone's and a
|
|
15
|
+
// simulator's `logs.syslog` extend it, so what Xenon reads is what the driver keeps.
|
|
16
|
+
const driver_log_1 = require("../helpers/driver-log");
|
|
17
|
+
const IPHONE_ERROR = 'Oct 5 20:31:40 iPhone SpringBoard(FrontBoard)[57] <Error>: Failed to launch com.example.shop';
|
|
18
|
+
const IPHONE_NOTICE = 'Oct 5 20:31:41 iPhone backboardd[61] <Notice>: touch began';
|
|
19
|
+
const SIM_FAULT = '2026-10-05 20:31:40.123 F Shop[4127:8812] (UIKitCore) [com.apple.UIKit:Crash] Terminating app';
|
|
20
|
+
const SIM_DEFAULT = '2026-10-05 20:31:40.200 Df SpringBoard[1234:5678] (FrontBoard) launched';
|
|
21
|
+
describe('iosDriverLog', () => {
|
|
22
|
+
describe('iosLineLevel', () => {
|
|
23
|
+
it("reads an iPhone's syslog level", () => {
|
|
24
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(IPHONE_ERROR)).to.equal('E');
|
|
25
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(IPHONE_NOTICE)).to.equal('I');
|
|
26
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(IPHONE_ERROR.replace('<Error>', '<Fault>'))).to.equal('F');
|
|
27
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(IPHONE_ERROR.replace('<Error>', '<Warning>'))).to.equal('W');
|
|
28
|
+
});
|
|
29
|
+
it("reads a simulator's compact type", () => {
|
|
30
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(SIM_FAULT)).to.equal('F');
|
|
31
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(SIM_DEFAULT)).to.equal('I');
|
|
32
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(SIM_DEFAULT.replace(' Df ', ' E '))).to.equal('E');
|
|
33
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)(SIM_DEFAULT.replace(' Df ', ' Db '))).to.equal('D');
|
|
34
|
+
});
|
|
35
|
+
it('takes a line with no level of its own as I', () => {
|
|
36
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineLevel)('\t0 Shop 0x0000000100abc123 main + 42')).to.equal('I');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('recordFromDriverLog', () => {
|
|
40
|
+
it('keeps the line as printed, at the time it reached the server', () => {
|
|
41
|
+
const rec = (0, iosDriverLog_1.recordFromDriverLog)({
|
|
42
|
+
timestamp: 1791000000123,
|
|
43
|
+
level: 'ALL',
|
|
44
|
+
message: IPHONE_ERROR,
|
|
45
|
+
});
|
|
46
|
+
(0, chai_1.expect)(rec).to.include({ ts: 1791000000123, level: 'E', message: IPHONE_ERROR });
|
|
47
|
+
(0, chai_1.expect)((0, iosDriverLog_1.iosLineText)(rec)).to.equal(IPHONE_ERROR);
|
|
48
|
+
});
|
|
49
|
+
it("drops what `log stream` prints before the simulator's first line", () => {
|
|
50
|
+
for (const banner of [
|
|
51
|
+
'Filtering the log data using "subsystem != \\"com.apple.CoreTelephony\\""',
|
|
52
|
+
'Timestamp Ty Process[PID:TID]',
|
|
53
|
+
]) {
|
|
54
|
+
(0, chai_1.expect)((0, iosDriverLog_1.recordFromDriverLog)({ timestamp: 1, message: banner }), banner).to.equal(null);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it("keeps a crash report's continuation lines, and drops empty ones", () => {
|
|
58
|
+
(0, chai_1.expect)((0, iosDriverLog_1.recordFromDriverLog)({ timestamp: 1, message: '\tThread 0 Crashed:' })).to.not.equal(null);
|
|
59
|
+
(0, chai_1.expect)((0, iosDriverLog_1.recordFromDriverLog)({ timestamp: 1, message: ' ' })).to.equal(null);
|
|
60
|
+
(0, chai_1.expect)((0, iosDriverLog_1.recordFromDriverLog)(undefined)).to.equal(null);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("the driver's own log", () => {
|
|
64
|
+
it('is something Xenon can listen to', () => {
|
|
65
|
+
(0, chai_1.expect)((0, iosDriverLog_1.isDriverLog)(new driver_log_1.TestDriverLog())).to.equal(true);
|
|
66
|
+
(0, chai_1.expect)((0, iosDriverLog_1.isDriverLog)(undefined)).to.equal(false);
|
|
67
|
+
(0, chai_1.expect)((0, iosDriverLog_1.isDriverLog)({ on() { } })).to.equal(false);
|
|
68
|
+
});
|
|
69
|
+
it('gives the lines it holds, oldest first, without taking them from the test', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
const driverLog = new driver_log_1.TestDriverLog();
|
|
71
|
+
driverLog.line('first');
|
|
72
|
+
driverLog.line('second');
|
|
73
|
+
driverLog.line('third');
|
|
74
|
+
(0, chai_1.expect)((0, iosDriverLog_1.bufferedDriverLog)(driverLog).map((e) => e.message)).to.deep.equal([
|
|
75
|
+
'first',
|
|
76
|
+
'second',
|
|
77
|
+
'third',
|
|
78
|
+
]);
|
|
79
|
+
// A test's own `getLog('syslog')` still gets every line.
|
|
80
|
+
const forTheTest = yield driverLog.getLogs();
|
|
81
|
+
(0, chai_1.expect)(forTheTest.map((e) => e.message)).to.deep.equal([
|
|
82
|
+
'first',
|
|
83
|
+
'second',
|
|
84
|
+
'third',
|
|
85
|
+
]);
|
|
86
|
+
}));
|
|
87
|
+
it('gives nothing for a log of another shape', () => {
|
|
88
|
+
(0, chai_1.expect)((0, iosDriverLog_1.bufferedDriverLog)({})).to.deep.equal([]);
|
|
89
|
+
(0, chai_1.expect)((0, iosDriverLog_1.bufferedDriverLog)({ logs: { rvalues: () => [1] } })).to.deep.equal([]);
|
|
90
|
+
(0, chai_1.expect)((0, iosDriverLog_1.bufferedDriverLog)({
|
|
91
|
+
logs: { rvalues: () => [1] },
|
|
92
|
+
_deserializeEntry: () => {
|
|
93
|
+
throw new Error('changed');
|
|
94
|
+
},
|
|
95
|
+
})).to.deep.equal([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// Lines as an iPhone 14 Plus (iOS 26.5) printed them during a session on
|
|
99
|
+
// Settings (com.apple.Preferences), pid 8429.
|
|
100
|
+
describe('IosAppLines', () => {
|
|
101
|
+
const at = (proc, text, level = 'Notice') => `Oct 5 23:48:33 Rabindras-iPhone ${proc} <${level}>: ${text}`;
|
|
102
|
+
const ANNOUNCE = at('runningboardd(RunningBoard)[34]', 'Acquiring assertion targeting [app<com.apple.Preferences(FEEDEEEE-DDDD)>:8429] from originator [osservice<com.apple.SpringBoard>:36947]');
|
|
103
|
+
// Written by the app's own code: no library, or the app's own binary.
|
|
104
|
+
const APP = at('Preferences[8429]', 'loading content for context');
|
|
105
|
+
const DAEMON = at('duetexpertd(DuetExpertCenter)[412]', 'Updating predictions');
|
|
106
|
+
const keeps = (f, m) => f.keeps(m, (0, iosDriverLog_1.iosLineLevel)(m));
|
|
107
|
+
it("keeps the app's own lines, the app manager's lines about it and faults, and leaves the rest", () => {
|
|
108
|
+
const f = new iosDriverLog_1.IosAppLines('com.apple.Preferences');
|
|
109
|
+
(0, chai_1.expect)(keeps(f, ANNOUNCE), 'the app manager names the app').to.equal(true);
|
|
110
|
+
(0, chai_1.expect)(keeps(f, APP), "the app's own code").to.equal(true);
|
|
111
|
+
(0, chai_1.expect)(keeps(f, at('Preferences(Preferences)[8429]', 'opened a pane')), 'its own binary').to.equal(true);
|
|
112
|
+
(0, chai_1.expect)(keeps(f, DAEMON), 'daemon chatter').to.equal(false);
|
|
113
|
+
(0, chai_1.expect)(keeps(f, at('wifid(WiFiPolicy)[61]', 'link quality dropped', 'Fault')), 'a fault').to.equal(true);
|
|
114
|
+
(0, chai_1.expect)(keeps(f, at('trustd(Security)[127]', 'SecKeyVerifySignature failed', 'Error'))).to.equal(false);
|
|
115
|
+
});
|
|
116
|
+
// Measured on Edge: 8,839 lines from its process, 5 of them its own code.
|
|
117
|
+
it("leaves out the frameworks' ordinary lines inside the app, and keeps their errors", () => {
|
|
118
|
+
const f = new iosDriverLog_1.IosAppLines('com.apple.Preferences');
|
|
119
|
+
f.learn(ANNOUNCE);
|
|
120
|
+
(0, chai_1.expect)(keeps(f, at('Preferences(UIKitCore)[8429]', 'Ending task with identifier 43'))).to.equal(false);
|
|
121
|
+
(0, chai_1.expect)(keeps(f, at('Preferences(CFNetwork)[8429]', 'Task finished', 'Error'))).to.equal(true);
|
|
122
|
+
(0, chai_1.expect)(keeps(f, at('Preferences(WebKit)[8429]', 'WebContent crashed', 'Fault'))).to.equal(true);
|
|
123
|
+
});
|
|
124
|
+
it('keeps a crash report or a memory kill that names the app by its executable', () => {
|
|
125
|
+
const f = new iosDriverLog_1.IosAppLines('com.apple.Preferences');
|
|
126
|
+
f.learn(ANNOUNCE);
|
|
127
|
+
f.learn(APP);
|
|
128
|
+
(0, chai_1.expect)(keeps(f, at('ReportCrash[9001]', 'Formulating fatal 309 report for corpse[8429] Preferences'))).to.equal(true);
|
|
129
|
+
(0, chai_1.expect)(keeps(f, at('ReportCrash[9001]', 'Saved crash report for Preferences[8429]'))).to.equal(true);
|
|
130
|
+
(0, chai_1.expect)(keeps(f, 'Oct 5 23:48:40 Rabindras-iPhone kernel[0] <Notice>: memorystatus: killing pid 8429 [Preferences]')).to.equal(true);
|
|
131
|
+
// A relaunch: a new pid, the same executable.
|
|
132
|
+
(0, chai_1.expect)(keeps(f, at('Preferences[8447]', 'scene connected'))).to.equal(true);
|
|
133
|
+
});
|
|
134
|
+
it("leaves out the daemons that only repeat the app's state", () => {
|
|
135
|
+
const f = new iosDriverLog_1.IosAppLines('com.apple.Preferences');
|
|
136
|
+
f.learn(ANNOUNCE);
|
|
137
|
+
for (const line of [
|
|
138
|
+
at('CommCenter(RunningBoardServices)[100]', 'Received state update for 8429 (app<com.apple.Preferences(FEEDEEEE)>, running-active-NotVisible'),
|
|
139
|
+
at('mobileassetd(MobileAssetDaemon)[120]', 'com.apple.Preferences issued query command'),
|
|
140
|
+
]) {
|
|
141
|
+
(0, chai_1.expect)(keeps(f, line), line).to.equal(false);
|
|
142
|
+
}
|
|
143
|
+
(0, chai_1.expect)(keeps(f, at('SpringBoard(FrontBoard)[36947]', 'Application com.apple.Preferences exited'))).to.equal(true);
|
|
144
|
+
});
|
|
145
|
+
it('lets a line with no header follow the line before it', () => {
|
|
146
|
+
const f = new iosDriverLog_1.IosAppLines('com.apple.Preferences');
|
|
147
|
+
(0, chai_1.expect)(keeps(f, ANNOUNCE)).to.equal(true);
|
|
148
|
+
(0, chai_1.expect)(keeps(f, ' AssetLocale = "en_IN";')).to.equal(true);
|
|
149
|
+
(0, chai_1.expect)(keeps(f, DAEMON)).to.equal(false);
|
|
150
|
+
(0, chai_1.expect)(keeps(f, ' AssetLocale = "en_IN";')).to.equal(false);
|
|
151
|
+
});
|
|
152
|
+
it("reads a simulator's compact lines the same way", () => {
|
|
153
|
+
const f = new iosDriverLog_1.IosAppLines('com.example.shop');
|
|
154
|
+
const sim = (proc, text, type = 'Df') => `2026-10-05 20:31:40.123 ${type} ${proc} ${text}`;
|
|
155
|
+
(0, chai_1.expect)(keeps(f, sim('runningboardd[90:1200]', 'Acquiring assertion [app<com.example.shop(UUID)>:4127]'))).to.equal(true);
|
|
156
|
+
(0, chai_1.expect)(keeps(f, sim('Shop[4127:8812]', '(Shop) launched'))).to.equal(true);
|
|
157
|
+
(0, chai_1.expect)(keeps(f, sim('Shop[4127:8812]', '(UIKitCore) scene active'))).to.equal(false);
|
|
158
|
+
(0, chai_1.expect)(keeps(f, sim('Shop[4127:8812]', '(CFNetwork) task failed', 'E'))).to.equal(true);
|
|
159
|
+
(0, chai_1.expect)(keeps(f, sim('locationd[77:300]', 'region update'))).to.equal(false);
|
|
160
|
+
(0, chai_1.expect)(keeps(f, sim('Shop[4127:8812]', 'Terminating app', 'F'))).to.equal(true);
|
|
161
|
+
});
|
|
162
|
+
it("doesn't take a bundle id's dots as any character", () => {
|
|
163
|
+
const f = new iosDriverLog_1.IosAppLines('com.example.shop');
|
|
164
|
+
f.learn('Oct 5 23:48:33 iPhone runningboardd[34] <Notice>: [app<comXexampleXshop(UUID)>:5555]');
|
|
165
|
+
(0, chai_1.expect)(keeps(f, 'Oct 5 23:48:33 iPhone Other[5555] <Notice>: hello')).to.equal(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
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 = __importDefault(require("chai"));
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const node_simctl_1 = require("node-simctl");
|
|
52
|
+
const DeviceUtils = __importStar(require("../../src/device-utils"));
|
|
53
|
+
const db_1 = require("../../src/data-service/db");
|
|
54
|
+
const device_service_1 = require("../../src/data-service/device-service");
|
|
55
|
+
const IOSDeviceManager_1 = __importDefault(require("../../src/device-managers/IOSDeviceManager"));
|
|
56
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
57
|
+
const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
|
|
58
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
59
|
+
const XenonCapabilityManager_1 = require("../../src/XenonCapabilityManager");
|
|
60
|
+
const test_container_1 = require("../helpers/test-container");
|
|
61
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
62
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
63
|
+
const expect = chai_1.default.expect;
|
|
64
|
+
/**
|
|
65
|
+
* A simulator's WebDriverAgent is the XCUITest driver's: the driver builds,
|
|
66
|
+
* installs and launches it on the session's `wdaLocalPort` once Appium
|
|
67
|
+
* creates the session, as plain Appium does. Xenon runs WDA only on an
|
|
68
|
+
* iPhone (go-ios `runwda`, for the live preview).
|
|
69
|
+
*
|
|
70
|
+
* Allocation used to require WDA to answer on the simulator's port before
|
|
71
|
+
* handing it over, and to "recover" it by rebooting the simulator, which
|
|
72
|
+
* starts no WDA. So every session on a simulator whose WDA wasn't already up
|
|
73
|
+
* was refused: "Device <udid> is unhealthy and could not be autonomously
|
|
74
|
+
* recovered."
|
|
75
|
+
*/
|
|
76
|
+
describe('iOS simulator sessions: the driver starts WebDriverAgent', () => {
|
|
77
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
78
|
+
(0, loki_stores_1.useLokiStores)();
|
|
79
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
80
|
+
const HOST = 'http://127.0.0.1:4723';
|
|
81
|
+
const SIMULATOR = '35429994-B029-4AF4-9634-880B89DAC782';
|
|
82
|
+
const IPHONE = '00008150-000E78612168C01C';
|
|
83
|
+
const pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, {
|
|
84
|
+
platform: 'ios',
|
|
85
|
+
iosDeviceType: 'both',
|
|
86
|
+
});
|
|
87
|
+
const device = (udid, over = {}) => (Object.assign({ udid, name: 'iPhone 16 Pro', platform: 'ios', sdk: '18.4', host: HOST, busy: false, offline: false, userBlocked: false, state: 'Booted', deviceType: 'simulator', realDevice: false,
|
|
88
|
+
// Leased by discovery for a booted simulator; nothing listens on it
|
|
89
|
+
// until the driver launches WDA there.
|
|
90
|
+
wdaLocalPort: 8102, mjpegServerPort: 9102, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
|
|
91
|
+
let wdaAsked;
|
|
92
|
+
let simctlCommands;
|
|
93
|
+
before(() => (0, test_container_1.resetTestContainer)());
|
|
94
|
+
after(() => (0, test_container_1.resetTestContainer)());
|
|
95
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
sandbox.stub(process, 'kill');
|
|
97
|
+
(yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
|
|
98
|
+
yield scratch.db.lease.deleteMany({});
|
|
99
|
+
sandbox.stub(DeviceUtils, 'setUtilizationTime').resolves();
|
|
100
|
+
sandbox.stub(IOSStreamService_1.default.prototype, 'getStreamStatus').returns(undefined);
|
|
101
|
+
// Nothing answers on the simulator's port, as on a freshly booted one.
|
|
102
|
+
wdaAsked = sandbox.stub(WDAClient_1.WDAClient.prototype, 'verifyWDAStatus').resolves(false);
|
|
103
|
+
// Every simctl call (shutdown, boot, list) goes through exec.
|
|
104
|
+
simctlCommands = [];
|
|
105
|
+
sandbox.stub(node_simctl_1.Simctl.prototype, 'exec').callsFake((subcommand) => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
+
simctlCommands.push(subcommand);
|
|
107
|
+
return { stdout: '', stderr: '', code: 0 };
|
|
108
|
+
}));
|
|
109
|
+
(0, test_container_1.createTestXenonManager)(pluginArgs);
|
|
110
|
+
}));
|
|
111
|
+
afterEach(() => sandbox.restore());
|
|
112
|
+
const caps = (udid) => ({
|
|
113
|
+
alwaysMatch: {
|
|
114
|
+
platformName: 'iOS',
|
|
115
|
+
'appium:automationName': 'XCUITest',
|
|
116
|
+
'appium:udid': udid,
|
|
117
|
+
},
|
|
118
|
+
firstMatch: [{}],
|
|
119
|
+
});
|
|
120
|
+
it('allocates a booted simulator whose WebDriverAgent is not running yet', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
+
yield (0, device_service_1.addNewDevice)([device(SIMULATOR)]);
|
|
122
|
+
const requested = caps(SIMULATOR);
|
|
123
|
+
const allocated = yield DeviceUtils.allocateDeviceForSession(requested, 2000, 100, pluginArgs);
|
|
124
|
+
expect(allocated.udid).to.equal(SIMULATOR);
|
|
125
|
+
expect(wdaAsked.called, 'asked WebDriverAgent').to.equal(false);
|
|
126
|
+
expect(simctlCommands, 'rebooted the simulator').to.not.include('shutdown');
|
|
127
|
+
expect(simctlCommands, 'rebooted the simulator').to.not.include('boot');
|
|
128
|
+
// The driver gets the port to launch its own WDA on, and no URL of one.
|
|
129
|
+
const fm = requested.firstMatch[0];
|
|
130
|
+
expect(fm['appium:wdaLocalPort']).to.equal(8102);
|
|
131
|
+
expect(fm).to.not.have.property('appium:webDriverAgentUrl');
|
|
132
|
+
}));
|
|
133
|
+
it('allocates a shut-down simulator, which the driver boots', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
// Discovery leases no port to a shut-down simulator.
|
|
135
|
+
yield (0, device_service_1.addNewDevice)([
|
|
136
|
+
device(SIMULATOR, { state: 'Shutdown', wdaLocalPort: undefined, mjpegServerPort: 9103 }),
|
|
137
|
+
]);
|
|
138
|
+
const requested = caps(SIMULATOR);
|
|
139
|
+
const allocated = yield DeviceUtils.allocateDeviceForSession(requested, 2000, 100, pluginArgs);
|
|
140
|
+
expect(allocated.udid).to.equal(SIMULATOR);
|
|
141
|
+
expect(wdaAsked.called).to.equal(false);
|
|
142
|
+
expect(simctlCommands).to.not.include('boot');
|
|
143
|
+
// It gets one now, for the driver's WDA.
|
|
144
|
+
expect(requested.firstMatch[0]['appium:wdaLocalPort']).to.be.a('number');
|
|
145
|
+
}));
|
|
146
|
+
describe('a live preview open on the phone', () => {
|
|
147
|
+
// The preview's stream entry for the phone: on an iPhone, the WDA Xenon
|
|
148
|
+
// runs; on a simulator, at most an attach to a driver's WDA, which goes
|
|
149
|
+
// with the session that started it.
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
IOSStreamService_1.default.prototype.getStreamStatus.returns({
|
|
152
|
+
status: 'running',
|
|
153
|
+
wdaPort: 8150,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it("never points a simulator session at it: the driver's own WDA, on the simulator's port", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
|
+
const requested = caps(SIMULATOR);
|
|
158
|
+
yield (0, XenonCapabilityManager_1.iOSCapabilities)(requested, device(SIMULATOR));
|
|
159
|
+
const fm = requested.firstMatch[0];
|
|
160
|
+
expect(fm).to.not.have.property('appium:webDriverAgentUrl');
|
|
161
|
+
expect(fm['appium:wdaLocalPort']).to.equal(8102);
|
|
162
|
+
expect(fm['appium:mjpegServerPort']).to.equal(9102);
|
|
163
|
+
}));
|
|
164
|
+
it('still points an iPhone session at the WDA Xenon runs there', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
165
|
+
const requested = caps(IPHONE);
|
|
166
|
+
yield (0, XenonCapabilityManager_1.iOSCapabilities)(requested, device(IPHONE, { realDevice: true, deviceType: 'real' }));
|
|
167
|
+
const fm = requested.firstMatch[0];
|
|
168
|
+
expect(fm['appium:webDriverAgentUrl']).to.equal('http://127.0.0.1:8150');
|
|
169
|
+
expect(fm).to.not.have.property('appium:wdaLocalPort');
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
describe('an iPhone, whose WebDriverAgent Xenon runs', () => {
|
|
173
|
+
const iphone = () => device(IPHONE, { realDevice: true, deviceType: 'real' });
|
|
174
|
+
it('is still checked: ready when WebDriverAgent answers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
175
|
+
wdaAsked.resolves(true);
|
|
176
|
+
const manager = new IOSDeviceManager_1.default({ pluginArgs });
|
|
177
|
+
expect(yield manager.readyForSession(iphone())).to.equal(true);
|
|
178
|
+
expect(wdaAsked.calledOnceWith(IPHONE)).to.equal(true);
|
|
179
|
+
}));
|
|
180
|
+
it('is still refused when WebDriverAgent does not answer and cannot be started', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
181
|
+
const manager = new IOSDeviceManager_1.default({ pluginArgs });
|
|
182
|
+
const recover = sandbox.stub(manager, 'recoverHealth').resolves(false);
|
|
183
|
+
expect(yield manager.readyForSession(iphone())).to.equal(false);
|
|
184
|
+
expect(recover.calledOnce).to.equal(true);
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
});
|