@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,217 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import Locator from "../locator/locator.js";
4
- import { stubConnector, type StubResponse } from "../testing/stub-connector.js";
5
- import { expect, takePendingAssertions } from "./expect.js";
6
- import { AssertionError } from "./assertion-error.js";
7
-
8
- function locatorWith(xpath: string, ...responses: StubResponse[]) {
9
- const { connector, expressions, nodeCounts } = stubConnector(...responses);
10
- return { locator: new Locator(xpath, connector, "ctx"), expressions, nodeCounts };
11
- }
12
-
13
- const text = (value: string): StubResponse => ({ type: "string", value });
14
- const missing: StubResponse = { type: "null" };
15
- const bool = (value: boolean): StubResponse => ({ type: "boolean", value });
16
-
17
- test("toHaveText retries until the text matches", async () => {
18
- const { locator, expressions } = locatorWith("h1", missing, missing, text("Contact"));
19
- await expect(locator).toHaveText("Contact", { timeout: 2000 });
20
- assert.equal(expressions.length, 3);
21
- });
22
-
23
- test("times out with the last received value", async () => {
24
- const { locator } = locatorWith("h1", text("Loading"), text("Home"));
25
- const start = Date.now();
26
- await assert.rejects(expect(locator).toHaveText("Contact", { timeout: 250 }), (err) => {
27
- assert.ok(err instanceof AssertionError);
28
- assert.equal(err.matcher, "toHaveText");
29
- assert.equal(err.expected, "Contact");
30
- assert.equal(err.actual, "Home");
31
- assert.equal(err.locator, "//h1");
32
- return true;
33
- });
34
- const elapsed = Date.now() - start;
35
- assert.ok(elapsed >= 245 && elapsed < 1000, `elapsed ${elapsed}ms`);
36
- });
37
-
38
- test("timeout 0 reads exactly once", async () => {
39
- const { locator, expressions } = locatorWith("h1", text("Home"));
40
- await assert.rejects(expect(locator).toHaveText("Contact", { timeout: 0 }), AssertionError);
41
- assert.equal(expressions.length, 1);
42
- });
43
-
44
- test("read errors are rethrown without retrying", async () => {
45
- const { locator, expressions } = locatorWith("h1[", { exception: "SyntaxError" });
46
- await assert.rejects(expect(locator).toBeVisible({ timeout: 2000 }), (err) => {
47
- assert.ok(!(err instanceof AssertionError));
48
- assert.match((err as Error).message, /Failed to locate element/);
49
- return true;
50
- });
51
- assert.equal(expressions.length, 1);
52
- });
53
-
54
- test("not.toBeVisible waits until the element is hidden", async () => {
55
- const { locator, expressions } = locatorWith("div", bool(true), bool(false));
56
- await expect(locator).not.toBeVisible({ timeout: 2000 });
57
- assert.equal(expressions.length, 2);
58
- });
59
-
60
- test("negated failure is named not.<matcher>", async () => {
61
- const { locator } = locatorWith("h1", text("Contact"));
62
- await assert.rejects(expect(locator).not.toHaveText("Contact", { timeout: 0 }), (err) => {
63
- assert.ok(err instanceof AssertionError);
64
- assert.equal(err.matcher, "not.toHaveText");
65
- return true;
66
- });
67
- });
68
-
69
- test("toHaveText is exact, toContainText is a substring, both accept regexps", async () => {
70
- const { locator } = locatorWith("p", text("Hello world"));
71
- await assert.rejects(expect(locator).toHaveText("Hello", { timeout: 0 }), AssertionError);
72
- await expect(locator).toContainText("Hello", { timeout: 0 });
73
- await expect(locator).toHaveText(/world$/, { timeout: 0 });
74
- await expect(locator).toContainText(/WORLD/i, { timeout: 0 });
75
- });
76
-
77
- test("a missing element never matches text", async () => {
78
- const { locator } = locatorWith("p", missing);
79
- await assert.rejects(expect(locator).toContainText("", { timeout: 0 }), (err) => {
80
- assert.ok(err instanceof AssertionError);
81
- assert.equal(err.actual, null);
82
- return true;
83
- });
84
- });
85
-
86
- test("toHaveValue, toHaveAttribute and toHaveCount", async () => {
87
- await expect(locatorWith("input", text("test")).locator).toHaveValue("test", { timeout: 0 });
88
- await expect(locatorWith("a", text("/home")).locator).toHaveAttribute("href", /home/, { timeout: 0 });
89
- const three = locatorWith("li", missing);
90
- three.nodeCounts(3);
91
- await expect(three.locator).toHaveCount(3, { timeout: 0 });
92
- const two = locatorWith("li", missing);
93
- two.nodeCounts(2);
94
- await assert.rejects(expect(two.locator).toHaveCount(3, { timeout: 0 }), AssertionError);
95
- });
96
-
97
- test("expect(value) still returns value matchers", () => {
98
- expect(1).toBe(1);
99
- assert.throws(() => expect("a").toBe("b"), AssertionError);
100
- });
101
-
102
- test("takePendingAssertions lists assertions that were not awaited", async () => {
103
- takePendingAssertions();
104
- const { locator } = locatorWith("div", bool(false));
105
- const pending = expect(locator).not.toBeVisible({ timeout: 200 });
106
- const failing = expect(locator).toBeVisible({ timeout: 200 });
107
- assert.deepEqual(takePendingAssertions(), ["not.toBeVisible", "toBeVisible"]);
108
- assert.deepEqual(takePendingAssertions(), []);
109
- await pending;
110
- await assert.rejects(failing, AssertionError);
111
- });
112
-
113
- test("awaited assertions are not pending", async () => {
114
- takePendingAssertions();
115
- await expect(locatorWith("div", bool(true)).locator).toBeVisible({ timeout: 0 });
116
- await assert.rejects(
117
- expect(locatorWith("div", bool(false)).locator).toBeVisible({ timeout: 0 }),
118
- AssertionError,
119
- );
120
- assert.deepEqual(takePendingAssertions(), []);
121
- });
122
-
123
- const settle = () => new Promise((resolve) => setTimeout(resolve, 20));
124
-
125
- test("an un-awaited assertion that already failed is still reported", async () => {
126
- takePendingAssertions();
127
- const { locator } = locatorWith("div", bool(false));
128
- expect(locator).toBeVisible({ timeout: 0 });
129
- // Let the single read finish and the assertion reject
130
- await settle();
131
- assert.deepEqual(takePendingAssertions(), ["toBeVisible"]);
132
- });
133
-
134
- test("an un-awaited assertion that already passed is not reported", async () => {
135
- takePendingAssertions();
136
- const { locator } = locatorWith("div", bool(true));
137
- expect(locator).toBeVisible({ timeout: 0 });
138
- await settle();
139
- assert.deepEqual(takePendingAssertions(), []);
140
- });
141
-
142
- test("a failing assertion awaited through assert.rejects is not reported", async () => {
143
- takePendingAssertions();
144
- const failing = expect(locatorWith("div", bool(false)).locator).toBeVisible({ timeout: 0 });
145
- await settle();
146
- await assert.rejects(failing, AssertionError);
147
- assert.deepEqual(takePendingAssertions(), []);
148
- });
149
-
150
- test("a failing assertion handled with .catch() is not reported", async () => {
151
- takePendingAssertions();
152
- let caught: unknown;
153
- expect(locatorWith("div", bool(false)).locator)
154
- .toBeVisible({ timeout: 0 })
155
- .catch((err) => {
156
- caught = err;
157
- });
158
- await settle();
159
- assert.ok(caught instanceof AssertionError);
160
- assert.deepEqual(takePendingAssertions(), []);
161
- });
162
-
163
- test("assertions handled with await, .then, .finally and Promise.all are not reported", async () => {
164
- takePendingAssertions();
165
- const pass = () => expect(locatorWith("div", bool(true)).locator).toBeVisible({ timeout: 0 });
166
- const fail = () => expect(locatorWith("div", bool(false)).locator).toBeVisible({ timeout: 0 });
167
- const failing = fail();
168
- await settle();
169
- try {
170
- await failing;
171
- assert.fail("expected a rejection");
172
- } catch (err) {
173
- assert.ok(err instanceof AssertionError);
174
- }
175
- await new Promise<void>((resolve) => {
176
- fail().then(undefined, () => resolve());
177
- });
178
- await assert.rejects(fail().finally(() => {}), AssertionError);
179
- await Promise.all([pass(), pass()]);
180
- await assert.rejects(Promise.all([pass(), fail()]), AssertionError);
181
- assert.deepEqual(takePendingAssertions(), []);
182
- });
183
-
184
- const hang: StubResponse = { hang: true };
185
- /** Fails a regression instead of hanging the suite */
186
- const bounded = { timeout: 3000 };
187
-
188
- test("a read that never answers fails at the timeout with the last received value", bounded, async () => {
189
- const { locator, expressions } = locatorWith("h1", text("Home"), hang);
190
- const start = Date.now();
191
- await assert.rejects(expect(locator).toHaveText("Contact", { timeout: 200 }), (err) => {
192
- assert.ok(err instanceof AssertionError);
193
- assert.equal(err.matcher, "toHaveText");
194
- assert.equal(err.expected, "Contact");
195
- assert.equal(err.actual, "Home");
196
- assert.equal(err.locator, "//h1");
197
- return true;
198
- });
199
- const elapsed = Date.now() - start;
200
- assert.ok(elapsed >= 195 && elapsed < 500, `elapsed ${elapsed}ms`);
201
- assert.equal(expressions.length, 2);
202
- });
203
-
204
- test("a first read that never answers fails at the timeout with a plain error", bounded, async () => {
205
- const { locator } = locatorWith("h1", hang);
206
- const start = Date.now();
207
- await assert.rejects(expect(locator).not.toBeVisible({ timeout: 200 }), (err) => {
208
- assert.ok(!(err instanceof AssertionError));
209
- assert.equal(
210
- (err as Error).message,
211
- "expect(locator).not.toBeVisible: could not read //h1 within 200ms",
212
- );
213
- return true;
214
- });
215
- const elapsed = Date.now() - start;
216
- assert.ok(elapsed >= 195 && elapsed < 500, `elapsed ${elapsed}ms`);
217
- });
@@ -1,73 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { expect } from "./expect.js";
4
- import { AssertionError } from "./assertion-error.js";
5
-
6
- /** Asserts `fn` throws an AssertionError with the given matcher/expected/actual */
7
- function assertFails(
8
- fn: () => void,
9
- matcher: string,
10
- expected: unknown,
11
- actual: unknown,
12
- ) {
13
- assert.throws(fn, (err: unknown) => {
14
- assert.ok(err instanceof AssertionError, "expected an AssertionError");
15
- assert.equal(err.matcher, matcher);
16
- assert.deepEqual(err.expected, expected);
17
- assert.deepEqual(err.actual, actual);
18
- return true;
19
- });
20
- }
21
-
22
- test("toBe uses Object.is", () => {
23
- expect(1).toBe(1);
24
- expect(NaN).toBe(NaN);
25
- assertFails(() => expect(1).toBe(2), "toBe", 2, 1);
26
- assertFails(() => expect({ a: 1 }).toBe({ a: 1 }), "toBe", { a: 1 }, { a: 1 });
27
- expect(1).not.toBe(2);
28
- assertFails(() => expect(1).not.toBe(1), "not.toBe", 1, 1);
29
- });
30
-
31
- test("toEqual compares deeply", () => {
32
- expect({ a: [1, 2] }).toEqual({ a: [1, 2] });
33
- assertFails(() => expect({ a: 1 }).toEqual({ a: 2 }), "toEqual", { a: 2 }, { a: 1 });
34
- expect({ a: 1 }).not.toEqual({ a: 2 });
35
- });
36
-
37
- test("toBeTruthy / toBeFalsy", () => {
38
- expect(1).toBeTruthy();
39
- expect("").toBeFalsy();
40
- assertFails(() => expect(0).toBeTruthy(), "toBeTruthy", "truthy", 0);
41
- assertFails(() => expect("x").toBeFalsy(), "toBeFalsy", "falsy", "x");
42
- expect(0).not.toBeTruthy();
43
- });
44
-
45
- test("toContain works on arrays and strings", () => {
46
- expect([1, 2, 3]).toContain(2);
47
- expect("hello world").toContain("world");
48
- assertFails(() => expect([1, 2]).toContain(3), "toContain", 3, [1, 2]);
49
- expect("abc").not.toContain("z");
50
- assert.throws(() => expect(42).toContain(4), TypeError);
51
- assert.throws(() => expect("abc").toContain(1), TypeError);
52
- });
53
-
54
- test("toMatch accepts substrings and regexps", () => {
55
- expect("Contact us").toMatch("Contact");
56
- expect("Contact us").toMatch(/contact/i);
57
- expect("aaa").toMatch(/a/g);
58
- expect("aaa").toMatch(/a/g); // global regexps must not keep lastIndex state
59
- assertFails(() => expect("Home").toMatch(/contact/), "toMatch", /contact/, "Home");
60
- expect("Home").not.toMatch("Contact");
61
- assert.throws(() => expect(1).toMatch("1"), TypeError);
62
- });
63
-
64
- test("number comparisons", () => {
65
- expect(3).toBeGreaterThan(2);
66
- expect(3).toBeGreaterThanOrEqual(3);
67
- expect(2).toBeLessThan(3);
68
- expect(3).toBeLessThanOrEqual(3);
69
- expect(3n).toBeGreaterThan(2);
70
- assertFails(() => expect(2).toBeGreaterThan(3), "toBeGreaterThan", 3, 2);
71
- assertFails(() => expect(3).not.toBeLessThanOrEqual(3), "not.toBeLessThanOrEqual", 3, 3);
72
- assert.throws(() => expect("3").toBeGreaterThan(2), TypeError);
73
- });
@@ -1,299 +0,0 @@
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
- /**
7
- * Locator assertions nobody has awaited yet that are still running or have failed,
8
- * mapped to their matcher name
9
- */
10
- const pendingAssertions = new Map<Promise<void>, string>();
11
-
12
- /**
13
- * Returns the matcher names of locator assertions that were never awaited and are
14
- * still running or already failed, and stops tracking them. The runner calls this
15
- * after a test to detect a missing `await`.
16
- */
17
- export function takePendingAssertions(): string[] {
18
- const names = [...pendingAssertions.values()];
19
- pendingAssertions.clear();
20
- return names;
21
- }
22
-
23
- /**
24
- * Promise that stops being pending as soon as anyone subscribes to it. `await`,
25
- * `.catch`, `.finally`, `Promise.all` and `assert.rejects` all go through `then`
26
- * (`await` calls it because the constructor is not the native Promise).
27
- */
28
- class TrackedAssertion extends Promise<void> {
29
- /** Promises derived through `then` are plain promises */
30
- static override get [Symbol.species]() {
31
- return Promise;
32
- }
33
-
34
- override then<TResult1 = void, TResult2 = never>(
35
- onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
36
- onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
37
- ): Promise<TResult1 | TResult2> {
38
- pendingAssertions.delete(this);
39
- return super.then(onfulfilled, onrejected);
40
- }
41
- }
42
-
43
- function track(matcher: string, assertion: Promise<void>): Promise<void> {
44
- const tracked = new TrackedAssertion((resolve, reject) => {
45
- assertion.then(resolve, reject);
46
- });
47
- pendingAssertions.set(tracked, matcher);
48
- // The native then does not count as awaiting: a pass stops tracking, a failure stays
49
- // tracked. Handling the rejection keeps an un-awaited failure from being unhandled.
50
- Promise.prototype.then.call(
51
- tracked,
52
- () => pendingAssertions.delete(tracked),
53
- () => {},
54
- );
55
- return tracked;
56
- }
57
-
58
- export interface LocatorAssertionOptions {
59
- /** Time (ms) to keep retrying. Defaults to config `expect.timeout`, then 5000 */
60
- timeout?: number;
61
- }
62
-
63
- function matchesText(
64
- actual: string,
65
- expected: string | RegExp,
66
- mode: "exact" | "contains",
67
- ) {
68
- // search() ignores lastIndex, so global regexps behave the same on every call
69
- if (expected instanceof RegExp) return actual.search(expected) !== -1;
70
- return mode === "exact" ? actual === expected : actual.includes(expected);
71
- }
72
-
73
- function isNumeric(value: unknown): value is number | bigint {
74
- return typeof value === "number" || typeof value === "bigint";
75
- }
76
-
77
- export class ValueAssertions<T> {
78
- private readonly actual: T;
79
- private readonly negated: boolean;
80
-
81
- constructor(actual: T, negated = false) {
82
- this.actual = actual;
83
- this.negated = negated;
84
- }
85
-
86
- /** Inverts the next matcher */
87
- get not(): ValueAssertions<T> {
88
- return new ValueAssertions(this.actual, !this.negated);
89
- }
90
-
91
- private assert(pass: boolean, matcher: string, expected: unknown) {
92
- if (pass !== this.negated) return;
93
- throw new AssertionError({
94
- matcher: this.negated ? `not.${matcher}` : matcher,
95
- expected,
96
- actual: this.actual,
97
- });
98
- }
99
-
100
- private compare(
101
- matcher: string,
102
- expected: number | bigint,
103
- check: (actual: number | bigint, expected: number | bigint) => boolean,
104
- ) {
105
- const actual = this.actual;
106
- if (!isNumeric(actual) || !isNumeric(expected)) {
107
- throw new TypeError(
108
- `${matcher} expects numbers, received ${typeof actual} and ${typeof expected}`,
109
- );
110
- }
111
- this.assert(check(actual, expected), matcher, expected);
112
- }
113
-
114
- /** Strict identity (`Object.is`) */
115
- toBe(expected: unknown) {
116
- this.assert(Object.is(this.actual, expected), "toBe", expected);
117
- }
118
-
119
- /** Deep structural equality */
120
- toEqual(expected: unknown) {
121
- this.assert(isDeepStrictEqual(this.actual, expected), "toEqual", expected);
122
- }
123
-
124
- toBeTruthy() {
125
- this.assert(Boolean(this.actual), "toBeTruthy", "truthy");
126
- }
127
-
128
- toBeFalsy() {
129
- this.assert(!this.actual, "toBeFalsy", "falsy");
130
- }
131
-
132
- /** Array includes `item`, or string contains substring `item` */
133
- toContain(item: unknown) {
134
- const actual = this.actual;
135
- if (typeof actual === "string") {
136
- if (typeof item !== "string") {
137
- throw new TypeError(`toContain on a string expects a string, received ${typeof item}`);
138
- }
139
- this.assert(actual.includes(item), "toContain", item);
140
- return;
141
- }
142
- if (Array.isArray(actual)) {
143
- this.assert(actual.includes(item), "toContain", item);
144
- return;
145
- }
146
- throw new TypeError(`toContain expects an array or string, received ${typeof actual}`);
147
- }
148
-
149
- /** String contains `pattern` (string) or matches it (RegExp) */
150
- toMatch(pattern: string | RegExp) {
151
- const actual = this.actual;
152
- if (typeof actual !== "string") {
153
- throw new TypeError(`toMatch expects a string, received ${typeof actual}`);
154
- }
155
- this.assert(matchesText(actual, pattern, "contains"), "toMatch", pattern);
156
- }
157
-
158
- toBeGreaterThan(expected: number | bigint) {
159
- this.compare("toBeGreaterThan", expected, (a, b) => a > b);
160
- }
161
-
162
- toBeGreaterThanOrEqual(expected: number | bigint) {
163
- this.compare("toBeGreaterThanOrEqual", expected, (a, b) => a >= b);
164
- }
165
-
166
- toBeLessThan(expected: number | bigint) {
167
- this.compare("toBeLessThan", expected, (a, b) => a < b);
168
- }
169
-
170
- toBeLessThanOrEqual(expected: number | bigint) {
171
- this.compare("toBeLessThanOrEqual", expected, (a, b) => a <= b);
172
- }
173
- }
174
-
175
- export class LocatorAssertions {
176
- private readonly locator: Locator;
177
- private readonly negated: boolean;
178
-
179
- constructor(locator: Locator, negated = false) {
180
- this.locator = locator;
181
- this.negated = negated;
182
- }
183
-
184
- /** Inverts the next matcher: it retries until the condition is false */
185
- get not(): LocatorAssertions {
186
- return new LocatorAssertions(this.locator, !this.negated);
187
- }
188
-
189
- /** Re-reads the element until `check` passes or the timeout expires */
190
- private assertEventually<V>(
191
- matcher: string,
192
- expected: unknown,
193
- read: () => Promise<V>,
194
- check: (actual: V) => boolean,
195
- options?: LocatorAssertionOptions,
196
- ): Promise<void> {
197
- const name = this.negated ? `not.${matcher}` : matcher;
198
-
199
- return track(
200
- name,
201
- (async () => {
202
- const timeout = await resolveTimeout(options?.timeout);
203
- try {
204
- await waitUntil(read, (actual) => check(actual) !== this.negated, { timeout });
205
- } catch (err) {
206
- if (!(err instanceof WaitTimeoutError)) throw err;
207
- // Reads never resolve to undefined, so an undefined `last` means no read completed
208
- if (err.last === undefined) {
209
- throw new Error(
210
- `expect(locator).${name}: could not read ${this.locator.selector} within ${timeout}ms`,
211
- );
212
- }
213
- throw new AssertionError({
214
- matcher: name,
215
- expected,
216
- actual: err.last,
217
- locator: this.locator.selector,
218
- });
219
- }
220
- })(),
221
- );
222
- }
223
-
224
- toBeVisible(options?: LocatorAssertionOptions) {
225
- return this.assertEventually(
226
- "toBeVisible",
227
- true,
228
- () => this.locator.isVisible(),
229
- (visible) => visible,
230
- options,
231
- );
232
- }
233
-
234
- /** Trimmed text content equals `expected` (string) or matches it (RegExp) */
235
- toHaveText(expected: string | RegExp, options?: LocatorAssertionOptions) {
236
- return this.assertEventually(
237
- "toHaveText",
238
- expected,
239
- () => this.locator.textContent(),
240
- (text) => text !== null && matchesText(text, expected, "exact"),
241
- options,
242
- );
243
- }
244
-
245
- /** Text content contains `expected` (string) or matches it (RegExp) */
246
- toContainText(expected: string | RegExp, options?: LocatorAssertionOptions) {
247
- return this.assertEventually(
248
- "toContainText",
249
- expected,
250
- () => this.locator.textContent(),
251
- (text) => text !== null && matchesText(text, expected, "contains"),
252
- options,
253
- );
254
- }
255
-
256
- toHaveValue(expected: string | RegExp, options?: LocatorAssertionOptions) {
257
- return this.assertEventually(
258
- "toHaveValue",
259
- expected,
260
- () => this.locator.inputValue(),
261
- (value) => value !== null && matchesText(value, expected, "exact"),
262
- options,
263
- );
264
- }
265
-
266
- toHaveAttribute(
267
- name: string,
268
- expected: string | RegExp,
269
- options?: LocatorAssertionOptions,
270
- ) {
271
- return this.assertEventually(
272
- "toHaveAttribute",
273
- expected,
274
- () => this.locator.getAttribute(name),
275
- (value) => value !== null && matchesText(value, expected, "exact"),
276
- options,
277
- );
278
- }
279
-
280
- toHaveCount(expected: number, options?: LocatorAssertionOptions) {
281
- return this.assertEventually(
282
- "toHaveCount",
283
- expected,
284
- () => this.locator.count(),
285
- (count) => count === expected,
286
- options,
287
- );
288
- }
289
- }
290
-
291
- /** Auto-retrying assertions on a page element */
292
- export function expect(locator: Locator): LocatorAssertions;
293
- /** Synchronous assertions on a plain value */
294
- export function expect<T>(value: T): ValueAssertions<T>;
295
- export function expect(value: unknown) {
296
- return value instanceof Locator
297
- ? new LocatorAssertions(value)
298
- : new ValueAssertions(value);
299
- }