@itmetsam/samurai-framework 0.1.0 → 0.2.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 (328) hide show
  1. package/README.md +129 -0
  2. package/bin/samurai.mjs +31 -0
  3. package/dist/api.js +3 -0
  4. package/dist/assert/assertion-error.d.ts +23 -0
  5. package/dist/assert/assertion-error.js +45 -0
  6. package/dist/assert/expect.d.ts +55 -0
  7. package/dist/assert/expect.js +188 -0
  8. package/dist/browser/browser-context.d.ts +91 -0
  9. package/dist/browser/browser-context.js +146 -0
  10. package/dist/browser/browser-finder.d.ts +2 -0
  11. package/dist/browser/browser-finder.js +95 -0
  12. package/dist/browser/browser.d.ts +95 -0
  13. package/dist/browser/browser.js +372 -0
  14. package/dist/browser/context-tree.d.ts +23 -0
  15. package/dist/browser/context-tree.js +60 -0
  16. package/dist/browser/dialog.d.ts +37 -0
  17. package/dist/browser/dialog.js +61 -0
  18. package/dist/browser/download.d.ts +71 -0
  19. package/dist/browser/download.js +137 -0
  20. package/dist/browser/downloads-dir.d.ts +8 -0
  21. package/dist/browser/downloads-dir.js +38 -0
  22. package/dist/browser/emulation.d.ts +61 -0
  23. package/dist/browser/emulation.js +144 -0
  24. package/dist/browser/file-chooser.d.ts +39 -0
  25. package/dist/browser/file-chooser.js +86 -0
  26. package/dist/browser/file-paths.d.ts +4 -0
  27. package/dist/browser/file-paths.js +18 -0
  28. package/dist/browser/index.d.ts +4 -0
  29. package/dist/browser/index.js +4 -0
  30. package/dist/browser/navigation-error.d.ts +13 -0
  31. package/dist/browser/navigation-error.js +19 -0
  32. package/dist/browser/page-logs.d.ts +86 -0
  33. package/dist/browser/page-logs.js +149 -0
  34. package/dist/browser/page-wait.d.ts +9 -0
  35. package/dist/browser/page-wait.js +17 -0
  36. package/dist/browser/page.d.ts +370 -0
  37. package/dist/browser/page.js +1038 -0
  38. package/dist/browser/pdf.d.ts +72 -0
  39. package/dist/browser/pdf.js +37 -0
  40. package/dist/browser/screenshot.d.ts +29 -0
  41. package/dist/browser/screenshot.js +42 -0
  42. package/dist/cli/args.d.ts +35 -0
  43. package/dist/cli/args.js +69 -0
  44. package/dist/cli/cli.cli-test.d.ts +1 -0
  45. package/dist/cli/cli.cli-test.js +170 -0
  46. package/dist/cli/console-reporter.d.ts +15 -0
  47. package/dist/cli/console-reporter.js +52 -0
  48. package/dist/cli/init.d.ts +7 -0
  49. package/dist/cli/init.js +61 -0
  50. package/dist/cli/main.d.ts +12 -0
  51. package/dist/cli/main.js +156 -0
  52. package/dist/config/config.d.ts +24 -0
  53. package/dist/config/config.js +42 -0
  54. package/dist/config/default.d.ts +2 -0
  55. package/dist/config/default.js +3 -0
  56. package/dist/config/mask.d.ts +11 -0
  57. package/dist/config/mask.js +85 -0
  58. package/dist/config/run-settings.d.ts +28 -0
  59. package/dist/config/run-settings.js +61 -0
  60. package/dist/config/secrets.d.ts +10 -0
  61. package/dist/config/secrets.js +67 -0
  62. package/dist/config/variables.d.ts +11 -0
  63. package/dist/config/variables.js +26 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +15 -0
  66. package/dist/locator/action-timeout-error.d.ts +25 -0
  67. package/dist/locator/action-timeout-error.js +35 -0
  68. package/dist/locator/element-state.d.ts +44 -0
  69. package/dist/locator/element-state.js +109 -0
  70. package/dist/locator/label-locator.d.ts +13 -0
  71. package/dist/locator/label-locator.js +49 -0
  72. package/dist/locator/locator.d.ts +168 -0
  73. package/dist/locator/locator.js +591 -0
  74. package/dist/locator/selector-errors.d.ts +9 -0
  75. package/dist/locator/selector-errors.js +15 -0
  76. package/dist/locator/selector.d.ts +60 -0
  77. package/dist/locator/selector.js +113 -0
  78. package/dist/locator/text-locator.d.ts +13 -0
  79. package/dist/logger/index.d.ts +2 -0
  80. package/dist/logger/index.js +30 -0
  81. package/dist/network/data-collector.d.ts +26 -0
  82. package/dist/network/data-collector.js +77 -0
  83. package/dist/network/network-tracker.d.ts +94 -0
  84. package/dist/network/network-tracker.js +198 -0
  85. package/dist/network/network-wait.d.ts +16 -0
  86. package/dist/network/network-wait.js +24 -0
  87. package/dist/network/response.d.ts +22 -0
  88. package/dist/network/response.js +42 -0
  89. package/dist/network/router.d.ts +120 -0
  90. package/dist/network/router.js +343 -0
  91. package/dist/network/url-match.d.ts +8 -0
  92. package/dist/network/url-match.js +38 -0
  93. package/dist/recorder/apply.d.ts +3 -0
  94. package/dist/recorder/apply.js +10 -0
  95. package/dist/recorder/capture.d.ts +18 -0
  96. package/dist/recorder/capture.js +198 -0
  97. package/dist/recorder/index.d.ts +3 -0
  98. package/dist/recorder/index.js +3 -0
  99. package/dist/recorder/normalise.d.ts +68 -0
  100. package/dist/recorder/normalise.js +104 -0
  101. package/dist/recorder/ranking.d.ts +9 -0
  102. package/dist/recorder/ranking.js +21 -0
  103. package/dist/recorder/recorder.d.ts +52 -0
  104. package/dist/recorder/recorder.js +143 -0
  105. package/dist/recorder/session.d.ts +49 -0
  106. package/dist/recorder/session.js +113 -0
  107. package/dist/runner/module-type.d.ts +6 -0
  108. package/dist/runner/module-type.js +29 -0
  109. package/dist/runner/page-logs-report.d.ts +40 -0
  110. package/dist/runner/page-logs-report.js +44 -0
  111. package/dist/runner/prepare-run.d.ts +14 -0
  112. package/dist/runner/prepare-run.js +20 -0
  113. package/dist/runner/project.d.ts +17 -0
  114. package/dist/runner/project.js +33 -0
  115. package/dist/runner/registry.d.ts +7 -0
  116. package/dist/runner/registry.js +40 -0
  117. package/dist/runner/reporter.d.ts +43 -0
  118. package/dist/runner/reporter.js +83 -0
  119. package/dist/runner/run.d.ts +27 -0
  120. package/dist/runner/run.js +34 -0
  121. package/dist/runner/test-error.d.ts +3 -0
  122. package/dist/runner/test-error.js +18 -0
  123. package/dist/runner/test-runner.d.ts +44 -0
  124. package/dist/runner/test-runner.js +205 -0
  125. package/dist/script/call-function.d.ts +34 -0
  126. package/dist/script/call-function.js +71 -0
  127. package/dist/script/element-handle.d.ts +10 -0
  128. package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
  129. package/dist/script/helpers.d.ts +28 -0
  130. package/dist/script/helpers.js +50 -0
  131. package/dist/script/preload.d.ts +31 -0
  132. package/dist/script/preload.js +71 -0
  133. package/dist/script/serialize.d.ts +29 -0
  134. package/dist/script/serialize.js +207 -0
  135. package/dist/steps/build.d.ts +5 -0
  136. package/dist/steps/build.js +38 -0
  137. package/dist/steps/edit.d.ts +30 -0
  138. package/dist/steps/edit.js +80 -0
  139. package/dist/steps/emit.d.ts +6 -0
  140. package/dist/steps/emit.js +73 -0
  141. package/dist/steps/index.d.ts +5 -0
  142. package/dist/steps/index.js +4 -0
  143. package/dist/steps/model.d.ts +103 -0
  144. package/dist/steps/model.js +1 -0
  145. package/dist/steps/parse.d.ts +3 -0
  146. package/dist/steps/parse.js +272 -0
  147. package/dist/transport/bidi-connection.d.ts +46 -0
  148. package/dist/transport/bidi-connection.js +176 -0
  149. package/dist/transport/bidi-error.d.ts +10 -0
  150. package/dist/transport/bidi-error.js +10 -0
  151. package/dist/transport/subscriptions.d.ts +36 -0
  152. package/dist/transport/subscriptions.js +81 -0
  153. package/{src → dist}/types/test.d.ts +10 -4
  154. package/dist/wait/wait-until.d.ts +20 -0
  155. package/dist/wait/wait-until.js +63 -0
  156. package/package.json +44 -7
  157. package/.claude/settings.local.json +0 -7
  158. package/.env.example +0 -3
  159. package/.github/actions/setup/action.yml +0 -16
  160. package/.github/workflows/ci.yml +0 -71
  161. package/.trunk/configs/.markdownlint.yaml +0 -2
  162. package/.trunk/configs/.yamllint.yaml +0 -7
  163. package/.trunk/configs/svgo.config.mjs +0 -14
  164. package/.trunk/trunk.yaml +0 -34
  165. package/docs/superpowers/bidi-roadmap.md +0 -60
  166. package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
  167. package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
  168. package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
  169. package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
  170. package/docs/superpowers/recorder-roadmap.md +0 -52
  171. package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
  172. package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
  173. package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
  174. package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
  175. package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
  176. package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
  177. package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
  178. package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
  179. package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
  180. package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
  181. package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
  182. package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
  183. package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
  184. package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
  185. package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
  186. package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
  187. package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
  188. package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
  189. package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
  190. package/public/logo-dark.svg +0 -236
  191. package/public/logo-light.svg +0 -262
  192. package/samurai.config.ts +0 -7
  193. package/src/assert/assertion-error.test.ts +0 -55
  194. package/src/assert/assertion-error.ts +0 -62
  195. package/src/assert/expect-locator.test.ts +0 -217
  196. package/src/assert/expect.test.ts +0 -73
  197. package/src/assert/expect.ts +0 -299
  198. package/src/browser/browser-context.browser-test.ts +0 -201
  199. package/src/browser/browser-context.test.ts +0 -407
  200. package/src/browser/browser-context.ts +0 -208
  201. package/src/browser/browser-finder.ts +0 -110
  202. package/src/browser/browser.test.ts +0 -88
  203. package/src/browser/browser.ts +0 -515
  204. package/src/browser/context-tree.test.ts +0 -86
  205. package/src/browser/context-tree.ts +0 -70
  206. package/src/browser/dialog.browser-test.ts +0 -144
  207. package/src/browser/dialog.test.ts +0 -322
  208. package/src/browser/dialog.ts +0 -83
  209. package/src/browser/download.test.ts +0 -161
  210. package/src/browser/download.ts +0 -172
  211. package/src/browser/downloads-dir.test.ts +0 -75
  212. package/src/browser/downloads-dir.ts +0 -39
  213. package/src/browser/emulation.browser-test.ts +0 -186
  214. package/src/browser/emulation.test.ts +0 -200
  215. package/src/browser/emulation.ts +0 -204
  216. package/src/browser/file-chooser.test.ts +0 -142
  217. package/src/browser/file-chooser.ts +0 -99
  218. package/src/browser/file-paths.ts +0 -18
  219. package/src/browser/goto.browser-test.ts +0 -44
  220. package/src/browser/navigation-error.ts +0 -26
  221. package/src/browser/navigation.browser-test.ts +0 -172
  222. package/src/browser/page-api.browser-test.ts +0 -228
  223. package/src/browser/page-logs.browser-test.ts +0 -119
  224. package/src/browser/page-logs.test.ts +0 -212
  225. package/src/browser/page-logs.ts +0 -208
  226. package/src/browser/page-wait.ts +0 -19
  227. package/src/browser/page.test.ts +0 -1567
  228. package/src/browser/page.ts +0 -1176
  229. package/src/browser/pdf.ts +0 -62
  230. package/src/browser/screenshot.ts +0 -65
  231. package/src/browser/transfers.browser-test.ts +0 -180
  232. package/src/config/config.ts +0 -33
  233. package/src/config/default.ts +0 -5
  234. package/src/config/fixtures.test.ts +0 -137
  235. package/src/config/mask.test.ts +0 -143
  236. package/src/config/mask.ts +0 -87
  237. package/src/config/run-settings.test.ts +0 -163
  238. package/src/config/run-settings.ts +0 -114
  239. package/src/config/secrets.ts +0 -70
  240. package/src/config/variables.ts +0 -31
  241. package/src/index.ts +0 -10
  242. package/src/locator/action-timeout-error.test.ts +0 -83
  243. package/src/locator/action-timeout-error.ts +0 -72
  244. package/src/locator/element-state.test.ts +0 -181
  245. package/src/locator/element-state.ts +0 -159
  246. package/src/locator/locate-nodes.browser-test.ts +0 -169
  247. package/src/locator/locator-actions.test.ts +0 -355
  248. package/src/locator/locator-chain.test.ts +0 -292
  249. package/src/locator/locator.browser-test.ts +0 -137
  250. package/src/locator/locator.test.ts +0 -179
  251. package/src/locator/locator.ts +0 -656
  252. package/src/locator/selector-errors.ts +0 -18
  253. package/src/locator/selector.test.ts +0 -124
  254. package/src/locator/selector.ts +0 -107
  255. package/src/locator/set-files.browser-test.ts +0 -141
  256. package/src/locator/set-files.test.ts +0 -191
  257. package/src/locator/text-locator.test.ts +0 -118
  258. package/src/logger/index.test.ts +0 -28
  259. package/src/logger/index.ts +0 -40
  260. package/src/network/data-collector.test.ts +0 -111
  261. package/src/network/data-collector.ts +0 -92
  262. package/src/network/network-data.browser-test.ts +0 -192
  263. package/src/network/network-tracker.browser-test.ts +0 -151
  264. package/src/network/network-tracker.test.ts +0 -307
  265. package/src/network/network-tracker.ts +0 -286
  266. package/src/network/network-wait.ts +0 -28
  267. package/src/network/response.test.ts +0 -93
  268. package/src/network/response.ts +0 -58
  269. package/src/network/router.browser-test.ts +0 -260
  270. package/src/network/router.test.ts +0 -528
  271. package/src/network/router.ts +0 -420
  272. package/src/network/url-match.test.ts +0 -69
  273. package/src/network/url-match.ts +0 -41
  274. package/src/runner/page-logs-report.test.ts +0 -144
  275. package/src/runner/page-logs-report.ts +0 -82
  276. package/src/runner/prepare-run.test.ts +0 -33
  277. package/src/runner/prepare-run.ts +0 -36
  278. package/src/runner/registry.test.ts +0 -59
  279. package/src/runner/registry.ts +0 -46
  280. package/src/runner/reporter.test.ts +0 -73
  281. package/src/runner/reporter.ts +0 -109
  282. package/src/runner/test-error.test.ts +0 -39
  283. package/src/runner/test-error.ts +0 -20
  284. package/src/runner/test-runner.ts +0 -184
  285. package/src/script/call-function.browser-test.ts +0 -112
  286. package/src/script/call-function.test.ts +0 -174
  287. package/src/script/call-function.ts +0 -113
  288. package/src/script/helpers.test.ts +0 -116
  289. package/src/script/helpers.ts +0 -68
  290. package/src/script/preload.browser-test.ts +0 -144
  291. package/src/script/preload.test.ts +0 -180
  292. package/src/script/preload.ts +0 -102
  293. package/src/script/serialize.test.ts +0 -182
  294. package/src/script/serialize.ts +0 -203
  295. package/src/testing/browser-fixture.ts +0 -18
  296. package/src/testing/fake-websocket.ts +0 -72
  297. package/src/testing/stub-connector.ts +0 -154
  298. package/src/tests/index.spec.ts +0 -30
  299. package/src/transport/bidi-connection.test.ts +0 -155
  300. package/src/transport/bidi-connection.ts +0 -256
  301. package/src/transport/bidi-error.ts +0 -23
  302. package/src/transport/foundation.browser-test.ts +0 -73
  303. package/src/transport/subscriptions.test.ts +0 -140
  304. package/src/transport/subscriptions.ts +0 -110
  305. package/src/types/bidi-commands.test-d.ts +0 -55
  306. package/src/wait/wait-until.test.ts +0 -117
  307. package/src/wait/wait-until.ts +0 -86
  308. package/tsconfig.json +0 -44
  309. /package/{src/api.ts → dist/api.d.ts} +0 -0
  310. /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
  311. /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
  312. /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
  313. /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
  314. /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
  315. /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
  316. /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
  317. /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
  318. /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
  319. /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
  320. /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
  321. /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
  322. /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
  323. /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
  324. /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
  325. /package/{src → dist}/types/bidi.d.ts +0 -0
  326. /package/{src → dist}/types/browser.d.ts +0 -0
  327. /package/{src → dist}/types/config.d.ts +0 -0
  328. /package/{src → dist}/types/env.d.ts +0 -0
@@ -0,0 +1,207 @@
1
+ import { ElementHandle } from "./element-handle.js";
2
+ /** Deepest nesting `toLocalValue` accepts; a second guard next to cycle detection */
3
+ const MAX_DEPTH = 100;
4
+ /** A remote value that cannot be copied to plain JS (function, window, promise, …), kept by type and handle */
5
+ export class RemoteObject {
6
+ constructor(type, handle,
7
+ /** The deserialized value, when the browser sent one and handles were kept */
8
+ value) {
9
+ this.type = type;
10
+ this.handle = handle;
11
+ this.value = value;
12
+ }
13
+ }
14
+ /**
15
+ * A channel the page function can call as `send(value)`; the browser delivers each value as a
16
+ * `script.message` event. Only meaningful as a preload script or function argument.
17
+ */
18
+ export class ChannelArg {
19
+ constructor(channel) {
20
+ this.channel = channel;
21
+ }
22
+ }
23
+ /** Serializes `value` for `script.callFunction` arguments; throws TypeError for what cannot cross the wire */
24
+ export function toLocalValue(value) {
25
+ return serialize(value, new Set(), 0);
26
+ }
27
+ function serialize(value, ancestors, depth) {
28
+ if (depth > MAX_DEPTH)
29
+ throw new TypeError("cannot serialize a value that is too deeply nested");
30
+ switch (typeof value) {
31
+ case "undefined":
32
+ return { type: "undefined" };
33
+ case "boolean":
34
+ return { type: "boolean", value };
35
+ case "string":
36
+ return { type: "string", value };
37
+ case "number":
38
+ return { type: "number", value: numberValue(value) };
39
+ case "bigint":
40
+ return { type: "bigint", value: value.toString() };
41
+ case "function":
42
+ case "symbol":
43
+ throw new TypeError(`cannot serialize ${typeof value}`);
44
+ }
45
+ if (value === null)
46
+ return { type: "null" };
47
+ if (value instanceof ChannelArg) {
48
+ // Nodes arrive as references only, which is all the recorder needs
49
+ return {
50
+ type: "channel",
51
+ value: {
52
+ channel: value.channel,
53
+ serializationOptions: { maxDomDepth: 0, maxObjectDepth: 4 },
54
+ ownership: "none",
55
+ },
56
+ };
57
+ }
58
+ if (value instanceof ElementHandle) {
59
+ return value.handle === undefined
60
+ ? { sharedId: value.sharedId }
61
+ : { sharedId: value.sharedId, handle: value.handle };
62
+ }
63
+ if (value instanceof Date)
64
+ return { type: "date", value: value.toISOString() };
65
+ if (value instanceof RegExp) {
66
+ return {
67
+ type: "regexp",
68
+ value: { pattern: value.source, flags: value.flags },
69
+ };
70
+ }
71
+ const object = value;
72
+ if (ancestors.has(object))
73
+ throw new TypeError("cannot serialize a circular structure");
74
+ ancestors.add(object);
75
+ try {
76
+ const next = (item) => serialize(item, ancestors, depth + 1);
77
+ if (Array.isArray(object))
78
+ return { type: "array", value: object.map(next) };
79
+ if (object instanceof Set)
80
+ return { type: "set", value: [...object].map(next) };
81
+ if (object instanceof Map) {
82
+ return {
83
+ type: "map",
84
+ value: [...object].map(([key, item]) => [
85
+ typeof key === "string" ? key : next(key),
86
+ next(item),
87
+ ]),
88
+ };
89
+ }
90
+ const prototype = Object.getPrototypeOf(object);
91
+ if (prototype !== Object.prototype && prototype !== null) {
92
+ throw new TypeError(`cannot serialize ${prototype.constructor?.name ?? "object"}`);
93
+ }
94
+ return {
95
+ type: "object",
96
+ value: Object.entries(object).map(([key, item]) => [key, next(item)]),
97
+ };
98
+ }
99
+ finally {
100
+ ancestors.delete(object);
101
+ }
102
+ }
103
+ function numberValue(value) {
104
+ if (Number.isNaN(value))
105
+ return "NaN";
106
+ if (value === Infinity)
107
+ return "Infinity";
108
+ if (value === -Infinity)
109
+ return "-Infinity";
110
+ if (Object.is(value, -0))
111
+ return "-0";
112
+ return value;
113
+ }
114
+ /** Converts a remote value to plain JS; what cannot be copied becomes a `RemoteObject` */
115
+ export function fromRemoteValue(value, options = {}) {
116
+ return deserialize(value, new Map(), options);
117
+ }
118
+ function deserialize(value, known, options) {
119
+ const loose = value;
120
+ switch (value.type) {
121
+ case "undefined":
122
+ return undefined;
123
+ case "null":
124
+ return null;
125
+ case "string":
126
+ case "boolean":
127
+ return value.value;
128
+ case "number":
129
+ return specialNumber(value.value);
130
+ case "bigint":
131
+ return BigInt(value.value);
132
+ case "date":
133
+ return new Date(value.value);
134
+ case "regexp": {
135
+ const { pattern, flags } = loose.value;
136
+ return new RegExp(pattern, flags);
137
+ }
138
+ case "node":
139
+ return new ElementHandle(loose.sharedId ?? "", loose.handle);
140
+ }
141
+ const nested = (item) => deserialize(item, known, options);
142
+ const remember = (result) => {
143
+ if (loose.internalId !== undefined)
144
+ known.set(loose.internalId, result);
145
+ return result;
146
+ };
147
+ const backReference = () => loose.internalId !== undefined && known.has(loose.internalId)
148
+ ? { found: true, value: known.get(loose.internalId) }
149
+ : undefined;
150
+ let result;
151
+ switch (value.type) {
152
+ case "array":
153
+ case "set":
154
+ case "object":
155
+ case "map": {
156
+ const reference = backReference();
157
+ if (reference && loose.value === undefined)
158
+ return reference.value;
159
+ if (loose.value === undefined)
160
+ break; // cut off by the serialization depth
161
+ if (value.type === "array") {
162
+ const list = remember([]);
163
+ loose.value.forEach((item) => list.push(nested(item)));
164
+ result = list;
165
+ }
166
+ else if (value.type === "set") {
167
+ const set = remember(new Set());
168
+ loose.value.forEach((item) => set.add(nested(item)));
169
+ result = set;
170
+ }
171
+ else {
172
+ const entries = loose.value;
173
+ const key = (raw) => typeof raw === "string" ? raw : nested(raw);
174
+ if (value.type === "map") {
175
+ const map = remember(new Map());
176
+ entries.forEach(([k, item]) => map.set(key(k), nested(item)));
177
+ result = map;
178
+ }
179
+ else {
180
+ const object = remember({});
181
+ entries.forEach(([k, item]) => {
182
+ object[String(key(k))] = nested(item);
183
+ });
184
+ result = object;
185
+ }
186
+ }
187
+ return options.keepHandles && loose.handle !== undefined
188
+ ? new RemoteObject(value.type, loose.handle, result)
189
+ : result;
190
+ }
191
+ }
192
+ return new RemoteObject(value.type, loose.handle);
193
+ }
194
+ function specialNumber(value) {
195
+ switch (value) {
196
+ case "NaN":
197
+ return NaN;
198
+ case "Infinity":
199
+ return Infinity;
200
+ case "-Infinity":
201
+ return -Infinity;
202
+ case "-0":
203
+ return -0;
204
+ default:
205
+ return Number(value);
206
+ }
207
+ }
@@ -0,0 +1,5 @@
1
+ import type Locator from "../locator/locator.js";
2
+ import type Page from "../browser/page.js";
3
+ import type { LocatorSpec } from "./model.js";
4
+ /** The locator a step's `LocatorSpec` stands for, with its fallbacks */
5
+ export declare function locatorFromSpec(page: Page, spec: LocatorSpec): Locator;
@@ -0,0 +1,38 @@
1
+ function fromCalls(page, calls) {
2
+ const [first, ...rest] = calls;
3
+ if (!first)
4
+ throw new Error("A locator needs at least one call");
5
+ const apply = (target, call) => {
6
+ const text = {
7
+ ...(call.method !== "locator" &&
8
+ call.method !== "getByCss" &&
9
+ call.method !== "getByTestId" &&
10
+ call.method !== "getByRole" && {
11
+ ...(call.match !== undefined && { match: call.match }),
12
+ ...(call.ignoreCase !== undefined && { ignoreCase: call.ignoreCase }),
13
+ }),
14
+ };
15
+ switch (call.method) {
16
+ case "locator":
17
+ return target.locator(call.xpath);
18
+ case "getByCss":
19
+ return target.getByCss(call.css);
20
+ case "getByText":
21
+ return target.getByText(call.text, text);
22
+ case "getByLabel":
23
+ return target.getByLabel(call.text, text);
24
+ case "getByRole":
25
+ return target.getByRole(call.role, call.name === undefined ? undefined : { name: call.name });
26
+ case "getByTestId":
27
+ return target.getByTestId(call.testId);
28
+ }
29
+ };
30
+ return rest.reduce((locator, call) => apply(locator, call), apply(page, first));
31
+ }
32
+ /** The locator a step's `LocatorSpec` stands for, with its fallbacks */
33
+ export function locatorFromSpec(page, spec) {
34
+ const primary = fromCalls(page, spec.chain);
35
+ return spec.fallbacks.length === 0
36
+ ? primary
37
+ : primary.withFallbacks(...spec.fallbacks.map((chain) => fromCalls(page, chain)));
38
+ }
@@ -0,0 +1,30 @@
1
+ import type { Step } from "./model.js";
2
+ export type StepEdit = {
3
+ op: "replace";
4
+ test: number;
5
+ index: number;
6
+ step: Step;
7
+ }
8
+ /** `index` may equal the number of steps, to append */
9
+ | {
10
+ op: "insert";
11
+ test: number;
12
+ index: number;
13
+ step: Step;
14
+ } | {
15
+ op: "remove";
16
+ test: number;
17
+ index: number;
18
+ }
19
+ /** Moves a step so it ends up at position `to` */
20
+ | {
21
+ op: "move";
22
+ test: number;
23
+ from: number;
24
+ to: number;
25
+ };
26
+ /**
27
+ * Applies one edit and returns the new source. Only the touched statement changes: everything else,
28
+ * formatting and comments included, stays byte for byte.
29
+ */
30
+ export declare function applyEdit(source: string, edit: StepEdit): string;
@@ -0,0 +1,80 @@
1
+ import { stepToSource } from "./emit.js";
2
+ import { parseSpec } from "./parse.js";
3
+ const lineStart = (source, offset) => source.lastIndexOf("\n", offset - 1) + 1;
4
+ function lineEnd(source, offset) {
5
+ const newline = source.indexOf("\n", offset);
6
+ return newline === -1 ? source.length : newline + 1;
7
+ }
8
+ const indentOf = (source, offset) => /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
9
+ function pickTest(source, test) {
10
+ const found = parseSpec(source)[test];
11
+ if (!found)
12
+ throw new RangeError(`There is no test ${test} in this file`);
13
+ return found;
14
+ }
15
+ function pickStep(parsed, index) {
16
+ const step = parsed.steps[index];
17
+ if (!step)
18
+ throw new RangeError(`Test "${parsed.name}" has no step ${index}`);
19
+ return step;
20
+ }
21
+ function splice(source, from, to, text) {
22
+ return source.slice(0, from) + text + source.slice(to);
23
+ }
24
+ /** Inserts statement text so it becomes step `index`, copying the indentation of the steps around it */
25
+ function insertCode(source, parsed, index, code) {
26
+ if (index < 0 || index > parsed.steps.length) {
27
+ throw new RangeError(`Cannot insert at step ${index}: test "${parsed.name}" has ${parsed.steps.length}`);
28
+ }
29
+ const before = parsed.steps[index];
30
+ if (before) {
31
+ // Goes above the step's own comments, which belong to it
32
+ const at = lineStart(source, before.anchor);
33
+ return splice(source, at, at, `${indentOf(source, before.start)}${code}\n`);
34
+ }
35
+ const last = parsed.steps[parsed.steps.length - 1];
36
+ if (last) {
37
+ const indent = indentOf(source, last.start);
38
+ const next = lineEnd(source, last.end);
39
+ // After the last statement's line, so a trailing comment stays with it
40
+ if (next <= parsed.bodyEnd && source[next - 1] === "\n")
41
+ return splice(source, next, next, `${indent}${code}\n`);
42
+ return splice(source, last.end, last.end, `\n${indent}${code}`);
43
+ }
44
+ // Empty body: indent one level deeper than the line that closes it
45
+ const closing = indentOf(source, parsed.bodyEnd);
46
+ const rest = source.slice(parsed.bodyStart, parsed.bodyEnd);
47
+ const tail = rest.includes("\n") ? "" : `\n${closing}`;
48
+ return splice(source, parsed.bodyStart, parsed.bodyStart, `\n${closing} ${code}${tail}`);
49
+ }
50
+ function removeStep(source, parsed, index) {
51
+ const { anchor, end } = pickStep(parsed, index);
52
+ return splice(source, lineStart(source, anchor), lineEnd(source, end), "");
53
+ }
54
+ /**
55
+ * Applies one edit and returns the new source. Only the touched statement changes: everything else,
56
+ * formatting and comments included, stays byte for byte.
57
+ */
58
+ export function applyEdit(source, edit) {
59
+ const parsed = pickTest(source, edit.test);
60
+ switch (edit.op) {
61
+ case "replace": {
62
+ const { start, end } = pickStep(parsed, edit.index);
63
+ return splice(source, start, end, stepToSource(edit.step));
64
+ }
65
+ case "insert":
66
+ return insertCode(source, parsed, edit.index, stepToSource(edit.step));
67
+ case "remove":
68
+ return removeStep(source, parsed, edit.index);
69
+ case "move": {
70
+ const { start, end } = pickStep(parsed, edit.from);
71
+ if (edit.to < 0 || edit.to >= parsed.steps.length)
72
+ throw new RangeError(`Cannot move to step ${edit.to}`);
73
+ if (edit.to === edit.from)
74
+ return source;
75
+ const code = source.slice(start, end);
76
+ const removed = removeStep(source, parsed, edit.from);
77
+ return insertCode(removed, pickTest(removed, edit.test), edit.to, code);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,6 @@
1
+ import type { LocatorCall, LocatorSpec, Step, TextValue } from "./model.js";
2
+ export declare function callToSource(call: LocatorCall): string;
3
+ export declare function locatorToSource({ chain, fallbacks }: LocatorSpec): string;
4
+ export declare function valueToSource(value: TextValue): string;
5
+ /** The statement for a step, on one line */
6
+ export declare function stepToSource(step: Step): string;
@@ -0,0 +1,73 @@
1
+ const str = (value) => JSON.stringify(value);
2
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
3
+ function textOptions(call) {
4
+ const parts = [
5
+ call.match !== undefined && `match: ${str(call.match)}`,
6
+ call.ignoreCase !== undefined && `ignoreCase: ${call.ignoreCase}`,
7
+ ].filter(Boolean);
8
+ return parts.length ? `, { ${parts.join(", ")} }` : "";
9
+ }
10
+ export function callToSource(call) {
11
+ switch (call.method) {
12
+ case "locator":
13
+ return `locator(${str(call.xpath)})`;
14
+ case "getByCss":
15
+ return `getByCss(${str(call.css)})`;
16
+ case "getByText":
17
+ case "getByLabel":
18
+ return `${call.method}(${str(call.text)}${textOptions(call)})`;
19
+ case "getByRole":
20
+ return `getByRole(${str(call.role)}${call.name === undefined ? "" : `, { name: ${str(call.name)} }`})`;
21
+ case "getByTestId":
22
+ return `getByTestId(${str(call.testId)})`;
23
+ }
24
+ }
25
+ const chainToSource = (chain) => ["page", ...chain.map(callToSource)].join(".");
26
+ export function locatorToSource({ chain, fallbacks }) {
27
+ const primary = chainToSource(chain);
28
+ return fallbacks.length === 0
29
+ ? primary
30
+ : `${primary}.withFallbacks(${fallbacks.map(chainToSource).join(", ")})`;
31
+ }
32
+ export function valueToSource(value) {
33
+ if (value.kind === "literal")
34
+ return str(value.value);
35
+ const object = value.kind === "env" ? "env" : "secrets";
36
+ return IDENTIFIER.test(value.name)
37
+ ? `${object}.${value.name}`
38
+ : `${object}[${str(value.name)}]`;
39
+ }
40
+ function matcherToSource(expected) {
41
+ return typeof expected === "string"
42
+ ? str(expected)
43
+ : `/${expected.regex.source}/${expected.regex.flags}`;
44
+ }
45
+ function expectationToSource(expectation) {
46
+ switch (expectation.matcher) {
47
+ case "toBeVisible":
48
+ return "toBeVisible()";
49
+ case "toHaveAttribute":
50
+ return `toHaveAttribute(${str(expectation.name)}, ${matcherToSource(expectation.expected)})`;
51
+ case "toHaveCount":
52
+ return `toHaveCount(${expectation.expected})`;
53
+ default:
54
+ return `${expectation.matcher}(${matcherToSource(expectation.expected)})`;
55
+ }
56
+ }
57
+ /** The statement for a step, on one line */
58
+ export function stepToSource(step) {
59
+ switch (step.kind) {
60
+ case "goto":
61
+ return `await page.goto(${str(step.url)});`;
62
+ case "click":
63
+ return `await ${locatorToSource(step.locator)}.click();`;
64
+ case "fill":
65
+ return `await ${locatorToSource(step.locator)}.fill(${valueToSource(step.value)});`;
66
+ case "expect":
67
+ return `await expect(${locatorToSource(step.locator)})${step.not ? ".not" : ""}.${expectationToSource(step.expectation)};`;
68
+ case "waitForNetworkIdle":
69
+ return "await page.waitForNetworkIdle();";
70
+ case "custom":
71
+ return step.code;
72
+ }
73
+ }
@@ -0,0 +1,5 @@
1
+ export { parseSpec } from "./parse.js";
2
+ export { applyEdit, type StepEdit } from "./edit.js";
3
+ export { stepToSource, locatorToSource, callToSource, valueToSource, } from "./emit.js";
4
+ export { locatorFromSpec } from "./build.js";
5
+ export type { Expectation, LocatorCall, LocatorSpec, ParsedStep, ParsedTest, Step, TextMatcher, TextValue, } from "./model.js";
@@ -0,0 +1,4 @@
1
+ export { parseSpec } from "./parse.js";
2
+ export { applyEdit } from "./edit.js";
3
+ export { stepToSource, locatorToSource, callToSource, valueToSource, } from "./emit.js";
4
+ export { locatorFromSpec } from "./build.js";
@@ -0,0 +1,103 @@
1
+ /** One call of a locator expression, as written in a spec: `page.getByRole("button", { name: "Save" })` */
2
+ export type LocatorCall = {
3
+ method: "locator";
4
+ xpath: string;
5
+ } | {
6
+ method: "getByCss";
7
+ css: string;
8
+ } | {
9
+ method: "getByText";
10
+ text: string;
11
+ match?: "full" | "partial";
12
+ ignoreCase?: boolean;
13
+ } | {
14
+ method: "getByLabel";
15
+ text: string;
16
+ match?: "full" | "partial";
17
+ ignoreCase?: boolean;
18
+ } | {
19
+ method: "getByRole";
20
+ role: string;
21
+ name?: string;
22
+ } | {
23
+ method: "getByTestId";
24
+ testId: string;
25
+ };
26
+ /** A chain of locator calls on `page`, and the locators `withFallbacks` falls back to, most preferred first */
27
+ export interface LocatorSpec {
28
+ chain: LocatorCall[];
29
+ fallbacks: LocatorCall[][];
30
+ }
31
+ /** A text to type or compare with */
32
+ export type TextValue = {
33
+ kind: "literal";
34
+ value: string;
35
+ } | {
36
+ kind: "env";
37
+ name: string;
38
+ } | {
39
+ kind: "secret";
40
+ name: string;
41
+ };
42
+ /** What an assertion expects of text: a string or a regular expression */
43
+ export type TextMatcher = string | {
44
+ regex: {
45
+ source: string;
46
+ flags: string;
47
+ };
48
+ };
49
+ export type Expectation = {
50
+ matcher: "toBeVisible";
51
+ } | {
52
+ matcher: "toHaveText" | "toContainText" | "toHaveValue";
53
+ expected: TextMatcher;
54
+ } | {
55
+ matcher: "toHaveAttribute";
56
+ name: string;
57
+ expected: TextMatcher;
58
+ } | {
59
+ matcher: "toHaveCount";
60
+ expected: number;
61
+ };
62
+ /** The restricted set of statements the recorder writes. Anything else in a test is a `custom` step */
63
+ export type Step = {
64
+ kind: "goto";
65
+ url: string;
66
+ } | {
67
+ kind: "click";
68
+ locator: LocatorSpec;
69
+ } | {
70
+ kind: "fill";
71
+ locator: LocatorSpec;
72
+ value: TextValue;
73
+ } | {
74
+ kind: "expect";
75
+ locator: LocatorSpec;
76
+ not: boolean;
77
+ expectation: Expectation;
78
+ } | {
79
+ kind: "waitForNetworkIdle";
80
+ }
81
+ /** A statement the codec does not understand, kept verbatim */
82
+ | {
83
+ kind: "custom";
84
+ code: string;
85
+ };
86
+ /** A step and where its statement sits in the source */
87
+ export interface ParsedStep {
88
+ step: Step;
89
+ /** Offset of the statement's first character (after leading comments) */
90
+ start: number;
91
+ end: number;
92
+ /** Offset where the statement's own lines begin, leading comments included */
93
+ anchor: number;
94
+ }
95
+ export interface ParsedTest {
96
+ /** describe titles, then the test title */
97
+ titlePath: string[];
98
+ name: string;
99
+ steps: ParsedStep[];
100
+ /** Offset just after the `{` of the test body, and of its closing `}` */
101
+ bodyStart: number;
102
+ bodyEnd: number;
103
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { ParsedTest } from "./model.js";
2
+ /** The tests in a spec file, with their steps and where each statement sits */
3
+ export declare function parseSpec(source: string): ParsedTest[];