@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
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Samurai
2
+
3
+ A browser test framework written in TypeScript. It drives a real browser over the [WebDriver BiDi](https://www.w3.org/TR/webdriver-bidi/) protocol, with no WebDriver server and no Playwright or Puppeteer underneath.
4
+
5
+ ```ts
6
+ import { expect, test } from "../api.js";
7
+
8
+ test("Visitor can sign in", async ({ page, env, secrets }) => {
9
+ await page.goto("/login");
10
+ await page.getByLabel("Email").fill(env.testUser as string);
11
+ await page.getByLabel("Password").fill(secrets.TEST_PASSWORD as string);
12
+ await page.getByRole("button", { name: "Sign in" }).click();
13
+ await expect(page.getByTestId("welcome")).toHaveText("Welcome back");
14
+ });
15
+ ```
16
+
17
+ - **Auto-waiting.** Actions wait until the element is attached, visible, stable, enabled and not covered. Assertions retry until they pass or time out.
18
+ - **Semantic locators.** By role, label, text, test id, CSS or XPath, with ordered fallbacks.
19
+ - **Environments and secrets.** One test file runs against `dev`, `staging` or `production`; secrets come from the environment or a `.env.<name>` file and are masked in logs and reports.
20
+ - **Network control.** Mock, block or modify requests; wait for requests, responses and network idle; read response bodies.
21
+ - **Recorder building blocks.** Specs are parsed into steps and edited back with minimal diffs, and an engine records what a person does in the browser. The UI that ties them together is not built yet (see the [roadmap](docs/roadmap.md)).
22
+
23
+ > **Status:** early. Firefox only, run from a checkout of this repository (not published to npm yet). See [what is still open](docs/roadmap.md).
24
+
25
+ ## Getting started
26
+
27
+ ### 1. Requirements
28
+
29
+ - [Node.js](https://nodejs.org) 26 or newer and [Bun](https://bun.sh) 1.3 or newer (CI uses these versions)
30
+ - Firefox installed on the machine. It is found in the usual install locations on Linux, macOS and Windows.
31
+
32
+ ### 2. Install
33
+
34
+ ```sh
35
+ git clone https://github.com/degrootsam/samurai-framework.git
36
+ cd samurai-framework
37
+ bun install
38
+ ```
39
+
40
+ ### 3. Write a test
41
+
42
+ Tests live in the folder `srcDir` points to (`./src/tests` in `samurai.config.ts`) and end in `.spec.ts`. Create `src/tests/example.spec.ts`:
43
+
44
+ ```ts
45
+ import { expect, test } from "../api.js";
46
+
47
+ test("example.com has its heading", async ({ page }) => {
48
+ await page.goto("https://example.com");
49
+ await expect(
50
+ page.getByRole("heading", { name: "Example Domain" }),
51
+ ).toBeVisible();
52
+ await page.getByText("More information", { match: "partial" }).click();
53
+ await page.waitForLoadState();
54
+ expect(await page.url()).toContain("iana.org");
55
+ });
56
+ ```
57
+
58
+ ### 4. Configure
59
+
60
+ `samurai.config.ts` in the project root:
61
+
62
+ ```ts
63
+ import { defineConfig } from "./src/config/config.js";
64
+
65
+ export default defineConfig({
66
+ srcDir: "./src/tests",
67
+ browser: "firefox",
68
+ timeout: 30000,
69
+ environments: {
70
+ staging: { baseURL: "https://staging.example.com" },
71
+ production: { baseURL: "https://www.example.com" },
72
+ },
73
+ defaultEnvironment: "staging",
74
+ });
75
+ ```
76
+
77
+ With `baseURL` set, `page.goto("/login")` opens `https://staging.example.com/login`.
78
+
79
+ ### 5. Run
80
+
81
+ ```sh
82
+ bun run dev # every *.spec.ts under srcDir, in the default environment
83
+ bun run dev --env production # another environment (or SAMURAI_ENV=production)
84
+ bun run dev --timeout 60000 # per-test timeout in ms; also --expect-timeout
85
+ ```
86
+
87
+ Once the package is installed in a project, `samurai run` does the same with a readable summary and a proper exit code, and `samurai init` scaffolds a new project (see [Command line](docs/cli.md)).
88
+
89
+ A browser window opens for each test and closes when the test ends. The result is written to `result/report.json` (status, duration and error per test); nothing summarises it on the console yet. The process exits with code 0 when every test passed and 1 otherwise (add `--headless` to hide the window). Framework logs go to the console and `logs/`.
90
+
91
+ > `src/tests/index.spec.ts` is an example that submits a real contact form. Delete it or replace it before running everything.
92
+
93
+ ### 6. Secrets (optional)
94
+
95
+ ```sh
96
+ cp .env.example .env.staging
97
+ # SAMURAI_SECRET_TEST_PASSWORD=...
98
+ ```
99
+
100
+ Read it in a test as `secrets.TEST_PASSWORD`. Real environment variables win over the file, and `.env.*` files are git-ignored. See [Environments and secrets](docs/environments-and-secrets.md).
101
+
102
+ ## Documentation
103
+
104
+ | Guide | What it covers |
105
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
106
+ | [Writing tests](docs/writing-tests.md) | `test`, `describe`, fixtures, how a test passes or fails |
107
+ | [Locators](docs/locators.md) | Finding elements, chaining, fallbacks, actions and their auto-waiting |
108
+ | [Assertions](docs/assertions.md) | `expect` for elements and for plain values |
109
+ | [Pages and browsers](docs/pages-and-browsers.md) | Navigation, waits, screenshots, PDF, viewport, emulation, dialogs, downloads, contexts, cookies |
110
+ | [Network](docs/network.md) | Waiting for traffic, mocking, blocking, response bodies |
111
+ | [Configuration](docs/configuration.md) | Every option in `samurai.config.ts`, CLI flags and precedence |
112
+ | [Environments and secrets](docs/environments-and-secrets.md) | Per-environment settings, variables, secrets and masking |
113
+ | [Command line](docs/cli.md) | `samurai run`, `list`, `record` and `init`, options, output, exit codes, `--json` |
114
+ | [Running tests and reports](docs/running-and-reports.md) | What a run does, `report.json`, page logs, timeouts |
115
+ | [Steps and the recorder](docs/recorder.md) | The step codec and the recording engine |
116
+ | [Embedding the framework](docs/embedding.md) | Using the package from another app (such as the Electron UI): build, entry points, `runTests`, events |
117
+ | [Contributing](docs/contributing.md) | Project layout, running the framework's own tests, conventions |
118
+ | [Roadmap](docs/roadmap.md) | What is built and what is still open |
119
+
120
+ Design notes for every feature live in [`docs/superpowers/specs`](docs/superpowers/specs).
121
+
122
+ ## Scripts
123
+
124
+ | Command | Does |
125
+ | ---------------------- | ----------------------------------------------- |
126
+ | `bun run dev` | Runs the test suite (`tsx src/index.ts`) |
127
+ | `bun run test` | Unit tests of the framework (no browser needed) |
128
+ | `bun run test:browser` | Browser tests of the framework (needs Firefox) |
129
+ | `bun run build` | Typechecks and compiles to `dist/` |
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // Quiet the framework's console logging unless the caller asked for more; the log files keep everything
3
+ process.env.SAMURAI_LOG_LEVEL ??= "warn";
4
+
5
+ // Specs are TypeScript: let this process import them. tsx is an optional peer dependency of the package
6
+ try {
7
+ const { register } = await import("tsx/esm/api");
8
+ register();
9
+ } catch (err) {
10
+ if (err?.code !== "ERR_MODULE_NOT_FOUND") throw err;
11
+ process.stderr.write(
12
+ "samurai loads TypeScript specs with tsx, which is not installed. Install it: npm install --save-dev tsx\n",
13
+ );
14
+ process.exit(2);
15
+ }
16
+
17
+ const { main } = await import("../dist/cli/main.js");
18
+ // Ctrl+C ends a recording cleanly (the browser is closed, the file is complete); a second one stops at once
19
+ const interrupt = new AbortController();
20
+ process.on("SIGINT", () => {
21
+ if (interrupt.signal.aborted) process.exit(130);
22
+ interrupt.abort();
23
+ });
24
+ const code = await main(process.argv.slice(2), {
25
+ out: (text) => process.stdout.write(text),
26
+ err: (text) => process.stderr.write(text),
27
+ color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
28
+ env: process.env,
29
+ signal: interrupt.signal,
30
+ });
31
+ process.exit(code);
package/dist/api.js ADDED
@@ -0,0 +1,3 @@
1
+ export { test, describe } from "./runner/registry.js";
2
+ export { expect } from "./assert/expect.js";
3
+ export { defineConfig } from "./config/config.js";
@@ -0,0 +1,23 @@
1
+ /** Formats a value for assertion messages */
2
+ export declare function format(value: unknown): string;
3
+ /**
4
+ * Converts a value into something JSON can store in the report: regexps, bigints,
5
+ * symbols, functions and anything else JSON.stringify rejects (e.g. circular objects)
6
+ * become their `format()` string; other values are kept as-is.
7
+ */
8
+ export declare function toReportValue(value: unknown): unknown;
9
+ export interface AssertionErrorDetails {
10
+ /** Matcher name, prefixed with `not.` when negated */
11
+ matcher: string;
12
+ expected: unknown;
13
+ actual: unknown;
14
+ /** XPath of the asserted locator, only for locator assertions */
15
+ locator?: string | undefined;
16
+ }
17
+ export declare class AssertionError extends Error {
18
+ readonly matcher: string;
19
+ readonly expected: unknown;
20
+ readonly actual: unknown;
21
+ readonly locator: string | undefined;
22
+ constructor({ matcher, expected, actual, locator }: AssertionErrorDetails);
23
+ }
@@ -0,0 +1,45 @@
1
+ import { inspect } from "node:util";
2
+ /** Formats a value for assertion messages */
3
+ export function format(value) {
4
+ if (typeof value === "string")
5
+ return JSON.stringify(value);
6
+ if (value instanceof RegExp)
7
+ return value.toString();
8
+ return inspect(value, { depth: 2 });
9
+ }
10
+ /**
11
+ * Converts a value into something JSON can store in the report: regexps, bigints,
12
+ * symbols, functions and anything else JSON.stringify rejects (e.g. circular objects)
13
+ * become their `format()` string; other values are kept as-is.
14
+ */
15
+ export function toReportValue(value) {
16
+ if (value instanceof RegExp ||
17
+ typeof value === "bigint" ||
18
+ typeof value === "symbol" ||
19
+ typeof value === "function") {
20
+ return format(value);
21
+ }
22
+ try {
23
+ JSON.stringify(value);
24
+ return value;
25
+ }
26
+ catch {
27
+ return format(value);
28
+ }
29
+ }
30
+ export class AssertionError extends Error {
31
+ constructor({ matcher, expected, actual, locator }) {
32
+ const subject = locator === undefined ? "value" : "locator";
33
+ const lines = [`expect(${subject}).${matcher}`];
34
+ if (locator !== undefined) {
35
+ lines.push(` locator: ${locator}`);
36
+ }
37
+ lines.push(` expected: ${format(expected)}`, ` received: ${format(actual)}`);
38
+ super(lines.join("\n"));
39
+ this.name = "AssertionError";
40
+ this.matcher = matcher;
41
+ this.expected = expected;
42
+ this.actual = actual;
43
+ this.locator = locator;
44
+ }
45
+ }
@@ -0,0 +1,55 @@
1
+ import Locator from "../locator/locator.js";
2
+ /**
3
+ * Returns the matcher names of locator assertions that were never awaited and are
4
+ * still running or already failed, and stops tracking them. The runner calls this
5
+ * after a test to detect a missing `await`.
6
+ */
7
+ export declare function takePendingAssertions(): string[];
8
+ export interface LocatorAssertionOptions {
9
+ /** Time (ms) to keep retrying. Defaults to config `expect.timeout`, then 5000 */
10
+ timeout?: number;
11
+ }
12
+ export declare class ValueAssertions<T> {
13
+ private readonly actual;
14
+ private readonly negated;
15
+ constructor(actual: T, negated?: boolean);
16
+ /** Inverts the next matcher */
17
+ get not(): ValueAssertions<T>;
18
+ private assert;
19
+ private compare;
20
+ /** Strict identity (`Object.is`) */
21
+ toBe(expected: unknown): void;
22
+ /** Deep structural equality */
23
+ toEqual(expected: unknown): void;
24
+ toBeTruthy(): void;
25
+ toBeFalsy(): void;
26
+ /** Array includes `item`, or string contains substring `item` */
27
+ toContain(item: unknown): void;
28
+ /** String contains `pattern` (string) or matches it (RegExp) */
29
+ toMatch(pattern: string | RegExp): void;
30
+ toBeGreaterThan(expected: number | bigint): void;
31
+ toBeGreaterThanOrEqual(expected: number | bigint): void;
32
+ toBeLessThan(expected: number | bigint): void;
33
+ toBeLessThanOrEqual(expected: number | bigint): void;
34
+ }
35
+ export declare class LocatorAssertions {
36
+ private readonly locator;
37
+ private readonly negated;
38
+ constructor(locator: Locator, negated?: boolean);
39
+ /** Inverts the next matcher: it retries until the condition is false */
40
+ get not(): LocatorAssertions;
41
+ /** Re-reads the element until `check` passes or the timeout expires */
42
+ private assertEventually;
43
+ toBeVisible(options?: LocatorAssertionOptions): Promise<void>;
44
+ /** Trimmed text content equals `expected` (string) or matches it (RegExp) */
45
+ toHaveText(expected: string | RegExp, options?: LocatorAssertionOptions): Promise<void>;
46
+ /** Text content contains `expected` (string) or matches it (RegExp) */
47
+ toContainText(expected: string | RegExp, options?: LocatorAssertionOptions): Promise<void>;
48
+ toHaveValue(expected: string | RegExp, options?: LocatorAssertionOptions): Promise<void>;
49
+ toHaveAttribute(name: string, expected: string | RegExp, options?: LocatorAssertionOptions): Promise<void>;
50
+ toHaveCount(expected: number, options?: LocatorAssertionOptions): Promise<void>;
51
+ }
52
+ /** Auto-retrying assertions on a page element */
53
+ export declare function expect(locator: Locator): LocatorAssertions;
54
+ /** Synchronous assertions on a plain value */
55
+ export declare function expect<T>(value: T): ValueAssertions<T>;
@@ -0,0 +1,188 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { AssertionError } from "./assertion-error.js";
3
+ import Locator from "../locator/locator.js";
4
+ import { resolveTimeout, waitUntil, WaitTimeoutError } from "../wait/wait-until.js";
5
+ /**
6
+ * Locator assertions nobody has awaited yet that are still running or have failed,
7
+ * mapped to their matcher name
8
+ */
9
+ const pendingAssertions = new Map();
10
+ /**
11
+ * Returns the matcher names of locator assertions that were never awaited and are
12
+ * still running or already failed, and stops tracking them. The runner calls this
13
+ * after a test to detect a missing `await`.
14
+ */
15
+ export function takePendingAssertions() {
16
+ const names = [...pendingAssertions.values()];
17
+ pendingAssertions.clear();
18
+ return names;
19
+ }
20
+ /**
21
+ * Promise that stops being pending as soon as anyone subscribes to it. `await`,
22
+ * `.catch`, `.finally`, `Promise.all` and `assert.rejects` all go through `then`
23
+ * (`await` calls it because the constructor is not the native Promise).
24
+ */
25
+ class TrackedAssertion extends Promise {
26
+ /** Promises derived through `then` are plain promises */
27
+ static get [Symbol.species]() {
28
+ return Promise;
29
+ }
30
+ then(onfulfilled, onrejected) {
31
+ pendingAssertions.delete(this);
32
+ return super.then(onfulfilled, onrejected);
33
+ }
34
+ }
35
+ function track(matcher, assertion) {
36
+ const tracked = new TrackedAssertion((resolve, reject) => {
37
+ assertion.then(resolve, reject);
38
+ });
39
+ pendingAssertions.set(tracked, matcher);
40
+ // The native then does not count as awaiting: a pass stops tracking, a failure stays
41
+ // tracked. Handling the rejection keeps an un-awaited failure from being unhandled.
42
+ Promise.prototype.then.call(tracked, () => pendingAssertions.delete(tracked), () => { });
43
+ return tracked;
44
+ }
45
+ function matchesText(actual, expected, mode) {
46
+ // search() ignores lastIndex, so global regexps behave the same on every call
47
+ if (expected instanceof RegExp)
48
+ return actual.search(expected) !== -1;
49
+ return mode === "exact" ? actual === expected : actual.includes(expected);
50
+ }
51
+ function isNumeric(value) {
52
+ return typeof value === "number" || typeof value === "bigint";
53
+ }
54
+ export class ValueAssertions {
55
+ constructor(actual, negated = false) {
56
+ this.actual = actual;
57
+ this.negated = negated;
58
+ }
59
+ /** Inverts the next matcher */
60
+ get not() {
61
+ return new ValueAssertions(this.actual, !this.negated);
62
+ }
63
+ assert(pass, matcher, expected) {
64
+ if (pass !== this.negated)
65
+ return;
66
+ throw new AssertionError({
67
+ matcher: this.negated ? `not.${matcher}` : matcher,
68
+ expected,
69
+ actual: this.actual,
70
+ });
71
+ }
72
+ compare(matcher, expected, check) {
73
+ const actual = this.actual;
74
+ if (!isNumeric(actual) || !isNumeric(expected)) {
75
+ throw new TypeError(`${matcher} expects numbers, received ${typeof actual} and ${typeof expected}`);
76
+ }
77
+ this.assert(check(actual, expected), matcher, expected);
78
+ }
79
+ /** Strict identity (`Object.is`) */
80
+ toBe(expected) {
81
+ this.assert(Object.is(this.actual, expected), "toBe", expected);
82
+ }
83
+ /** Deep structural equality */
84
+ toEqual(expected) {
85
+ this.assert(isDeepStrictEqual(this.actual, expected), "toEqual", expected);
86
+ }
87
+ toBeTruthy() {
88
+ this.assert(Boolean(this.actual), "toBeTruthy", "truthy");
89
+ }
90
+ toBeFalsy() {
91
+ this.assert(!this.actual, "toBeFalsy", "falsy");
92
+ }
93
+ /** Array includes `item`, or string contains substring `item` */
94
+ toContain(item) {
95
+ const actual = this.actual;
96
+ if (typeof actual === "string") {
97
+ if (typeof item !== "string") {
98
+ throw new TypeError(`toContain on a string expects a string, received ${typeof item}`);
99
+ }
100
+ this.assert(actual.includes(item), "toContain", item);
101
+ return;
102
+ }
103
+ if (Array.isArray(actual)) {
104
+ this.assert(actual.includes(item), "toContain", item);
105
+ return;
106
+ }
107
+ throw new TypeError(`toContain expects an array or string, received ${typeof actual}`);
108
+ }
109
+ /** String contains `pattern` (string) or matches it (RegExp) */
110
+ toMatch(pattern) {
111
+ const actual = this.actual;
112
+ if (typeof actual !== "string") {
113
+ throw new TypeError(`toMatch expects a string, received ${typeof actual}`);
114
+ }
115
+ this.assert(matchesText(actual, pattern, "contains"), "toMatch", pattern);
116
+ }
117
+ toBeGreaterThan(expected) {
118
+ this.compare("toBeGreaterThan", expected, (a, b) => a > b);
119
+ }
120
+ toBeGreaterThanOrEqual(expected) {
121
+ this.compare("toBeGreaterThanOrEqual", expected, (a, b) => a >= b);
122
+ }
123
+ toBeLessThan(expected) {
124
+ this.compare("toBeLessThan", expected, (a, b) => a < b);
125
+ }
126
+ toBeLessThanOrEqual(expected) {
127
+ this.compare("toBeLessThanOrEqual", expected, (a, b) => a <= b);
128
+ }
129
+ }
130
+ export class LocatorAssertions {
131
+ constructor(locator, negated = false) {
132
+ this.locator = locator;
133
+ this.negated = negated;
134
+ }
135
+ /** Inverts the next matcher: it retries until the condition is false */
136
+ get not() {
137
+ return new LocatorAssertions(this.locator, !this.negated);
138
+ }
139
+ /** Re-reads the element until `check` passes or the timeout expires */
140
+ assertEventually(matcher, expected, read, check, options) {
141
+ const name = this.negated ? `not.${matcher}` : matcher;
142
+ return track(name, (async () => {
143
+ const timeout = await resolveTimeout(options?.timeout);
144
+ try {
145
+ await waitUntil(read, (actual) => check(actual) !== this.negated, { timeout });
146
+ }
147
+ catch (err) {
148
+ if (!(err instanceof WaitTimeoutError))
149
+ throw err;
150
+ // Reads never resolve to undefined, so an undefined `last` means no read completed
151
+ if (err.last === undefined) {
152
+ throw new Error(`expect(locator).${name}: could not read ${this.locator.selector} within ${timeout}ms`);
153
+ }
154
+ throw new AssertionError({
155
+ matcher: name,
156
+ expected,
157
+ actual: err.last,
158
+ locator: this.locator.selector,
159
+ });
160
+ }
161
+ })());
162
+ }
163
+ toBeVisible(options) {
164
+ return this.assertEventually("toBeVisible", true, () => this.locator.isVisible(), (visible) => visible, options);
165
+ }
166
+ /** Trimmed text content equals `expected` (string) or matches it (RegExp) */
167
+ toHaveText(expected, options) {
168
+ return this.assertEventually("toHaveText", expected, () => this.locator.textContent(), (text) => text !== null && matchesText(text, expected, "exact"), options);
169
+ }
170
+ /** Text content contains `expected` (string) or matches it (RegExp) */
171
+ toContainText(expected, options) {
172
+ return this.assertEventually("toContainText", expected, () => this.locator.textContent(), (text) => text !== null && matchesText(text, expected, "contains"), options);
173
+ }
174
+ toHaveValue(expected, options) {
175
+ return this.assertEventually("toHaveValue", expected, () => this.locator.inputValue(), (value) => value !== null && matchesText(value, expected, "exact"), options);
176
+ }
177
+ toHaveAttribute(name, expected, options) {
178
+ return this.assertEventually("toHaveAttribute", expected, () => this.locator.getAttribute(name), (value) => value !== null && matchesText(value, expected, "exact"), options);
179
+ }
180
+ toHaveCount(expected, options) {
181
+ return this.assertEventually("toHaveCount", expected, () => this.locator.count(), (count) => count === expected, options);
182
+ }
183
+ }
184
+ export function expect(value) {
185
+ return value instanceof Locator
186
+ ? new LocatorAssertions(value)
187
+ : new ValueAssertions(value);
188
+ }
@@ -0,0 +1,91 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import type { SameSite } from "../types/bidi-modules/network.js";
3
+ import { type EmulationOptions } from "./emulation.js";
4
+ import type Page from "./page.js";
5
+ export type { SameSite } from "../types/bidi-modules/network.js";
6
+ /** A cookie as `context.cookies()` returns it */
7
+ export interface Cookie {
8
+ name: string;
9
+ value: string;
10
+ domain: string;
11
+ path: string;
12
+ httpOnly: boolean;
13
+ secure: boolean;
14
+ sameSite: SameSite;
15
+ /** Seconds since the epoch; absent for a session cookie */
16
+ expiry?: number;
17
+ }
18
+ /** A cookie to add: `domain`, or a `url` to take domain, path and `secure` from */
19
+ export interface CookieInput {
20
+ name: string;
21
+ value: string;
22
+ domain?: string;
23
+ /** @default "/", or the path of `url` */
24
+ path?: string;
25
+ url?: string;
26
+ httpOnly?: boolean;
27
+ secure?: boolean;
28
+ sameSite?: SameSite;
29
+ /** Seconds since the epoch */
30
+ expiry?: number;
31
+ }
32
+ export interface CookieFilter {
33
+ name?: string;
34
+ domain?: string;
35
+ path?: string;
36
+ }
37
+ /** What a context needs from its browser */
38
+ export interface ContextHost {
39
+ connector: BiDiConnector;
40
+ newPage(options: {
41
+ type: "tab";
42
+ userContext?: string;
43
+ }): Promise<Page>;
44
+ /** Called when a context has been closed */
45
+ forget(context: BrowserContext): void;
46
+ }
47
+ /**
48
+ * An isolated browsing session inside one browser: its own cookies, storage and cache. Pages opened in it
49
+ * share them; pages of other contexts see none of it.
50
+ */
51
+ export declare class BrowserContext {
52
+ private host;
53
+ /** The BiDi user context id; "default" for the browser's own */
54
+ readonly id: string;
55
+ readonly isDefault: boolean;
56
+ private opened;
57
+ private isClosed;
58
+ constructor(host: ContextHost,
59
+ /** The BiDi user context id; "default" for the browser's own */
60
+ id: string, isDefault: boolean);
61
+ /** True once `close()` has run */
62
+ get closed(): boolean;
63
+ /** Opens a tab in this context */
64
+ newPage(): Promise<Page>;
65
+ /** The pages opened in this context that are still open */
66
+ pages(): Page[];
67
+ /** @internal Called when a page is opened in this context */
68
+ register(page: Page): void;
69
+ /** Adds cookies to this context's storage. Every cookie is checked before any is stored */
70
+ addCookies(cookies: CookieInput[]): Promise<void>;
71
+ /** The cookies of this context, optionally only those matching `filter` */
72
+ cookies(filter?: CookieFilter): Promise<Cookie[]>;
73
+ /** Deletes all cookies of this context, or those matching `filter` */
74
+ clearCookies(filter?: CookieFilter): Promise<void>;
75
+ /**
76
+ * Pretends things about every page of this context, including pages opened later: locale, timezone, user agent,
77
+ * geolocation, offline, orientation, screen size. A page's own `emulate()` wins over the context's.
78
+ * @example
79
+ * await context.emulate({ locale: "nl-NL", timezone: "Europe/Amsterdam", offline: false });
80
+ */
81
+ emulate(options: EmulationOptions): Promise<void>;
82
+ /**
83
+ * Grants, denies or resets a browser permission ("geolocation", "notifications", "camera", …) for `origin`
84
+ * in this context. Geolocation emulation only answers once the permission is granted.
85
+ */
86
+ setPermission(name: string, state: "granted" | "denied" | "prompt", origin: string): Promise<void>;
87
+ /** Removes the context; the browser closes its pages. The default context cannot be closed */
88
+ close(): Promise<void>;
89
+ private partition;
90
+ private assertOpen;
91
+ }