@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,272 @@
1
+ import ts from "typescript";
2
+ const TEXT_MATCHERS = ["toHaveText", "toContainText", "toHaveValue"];
3
+ function stringOf(node) {
4
+ return node &&
5
+ (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
6
+ ? node.text
7
+ : undefined;
8
+ }
9
+ /** `{ key: literal, … }` with only the allowed keys, or undefined when it has anything else */
10
+ function readObject(node, allowed) {
11
+ if (!node || !ts.isObjectLiteralExpression(node))
12
+ return undefined;
13
+ const result = {};
14
+ for (const property of node.properties) {
15
+ if (!ts.isPropertyAssignment(property) ||
16
+ !allowed.includes(property.name.getText()))
17
+ return undefined;
18
+ const value = stringOf(property.initializer);
19
+ if (value !== undefined)
20
+ result[property.name.getText()] = value;
21
+ else if (property.initializer.kind === ts.SyntaxKind.TrueKeyword)
22
+ result[property.name.getText()] = true;
23
+ else if (property.initializer.kind === ts.SyntaxKind.FalseKeyword)
24
+ result[property.name.getText()] = false;
25
+ else
26
+ return undefined;
27
+ }
28
+ return result;
29
+ }
30
+ function readCall(method, args) {
31
+ const first = stringOf(args[0]);
32
+ if (first === undefined)
33
+ return undefined;
34
+ switch (method) {
35
+ case "locator":
36
+ return args.length === 1 ? { method, xpath: first } : undefined;
37
+ case "getByCss":
38
+ return args.length === 1 ? { method, css: first } : undefined;
39
+ case "getByTestId":
40
+ return args.length === 1 ? { method, testId: first } : undefined;
41
+ case "getByText":
42
+ case "getByLabel": {
43
+ if (args.length > 2)
44
+ return undefined;
45
+ const options = args.length === 2 ? readObject(args[1], ["match", "ignoreCase"]) : {};
46
+ if (!options)
47
+ return undefined;
48
+ const { match, ignoreCase } = options;
49
+ if ((match !== undefined && match !== "full" && match !== "partial") ||
50
+ (ignoreCase !== undefined && typeof ignoreCase !== "boolean")) {
51
+ return undefined;
52
+ }
53
+ return {
54
+ method,
55
+ text: first,
56
+ ...(match !== undefined && { match: match }),
57
+ ...(ignoreCase !== undefined && { ignoreCase: ignoreCase }),
58
+ };
59
+ }
60
+ case "getByRole": {
61
+ if (args.length > 2)
62
+ return undefined;
63
+ const options = args.length === 2 ? readObject(args[1], ["name"]) : {};
64
+ if (!options ||
65
+ (options.name !== undefined && typeof options.name !== "string"))
66
+ return undefined;
67
+ return {
68
+ method,
69
+ role: first,
70
+ ...(options.name !== undefined && { name: options.name }),
71
+ };
72
+ }
73
+ default:
74
+ return undefined;
75
+ }
76
+ }
77
+ /** `page.a(…).b(…)` as calls in order; undefined when it is anything else */
78
+ function readChain(node) {
79
+ const calls = [];
80
+ let current = node;
81
+ while (ts.isCallExpression(current)) {
82
+ if (!ts.isPropertyAccessExpression(current.expression))
83
+ return undefined;
84
+ const call = readCall(current.expression.name.text, current.arguments);
85
+ if (!call)
86
+ return undefined;
87
+ calls.unshift(call);
88
+ current = current.expression.expression;
89
+ }
90
+ return ts.isIdentifier(current) && current.text === "page" && calls.length > 0
91
+ ? calls
92
+ : undefined;
93
+ }
94
+ function readLocator(node) {
95
+ if (ts.isCallExpression(node) &&
96
+ ts.isPropertyAccessExpression(node.expression) &&
97
+ node.expression.name.text === "withFallbacks") {
98
+ const chain = readChain(node.expression.expression);
99
+ const fallbacks = node.arguments.map(readChain);
100
+ if (!chain ||
101
+ node.arguments.length === 0 ||
102
+ fallbacks.some((fallback) => !fallback))
103
+ return undefined;
104
+ return { chain, fallbacks: fallbacks };
105
+ }
106
+ const chain = readChain(node);
107
+ return chain && { chain, fallbacks: [] };
108
+ }
109
+ /** `env.NAME`, `env["NAME"]`, `secrets.NAME`, `secrets["NAME"]` or a string literal */
110
+ function readValue(node) {
111
+ if (!node)
112
+ return undefined;
113
+ const literal = stringOf(node);
114
+ if (literal !== undefined)
115
+ return { kind: "literal", value: literal };
116
+ let object;
117
+ let name;
118
+ if (ts.isPropertyAccessExpression(node)) {
119
+ object = node.expression;
120
+ name = node.name.text;
121
+ }
122
+ else if (ts.isElementAccessExpression(node)) {
123
+ object = node.expression;
124
+ name = stringOf(node.argumentExpression);
125
+ }
126
+ else
127
+ return undefined;
128
+ if (name === undefined || !ts.isIdentifier(object))
129
+ return undefined;
130
+ if (object.text === "env")
131
+ return { kind: "env", name };
132
+ if (object.text === "secrets")
133
+ return { kind: "secret", name };
134
+ return undefined;
135
+ }
136
+ function readTextMatcher(node) {
137
+ if (!node)
138
+ return undefined;
139
+ const literal = stringOf(node);
140
+ if (literal !== undefined)
141
+ return literal;
142
+ if (ts.isRegularExpressionLiteral(node)) {
143
+ const end = node.text.lastIndexOf("/");
144
+ return {
145
+ regex: {
146
+ source: node.text.slice(1, end),
147
+ flags: node.text.slice(end + 1),
148
+ },
149
+ };
150
+ }
151
+ return undefined;
152
+ }
153
+ function readExpectation(matcher, args) {
154
+ if (matcher === "toBeVisible")
155
+ return args.length === 0 ? { matcher } : undefined;
156
+ if (matcher === "toHaveCount") {
157
+ const [count] = args;
158
+ return args.length === 1 && count && ts.isNumericLiteral(count)
159
+ ? { matcher, expected: Number(count.text) }
160
+ : undefined;
161
+ }
162
+ if (matcher === "toHaveAttribute") {
163
+ const name = stringOf(args[0]);
164
+ const expected = readTextMatcher(args[1]);
165
+ return args.length === 2 && name !== undefined && expected !== undefined
166
+ ? { matcher, name, expected }
167
+ : undefined;
168
+ }
169
+ if (TEXT_MATCHERS.includes(matcher)) {
170
+ const expected = readTextMatcher(args[0]);
171
+ return args.length === 1 && expected !== undefined
172
+ ? { matcher: matcher, expected }
173
+ : undefined;
174
+ }
175
+ return undefined;
176
+ }
177
+ /** The step a statement stands for, or undefined when it is outside the recorder's set */
178
+ function readStep(statement) {
179
+ if (!ts.isExpressionStatement(statement) ||
180
+ !ts.isAwaitExpression(statement.expression))
181
+ return undefined;
182
+ const call = statement.expression.expression;
183
+ if (!ts.isCallExpression(call) ||
184
+ !ts.isPropertyAccessExpression(call.expression))
185
+ return undefined;
186
+ const { expression: target, name } = call.expression;
187
+ const args = call.arguments;
188
+ if (ts.isIdentifier(target) && target.text === "page") {
189
+ if (name.text === "goto" && args.length === 1) {
190
+ const url = stringOf(args[0]);
191
+ return url === undefined ? undefined : { kind: "goto", url };
192
+ }
193
+ return name.text === "waitForNetworkIdle" && args.length === 0
194
+ ? { kind: "waitForNetworkIdle" }
195
+ : undefined;
196
+ }
197
+ // expect(locator).matcher(…) and expect(locator).not.matcher(…)
198
+ const negated = ts.isPropertyAccessExpression(target) && target.name.text === "not";
199
+ const subject = negated
200
+ ? target.expression
201
+ : target;
202
+ if (ts.isCallExpression(subject) &&
203
+ ts.isIdentifier(subject.expression) &&
204
+ subject.expression.text === "expect" &&
205
+ subject.arguments.length === 1) {
206
+ const locator = readLocator(subject.arguments[0]);
207
+ const expectation = readExpectation(name.text, args);
208
+ return locator && expectation
209
+ ? { kind: "expect", locator, not: negated, expectation }
210
+ : undefined;
211
+ }
212
+ const locator = readLocator(target);
213
+ if (!locator)
214
+ return undefined;
215
+ if (name.text === "click" && args.length === 0)
216
+ return { kind: "click", locator };
217
+ if (name.text === "fill" && args.length === 1) {
218
+ const value = readValue(args[0]);
219
+ return value ? { kind: "fill", locator, value } : undefined;
220
+ }
221
+ return undefined;
222
+ }
223
+ function parseStatement(source, statement, sourceFile) {
224
+ const start = statement.getStart(sourceFile);
225
+ const end = statement.end;
226
+ const comments = ts.getLeadingCommentRanges(source, statement.getFullStart());
227
+ return {
228
+ step: readStep(statement) ?? {
229
+ kind: "custom",
230
+ code: source.slice(start, end),
231
+ },
232
+ start,
233
+ end,
234
+ anchor: comments?.[0]?.pos ?? start,
235
+ };
236
+ }
237
+ function callbackBody(node) {
238
+ return node &&
239
+ (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
240
+ ts.isBlock(node.body)
241
+ ? node.body
242
+ : undefined;
243
+ }
244
+ /** The tests in a spec file, with their steps and where each statement sits */
245
+ export function parseSpec(source) {
246
+ const sourceFile = ts.createSourceFile("spec.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
247
+ const tests = [];
248
+ const visit = (node, titles) => {
249
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
250
+ const title = stringOf(node.arguments[0]);
251
+ const body = callbackBody(node.arguments[1]);
252
+ if (title !== undefined && body && node.expression.text === "test") {
253
+ const titlePath = [...titles, title];
254
+ tests.push({
255
+ titlePath,
256
+ name: titlePath.join(" > "),
257
+ steps: body.statements.map((statement) => parseStatement(source, statement, sourceFile)),
258
+ bodyStart: body.getStart(sourceFile) + 1,
259
+ bodyEnd: body.end - 1,
260
+ });
261
+ return;
262
+ }
263
+ if (title !== undefined && body && node.expression.text === "describe") {
264
+ ts.forEachChild(body, (child) => visit(child, [...titles, title]));
265
+ return;
266
+ }
267
+ }
268
+ ts.forEachChild(node, (child) => visit(child, titles));
269
+ };
270
+ visit(sourceFile, []);
271
+ return tests;
272
+ }
@@ -0,0 +1,46 @@
1
+ import type { BiDiCommands, BiDiEvents } from "../types/bidi.js";
2
+ import { type EventName, type Subscription, type SubscribeOptions } from "./subscriptions.js";
3
+ export interface ConnectorOptions {
4
+ /** @default 30000 */
5
+ commandTimeout?: number;
6
+ }
7
+ export interface SendOptions {
8
+ /** Overrides the connector's commandTimeout for this command */
9
+ timeout?: number;
10
+ }
11
+ export declare class BiDiConnector {
12
+ private resolveMap;
13
+ private currentId;
14
+ private webSocket;
15
+ private eventEmitter;
16
+ private commandTimeout;
17
+ private subscriptions;
18
+ constructor(ws: WebSocket, options?: ConnectorOptions);
19
+ static connect(url: string, options?: ConnectorOptions): Promise<BiDiConnector>;
20
+ private onWebsocketClose;
21
+ private onWebsocketError;
22
+ /** Rejects every pending command; `reason` gets the pending method names for its message */
23
+ private rejectAll;
24
+ private getId;
25
+ send<M extends keyof BiDiCommands>(method: M, params?: BiDiCommands[M]["params"], options?: SendOptions): Promise<BiDiCommands[M]["result"]>;
26
+ /**
27
+ * Subscribes to events. Unscoped subscriptions are refcounted per event, so features can
28
+ * subscribe independently; scoped ones (`contexts`) always send their own command.
29
+ */
30
+ subscribe(events: EventName[], options?: SubscribeOptions): Promise<Subscription>;
31
+ /**
32
+ * Resolves with the params of the first `event` for which `predicate` is true.
33
+ * The listener is registered before this returns, so start the triggering command afterwards.
34
+ * Rejects with WaitTimeoutError after `timeout` ms, or with the abort reason.
35
+ */
36
+ waitForEvent<E extends keyof BiDiEvents>(event: E, predicate: (params: BiDiEvents[E]["params"]) => boolean, options: {
37
+ timeout: number;
38
+ signal?: AbortSignal;
39
+ }): Promise<BiDiEvents[E]["params"]>;
40
+ /** Register an event listener */
41
+ onEvent<E extends keyof BiDiEvents>(event: E, listener: (params: BiDiEvents[E]["params"]) => void): void;
42
+ /** Remove an event listener */
43
+ offEvent<E extends keyof BiDiEvents>(event: E, listener: (params: BiDiEvents[E]["params"]) => void): void;
44
+ private messageListener;
45
+ kill(): void;
46
+ }
@@ -0,0 +1,176 @@
1
+ import EventEmitter from "node:events";
2
+ import logger from "../logger/index.js";
3
+ import { WaitTimeoutError } from "../wait/wait-until.js";
4
+ import { BiDiError } from "./bidi-error.js";
5
+ import { SubscriptionManager, } from "./subscriptions.js";
6
+ /** Time (ms) a command may wait for its reply before it fails with code "timeout" */
7
+ const DEFAULT_COMMAND_TIMEOUT = 30000;
8
+ export class BiDiConnector {
9
+ constructor(ws, options = {}) {
10
+ this.resolveMap = new Map();
11
+ this.currentId = 0;
12
+ this.onWebsocketClose = () => {
13
+ this.rejectAll((pending) => new Error(`Websocket closed${pendingSuffix(pending)}`));
14
+ };
15
+ this.onWebsocketError = (ev) => {
16
+ this.rejectAll((pending) => Object.assign(new Error(`Websocket error${pendingSuffix(pending)}`), {
17
+ cause: ev,
18
+ }));
19
+ };
20
+ this.messageListener = (ev) => {
21
+ const message = JSON.parse(ev.data);
22
+ logger.debug("Received message from BiDi Websocket: ", message);
23
+ if (message.type === "success" && message.id !== undefined) {
24
+ const targetToResolve = this.resolveMap.get(message.id);
25
+ if (targetToResolve) {
26
+ this.resolveMap.delete(message.id);
27
+ targetToResolve.resolve(message.result);
28
+ }
29
+ }
30
+ else if (message.type === "error") {
31
+ const target = message.id === null || message.id === undefined
32
+ ? undefined
33
+ : this.resolveMap.get(message.id);
34
+ if (!target) {
35
+ logger.debug("BiDi error reply without a pending command", message);
36
+ return;
37
+ }
38
+ this.resolveMap.delete(message.id);
39
+ target.reject(new BiDiError(target.method, message.error, message.message, message.stacktrace));
40
+ }
41
+ else if (message.type === "event") {
42
+ this.eventEmitter.emit(message.method, message.params);
43
+ }
44
+ };
45
+ this.commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT;
46
+ this.subscriptions = new SubscriptionManager({
47
+ subscribe: (params) => this.send("session.subscribe", params),
48
+ unsubscribe: (params) => this.send("session.unsubscribe", params),
49
+ });
50
+ this.webSocket = ws;
51
+ this.webSocket.addEventListener("message", this.messageListener);
52
+ this.webSocket.addEventListener("close", this.onWebsocketClose);
53
+ this.webSocket.addEventListener("error", this.onWebsocketError);
54
+ this.eventEmitter = new EventEmitter();
55
+ logger.debug("BiDiConnector: Websocket handshake finished");
56
+ }
57
+ static async connect(url, options) {
58
+ logger.debug("Connecting to WebSocket");
59
+ const ws = new WebSocket(url);
60
+ return new Promise((resolve, reject) => {
61
+ logger.debug("Waiting for WebSocket handshake");
62
+ ws.addEventListener("open", () => {
63
+ logger.debug("WebSocket handshake finished");
64
+ resolve(new BiDiConnector(ws, options));
65
+ });
66
+ ws.addEventListener("error", (err) => {
67
+ logger.debug("WebSocket error", { err });
68
+ reject(new Error("WebSocket connection failed"));
69
+ });
70
+ });
71
+ }
72
+ /** Rejects every pending command; `reason` gets the pending method names for its message */
73
+ rejectAll(reason) {
74
+ const entries = [...this.resolveMap.values()];
75
+ const error = reason(entries.map((entry) => entry.method));
76
+ this.resolveMap.clear();
77
+ entries.forEach(({ reject }) => reject(error));
78
+ }
79
+ getId() {
80
+ return this.currentId++;
81
+ }
82
+ send(method, params, options = {}) {
83
+ return new Promise((resolve, reject) => {
84
+ try {
85
+ const id = this.getId();
86
+ const timeout = options.timeout ?? this.commandTimeout;
87
+ const timer = setTimeout(() => {
88
+ this.resolveMap.delete(id);
89
+ reject(new BiDiError(method, "timeout", `no reply within ${timeout}ms`));
90
+ }, timeout);
91
+ this.resolveMap.set(id, {
92
+ method,
93
+ resolve: (value) => {
94
+ clearTimeout(timer);
95
+ resolve(value);
96
+ },
97
+ reject: (reason) => {
98
+ clearTimeout(timer);
99
+ reject(reason);
100
+ },
101
+ });
102
+ const commandBody = {
103
+ id,
104
+ method,
105
+ params,
106
+ };
107
+ logger.debug("Sending message to BiDi Websocket", commandBody);
108
+ this.webSocket.send(JSON.stringify(commandBody));
109
+ }
110
+ catch (err) {
111
+ reject(err);
112
+ }
113
+ });
114
+ }
115
+ /**
116
+ * Subscribes to events. Unscoped subscriptions are refcounted per event, so features can
117
+ * subscribe independently; scoped ones (`contexts`) always send their own command.
118
+ */
119
+ subscribe(events, options) {
120
+ return this.subscriptions.subscribe(events, options);
121
+ }
122
+ /**
123
+ * Resolves with the params of the first `event` for which `predicate` is true.
124
+ * The listener is registered before this returns, so start the triggering command afterwards.
125
+ * Rejects with WaitTimeoutError after `timeout` ms, or with the abort reason.
126
+ */
127
+ waitForEvent(event, predicate, options) {
128
+ const { timeout, signal } = options;
129
+ return new Promise((resolve, reject) => {
130
+ if (signal?.aborted) {
131
+ reject(signal.reason);
132
+ return;
133
+ }
134
+ const cleanup = () => {
135
+ clearTimeout(timer);
136
+ this.offEvent(event, listener);
137
+ signal?.removeEventListener("abort", onAbort);
138
+ };
139
+ const listener = (params) => {
140
+ if (!predicate(params))
141
+ return;
142
+ cleanup();
143
+ resolve(params);
144
+ };
145
+ const onAbort = () => {
146
+ cleanup();
147
+ reject(signal?.reason);
148
+ };
149
+ const timer = setTimeout(() => {
150
+ cleanup();
151
+ reject(new WaitTimeoutError(timeout, undefined));
152
+ }, timeout);
153
+ this.onEvent(event, listener);
154
+ signal?.addEventListener("abort", onAbort, { once: true });
155
+ });
156
+ }
157
+ /** Register an event listener */
158
+ onEvent(event, listener) {
159
+ this.eventEmitter.on(event, listener);
160
+ }
161
+ /** Remove an event listener */
162
+ offEvent(event, listener) {
163
+ this.eventEmitter.off(event, listener);
164
+ }
165
+ kill() {
166
+ this.webSocket.removeEventListener("message", this.messageListener);
167
+ this.webSocket.removeEventListener("close", this.onWebsocketClose);
168
+ this.webSocket.removeEventListener("error", this.onWebsocketError);
169
+ this.rejectAll((pending) => new Error(`BiDiConnector killed${pendingSuffix(pending)}`));
170
+ this.eventEmitter.removeAllListeners();
171
+ this.webSocket.close();
172
+ }
173
+ }
174
+ function pendingSuffix(pending) {
175
+ return pending.length > 0 ? ` (pending: ${pending.join(", ")})` : "";
176
+ }
@@ -0,0 +1,10 @@
1
+ import type { ErrorCode } from "../types/bidi-protocols/error.js";
2
+ /** A failed BiDi command: an error reply from the browser, or `"timeout"` when none came in time */
3
+ export declare class BiDiError extends Error {
4
+ /** BiDi error code such as "no such node", or "timeout" */
5
+ readonly code: ErrorCode | "timeout";
6
+ /** The command that failed, e.g. "script.callFunction" */
7
+ readonly command: string;
8
+ readonly remoteStack?: string | undefined;
9
+ constructor(command: string, code: ErrorCode | "timeout", message: string, remoteStack?: string);
10
+ }
@@ -0,0 +1,10 @@
1
+ /** A failed BiDi command: an error reply from the browser, or `"timeout"` when none came in time */
2
+ export class BiDiError extends Error {
3
+ constructor(command, code, message, remoteStack) {
4
+ super(`${command} failed (${code}): ${message}`);
5
+ this.name = "BiDiError";
6
+ this.command = command;
7
+ this.code = code;
8
+ this.remoteStack = remoteStack;
9
+ }
10
+ }
@@ -0,0 +1,36 @@
1
+ import type { BiDiEvents } from "../types/bidi.js";
2
+ export type EventName = keyof BiDiEvents;
3
+ export interface Subscription {
4
+ /** Idempotent */
5
+ unsubscribe(): Promise<void>;
6
+ }
7
+ export interface SubscribeOptions {
8
+ /** Scope to these browsing contexts. Scoped subscriptions are never shared */
9
+ contexts?: string[];
10
+ }
11
+ /** Sends the two session commands; the connector provides it */
12
+ export interface SessionCommands {
13
+ subscribe(params: {
14
+ events: string[];
15
+ contexts?: string[];
16
+ }): Promise<{
17
+ subscription: string;
18
+ }>;
19
+ unsubscribe(params: {
20
+ subscriptions: string[];
21
+ }): Promise<unknown>;
22
+ }
23
+ /**
24
+ * Global (unscoped) subscriptions are refcounted per event name: the first subscriber sends
25
+ * `session.subscribe`, the last one leaving sends `session.unsubscribe`.
26
+ */
27
+ export declare class SubscriptionManager {
28
+ private commands;
29
+ private shared;
30
+ constructor(commands: SessionCommands);
31
+ subscribe(events: EventName[], options?: SubscribeOptions): Promise<Subscription>;
32
+ private subscribeScoped;
33
+ private state;
34
+ private acquire;
35
+ private release;
36
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Global (unscoped) subscriptions are refcounted per event name: the first subscriber sends
3
+ * `session.subscribe`, the last one leaving sends `session.unsubscribe`.
4
+ */
5
+ export class SubscriptionManager {
6
+ constructor(commands) {
7
+ this.commands = commands;
8
+ this.shared = new Map();
9
+ }
10
+ async subscribe(events, options = {}) {
11
+ if (options.contexts)
12
+ return this.subscribeScoped(events, options.contexts);
13
+ const acquired = [];
14
+ try {
15
+ for (const event of events) {
16
+ await this.acquire(event);
17
+ acquired.push(event);
18
+ }
19
+ }
20
+ catch (err) {
21
+ await Promise.allSettled(acquired.map((event) => this.release(event)));
22
+ throw err;
23
+ }
24
+ let released = false;
25
+ return {
26
+ unsubscribe: async () => {
27
+ if (released)
28
+ return;
29
+ released = true;
30
+ await Promise.all(acquired.map((event) => this.release(event)));
31
+ },
32
+ };
33
+ }
34
+ async subscribeScoped(events, contexts) {
35
+ const { subscription } = await this.commands.subscribe({ events, contexts });
36
+ let released = false;
37
+ return {
38
+ unsubscribe: async () => {
39
+ if (released)
40
+ return;
41
+ released = true;
42
+ await this.commands.unsubscribe({ subscriptions: [subscription] });
43
+ },
44
+ };
45
+ }
46
+ state(event) {
47
+ let state = this.shared.get(event);
48
+ if (!state) {
49
+ state = { count: 0, id: undefined, chain: Promise.resolve() };
50
+ this.shared.set(event, state);
51
+ }
52
+ return state;
53
+ }
54
+ acquire(event) {
55
+ const state = this.state(event);
56
+ state.count++;
57
+ const step = state.chain.then(async () => {
58
+ if (state.count > 0 && state.id === undefined) {
59
+ state.id = (await this.commands.subscribe({ events: [event] })).subscription;
60
+ }
61
+ });
62
+ state.chain = step.catch(() => { });
63
+ return step.catch((err) => {
64
+ state.count--;
65
+ throw err;
66
+ });
67
+ }
68
+ release(event) {
69
+ const state = this.state(event);
70
+ state.count--;
71
+ const step = state.chain.then(async () => {
72
+ if (state.count === 0 && state.id !== undefined) {
73
+ const id = state.id;
74
+ state.id = undefined;
75
+ await this.commands.unsubscribe({ subscriptions: [id] });
76
+ }
77
+ });
78
+ state.chain = step.catch(() => { });
79
+ return step;
80
+ }
81
+ }
@@ -33,7 +33,7 @@ export interface PartialTestResult extends TestResultBase {
33
33
 
34
34
  export type TestResult = TestResultSuccess | TestResultFailed | TestResultBase;
35
35
 
36
- interface TestResultSuccess {
36
+ interface TestResultSuccess extends TestResultIdentity {
37
37
  status: "success";
38
38
  duration: number;
39
39
  logs?: TestLogEntry[];
@@ -41,9 +41,9 @@ interface TestResultSuccess {
41
41
  logsDropped?: number;
42
42
  }
43
43
 
44
- interface TestResultFailed {
44
+ /** The error sits flat on the entry, as in the report: `message`, `type`, `stack`, and for assertions `expected` and `actual` */
45
+ interface TestResultFailed extends TestResultIdentity, TestError {
45
46
  status: "failed";
46
- error: TestError;
47
47
  duration: number;
48
48
  logs?: TestLogEntry[];
49
49
  logsDropped?: number;
@@ -59,6 +59,12 @@ export interface TestLogEntry {
59
59
  timestamp: number;
60
60
  }
61
61
 
62
+ interface TestResultIdentity {
63
+ name: string;
64
+ file: string;
65
+ startTime: number;
66
+ }
67
+
62
68
  interface TestResultBase {
63
69
  name: string;
64
70
  file: string;
@@ -66,7 +72,7 @@ interface TestResultBase {
66
72
  status: "started";
67
73
  }
68
74
 
69
- interface TestError {
75
+ export interface TestError {
70
76
  message: string;
71
77
  type: "timeout" | "error" | "assertion";
72
78
  stack?: string | undefined;