@xenon-device-management/xenon 2.15.0 → 2.17.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.
Files changed (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -0,0 +1,469 @@
1
+ <!doctype html>
2
+ <!--
3
+ A made-up shop app's checkout screen: what the sample phone "shows" in the
4
+ dashboard screenshots (device control's live preview, a session's recording).
5
+
6
+ 1344 x 2992 CSS px, the Pixel 8 Pro's screen. It is drawn at 448 x 997 dp and
7
+ scaled by 3, so the sizes below read like an Android layout. System fonts
8
+ only, so nothing is fetched.
9
+ -->
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=1344, initial-scale=1" />
14
+ <title>Checkout</title>
15
+ <style>
16
+ :root {
17
+ --ink: #17201a;
18
+ --ink-2: #4c5a50;
19
+ --ink-3: #7b887f;
20
+ --line: #e3e9e4;
21
+ --paper: #f6f8f6;
22
+ --card: #ffffff;
23
+ --green: #16a34a;
24
+ --green-soft: #e6f6ec;
25
+ }
26
+ * {
27
+ box-sizing: border-box;
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+ html,
32
+ body {
33
+ width: 1344px;
34
+ height: 2992px;
35
+ overflow: hidden;
36
+ background: var(--paper);
37
+ }
38
+ body {
39
+ font-family:
40
+ system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
41
+ color: var(--ink);
42
+ -webkit-font-smoothing: antialiased;
43
+ }
44
+ .screen {
45
+ width: 448px;
46
+ height: 997.34px;
47
+ transform: scale(3);
48
+ transform-origin: 0 0;
49
+ position: relative;
50
+ display: flex;
51
+ flex-direction: column;
52
+ background: var(--paper);
53
+ }
54
+
55
+ .status {
56
+ height: 30px;
57
+ padding: 0 22px;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ font-size: 13px;
62
+ font-weight: 600;
63
+ background: var(--card);
64
+ }
65
+ .status .icons {
66
+ display: flex;
67
+ gap: 6px;
68
+ align-items: center;
69
+ }
70
+ .status svg {
71
+ display: block;
72
+ }
73
+
74
+ .appbar {
75
+ height: 58px;
76
+ padding: 0 16px;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 14px;
80
+ background: var(--card);
81
+ border-bottom: 1px solid var(--line);
82
+ }
83
+ .appbar h1 {
84
+ font-size: 20px;
85
+ font-weight: 600;
86
+ flex: 1;
87
+ }
88
+ .secure {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 4px;
92
+ font-size: 12px;
93
+ color: var(--green);
94
+ font-weight: 600;
95
+ }
96
+
97
+ .content {
98
+ flex: 1;
99
+ padding: 14px 16px 0;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 12px;
103
+ }
104
+ .card {
105
+ background: var(--card);
106
+ border: 1px solid var(--line);
107
+ border-radius: 14px;
108
+ padding: 14px 16px;
109
+ }
110
+ .eyebrow {
111
+ font-size: 11px;
112
+ font-weight: 700;
113
+ letter-spacing: 0.06em;
114
+ text-transform: uppercase;
115
+ color: var(--ink-3);
116
+ }
117
+
118
+ .deliver {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 12px;
122
+ }
123
+ .deliver .pin {
124
+ width: 38px;
125
+ height: 38px;
126
+ border-radius: 50%;
127
+ background: var(--green-soft);
128
+ display: grid;
129
+ place-items: center;
130
+ flex: none;
131
+ }
132
+ .deliver b {
133
+ font-size: 16px;
134
+ display: block;
135
+ margin-top: 2px;
136
+ }
137
+ .deliver span {
138
+ font-size: 13px;
139
+ color: var(--ink-2);
140
+ }
141
+ .deliver .change {
142
+ margin-left: auto;
143
+ font-size: 13px;
144
+ font-weight: 600;
145
+ color: var(--green);
146
+ }
147
+
148
+ .items {
149
+ padding: 0;
150
+ }
151
+ .items .head {
152
+ padding: 14px 16px 6px;
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: baseline;
156
+ }
157
+ .items .head b {
158
+ font-size: 16px;
159
+ }
160
+ .item {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 14px;
164
+ padding: 10px 16px;
165
+ border-top: 1px solid var(--line);
166
+ }
167
+ .thumb {
168
+ width: 54px;
169
+ height: 54px;
170
+ border-radius: 12px;
171
+ flex: none;
172
+ display: grid;
173
+ place-items: center;
174
+ }
175
+ .item .name {
176
+ font-size: 15px;
177
+ font-weight: 600;
178
+ }
179
+ .item .meta {
180
+ font-size: 12.5px;
181
+ color: var(--ink-3);
182
+ margin-top: 2px;
183
+ }
184
+ .item .price {
185
+ margin-left: auto;
186
+ font-size: 15px;
187
+ font-weight: 600;
188
+ font-variant-numeric: tabular-nums;
189
+ }
190
+ .item .main {
191
+ min-width: 0;
192
+ }
193
+
194
+ .promo {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 10px;
198
+ font-size: 14px;
199
+ }
200
+ .promo .tag {
201
+ font-weight: 700;
202
+ font-size: 12px;
203
+ letter-spacing: 0.04em;
204
+ color: var(--green);
205
+ background: var(--green-soft);
206
+ border-radius: 7px;
207
+ padding: 4px 8px;
208
+ }
209
+ .promo .off {
210
+ margin-left: auto;
211
+ color: var(--green);
212
+ font-weight: 600;
213
+ font-variant-numeric: tabular-nums;
214
+ }
215
+
216
+ .totals {
217
+ display: flex;
218
+ flex-direction: column;
219
+ gap: 7px;
220
+ font-size: 14px;
221
+ color: var(--ink-2);
222
+ }
223
+ .totals .row {
224
+ display: flex;
225
+ justify-content: space-between;
226
+ font-variant-numeric: tabular-nums;
227
+ }
228
+ .totals .row.total {
229
+ margin-top: 4px;
230
+ padding-top: 10px;
231
+ border-top: 1px solid var(--line);
232
+ font-size: 18px;
233
+ font-weight: 700;
234
+ color: var(--ink);
235
+ }
236
+
237
+ .pay-method {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 12px;
241
+ border: 1.5px solid var(--green);
242
+ background: var(--green-soft);
243
+ border-radius: 12px;
244
+ padding: 12px 14px;
245
+ margin-top: 10px;
246
+ }
247
+ .pay-method.plain {
248
+ border-color: var(--line);
249
+ background: var(--card);
250
+ color: var(--ink-2);
251
+ font-size: 14px;
252
+ font-weight: 600;
253
+ }
254
+ .radio {
255
+ width: 20px;
256
+ height: 20px;
257
+ border-radius: 50%;
258
+ border: 2px solid var(--green);
259
+ display: grid;
260
+ place-items: center;
261
+ flex: none;
262
+ }
263
+ .radio::after {
264
+ content: '';
265
+ width: 10px;
266
+ height: 10px;
267
+ border-radius: 50%;
268
+ background: var(--green);
269
+ }
270
+ .radio.off {
271
+ border-color: var(--ink-3);
272
+ }
273
+ .radio.off::after {
274
+ display: none;
275
+ }
276
+ .card-chip {
277
+ width: 40px;
278
+ height: 28px;
279
+ border-radius: 6px;
280
+ background: linear-gradient(135deg, #1d4ed8, #3b82f6);
281
+ flex: none;
282
+ }
283
+ .pay-method b {
284
+ font-size: 15px;
285
+ display: block;
286
+ }
287
+ .pay-method span {
288
+ font-size: 12.5px;
289
+ color: var(--ink-2);
290
+ }
291
+
292
+ .footer {
293
+ padding: 12px 16px 10px;
294
+ background: var(--card);
295
+ border-top: 1px solid var(--line);
296
+ }
297
+ .pay-now {
298
+ height: 56px;
299
+ border-radius: 16px;
300
+ background: var(--green);
301
+ color: #fff;
302
+ font-size: 18px;
303
+ font-weight: 700;
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: center;
307
+ gap: 10px;
308
+ }
309
+ .footer .fine {
310
+ text-align: center;
311
+ font-size: 11.5px;
312
+ color: var(--ink-3);
313
+ margin-top: 8px;
314
+ }
315
+ .gesture {
316
+ height: 24px;
317
+ background: var(--card);
318
+ display: grid;
319
+ place-items: center;
320
+ }
321
+ .gesture i {
322
+ width: 120px;
323
+ height: 4px;
324
+ border-radius: 2px;
325
+ background: #1c2620;
326
+ opacity: 0.8;
327
+ }
328
+ </style>
329
+ </head>
330
+ <body>
331
+ <div class="screen">
332
+ <div class="status">
333
+ <span>10:42</span>
334
+ <span class="icons">
335
+ <svg width="16" height="12" viewBox="0 0 16 12" aria-hidden="true">
336
+ <path d="M8 11.5 0.8 3.3a10.4 10.4 0 0 1 14.4 0z" fill="#17201a" />
337
+ </svg>
338
+ <svg width="16" height="12" viewBox="0 0 16 12" aria-hidden="true">
339
+ <path d="M15 11V1L1 11z" fill="#17201a" />
340
+ </svg>
341
+ <svg width="26" height="12" viewBox="0 0 26 12" aria-hidden="true">
342
+ <rect x="0.5" y="0.5" width="22" height="11" rx="3" fill="none" stroke="#17201a" />
343
+ <rect x="2" y="2" width="16" height="8" rx="1.6" fill="#17201a" />
344
+ <rect x="23.5" y="4" width="2" height="4" rx="1" fill="#17201a" />
345
+ </svg>
346
+ </span>
347
+ </div>
348
+
349
+ <div class="appbar">
350
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
351
+ <path
352
+ d="M15 5 8 12l7 7"
353
+ stroke="#17201a"
354
+ stroke-width="2.4"
355
+ stroke-linecap="round"
356
+ stroke-linejoin="round"
357
+ />
358
+ </svg>
359
+ <h1>Checkout</h1>
360
+ <span class="secure">
361
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
362
+ <rect x="5" y="10.5" width="14" height="10" rx="2.4" fill="#16a34a" />
363
+ <path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" stroke="#16a34a" stroke-width="2.4" />
364
+ </svg>
365
+ Secure
366
+ </span>
367
+ </div>
368
+
369
+ <div class="content">
370
+ <div class="card deliver">
371
+ <div class="pin">
372
+ <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
373
+ <path
374
+ d="M12 2.5a7 7 0 0 0-7 7c0 5.2 7 12 7 12s7-6.8 7-12a7 7 0 0 0-7-7zm0 9.4a2.4 2.4 0 1 1 0-4.8 2.4 2.4 0 0 1 0 4.8z"
375
+ fill="#16a34a"
376
+ />
377
+ </svg>
378
+ </div>
379
+ <div>
380
+ <div class="eyebrow">Deliver to</div>
381
+ <b>Priya Sharma</b>
382
+ <span>12 Example Street, Springfield</span>
383
+ </div>
384
+ <span class="change">Change</span>
385
+ </div>
386
+
387
+ <div class="card items">
388
+ <div class="head">
389
+ <b>Order summary</b>
390
+ <span class="eyebrow">3 items</span>
391
+ </div>
392
+ <div class="item">
393
+ <div class="thumb" style="background: linear-gradient(135deg, #bbf7d0, #4ade80)">
394
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true">
395
+ <path
396
+ d="M3 16.5c0-3 2-4 4-4.2l2.4-3.8 2.6 2.4c2 .2 5 1 7 3.6 1 1.3 1 2.3 0 2.3H4.5C3.6 16.8 3 16.8 3 16.5z"
397
+ fill="#14532d"
398
+ />
399
+ </svg>
400
+ </div>
401
+ <div class="main">
402
+ <div class="name">Trail running shoes</div>
403
+ <div class="meta">Size 42 &middot; Forest &middot; Qty 1</div>
404
+ </div>
405
+ <div class="price">$89.00</div>
406
+ </div>
407
+ <div class="item">
408
+ <div class="thumb" style="background: linear-gradient(135deg, #fde68a, #fbbf24)">
409
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true">
410
+ <path d="M8 3h7v8l3.5 7a2.2 2.2 0 0 1-2 3.2H9.5A3.2 3.2 0 0 1 6.4 18z" fill="#78350f" />
411
+ </svg>
412
+ </div>
413
+ <div class="main">
414
+ <div class="name">Merino hiking socks</div>
415
+ <div class="meta">2-pack &middot; M &middot; Qty 1</div>
416
+ </div>
417
+ <div class="price">$24.00</div>
418
+ </div>
419
+ <div class="item">
420
+ <div class="thumb" style="background: linear-gradient(135deg, #bfdbfe, #60a5fa)">
421
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true">
422
+ <rect x="9.5" y="2.5" width="5" height="3" rx="1" fill="#1e3a8a" />
423
+ <rect x="7" y="6" width="10" height="15.5" rx="3" fill="#1e3a8a" />
424
+ </svg>
425
+ </div>
426
+ <div class="main">
427
+ <div class="name">Insulated bottle 750 ml</div>
428
+ <div class="meta">Slate &middot; Qty 1</div>
429
+ </div>
430
+ <div class="price">$32.50</div>
431
+ </div>
432
+ </div>
433
+
434
+ <div class="card promo">
435
+ <span class="tag">SPRING10</span>
436
+ <span>Promo code applied</span>
437
+ <span class="off">&minus;$14.55</span>
438
+ </div>
439
+
440
+ <div class="card">
441
+ <div class="totals">
442
+ <div class="row"><span>Subtotal</span><span>$145.50</span></div>
443
+ <div class="row"><span>Shipping</span><span>Free</span></div>
444
+ <div class="row"><span>Discount</span><span>&minus;$14.55</span></div>
445
+ <div class="row total"><span>Total</span><span>$130.95</span></div>
446
+ </div>
447
+ <div class="pay-method">
448
+ <div class="radio"></div>
449
+ <div class="card-chip"></div>
450
+ <div>
451
+ <b>Visa &bull;&bull;&bull;&bull; 4242</b>
452
+ <span>Saved card &middot; expires 08/28</span>
453
+ </div>
454
+ </div>
455
+ <div class="pay-method plain">
456
+ <div class="radio off"></div>
457
+ <span style="color: var(--ink-2); font-size: 14px; font-weight: 600">Add a new card</span>
458
+ </div>
459
+ </div>
460
+ </div>
461
+
462
+ <div class="footer">
463
+ <div class="pay-now">Pay now &middot; $130.95</div>
464
+ <div class="fine">Your card is charged when you tap Pay now.</div>
465
+ </div>
466
+ <div class="gesture"><i></i></div>
467
+ </div>
468
+ </body>
469
+ </html>
@@ -0,0 +1,242 @@
1
+ /**
2
+ * A sample finished session for the dashboard screenshots: a made-up
3
+ * "Checkout: pay by saved card" test that passed on the Pixel 8 Pro, with its
4
+ * build, command log, device and debug logs and CPU and memory figures.
5
+ *
6
+ * Every time is a fixed instant, so the pictures don't change from run to run
7
+ * (the script gives the browser the UTC time zone, so they don't change from
8
+ * machine to machine either).
9
+ */
10
+
11
+ const SESSION_ID = 'f3a91c5e-7d2b-4e08-9a64-0b8c1d2e3f47';
12
+ const BUILD_ID = 'bld-42';
13
+ const UDID = '28141FDH2000AB';
14
+ const APP = 'com.example.shop';
15
+ /** The recording's file name; the script serves a WebM under it. */
16
+ const VIDEO_FILE = 'session.webm';
17
+
18
+ /** 2026-10-04 02:14:09 UTC. */
19
+ const START = Date.UTC(2026, 9, 4, 2, 14, 9);
20
+ /** The session lasted 2 min 38 s. */
21
+ const DURATION_MS = 158_500;
22
+ const iso = (ms) => new Date(ms).toISOString();
23
+
24
+ const BUILD = {
25
+ id: BUILD_ID,
26
+ name: 'nightly-2026-10-04',
27
+ createdAt: iso(START - 4 * 60_000),
28
+ updatedAt: iso(START + DURATION_MS),
29
+ sessionCount: 1,
30
+ passedCount: 1,
31
+ failedCount: 0,
32
+ runningCount: 0,
33
+ };
34
+
35
+ const CAPABILITIES = {
36
+ platformName: 'Android',
37
+ 'appium:automationName': 'UiAutomator2',
38
+ 'appium:deviceName': 'Pixel 8 Pro',
39
+ 'appium:platformVersion': '15',
40
+ 'appium:appPackage': APP,
41
+ 'appium:appActivity': '.home.HomeActivity',
42
+ 'appium:newCommandTimeout': 120,
43
+ 'xe:options': { name: 'Checkout: pay by saved card', build: 'nightly-2026-10-04' },
44
+ };
45
+
46
+ const SESSION = {
47
+ id: SESSION_ID,
48
+ build_id: BUILD_ID,
49
+ name: 'Checkout: pay by saved card',
50
+ status: 'passed',
51
+ desired_capabilities: JSON.stringify(CAPABILITIES),
52
+ session_capabilities: JSON.stringify({
53
+ ...CAPABILITIES,
54
+ 'appium:udid': UDID,
55
+ 'appium:deviceScreenSize': '1344x2992',
56
+ 'appium:deviceApiLevel': 35,
57
+ }),
58
+ node_id: 'lab-mac-01',
59
+ has_live_video: false,
60
+ video_recording_enabled: true,
61
+ video_recording: `${SESSION_ID}/video/${VIDEO_FILE}`,
62
+ startTime: iso(START),
63
+ endTime: iso(START + DURATION_MS),
64
+ failure_reason: null,
65
+ failure_category: null,
66
+ ai_analysis: null,
67
+ tags: null,
68
+ device_udid: UDID,
69
+ device_platform: 'android',
70
+ device_version: '15',
71
+ device_name: 'Pixel 8 Pro',
72
+ performance_trace: null,
73
+ createdAt: iso(START),
74
+ updatedAt: iso(START + DURATION_MS),
75
+ owner: { name: 'Priya', email: 'priya@example.com' },
76
+ ranOn: 'here',
77
+ };
78
+
79
+ /**
80
+ * [end offset in seconds, command, duration in ms, extra fields]: oldest
81
+ * first. A row is written as its command ends. The one healed command is a
82
+ * find whose id changed from `btn_pay` to `btn_pay_now`.
83
+ */
84
+ const STEPS = [
85
+ [9.4, 'findElement', 410, { body: ['id', `${APP}:id/cart_checkout`] }],
86
+ [10.9, 'click', 92, {}],
87
+ [24.7, 'findElement', 288, { body: ['id', `${APP}:id/saved_card_visa`] }],
88
+ [26.2, 'click', 84, {}],
89
+ [38.8, 'getText', 71, {}],
90
+ [
91
+ 55.3,
92
+ 'findElement',
93
+ 1942,
94
+ {
95
+ body: ['id', `${APP}:id/btn_pay`],
96
+ healed: {
97
+ from: ['id', `${APP}:id/btn_pay`],
98
+ to: ['id', `${APP}:id/btn_pay_now`],
99
+ tier: 'Fuzzy XML',
100
+ confidence: 0.91,
101
+ },
102
+ },
103
+ ],
104
+ [57.0, 'click', 104, {}],
105
+ [88.6, 'findElement', 11_206, { body: ['id', `${APP}:id/order_confirmation`] }],
106
+ [90.2, 'getText', 64, {}],
107
+ [96.5, 'getScreenshot', 188, {}],
108
+ [112.0, 'findElement', 297, { body: ['id', `${APP}:id/order_number`] }],
109
+ [113.5, 'getText', 58, {}],
110
+ [131.8, 'getPageSource', 641, {}],
111
+ [158.4, 'deleteSession', 1204, {}],
112
+ ];
113
+
114
+ const titleOf = (name) => name.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
115
+ const pathOf = (name) =>
116
+ ({
117
+ findElement: '/element',
118
+ click: '/element/e1/click',
119
+ getText: '/element/e1/text',
120
+ getScreenshot: '/screenshot',
121
+ getPageSource: '/source',
122
+ deleteSession: '',
123
+ })[name] ?? '';
124
+
125
+ /** The command log as GET /session/:id/session_log answers it: newest first. */
126
+ const COMMANDS = STEPS.map(([end, name, duration, extra], i) => {
127
+ const at = iso(START + Math.round(end * 1000));
128
+ const heal = extra.healed;
129
+ return {
130
+ id: `cmd-${String(i + 1).padStart(2, '0')}`,
131
+ session_id: SESSION_ID,
132
+ command_name: name,
133
+ url: `/wd/hub/session/${SESSION_ID}${pathOf(name)}`,
134
+ method:
135
+ name === 'deleteSession'
136
+ ? 'DELETE'
137
+ : name === 'getText' || name === 'getPageSource' || name === 'getScreenshot'
138
+ ? 'GET'
139
+ : 'POST',
140
+ title: titleOf(name),
141
+ subtitle: '',
142
+ body: JSON.stringify(extra.body ?? {}),
143
+ response: JSON.stringify({ value: null }),
144
+ screenshot: null,
145
+ is_success: true,
146
+ is_error: false,
147
+ is_healed: !!heal,
148
+ original_strategy: heal ? heal.from[0] : (extra.body?.[0] ?? null),
149
+ original_selector: heal ? heal.from[1] : (extra.body?.[1] ?? null),
150
+ healed_strategy: heal ? heal.to[0] : null,
151
+ healed_selector: heal ? heal.to[1] : null,
152
+ healing_tier: heal ? heal.tier : null,
153
+ healing_confidence: heal ? heal.confidence : null,
154
+ duration,
155
+ createdAt: at,
156
+ updatedAt: at,
157
+ };
158
+ }).reverse();
159
+
160
+ const LOG_LINES = [
161
+ [1.2, 'DEVICE', 'I/ActivityTaskManager: START u0 {cmp=com.example.shop/.home.HomeActivity}'],
162
+ [8.7, 'DEVICE', 'D/ShopCheckout: CartViewModel: 3 items, subtotal=145.50 USD'],
163
+ [52.1, 'DEVICE', 'W/chromium: [WARNING:CONSOLE(1)] "Slow payment: 9.3s from tap to capture"'],
164
+ [88.0, 'DEVICE', 'I/ShopCheckout: payment captured paymentId=pay_3f9a2c attempt=3'],
165
+ [0.4, 'DEBUG', 'Session started on Pixel 8 Pro (Android 15)'],
166
+ [
167
+ 55.2,
168
+ 'DEBUG',
169
+ 'Selector id=com.example.shop:id/btn_pay not found; healed by Fuzzy XML to id=com.example.shop:id/btn_pay_now (91%)',
170
+ ],
171
+ [158.2, 'DEBUG', 'Session finished: passed'],
172
+ ];
173
+ const logRows = (type) =>
174
+ LOG_LINES.filter(([, t]) => t === type).map(([at, t, message], i) => ({
175
+ id: `log-${type.toLowerCase()}-${i + 1}`,
176
+ session_id: SESSION_ID,
177
+ log_type: t,
178
+ message,
179
+ timestamp: iso(START + Math.round(at * 1000)),
180
+ createdAt: iso(START + Math.round(at * 1000)),
181
+ updatedAt: iso(START + Math.round(at * 1000)),
182
+ }));
183
+ const DEVICE_LOGS = logRows('DEVICE');
184
+ const DEBUG_LOGS = logRows('DEBUG');
185
+
186
+ const clamp = (lo, hi, v) => Math.min(hi, Math.max(lo, v));
187
+ const round1 = (v) => Math.round(v * 10) / 10;
188
+ /** A smooth bump: `height` at sample `at`, fading over about `width` samples. */
189
+ const bump = (i, at, width, height) => height * Math.exp(-(((i - at) / width) ** 2));
190
+
191
+ /**
192
+ * 80 samples, one every 2 s, as GET /session/:id/metrics answers them. The
193
+ * device's CPU stays between 15 and 45 %, the app's between 5 and 25 %,
194
+ * with a rise as the app opens and another as the payment goes through. The
195
+ * first sample has no CPU: it comes from the change since the one before.
196
+ */
197
+ const SAMPLES = Array.from({ length: 80 }, (_, i) => {
198
+ const deviceCpu =
199
+ 27 +
200
+ 7 * Math.sin(i * 0.18) +
201
+ 3 * Math.sin(i * 0.55 + 1.3) +
202
+ bump(i, 4, 3, 7) +
203
+ bump(i, 43, 5, 9);
204
+ const appCpu =
205
+ 11 +
206
+ 4 * Math.sin(i * 0.21 + 0.7) +
207
+ 2 * Math.sin(i * 0.6) +
208
+ bump(i, 5, 3, 7) +
209
+ bump(i, 43, 5, 6);
210
+ const progress = i / 79;
211
+ return {
212
+ t: START + 300 + i * 2000,
213
+ deviceCpu: i === 0 ? null : round1(clamp(15, 45, deviceCpu)),
214
+ deviceMemMb: Math.round(5320 + 24 * Math.sin(i * 0.13) + 0.9 * i),
215
+ deviceMemTotalMb: 11776,
216
+ appCpu: i === 0 ? null : round1(clamp(5, 25, appCpu)),
217
+ appMemMb: round1(290 + 50 * (progress * (2 - progress)) + 2 * Math.sin(i * 0.9)),
218
+ app: APP,
219
+ };
220
+ });
221
+
222
+ const METRICS = {
223
+ platform: 'android',
224
+ intervalMs: 2000,
225
+ appId: APP,
226
+ series: { deviceCpu: true, deviceMem: true, appCpu: true, appMem: true },
227
+ recording: null,
228
+ samples: SAMPLES,
229
+ };
230
+
231
+ module.exports = {
232
+ SESSION_ID,
233
+ BUILD_ID,
234
+ UDID,
235
+ VIDEO_FILE,
236
+ BUILD,
237
+ SESSION,
238
+ COMMANDS,
239
+ DEVICE_LOGS,
240
+ DEBUG_LOGS,
241
+ METRICS,
242
+ };