@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
@@ -1,1350 +0,0 @@
1
- # `expect` Assertions Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Add a single `expect()` API with auto-retrying locator matchers and synchronous value matchers; failures throw `AssertionError` and are reported as `type: "assertion"`.
6
-
7
- **Architecture:** `expect(x)` dispatches on `x instanceof Locator`: locators get `LocatorAssertions` (async, polling `Locator` read methods every 100 ms until a timeout), everything else gets `ValueAssertions` (sync, check once). Both throw `AssertionError`, which the runner maps to a `type: "assertion"` test error. A module-level registry of running locator assertions lets the runner fail tests that forgot to `await` one.
8
-
9
- **Tech Stack:** TypeScript (`module: nodenext`, `strict`, `verbatimModuleSyntax`, `noUncheckedIndexedAccess`), tsx, Node 26 built-in `node:test` + `node:assert/strict`, WebDriver BiDi `script.evaluate`.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-09-29-expect-design.md`
12
-
13
- ## Global Constraints
14
-
15
- - No new dependencies. Unit tests use `node:test` via `tsx --test`.
16
- - Relative imports use the `.js` extension (e.g. `"./assertion-error.js"`), as everywhere in `src/`.
17
- - Type-only imports use `import type` (`verbatimModuleSyntax` is on).
18
- - Locator retry: poll interval `100` ms; timeout resolution per-call `{ timeout }` → config `expect.timeout` → default `5000` ms; at least one read even with `timeout: 0`.
19
- - Errors thrown while reading (evaluate exception, closed socket) are rethrown immediately, never retried.
20
- - `AssertionError` message format (locator):
21
- ```
22
- expect(locator).toHaveText
23
- locator: //h1
24
- expected: "Contact"
25
- received: "Home"
26
- ```
27
- For values the first line is `expect(value).<matcher>` and there is no `locator:` line. Negated matchers are named `not.<matcher>`.
28
- - Value formatting: strings quoted (`JSON.stringify`), RegExp as `/src/flags`, everything else `util.inspect(value, { depth: 2 })`.
29
- - Type misuse of a value matcher throws `TypeError`, not `AssertionError`.
30
- - Report: `type: "assertion"` with `expected` / `actual`; a RegExp is stored as its formatted string.
31
- - `tsc --noEmit -p .` must show no errors other than the pre-existing `src/config/default.ts(3,14): error TS2739`.
32
- - Leave `result/report.json` and `bun.lock` uncommitted; never `git add -A`.
33
-
34
- ---
35
-
36
- ## File Structure
37
-
38
- | File | Status | Responsibility |
39
- |---|---|---|
40
- | `package.json` | modify | `test` script → `tsx --test "src/**/*.test.ts"` |
41
- | `src/assert/assertion-error.ts` | create | `AssertionError`, `format()`, `toReportValue()` |
42
- | `src/assert/assertion-error.test.ts` | create | unit tests for the above |
43
- | `src/assert/expect.ts` | create (currently empty) | `expect()` overloads, `ValueAssertions`, `LocatorAssertions`, polling, pending-assertion registry |
44
- | `src/assert/expect.test.ts` | create | value matcher tests |
45
- | `src/assert/expect-locator.test.ts` | create | locator matcher + registry tests |
46
- | `src/locator/locator.ts` | modify | public reads: `selector`, `isVisible`, `textContent`, `inputValue`, `getAttribute`, `count` |
47
- | `src/locator/locator.test.ts` | create | unit tests for the reads |
48
- | `src/testing/stub-connector.ts` | create | fake `BiDiConnector` answering `script.evaluate` from a queue |
49
- | `src/types/config.d.ts` | modify | `expect?: { timeout?: number }` |
50
- | `src/types/test.d.ts` | modify | `TestError.type` adds `"assertion"`, optional `expected` / `actual` |
51
- | `src/runner/test-error.ts` | create | `toTestError(err)` mapping thrown values to `TestError` |
52
- | `src/runner/test-error.test.ts` | create | unit tests for `toTestError` |
53
- | `src/runner/test-runner.ts` | modify | use `toTestError`, missing-await guard, reset registry per test |
54
- | `src/tests/index.spec.ts` | modify | end-to-end assertions |
55
-
56
- ---
57
-
58
- ### Task 1: Test script, `AssertionError` and `format`
59
-
60
- **Files:**
61
- - Modify: `package.json` (`scripts.test`)
62
- - Create: `src/assert/assertion-error.ts`
63
- - Test: `src/assert/assertion-error.test.ts`
64
-
65
- **Interfaces:**
66
- - Consumes: nothing.
67
- - Produces:
68
- - `export function format(value: unknown): string`
69
- - `export function toReportValue(value: unknown): unknown` — RegExp → `format(value)`, anything else unchanged.
70
- - `export interface AssertionErrorDetails { matcher: string; expected: unknown; actual: unknown; locator?: string | undefined }`
71
- - `export class AssertionError extends Error { readonly matcher: string; readonly expected: unknown; readonly actual: unknown; readonly locator: string | undefined; constructor(details: AssertionErrorDetails) }` with `name === "AssertionError"`.
72
-
73
- - [ ] **Step 1: Point the test script at tsx**
74
-
75
- In `package.json` replace
76
-
77
- ```json
78
- "test": "echo \"Error: no test specified\" && exit 1"
79
- ```
80
-
81
- with
82
-
83
- ```json
84
- "test": "tsx --test \"src/**/*.test.ts\""
85
- ```
86
-
87
- - [ ] **Step 2: Write the failing test**
88
-
89
- Create `src/assert/assertion-error.test.ts`:
90
-
91
- ```ts
92
- import { test } from "node:test";
93
- import assert from "node:assert/strict";
94
- import { AssertionError, format, toReportValue } from "./assertion-error.js";
95
-
96
- test("format quotes strings, prints regexps and inspects other values", () => {
97
- assert.equal(format("Home"), '"Home"');
98
- assert.equal(format(/err/i), "/err/i");
99
- assert.equal(format(42), "42");
100
- assert.equal(format(null), "null");
101
- assert.equal(format({ a: 1 }), "{ a: 1 }");
102
- });
103
-
104
- test("toReportValue stringifies regexps and keeps other values", () => {
105
- assert.equal(toReportValue(/err/i), "/err/i");
106
- assert.equal(toReportValue("Home"), "Home");
107
- assert.deepEqual(toReportValue({ a: 1 }), { a: 1 });
108
- });
109
-
110
- test("locator assertion message includes the locator line", () => {
111
- const err = new AssertionError({
112
- matcher: "toHaveText",
113
- expected: "Contact",
114
- actual: "Home",
115
- locator: "//h1",
116
- });
117
- assert.ok(err instanceof Error);
118
- assert.equal(err.name, "AssertionError");
119
- assert.equal(
120
- err.message,
121
- 'expect(locator).toHaveText\n locator: //h1\n expected: "Contact"\n received: "Home"',
122
- );
123
- assert.equal(err.matcher, "toHaveText");
124
- assert.equal(err.expected, "Contact");
125
- assert.equal(err.actual, "Home");
126
- assert.equal(err.locator, "//h1");
127
- });
128
-
129
- test("value assertion message has no locator line", () => {
130
- const err = new AssertionError({ matcher: "toBe", expected: 2, actual: 1 });
131
- assert.equal(err.message, "expect(value).toBe\n expected: 2\n received: 1");
132
- assert.equal(err.locator, undefined);
133
- });
134
- ```
135
-
136
- - [ ] **Step 3: Run it to verify it fails**
137
-
138
- Run: `npm test`
139
- Expected: FAIL — `Cannot find module '.../src/assert/assertion-error.js'` (or `ERR_MODULE_NOT_FOUND`), `ℹ fail 1`.
140
-
141
- - [ ] **Step 4: Implement**
142
-
143
- Create `src/assert/assertion-error.ts`:
144
-
145
- ```ts
146
- import { inspect } from "node:util";
147
-
148
- /** Formats a value for assertion messages */
149
- export function format(value: unknown): string {
150
- if (typeof value === "string") return JSON.stringify(value);
151
- if (value instanceof RegExp) return value.toString();
152
- return inspect(value, { depth: 2 });
153
- }
154
-
155
- /** Converts a value into something JSON can store in the report */
156
- export function toReportValue(value: unknown): unknown {
157
- return value instanceof RegExp ? format(value) : value;
158
- }
159
-
160
- export interface AssertionErrorDetails {
161
- /** Matcher name, prefixed with `not.` when negated */
162
- matcher: string;
163
- expected: unknown;
164
- actual: unknown;
165
- /** XPath of the asserted locator, only for locator assertions */
166
- locator?: string | undefined;
167
- }
168
-
169
- export class AssertionError extends Error {
170
- public readonly matcher: string;
171
- public readonly expected: unknown;
172
- public readonly actual: unknown;
173
- public readonly locator: string | undefined;
174
-
175
- constructor({ matcher, expected, actual, locator }: AssertionErrorDetails) {
176
- const subject = locator === undefined ? "value" : "locator";
177
- const lines = [`expect(${subject}).${matcher}`];
178
- if (locator !== undefined) {
179
- lines.push(` locator: ${locator}`);
180
- }
181
- lines.push(` expected: ${format(expected)}`, ` received: ${format(actual)}`);
182
-
183
- super(lines.join("\n"));
184
- this.name = "AssertionError";
185
- this.matcher = matcher;
186
- this.expected = expected;
187
- this.actual = actual;
188
- this.locator = locator;
189
- }
190
- }
191
- ```
192
-
193
- - [ ] **Step 5: Run tests to verify they pass**
194
-
195
- Run: `npm test`
196
- Expected: `ℹ pass 4`, `ℹ fail 0`.
197
-
198
- - [ ] **Step 6: Type-check**
199
-
200
- Run: `npx tsc --noEmit -p .`
201
- Expected: only `src/config/default.ts(3,14): error TS2739`.
202
-
203
- - [ ] **Step 7: Commit**
204
-
205
- ```bash
206
- git add package.json src/assert/assertion-error.ts src/assert/assertion-error.test.ts
207
- git commit -m "feat(assert): add AssertionError and node:test script"
208
- ```
209
-
210
- ---
211
-
212
- ### Task 2: Value matchers
213
-
214
- **Files:**
215
- - Modify: `src/assert/expect.ts` (currently empty)
216
- - Test: `src/assert/expect.test.ts`
217
-
218
- **Interfaces:**
219
- - Consumes: `AssertionError` from `./assertion-error.js` (Task 1).
220
- - Produces:
221
- - `export function expect<T>(value: T): ValueAssertions<T>` (Task 4 turns this into overloads)
222
- - `export class ValueAssertions<T>` with `get not(): ValueAssertions<T>` and matchers returning `void`: `toBe(expected: unknown)`, `toEqual(expected: unknown)`, `toBeTruthy()`, `toBeFalsy()`, `toContain(item: unknown)`, `toMatch(pattern: string | RegExp)`, `toBeGreaterThan(expected: number | bigint)`, `toBeGreaterThanOrEqual(...)`, `toBeLessThan(...)`, `toBeLessThanOrEqual(...)`.
223
- - `function matchesText(actual: string, expected: string | RegExp, mode: "exact" | "contains"): boolean` (module-private; reused by Task 4).
224
-
225
- - [ ] **Step 1: Write the failing test**
226
-
227
- Create `src/assert/expect.test.ts`:
228
-
229
- ```ts
230
- import { test } from "node:test";
231
- import assert from "node:assert/strict";
232
- import { expect } from "./expect.js";
233
- import { AssertionError } from "./assertion-error.js";
234
-
235
- /** Asserts `fn` throws an AssertionError with the given matcher/expected/actual */
236
- function assertFails(
237
- fn: () => void,
238
- matcher: string,
239
- expected: unknown,
240
- actual: unknown,
241
- ) {
242
- assert.throws(fn, (err: unknown) => {
243
- assert.ok(err instanceof AssertionError, "expected an AssertionError");
244
- assert.equal(err.matcher, matcher);
245
- assert.deepEqual(err.expected, expected);
246
- assert.deepEqual(err.actual, actual);
247
- return true;
248
- });
249
- }
250
-
251
- test("toBe uses Object.is", () => {
252
- expect(1).toBe(1);
253
- expect(NaN).toBe(NaN);
254
- assertFails(() => expect(1).toBe(2), "toBe", 2, 1);
255
- assertFails(() => expect({ a: 1 }).toBe({ a: 1 }), "toBe", { a: 1 }, { a: 1 });
256
- expect(1).not.toBe(2);
257
- assertFails(() => expect(1).not.toBe(1), "not.toBe", 1, 1);
258
- });
259
-
260
- test("toEqual compares deeply", () => {
261
- expect({ a: [1, 2] }).toEqual({ a: [1, 2] });
262
- assertFails(() => expect({ a: 1 }).toEqual({ a: 2 }), "toEqual", { a: 2 }, { a: 1 });
263
- expect({ a: 1 }).not.toEqual({ a: 2 });
264
- });
265
-
266
- test("toBeTruthy / toBeFalsy", () => {
267
- expect(1).toBeTruthy();
268
- expect("").toBeFalsy();
269
- assertFails(() => expect(0).toBeTruthy(), "toBeTruthy", "truthy", 0);
270
- assertFails(() => expect("x").toBeFalsy(), "toBeFalsy", "falsy", "x");
271
- expect(0).not.toBeTruthy();
272
- });
273
-
274
- test("toContain works on arrays and strings", () => {
275
- expect([1, 2, 3]).toContain(2);
276
- expect("hello world").toContain("world");
277
- assertFails(() => expect([1, 2]).toContain(3), "toContain", 3, [1, 2]);
278
- expect("abc").not.toContain("z");
279
- assert.throws(() => expect(42).toContain(4), TypeError);
280
- assert.throws(() => expect("abc").toContain(1), TypeError);
281
- });
282
-
283
- test("toMatch accepts substrings and regexps", () => {
284
- expect("Contact us").toMatch("Contact");
285
- expect("Contact us").toMatch(/contact/i);
286
- expect("aaa").toMatch(/a/g);
287
- expect("aaa").toMatch(/a/g); // global regexps must not keep lastIndex state
288
- assertFails(() => expect("Home").toMatch(/contact/), "toMatch", /contact/, "Home");
289
- expect("Home").not.toMatch("Contact");
290
- assert.throws(() => expect(1).toMatch("1"), TypeError);
291
- });
292
-
293
- test("number comparisons", () => {
294
- expect(3).toBeGreaterThan(2);
295
- expect(3).toBeGreaterThanOrEqual(3);
296
- expect(2).toBeLessThan(3);
297
- expect(3).toBeLessThanOrEqual(3);
298
- expect(3n).toBeGreaterThan(2);
299
- assertFails(() => expect(2).toBeGreaterThan(3), "toBeGreaterThan", 3, 2);
300
- assertFails(() => expect(3).not.toBeLessThanOrEqual(3), "not.toBeLessThanOrEqual", 3, 3);
301
- assert.throws(() => expect("3").toBeGreaterThan(2), TypeError);
302
- });
303
- ```
304
-
305
- - [ ] **Step 2: Run it to verify it fails**
306
-
307
- Run: `npm test`
308
- Expected: FAIL — `expect.test.ts` errors with `does not provide an export named 'expect'` (the file is empty); Task 1 tests still pass.
309
-
310
- - [ ] **Step 3: Implement**
311
-
312
- Write `src/assert/expect.ts`:
313
-
314
- ```ts
315
- import { isDeepStrictEqual } from "node:util";
316
- import { AssertionError } from "./assertion-error.js";
317
-
318
- function matchesText(
319
- actual: string,
320
- expected: string | RegExp,
321
- mode: "exact" | "contains",
322
- ) {
323
- // search() ignores lastIndex, so global regexps behave the same on every call
324
- if (expected instanceof RegExp) return actual.search(expected) !== -1;
325
- return mode === "exact" ? actual === expected : actual.includes(expected);
326
- }
327
-
328
- function isNumeric(value: unknown): value is number | bigint {
329
- return typeof value === "number" || typeof value === "bigint";
330
- }
331
-
332
- export class ValueAssertions<T> {
333
- private readonly actual: T;
334
- private readonly negated: boolean;
335
-
336
- constructor(actual: T, negated = false) {
337
- this.actual = actual;
338
- this.negated = negated;
339
- }
340
-
341
- /** Inverts the next matcher */
342
- get not(): ValueAssertions<T> {
343
- return new ValueAssertions(this.actual, !this.negated);
344
- }
345
-
346
- private assert(pass: boolean, matcher: string, expected: unknown) {
347
- if (pass !== this.negated) return;
348
- throw new AssertionError({
349
- matcher: this.negated ? `not.${matcher}` : matcher,
350
- expected,
351
- actual: this.actual,
352
- });
353
- }
354
-
355
- private compare(
356
- matcher: string,
357
- expected: number | bigint,
358
- check: (actual: number | bigint, expected: number | bigint) => boolean,
359
- ) {
360
- const actual = this.actual;
361
- if (!isNumeric(actual) || !isNumeric(expected)) {
362
- throw new TypeError(
363
- `${matcher} expects numbers, received ${typeof actual} and ${typeof expected}`,
364
- );
365
- }
366
- this.assert(check(actual, expected), matcher, expected);
367
- }
368
-
369
- /** Strict identity (`Object.is`) */
370
- toBe(expected: unknown) {
371
- this.assert(Object.is(this.actual, expected), "toBe", expected);
372
- }
373
-
374
- /** Deep structural equality */
375
- toEqual(expected: unknown) {
376
- this.assert(isDeepStrictEqual(this.actual, expected), "toEqual", expected);
377
- }
378
-
379
- toBeTruthy() {
380
- this.assert(Boolean(this.actual), "toBeTruthy", "truthy");
381
- }
382
-
383
- toBeFalsy() {
384
- this.assert(!this.actual, "toBeFalsy", "falsy");
385
- }
386
-
387
- /** Array includes `item`, or string contains substring `item` */
388
- toContain(item: unknown) {
389
- const actual = this.actual;
390
- if (typeof actual === "string") {
391
- if (typeof item !== "string") {
392
- throw new TypeError(`toContain on a string expects a string, received ${typeof item}`);
393
- }
394
- this.assert(actual.includes(item), "toContain", item);
395
- return;
396
- }
397
- if (Array.isArray(actual)) {
398
- this.assert(actual.includes(item), "toContain", item);
399
- return;
400
- }
401
- throw new TypeError(`toContain expects an array or string, received ${typeof actual}`);
402
- }
403
-
404
- /** String contains `pattern` (string) or matches it (RegExp) */
405
- toMatch(pattern: string | RegExp) {
406
- const actual = this.actual;
407
- if (typeof actual !== "string") {
408
- throw new TypeError(`toMatch expects a string, received ${typeof actual}`);
409
- }
410
- this.assert(matchesText(actual, pattern, "contains"), "toMatch", pattern);
411
- }
412
-
413
- toBeGreaterThan(expected: number | bigint) {
414
- this.compare("toBeGreaterThan", expected, (a, b) => a > b);
415
- }
416
-
417
- toBeGreaterThanOrEqual(expected: number | bigint) {
418
- this.compare("toBeGreaterThanOrEqual", expected, (a, b) => a >= b);
419
- }
420
-
421
- toBeLessThan(expected: number | bigint) {
422
- this.compare("toBeLessThan", expected, (a, b) => a < b);
423
- }
424
-
425
- toBeLessThanOrEqual(expected: number | bigint) {
426
- this.compare("toBeLessThanOrEqual", expected, (a, b) => a <= b);
427
- }
428
- }
429
-
430
- export function expect<T>(value: T): ValueAssertions<T> {
431
- return new ValueAssertions(value);
432
- }
433
- ```
434
-
435
- - [ ] **Step 4: Run tests to verify they pass**
436
-
437
- Run: `npm test`
438
- Expected: `ℹ pass 10`, `ℹ fail 0`.
439
-
440
- - [ ] **Step 5: Type-check**
441
-
442
- Run: `npx tsc --noEmit -p .`
443
- Expected: only the pre-existing `src/config/default.ts` error.
444
-
445
- - [ ] **Step 6: Commit**
446
-
447
- ```bash
448
- git add src/assert/expect.ts src/assert/expect.test.ts
449
- git commit -m "feat(assert): add value matchers"
450
- ```
451
-
452
- ---
453
-
454
- ### Task 3: Locator read methods + stub connector
455
-
456
- **Files:**
457
- - Create: `src/testing/stub-connector.ts`
458
- - Modify: `src/locator/locator.ts` (`buildExpression` at lines 51-75; new methods after `getBoundingClientRect`, before `wait`)
459
- - Test: `src/locator/locator.test.ts`
460
-
461
- **Interfaces:**
462
- - Consumes: existing `Locator` constructor `(xpath: string, biDiConnector: BiDiConnector, contextId: BrowsingContext, options?: LocatorOptions)` and private `evaluate(expression: string): Promise<RemoteValue>` (throws `Failed to locate element <xpath>. Details: <text>` on an evaluate exception).
463
- - Produces:
464
- - `stubConnector(...responses: StubResponse[]): { connector: BiDiConnector; expressions: string[] }` — each `script.evaluate` consumes the next response; the last one repeats. `type StubResponse = RemoteValue | { exception: string }`.
465
- - On `Locator`:
466
- - `get selector(): string` — normalised XPath (`"h1"` → `"//h1"`).
467
- - `isVisible(): Promise<boolean>`
468
- - `textContent(): Promise<string | null>` (trimmed)
469
- - `inputValue(): Promise<string | null>`
470
- - `getAttribute(name: string): Promise<string | null>`
471
- - `count(): Promise<number>`
472
-
473
- - [ ] **Step 1: Create the stub connector**
474
-
475
- Create `src/testing/stub-connector.ts`:
476
-
477
- ```ts
478
- import type { BiDiConnector } from "../transport/bidi-connection.js";
479
- import type { RemoteValue } from "../types/bidi-modules/script.js";
480
-
481
- export type StubResponse = RemoteValue | { exception: string };
482
-
483
- /**
484
- * Fake BiDiConnector for unit tests. Every `script.evaluate` answers with the next
485
- * queued response; the last response repeats. Evaluated expressions are recorded.
486
- */
487
- export function stubConnector(...responses: StubResponse[]) {
488
- if (responses.length === 0) {
489
- throw new Error("stubConnector needs at least one response");
490
- }
491
- const expressions: string[] = [];
492
- let calls = 0;
493
-
494
- const connector = {
495
- async send(method: string, params: { expression: string }) {
496
- if (method !== "script.evaluate") {
497
- throw new Error(`stubConnector does not handle ${method}`);
498
- }
499
- expressions.push(params.expression);
500
- const response = responses[Math.min(calls++, responses.length - 1)]!;
501
- if ("exception" in response) {
502
- return {
503
- type: "exception",
504
- exceptionDetails: { text: response.exception },
505
- realm: "stub",
506
- };
507
- }
508
- return { type: "success", result: response, realm: "stub" };
509
- },
510
- };
511
-
512
- return { connector: connector as unknown as BiDiConnector, expressions };
513
- }
514
- ```
515
-
516
- - [ ] **Step 2: Write the failing test**
517
-
518
- Create `src/locator/locator.test.ts`:
519
-
520
- ```ts
521
- import { test } from "node:test";
522
- import assert from "node:assert/strict";
523
- import Locator from "./locator.js";
524
- import { stubConnector, type StubResponse } from "../testing/stub-connector.js";
525
-
526
- function locatorWith(xpath: string, ...responses: StubResponse[]) {
527
- const { connector, expressions } = stubConnector(...responses);
528
- return { locator: new Locator(xpath, connector, "ctx"), expressions };
529
- }
530
-
531
- test("selector normalises the xpath", () => {
532
- assert.equal(locatorWith("h1", { type: "null" }).locator.selector, "//h1");
533
- assert.equal(locatorWith("//div", { type: "null" }).locator.selector, "//div");
534
- assert.equal(
535
- locatorWith("a[text()='Start']", { type: "null" }).locator.selector,
536
- '//a[text()="Start"]',
537
- );
538
- });
539
-
540
- test("textContent returns the string, or null when the element is missing", async () => {
541
- const found = locatorWith("h1", { type: "string", value: "Contact" });
542
- assert.equal(await found.locator.textContent(), "Contact");
543
- assert.match(found.expressions[0]!, /\/\/h1/);
544
- assert.match(found.expressions[0]!, /FIRST_ORDERED_NODE_TYPE/);
545
-
546
- const missing = locatorWith("h1", { type: "null" });
547
- assert.equal(await missing.locator.textContent(), null);
548
- });
549
-
550
- test("inputValue returns the value, or null", async () => {
551
- assert.equal(
552
- await locatorWith("input", { type: "string", value: "test" }).locator.inputValue(),
553
- "test",
554
- );
555
- assert.equal(await locatorWith("input", { type: "null" }).locator.inputValue(), null);
556
- });
557
-
558
- test("getAttribute passes the attribute name as a JSON string", async () => {
559
- const { locator, expressions } = locatorWith("a", { type: "string", value: "/home" });
560
- assert.equal(await locator.getAttribute("href"), "/home");
561
- assert.match(expressions[0]!, /getAttribute\("href"\)/);
562
- });
563
-
564
- test("isVisible returns the boolean result", async () => {
565
- assert.equal(await locatorWith("div", { type: "boolean", value: true }).locator.isVisible(), true);
566
- assert.equal(await locatorWith("div", { type: "boolean", value: false }).locator.isVisible(), false);
567
- });
568
-
569
- test("count evaluates an XPath count()", async () => {
570
- const { locator, expressions } = locatorWith("li", { type: "number", value: 3 });
571
- assert.equal(await locator.count(), 3);
572
- assert.match(expressions[0]!, /count\(\/\/li\)/);
573
- assert.match(expressions[0]!, /NUMBER_TYPE/);
574
- });
575
-
576
- test("reads throw on an evaluate exception", async () => {
577
- const { locator } = locatorWith("h1[", { exception: "SyntaxError: bad xpath" });
578
- await assert.rejects(locator.textContent(), /Failed to locate element/);
579
- });
580
-
581
- test("reads reject unexpected result types", async () => {
582
- const { locator } = locatorWith("h1", { type: "number", value: 1 });
583
- await assert.rejects(locator.textContent(), /Expected a string or null/);
584
- });
585
- ```
586
-
587
- - [ ] **Step 3: Run it to verify it fails**
588
-
589
- Run: `npm test`
590
- Expected: FAIL — `locator.test.ts` fails with `TypeError: ... is not a function` / `selector` is `undefined` (`ℹ fail` ≥ 7). Tasks 1-2 tests still pass.
591
-
592
- - [ ] **Step 4: Implement**
593
-
594
- In `src/locator/locator.ts`, replace the start of `buildExpression`:
595
-
596
- ```ts
597
- /** Builds the expression to evaluate on */
598
- private buildExpression(action?: string) {
599
- let parsedXpath = this.xpath.startsWith("//")
600
- ? this.xpath
601
- : "//" + this.xpath;
602
- parsedXpath = parsedXpath.replaceAll("'", '"');
603
-
604
- const xpathResultType = this.useMultiple
605
- ```
606
-
607
- with
608
-
609
- ```ts
610
- /** Normalises the xpath: always starts with `//` and uses double quotes */
611
- private parsedXpath() {
612
- const xpath = this.xpath.startsWith("//") ? this.xpath : "//" + this.xpath;
613
- return xpath.replaceAll("'", '"');
614
- }
615
-
616
- /** The normalised xpath this locator evaluates */
617
- public get selector(): string {
618
- return this.parsedXpath();
619
- }
620
-
621
- /** Builds the expression to evaluate on */
622
- private buildExpression(action?: string) {
623
- const parsedXpath = this.parsedXpath();
624
-
625
- const xpathResultType = this.useMultiple
626
- ```
627
-
628
- (the rest of `buildExpression` is unchanged; it still uses `parsedXpath`).
629
-
630
- Then insert after `getBoundingClientRect()` (before the `/** Wait for certain time` comment):
631
-
632
- ```ts
633
- /** Evaluates `body` with `el` bound to the first matching element, or null when nothing matches */
634
- private readElement(body: string) {
635
- return this.evaluate(`(() => {
636
- const el = document.evaluate(
637
- '${this.parsedXpath()}',
638
- document,
639
- null,
640
- XPathResult.FIRST_ORDERED_NODE_TYPE,
641
- null
642
- ).singleNodeValue;
643
- ${body}
644
- })()`);
645
- }
646
-
647
- private stringOrNull(value: RemoteValue): string | null {
648
- if (value.type === "string") return value.value;
649
- if (value.type === "null") return null;
650
- throw new Error(`Expected a string or null but received "${value.type}"`);
651
- }
652
-
653
- /** Whether the element exists, has a non-empty box and is not hidden by CSS */
654
- public async isVisible(): Promise<boolean> {
655
- const result = await this.readElement(`
656
- if (!el) return false;
657
- const rect = el.getBoundingClientRect();
658
- const style = getComputedStyle(el);
659
- return rect.width > 0 && rect.height > 0 &&
660
- style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
661
- `);
662
- return result.type === "boolean" && result.value;
663
- }
664
-
665
- /** Trimmed text content, or null when the element is missing */
666
- public async textContent(): Promise<string | null> {
667
- return this.stringOrNull(
668
- await this.readElement(`return el ? el.textContent.trim() : null;`),
669
- );
670
- }
671
-
672
- /** Value of an input/textarea/select, or null when missing */
673
- public async inputValue(): Promise<string | null> {
674
- return this.stringOrNull(
675
- await this.readElement(`return el && typeof el.value === "string" ? el.value : null;`),
676
- );
677
- }
678
-
679
- /** Attribute value, or null when the element or attribute is missing */
680
- public async getAttribute(name: string): Promise<string | null> {
681
- return this.stringOrNull(
682
- await this.readElement(`return el ? el.getAttribute(${JSON.stringify(name)}) : null;`),
683
- );
684
- }
685
-
686
- /** Number of elements matching the xpath */
687
- public async count(): Promise<number> {
688
- const result = await this.evaluate(
689
- `document.evaluate('count(${this.parsedXpath()})', document, null, XPathResult.NUMBER_TYPE, null).numberValue`,
690
- );
691
- if (result.type !== "number") {
692
- throw new Error(`Expected a number but received "${result.type}"`);
693
- }
694
- return result.value;
695
- }
696
- ```
697
-
698
- `RemoteValue` is already imported as a type at the top of `locator.ts`.
699
-
700
- - [ ] **Step 5: Run tests to verify they pass**
701
-
702
- Run: `npm test`
703
- Expected: `ℹ pass 18`, `ℹ fail 0`.
704
-
705
- - [ ] **Step 6: Type-check**
706
-
707
- Run: `npx tsc --noEmit -p .`
708
- Expected: only the pre-existing `src/config/default.ts` error.
709
-
710
- - [ ] **Step 7: Commit**
711
-
712
- ```bash
713
- git add src/testing/stub-connector.ts src/locator/locator.ts src/locator/locator.test.ts
714
- git commit -m "feat(locator): add null-safe read methods"
715
- ```
716
-
717
- ---
718
-
719
- ### Task 4: Locator matchers, polling and pending-assertion registry
720
-
721
- **Files:**
722
- - Modify: `src/types/config.d.ts` (add `expect` to `SamuraiTestConfig`)
723
- - Modify: `src/assert/expect.ts`
724
- - Test: `src/assert/expect-locator.test.ts`
725
-
726
- **Interfaces:**
727
- - Consumes: `Locator` reads + `selector` (Task 3), `AssertionError` (Task 1), `matchesText` + `ValueAssertions` (Task 2), `stubConnector` (Task 3), `readConfig(key)` from `src/config/config.ts` (`readConfig<K extends keyof SamuraiTestConfig>(key: K): Promise<SamuraiTestConfig[K]>`).
728
- - Produces:
729
- - `expect(locator: Locator): LocatorAssertions` and `expect<T>(value: T): ValueAssertions<T>` (overloads)
730
- - `export interface LocatorAssertionOptions { timeout?: number }`
731
- - `export class LocatorAssertions` with `get not(): LocatorAssertions` and matchers returning `Promise<void>`: `toBeVisible(options?)`, `toHaveText(expected: string | RegExp, options?)`, `toContainText(expected: string | RegExp, options?)`, `toHaveValue(expected: string | RegExp, options?)`, `toHaveAttribute(name: string, expected: string | RegExp, options?)`, `toHaveCount(expected: number, options?)`
732
- - `export function takePendingAssertions(): string[]` — matcher names (e.g. `"not.toBeVisible"`) of locator assertions still running; clears the registry.
733
-
734
- - [ ] **Step 1: Add the config option**
735
-
736
- In `src/types/config.d.ts`, inside `SamuraiTestConfig` after `groups?: SamuraiGroup[];` add:
737
-
738
- ```ts
739
- /** Options for `expect` assertions */
740
- expect?: {
741
- /**
742
- * Time (ms) locator assertions keep retrying before failing
743
- * @default 5000
744
- */
745
- timeout?: number;
746
- };
747
- ```
748
-
749
- - [ ] **Step 2: Write the failing test**
750
-
751
- Create `src/assert/expect-locator.test.ts`. Every call passes an explicit `timeout` so the tests never read `samurai.config.ts`.
752
-
753
- ```ts
754
- import { test } from "node:test";
755
- import assert from "node:assert/strict";
756
- import Locator from "../locator/locator.js";
757
- import { stubConnector, type StubResponse } from "../testing/stub-connector.js";
758
- import { expect, takePendingAssertions } from "./expect.js";
759
- import { AssertionError } from "./assertion-error.js";
760
-
761
- function locatorWith(xpath: string, ...responses: StubResponse[]) {
762
- const { connector, expressions } = stubConnector(...responses);
763
- return { locator: new Locator(xpath, connector, "ctx"), expressions };
764
- }
765
-
766
- const text = (value: string): StubResponse => ({ type: "string", value });
767
- const missing: StubResponse = { type: "null" };
768
- const bool = (value: boolean): StubResponse => ({ type: "boolean", value });
769
-
770
- test("toHaveText retries until the text matches", async () => {
771
- const { locator, expressions } = locatorWith("h1", missing, missing, text("Contact"));
772
- await expect(locator).toHaveText("Contact", { timeout: 2000 });
773
- assert.equal(expressions.length, 3);
774
- });
775
-
776
- test("times out with the last received value", async () => {
777
- const { locator } = locatorWith("h1", text("Loading"), text("Home"));
778
- const start = Date.now();
779
- await assert.rejects(expect(locator).toHaveText("Contact", { timeout: 250 }), (err) => {
780
- assert.ok(err instanceof AssertionError);
781
- assert.equal(err.matcher, "toHaveText");
782
- assert.equal(err.expected, "Contact");
783
- assert.equal(err.actual, "Home");
784
- assert.equal(err.locator, "//h1");
785
- return true;
786
- });
787
- const elapsed = Date.now() - start;
788
- assert.ok(elapsed >= 250 && elapsed < 1000, `elapsed ${elapsed}ms`);
789
- });
790
-
791
- test("timeout 0 reads exactly once", async () => {
792
- const { locator, expressions } = locatorWith("h1", text("Home"));
793
- await assert.rejects(expect(locator).toHaveText("Contact", { timeout: 0 }), AssertionError);
794
- assert.equal(expressions.length, 1);
795
- });
796
-
797
- test("read errors are rethrown without retrying", async () => {
798
- const { locator, expressions } = locatorWith("h1[", { exception: "SyntaxError" });
799
- await assert.rejects(expect(locator).toBeVisible({ timeout: 2000 }), (err) => {
800
- assert.ok(!(err instanceof AssertionError));
801
- assert.match((err as Error).message, /Failed to locate element/);
802
- return true;
803
- });
804
- assert.equal(expressions.length, 1);
805
- });
806
-
807
- test("not.toBeVisible waits until the element is hidden", async () => {
808
- const { locator, expressions } = locatorWith("div", bool(true), bool(false));
809
- await expect(locator).not.toBeVisible({ timeout: 2000 });
810
- assert.equal(expressions.length, 2);
811
- });
812
-
813
- test("negated failure is named not.<matcher>", async () => {
814
- const { locator } = locatorWith("h1", text("Contact"));
815
- await assert.rejects(expect(locator).not.toHaveText("Contact", { timeout: 0 }), (err) => {
816
- assert.ok(err instanceof AssertionError);
817
- assert.equal(err.matcher, "not.toHaveText");
818
- return true;
819
- });
820
- });
821
-
822
- test("toHaveText is exact, toContainText is a substring, both accept regexps", async () => {
823
- const { locator } = locatorWith("p", text("Hello world"));
824
- await assert.rejects(expect(locator).toHaveText("Hello", { timeout: 0 }), AssertionError);
825
- await expect(locator).toContainText("Hello", { timeout: 0 });
826
- await expect(locator).toHaveText(/world$/, { timeout: 0 });
827
- await expect(locator).toContainText(/WORLD/i, { timeout: 0 });
828
- });
829
-
830
- test("a missing element never matches text", async () => {
831
- const { locator } = locatorWith("p", missing);
832
- await assert.rejects(expect(locator).toContainText("", { timeout: 0 }), (err) => {
833
- assert.ok(err instanceof AssertionError);
834
- assert.equal(err.actual, null);
835
- return true;
836
- });
837
- });
838
-
839
- test("toHaveValue, toHaveAttribute and toHaveCount", async () => {
840
- await expect(locatorWith("input", text("test")).locator).toHaveValue("test", { timeout: 0 });
841
- await expect(locatorWith("a", text("/home")).locator).toHaveAttribute("href", /home/, { timeout: 0 });
842
- await expect(locatorWith("li", { type: "number", value: 3 }).locator).toHaveCount(3, { timeout: 0 });
843
- await assert.rejects(
844
- expect(locatorWith("li", { type: "number", value: 2 }).locator).toHaveCount(3, { timeout: 0 }),
845
- AssertionError,
846
- );
847
- });
848
-
849
- test("expect(value) still returns value matchers", () => {
850
- expect(1).toBe(1);
851
- assert.throws(() => expect("a").toBe("b"), AssertionError);
852
- });
853
-
854
- test("takePendingAssertions lists assertions that were not awaited", async () => {
855
- takePendingAssertions();
856
- const { locator } = locatorWith("div", bool(false));
857
- const pending = expect(locator).not.toBeVisible({ timeout: 200 });
858
- const failing = expect(locator).toBeVisible({ timeout: 200 });
859
- assert.deepEqual(takePendingAssertions(), ["not.toBeVisible", "toBeVisible"]);
860
- assert.deepEqual(takePendingAssertions(), []);
861
- await pending;
862
- await assert.rejects(failing, AssertionError);
863
- });
864
-
865
- test("awaited assertions are not pending", async () => {
866
- takePendingAssertions();
867
- await expect(locatorWith("div", bool(true)).locator).toBeVisible({ timeout: 0 });
868
- await assert.rejects(
869
- expect(locatorWith("div", bool(false)).locator).toBeVisible({ timeout: 0 }),
870
- AssertionError,
871
- );
872
- assert.deepEqual(takePendingAssertions(), []);
873
- });
874
- ```
875
-
876
- - [ ] **Step 3: Run it to verify it fails**
877
-
878
- Run: `npm test`
879
- Expected: FAIL — `does not provide an export named 'takePendingAssertions'`. Earlier test files still pass.
880
-
881
- - [ ] **Step 4: Implement**
882
-
883
- In `src/assert/expect.ts`:
884
-
885
- a) Replace the imports at the top with:
886
-
887
- ```ts
888
- import { isDeepStrictEqual } from "node:util";
889
- import { setTimeout as sleep } from "node:timers/promises";
890
- import { AssertionError } from "./assertion-error.js";
891
- import Locator from "../locator/locator.js";
892
- import { readConfig } from "../config/config.js";
893
-
894
- const DEFAULT_EXPECT_TIMEOUT = 5000;
895
- const POLL_INTERVAL = 100;
896
-
897
- /** Locator assertions still running, mapped to their matcher name */
898
- const pendingAssertions = new Map<Promise<void>, string>();
899
-
900
- /**
901
- * Returns the matcher names of locator assertions that are still running and stops
902
- * tracking them. The runner calls this after a test to detect a missing `await`.
903
- */
904
- export function takePendingAssertions(): string[] {
905
- const names = [...pendingAssertions.values()];
906
- pendingAssertions.clear();
907
- return names;
908
- }
909
-
910
- function track(matcher: string, assertion: Promise<void>): Promise<void> {
911
- pendingAssertions.set(assertion, matcher);
912
- // Handling both outcomes also keeps an un-awaited failure from becoming an unhandled rejection
913
- const untrack = () => {
914
- pendingAssertions.delete(assertion);
915
- };
916
- assertion.then(untrack, untrack);
917
- return assertion;
918
- }
919
-
920
- export interface LocatorAssertionOptions {
921
- /** Time (ms) to keep retrying. Defaults to config `expect.timeout`, then 5000 */
922
- timeout?: number;
923
- }
924
-
925
- async function resolveTimeout(options?: LocatorAssertionOptions) {
926
- if (options?.timeout !== undefined) return options.timeout;
927
- return (await readConfig("expect"))?.timeout ?? DEFAULT_EXPECT_TIMEOUT;
928
- }
929
- ```
930
-
931
- b) Keep `matchesText`, `isNumeric` and `ValueAssertions` from Task 2 unchanged.
932
-
933
- c) Add after `ValueAssertions`:
934
-
935
- ```ts
936
- export class LocatorAssertions {
937
- private readonly locator: Locator;
938
- private readonly negated: boolean;
939
-
940
- constructor(locator: Locator, negated = false) {
941
- this.locator = locator;
942
- this.negated = negated;
943
- }
944
-
945
- /** Inverts the next matcher: it retries until the condition is false */
946
- get not(): LocatorAssertions {
947
- return new LocatorAssertions(this.locator, !this.negated);
948
- }
949
-
950
- /** Re-reads the element every POLL_INTERVAL ms until `check` passes or the timeout expires */
951
- private assertEventually<V>(
952
- matcher: string,
953
- expected: unknown,
954
- read: () => Promise<V>,
955
- check: (actual: V) => boolean,
956
- options?: LocatorAssertionOptions,
957
- ): Promise<void> {
958
- const name = this.negated ? `not.${matcher}` : matcher;
959
-
960
- return track(
961
- name,
962
- (async () => {
963
- const timeout = await resolveTimeout(options);
964
- const start = Date.now();
965
- while (true) {
966
- const actual = await read();
967
- if (check(actual) !== this.negated) return;
968
- if (Date.now() - start >= timeout) {
969
- throw new AssertionError({
970
- matcher: name,
971
- expected,
972
- actual,
973
- locator: this.locator.selector,
974
- });
975
- }
976
- await sleep(POLL_INTERVAL);
977
- }
978
- })(),
979
- );
980
- }
981
-
982
- toBeVisible(options?: LocatorAssertionOptions) {
983
- return this.assertEventually(
984
- "toBeVisible",
985
- true,
986
- () => this.locator.isVisible(),
987
- (visible) => visible,
988
- options,
989
- );
990
- }
991
-
992
- /** Trimmed text content equals `expected` (string) or matches it (RegExp) */
993
- toHaveText(expected: string | RegExp, options?: LocatorAssertionOptions) {
994
- return this.assertEventually(
995
- "toHaveText",
996
- expected,
997
- () => this.locator.textContent(),
998
- (text) => text !== null && matchesText(text, expected, "exact"),
999
- options,
1000
- );
1001
- }
1002
-
1003
- /** Text content contains `expected` (string) or matches it (RegExp) */
1004
- toContainText(expected: string | RegExp, options?: LocatorAssertionOptions) {
1005
- return this.assertEventually(
1006
- "toContainText",
1007
- expected,
1008
- () => this.locator.textContent(),
1009
- (text) => text !== null && matchesText(text, expected, "contains"),
1010
- options,
1011
- );
1012
- }
1013
-
1014
- toHaveValue(expected: string | RegExp, options?: LocatorAssertionOptions) {
1015
- return this.assertEventually(
1016
- "toHaveValue",
1017
- expected,
1018
- () => this.locator.inputValue(),
1019
- (value) => value !== null && matchesText(value, expected, "exact"),
1020
- options,
1021
- );
1022
- }
1023
-
1024
- toHaveAttribute(
1025
- name: string,
1026
- expected: string | RegExp,
1027
- options?: LocatorAssertionOptions,
1028
- ) {
1029
- return this.assertEventually(
1030
- "toHaveAttribute",
1031
- expected,
1032
- () => this.locator.getAttribute(name),
1033
- (value) => value !== null && matchesText(value, expected, "exact"),
1034
- options,
1035
- );
1036
- }
1037
-
1038
- toHaveCount(expected: number, options?: LocatorAssertionOptions) {
1039
- return this.assertEventually(
1040
- "toHaveCount",
1041
- expected,
1042
- () => this.locator.count(),
1043
- (count) => count === expected,
1044
- options,
1045
- );
1046
- }
1047
- }
1048
- ```
1049
-
1050
- d) Replace the Task 2 `expect` function with the overloads:
1051
-
1052
- ```ts
1053
- /** Auto-retrying assertions on a page element */
1054
- export function expect(locator: Locator): LocatorAssertions;
1055
- /** Synchronous assertions on a plain value */
1056
- export function expect<T>(value: T): ValueAssertions<T>;
1057
- export function expect(value: unknown) {
1058
- return value instanceof Locator
1059
- ? new LocatorAssertions(value)
1060
- : new ValueAssertions(value);
1061
- }
1062
- ```
1063
-
1064
- - [ ] **Step 5: Run tests to verify they pass**
1065
-
1066
- Run: `npm test`
1067
- Expected: `ℹ pass 30`, `ℹ fail 0`, and no `unhandledRejection` warnings in the output.
1068
-
1069
- - [ ] **Step 6: Type-check**
1070
-
1071
- Run: `npx tsc --noEmit -p .`
1072
- Expected: only the pre-existing `src/config/default.ts` error.
1073
-
1074
- - [ ] **Step 7: Commit**
1075
-
1076
- ```bash
1077
- git add src/types/config.d.ts src/assert/expect.ts src/assert/expect-locator.test.ts
1078
- git commit -m "feat(assert): add auto-retrying locator matchers"
1079
- ```
1080
-
1081
- ---
1082
-
1083
- ### Task 5: Report assertion failures and missing awaits
1084
-
1085
- **Files:**
1086
- - Modify: `src/types/test.d.ts:40-44` (`TestError`)
1087
- - Create: `src/runner/test-error.ts`
1088
- - Test: `src/runner/test-error.test.ts`
1089
- - Modify: `src/runner/test-runner.ts` (`executeTestCase`, currently lines 70-130)
1090
-
1091
- **Interfaces:**
1092
- - Consumes: `AssertionError`, `toReportValue` (Task 1); `takePendingAssertions` (Task 4).
1093
- - Produces:
1094
- - `TestError` = `{ message: string; type: "timeout" | "error" | "assertion"; stack?: string | undefined; expected?: unknown; actual?: unknown }`
1095
- - `export function toTestError(err: unknown): TestError`
1096
-
1097
- - [ ] **Step 1: Extend `TestError`**
1098
-
1099
- In `src/types/test.d.ts` replace
1100
-
1101
- ```ts
1102
- interface TestError {
1103
- message: string;
1104
- type: "timeout" | "error";
1105
- stack?: string | undefined;
1106
- }
1107
- ```
1108
-
1109
- with
1110
-
1111
- ```ts
1112
- interface TestError {
1113
- message: string;
1114
- type: "timeout" | "error" | "assertion";
1115
- stack?: string | undefined;
1116
- /** Only for `type: "assertion"` */
1117
- expected?: unknown;
1118
- /** Only for `type: "assertion"` */
1119
- actual?: unknown;
1120
- }
1121
- ```
1122
-
1123
- - [ ] **Step 2: Write the failing test**
1124
-
1125
- Create `src/runner/test-error.test.ts`:
1126
-
1127
- ```ts
1128
- import { test } from "node:test";
1129
- import assert from "node:assert/strict";
1130
- import { toTestError } from "./test-error.js";
1131
- import { AssertionError } from "../assert/assertion-error.js";
1132
-
1133
- test("AssertionError becomes an assertion error with expected/actual", () => {
1134
- const err = new AssertionError({
1135
- matcher: "toHaveText",
1136
- expected: /contact/i,
1137
- actual: "Home",
1138
- locator: "//h1",
1139
- });
1140
- const result = toTestError(err);
1141
- assert.equal(result.type, "assertion");
1142
- assert.equal(result.message, err.message);
1143
- assert.equal(result.stack, err.stack);
1144
- assert.equal(result.expected, "/contact/i");
1145
- assert.equal(result.actual, "Home");
1146
- });
1147
-
1148
- test("other errors stay type error", () => {
1149
- const err = new Error("boom");
1150
- assert.deepEqual(toTestError(err), { message: "boom", stack: err.stack, type: "error" });
1151
- });
1152
-
1153
- test("thrown strings stay type error", () => {
1154
- assert.deepEqual(toTestError("PORT 9223 is already in use!"), {
1155
- message: "PORT 9223 is already in use!",
1156
- stack: undefined,
1157
- type: "error",
1158
- });
1159
- });
1160
- ```
1161
-
1162
- - [ ] **Step 3: Run it to verify it fails**
1163
-
1164
- Run: `npm test`
1165
- Expected: FAIL — `Cannot find module '.../src/runner/test-error.js'`.
1166
-
1167
- - [ ] **Step 4: Implement `toTestError`**
1168
-
1169
- Create `src/runner/test-error.ts`:
1170
-
1171
- ```ts
1172
- import { AssertionError, toReportValue } from "../assert/assertion-error.js";
1173
- import type { TestError } from "../types/test.js";
1174
-
1175
- /** Maps anything a test threw to the error stored in the report */
1176
- export function toTestError(err: unknown): TestError {
1177
- if (err instanceof AssertionError) {
1178
- return {
1179
- message: err.message,
1180
- stack: err.stack,
1181
- type: "assertion",
1182
- expected: toReportValue(err.expected),
1183
- actual: toReportValue(err.actual),
1184
- };
1185
- }
1186
- return {
1187
- message: err instanceof Error ? err.message : String(err),
1188
- stack: err instanceof Error ? err.stack : undefined,
1189
- type: "error",
1190
- };
1191
- }
1192
- ```
1193
-
1194
- - [ ] **Step 5: Run tests to verify they pass**
1195
-
1196
- Run: `npm test`
1197
- Expected: `ℹ pass 33`, `ℹ fail 0`.
1198
-
1199
- - [ ] **Step 6: Wire it into the runner**
1200
-
1201
- In `src/runner/test-runner.ts`:
1202
-
1203
- a) Add imports next to the existing ones:
1204
-
1205
- ```ts
1206
- import { takePendingAssertions } from "../assert/expect.js";
1207
- import { toTestError } from "./test-error.js";
1208
- ```
1209
-
1210
- b) In `executeTestCase`, replace
1211
-
1212
- ```ts
1213
- await test?.function(launched.page, launched.browser);
1214
- // Already reported as timed out
1215
- if (timeoutController.signal.aborted) return resolve();
1216
- reporter.onTestEnd(test);
1217
- resolve();
1218
- } catch (err) {
1219
- if (timeoutController.signal.aborted) return resolve();
1220
- reporter.onTestEnd(test, {
1221
- message: err instanceof Error ? err.message : (err as string),
1222
- stack: err instanceof Error ? err.stack : undefined,
1223
- type: "error",
1224
- });
1225
- resolve();
1226
- }
1227
- ```
1228
-
1229
- with
1230
-
1231
- ```ts
1232
- await test?.function(launched.page, launched.browser);
1233
- // Already reported as timed out
1234
- if (timeoutController.signal.aborted) return resolve();
1235
- const unawaited = takePendingAssertions();
1236
- if (unawaited.length > 0) {
1237
- reporter.onTestEnd(test, {
1238
- message: unawaited
1239
- .map((matcher) => `expect(locator).${matcher}() was not awaited`)
1240
- .join("\n"),
1241
- type: "assertion",
1242
- });
1243
- return resolve();
1244
- }
1245
- reporter.onTestEnd(test);
1246
- resolve();
1247
- } catch (err) {
1248
- if (timeoutController.signal.aborted) return resolve();
1249
- reporter.onTestEnd(test, toTestError(err));
1250
- resolve();
1251
- }
1252
- ```
1253
-
1254
- c) After the race, reset the registry so leftovers from a failed or timed-out test cannot fail the next one. Replace
1255
-
1256
- ```ts
1257
- clearTimeout(timeoutTimer);
1258
- ```
1259
-
1260
- with
1261
-
1262
- ```ts
1263
- clearTimeout(timeoutTimer);
1264
- takePendingAssertions();
1265
- ```
1266
-
1267
- - [ ] **Step 7: Type-check and run unit tests**
1268
-
1269
- Run: `npx tsc --noEmit -p .`
1270
- Expected: only the pre-existing `src/config/default.ts` error.
1271
-
1272
- Run: `npm test`
1273
- Expected: `ℹ pass 33`, `ℹ fail 0`.
1274
-
1275
- - [ ] **Step 8: Commit**
1276
-
1277
- ```bash
1278
- git add src/types/test.d.ts src/runner/test-error.ts src/runner/test-error.test.ts src/runner/test-runner.ts
1279
- git commit -m "feat(runner): report assertion failures and missing awaits"
1280
- ```
1281
-
1282
- ---
1283
-
1284
- ### Task 6: End-to-end verification against Firefox
1285
-
1286
- **Files:**
1287
- - Modify: `src/tests/index.spec.ts`
1288
-
1289
- **Interfaces:**
1290
- - Consumes: `expect` (Task 4); runner reporting (Task 5).
1291
- - Produces: nothing new.
1292
-
1293
- Note: `bun run dev` launches real Firefox against `https://itmetsam.nl` and needs network access. Run it from a normal terminal (not a restricted sandbox). The report is written to `result/report.json`.
1294
-
1295
- - [ ] **Step 1: Add assertions to the spec**
1296
-
1297
- `src/tests/index.spec.ts` currently has an uncommitted blank line before the commented-out submit block; keep it. Change the import line to:
1298
-
1299
- ```ts
1300
- import { test } from "../runner/test-runner.js";
1301
- import { expect } from "../assert/expect.js";
1302
- ```
1303
-
1304
- and insert directly after the `messageField.fill(...)` call:
1305
-
1306
- ```ts
1307
- await expect(nameField).toBeVisible();
1308
- await expect(nameField).toHaveValue("test");
1309
- await expect(emailField).toHaveValue("sammiedegroot@gmail.com");
1310
- await expect(subjectField).toHaveValue("Test subject");
1311
- await expect(messageField).toHaveValue(/very long message/);
1312
- ```
1313
-
1314
- - [ ] **Step 2: Run end-to-end, expect success**
1315
-
1316
- Run: `bun run dev; echo "exit $?"; grep -E '"(status|type|message)"' result/report.json`
1317
- Expected: `exit 0`; report shows `"status": "success"` for the summary and the test.
1318
- If a `toHaveValue` fails, the report's `actual` shows what `fill()` really typed — that is a genuine finding about `fill()`; report it rather than loosening the assertion.
1319
-
1320
- - [ ] **Step 3: Verify an assertion failure is reported (temporary change)**
1321
-
1322
- Temporarily change `toHaveValue("test")` to `toHaveValue("wrong", { timeout: 1000 })`.
1323
- Run: `bun run dev; grep -E '"(status|type|message|expected|actual)"' result/report.json`
1324
- Expected: test `"status": "failed"`, `"type": "assertion"`, `"expected": "wrong"`, `"actual": "test"`, message starts with `expect(locator).toHaveValue\n locator: //input[@id=\"contact-name\"]`.
1325
- Revert the change.
1326
-
1327
- - [ ] **Step 4: Verify the missing-await guard (temporary change)**
1328
-
1329
- Temporarily add, as the last line of the test body, without `await`:
1330
-
1331
- ```ts
1332
- expect(page.locator("div[@id='does-not-exist']")).toBeVisible({ timeout: 3000 });
1333
- ```
1334
-
1335
- Run: `bun run dev; grep -E '"(status|type|message)"' result/report.json`
1336
- Expected: `"type": "assertion"`, `"message": "expect(locator).toBeVisible() was not awaited"`, and the process still exits by itself.
1337
- Remove the line.
1338
-
1339
- - [ ] **Step 5: Final checks**
1340
-
1341
- Run: `npm test` → `ℹ fail 0`.
1342
- Run: `npx tsc --noEmit -p .` → only the pre-existing `src/config/default.ts` error.
1343
- Run: `git diff src/tests/index.spec.ts` → only the import, the five assertions and the pre-existing blank line.
1344
-
1345
- - [ ] **Step 6: Commit**
1346
-
1347
- ```bash
1348
- git add src/tests/index.spec.ts
1349
- git commit -m "test: assert filled contact form values end-to-end"
1350
- ```