@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,355 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import Locator from "./locator.js";
4
- import { ActionTimeoutError } from "./action-timeout-error.js";
5
- import type { ElementState } from "./element-state.js";
6
- import { remote, stubConnector, type StubResponse } from "../testing/stub-connector.js";
7
-
8
- const BOX = { x: 10, y: 20, width: 100, height: 40 };
9
-
10
- /** Element state response; defaults describe an element ready for any action */
11
- function state(overrides: Partial<ElementState> = {}): StubResponse {
12
- return remote({
13
- attached: true,
14
- visible: true,
15
- enabled: true,
16
- editable: true,
17
- box: BOX,
18
- hitTarget: "self",
19
- ...overrides,
20
- });
21
- }
22
-
23
- const detached = state({
24
- attached: false,
25
- visible: false,
26
- enabled: false,
27
- editable: false,
28
- box: null,
29
- hitTarget: null,
30
- });
31
-
32
- function locatorWith(xpath: string, ...responses: StubResponse[]) {
33
- const stub = stubConnector(...responses);
34
- return { locator: new Locator(xpath, stub.connector, "ctx"), ...stub };
35
- }
36
-
37
- type Performed = {
38
- actions: Array<{
39
- type: string;
40
- actions: Array<{ type: string; x?: number; y?: number; value?: string }>;
41
- }>;
42
- };
43
- type Sent = Array<{ method: string; params: unknown }>;
44
-
45
- function performed(sent: Sent, sourceType: string) {
46
- return sent
47
- .filter(({ method }) => method === "input.performActions")
48
- .map(({ params }) => (params as Performed).actions[0]!)
49
- .filter((source) => source.type === sourceType);
50
- }
51
-
52
- /** [x, y] of the pointerMove of every pointer click sent */
53
- function pointerClicks(sent: Sent) {
54
- return performed(sent, "pointer").map((source) => [source.actions[0]!.x, source.actions[0]!.y]);
55
- }
56
-
57
- function keysTyped(sent: Sent) {
58
- return performed(sent, "key")
59
- .flatMap((source) => source.actions.map((action) => action.value))
60
- .join("");
61
- }
62
-
63
- test("click() retries through every check, then clicks the latest box centre once", async () => {
64
- const { locator, sent, calls, expressions } = locatorWith(
65
- "button",
66
- detached, // attached ✗
67
- state({ visible: false }), // visible ✗
68
- state({ box: { ...BOX, x: 0 } }), // stable ✗ (moved)
69
- state(), // stable ✗ (moved back)
70
- state({ enabled: false }), // enabled ✗
71
- state({ hitTarget: "div#overlay" }), // hit target ✗
72
- state(), // actionable
73
- );
74
- assert.equal(await locator.click({ timeout: 3000 }), undefined);
75
- assert.equal(expressions.length, 7);
76
- assert.deepEqual(calls[0]!.args[1], { scroll: true, hitTest: true });
77
- assert.deepEqual(pointerClicks(sent), [[60, 40]]);
78
- });
79
-
80
- test("click() needs the same box on two probes, so a ready element is probed twice", async () => {
81
- const { locator, expressions } = locatorWith("button", state());
82
- await locator.click({ timeout: 3000 });
83
- assert.equal(expressions.length, 2);
84
- });
85
-
86
- test("click() timeout lists the checks of the last probe", async () => {
87
- const { locator, sent } = locatorWith("button", state({ enabled: false }));
88
- await assert.rejects(locator.click({ timeout: 250 }), (err) => {
89
- assert.ok(err instanceof ActionTimeoutError);
90
- assert.equal(err.reason, "not-actionable");
91
- assert.equal(
92
- err.message,
93
- "click(): //button was not actionable within 250ms\n" +
94
- " attached ✓ visible ✓ stable ✓ enabled ✗ hit target —",
95
- );
96
- return true;
97
- });
98
- assert.deepEqual(pointerClicks(sent), []);
99
- });
100
-
101
- test("click() timeout names a covering element", async () => {
102
- const { locator } = locatorWith("button", state({ hitTarget: "div#cookie-banner.overlay" }));
103
- await assert.rejects(
104
- locator.click({ timeout: 250 }),
105
- new ActionTimeoutError({
106
- action: "click",
107
- selector: "//button",
108
- timeout: 250,
109
- reason: "not-actionable",
110
- checks: {
111
- attached: "pass",
112
- visible: "pass",
113
- stable: "pass",
114
- enabled: "pass",
115
- "hit target": "fail",
116
- },
117
- coveredBy: "div#cookie-banner.overlay",
118
- }),
119
- );
120
- });
121
-
122
- test("click() with timeout 0 probes once and skips the stability check", async () => {
123
- const { locator, sent, expressions } = locatorWith("button", state());
124
- assert.equal(await locator.click({ timeout: 0 }), undefined);
125
- assert.equal(expressions.length, 1);
126
- assert.deepEqual(pointerClicks(sent), [[60, 40]]);
127
- });
128
-
129
- test("click() with timeout 0 on a disabled element reports no stable entry", async () => {
130
- const { locator, sent } = locatorWith("button", state({ enabled: false }));
131
- await assert.rejects(locator.click({ timeout: 0 }), (err) => {
132
- assert.ok(err instanceof ActionTimeoutError);
133
- assert.equal(
134
- err.message,
135
- "click(): //button was not actionable within 0ms\n" +
136
- " attached ✓ visible ✓ enabled ✗ hit target —",
137
- );
138
- return true;
139
- });
140
- assert.deepEqual(pointerClicks(sent), []);
141
- });
142
-
143
- test("click() timeout on a hidden element does not report a covering element", async () => {
144
- const { locator } = locatorWith("button", state({ visible: false, hitTarget: "html" }));
145
- await assert.rejects(locator.click({ timeout: 250 }), (err) => {
146
- assert.ok(err instanceof ActionTimeoutError);
147
- assert.equal(err.coveredBy, undefined);
148
- return true;
149
- });
150
- });
151
-
152
- test("click() on an element that never appears reports it was not attached", async () => {
153
- const { locator } = locatorWith("button", detached);
154
- await assert.rejects(
155
- locator.click({ timeout: 250 }),
156
- /^ActionTimeoutError: click\(\): \/\/button was not attached within 250ms$/,
157
- );
158
- });
159
-
160
- test("click() when no probe completes reports it could not read the element", async () => {
161
- const { locator } = locatorWith("button", { hang: true });
162
- await assert.rejects(locator.click({ timeout: 250 }), (err) => {
163
- assert.ok(err instanceof ActionTimeoutError);
164
- assert.equal(err.reason, "unreadable");
165
- assert.equal(err.message, "click(): could not read //button within 250ms");
166
- return true;
167
- });
168
- });
169
-
170
- test("force skips every check except attached", async () => {
171
- const { locator, sent, expressions } = locatorWith(
172
- "button",
173
- state({ visible: false, enabled: false, hitTarget: "div#overlay" }),
174
- );
175
- await locator.click({ force: true, timeout: 1000 });
176
- assert.equal(expressions.length, 1);
177
- assert.ok(expressions[0]!.includes("scrollIntoView"));
178
- assert.deepEqual(pointerClicks(sent), [[60, 40]]);
179
- });
180
-
181
- test("force still waits for the element to be attached", async () => {
182
- const { locator } = locatorWith("button", detached);
183
- await assert.rejects(
184
- locator.click({ force: true, timeout: 250 }),
185
- /was not attached within 250ms/,
186
- );
187
- });
188
-
189
- test("fill() waits until the field is editable, then clicks, selects and types", async () => {
190
- const { locator, sent, expressions } = locatorWith(
191
- "input",
192
- state({ editable: false }),
193
- state(),
194
- { type: "undefined" },
195
- );
196
- await locator.fill("ab", { timeout: 3000 });
197
- assert.equal(expressions.length, 3);
198
- assert.match(expressions[2]!, /\.select\(\)/);
199
- assert.deepEqual(pointerClicks(sent), [[60, 40]]);
200
- assert.equal(keysTyped(sent), "aabb");
201
- });
202
-
203
- test("fill() timeout lists the fill checks", async () => {
204
- const { locator } = locatorWith("input", state({ editable: false }));
205
- await assert.rejects(
206
- locator.fill("x", { timeout: 250 }),
207
- (err) =>
208
- err instanceof ActionTimeoutError &&
209
- err.message ===
210
- "fill(): //input was not actionable within 250ms\n" +
211
- " attached ✓ visible ✓ enabled ✓ editable ✗ hit target —",
212
- );
213
- });
214
-
215
- test("focus() waits only for the element to be attached", async () => {
216
- const { locator, expressions, calls } = locatorWith(
217
- "input",
218
- detached,
219
- state({ visible: false, enabled: false }),
220
- { type: "undefined" },
221
- );
222
- assert.equal(await locator.focus({ timeout: 3000 }), undefined);
223
- assert.equal(expressions.length, 3);
224
- assert.deepEqual(calls[0]!.args[1], { scroll: false, hitTest: false });
225
- assert.match(expressions[2]!, /el\.focus\(\)/);
226
- });
227
-
228
- test("isEnabled() and isEditable() read the state without waiting", async () => {
229
- assert.equal(await locatorWith("button", state()).locator.isEnabled(), true);
230
- assert.equal(await locatorWith("button", state({ enabled: false })).locator.isEnabled(), false);
231
- assert.equal(await locatorWith("button", detached).locator.isEnabled(), false);
232
- assert.equal(await locatorWith("input", state()).locator.isEditable(), true);
233
- assert.equal(await locatorWith("input", state({ editable: false })).locator.isEditable(), false);
234
- assert.equal(await locatorWith("input", detached).locator.isEditable(), false);
235
- });
236
-
237
- test("waitFor() waits for visible by default, without scrolling", async () => {
238
- const { locator, expressions, calls } = locatorWith("div", state({ visible: false }), state());
239
- await locator.waitFor({ timeout: 3000 });
240
- assert.equal(expressions.length, 2);
241
- assert.deepEqual(calls[0]!.args[1], { scroll: false, hitTest: false });
242
- });
243
-
244
- test("waitFor({ state: \"hidden\" }) resolves once the element is gone or invisible", async () => {
245
- const gone = locatorWith("div", state(), detached);
246
- await gone.locator.waitFor({ state: "hidden", timeout: 3000 });
247
- assert.equal(gone.expressions.length, 2);
248
-
249
- const invisible = locatorWith("div", state(), state({ visible: false }));
250
- await invisible.locator.waitFor({ state: "hidden", timeout: 3000 });
251
- assert.equal(invisible.expressions.length, 2);
252
- });
253
-
254
- test("waitFor() for attached and detached", async () => {
255
- const appears = locatorWith("div", detached, state({ visible: false }));
256
- await appears.locator.waitFor({ state: "attached", timeout: 3000 });
257
- assert.equal(appears.expressions.length, 2);
258
-
259
- const disappears = locatorWith("div", state(), detached);
260
- await disappears.locator.waitFor({ state: "detached", timeout: 3000 });
261
- assert.equal(disappears.expressions.length, 2);
262
- });
263
-
264
- test("waitFor() timeout names the state it waited for", async () => {
265
- const { locator } = locatorWith("div", state({ visible: false }));
266
- await assert.rejects(locator.waitFor({ timeout: 250 }), (err) => {
267
- assert.ok(err instanceof ActionTimeoutError);
268
- assert.equal(err.reason, "wrong-state");
269
- assert.equal(err.state, "visible");
270
- assert.equal(err.message, "waitFor(): //div did not become visible within 250ms");
271
- return true;
272
- });
273
- });
274
-
275
- test("waitFor() when no probe completes reports it could not read the element", async () => {
276
- const { locator } = locatorWith("div", { hang: true });
277
- await assert.rejects(
278
- locator.waitFor({ state: "hidden", timeout: 250 }),
279
- /^ActionTimeoutError: waitFor\(\): could not read \/\/div within 250ms$/,
280
- );
281
- });
282
-
283
- /** Helper installer that records how often it was asked to (re)install */
284
- function fakeHelpers() {
285
- const log: string[] = [];
286
- return {
287
- log,
288
- helpers: {
289
- async ensureInstalled() {
290
- log.push("ensure");
291
- },
292
- async reinstall() {
293
- log.push("reinstall");
294
- },
295
- },
296
- };
297
- }
298
-
299
- function locatorWithHelpers(xpath: string, ...responses: StubResponse[]) {
300
- const stub = stubConnector(...responses);
301
- const { log, helpers } = fakeHelpers();
302
- return { locator: new Locator(xpath, stub.connector, "ctx", helpers), log, ...stub };
303
- }
304
-
305
- const HELPERS_MISSING: StubResponse = { type: "string", value: "samurai:helpers-missing" };
306
-
307
- test("with helpers the probe runs in the samurai sandbox after ensuring the install", async () => {
308
- const { locator, log, sent, calls } = locatorWithHelpers("button", state());
309
- assert.equal(await locator.isEnabled(), true);
310
- assert.deepEqual(log, ["ensure"]);
311
- const params = sent.find(({ method }) => method === "script.callFunction")!.params as { target: unknown };
312
- assert.deepEqual(params.target, { context: "ctx", sandbox: "samurai" });
313
- assert.match(calls[0]!.functionDeclaration, /__samurai/);
314
- assert.deepEqual(calls[0]!.args, [{ sharedId: "stub-node-0" }, { scroll: false, hitTest: false }]);
315
- });
316
-
317
- test("without helpers the probe source is sent to the page realm as before", async () => {
318
- const { locator, sent, calls } = locatorWith("button", state());
319
- await locator.isEnabled();
320
- const call = sent.find(({ method }) => method === "script.callFunction")!;
321
- assert.deepEqual((call.params as { target: unknown }).target, { context: "ctx" });
322
- assert.match(calls[0]!.functionDeclaration, /^\(el, options\) =>/);
323
- });
324
-
325
- test("a missing helper triggers one reinstall and a retry", async () => {
326
- const { locator, log, expressions } = locatorWithHelpers("button", HELPERS_MISSING, state());
327
- assert.equal(await locator.isEnabled(), true);
328
- assert.deepEqual(log, ["ensure", "reinstall"]);
329
- assert.equal(expressions.length, 2);
330
- });
331
-
332
- test("helpers still missing after the reinstall is an error, not a loop", async () => {
333
- const { locator, log, expressions } = locatorWithHelpers("button", HELPERS_MISSING);
334
- await assert.rejects(locator.isEnabled(), /helpers are missing/);
335
- assert.deepEqual(log, ["ensure", "reinstall"]);
336
- assert.equal(expressions.length, 2);
337
- });
338
-
339
- test("ensureInstalled runs once per poll, so a click polls and clicks with the same setup", async () => {
340
- const { locator, log, sent } = locatorWithHelpers("button", state());
341
- await locator.click({ timeout: 3000 });
342
- assert.ok(log.every((entry) => entry === "ensure"));
343
- assert.equal(pointerClicks(sent).length, 1);
344
- });
345
-
346
- test("all() items keep using the helpers", async () => {
347
- const stub = stubConnector(state());
348
- stub.nodeCounts(2, 1);
349
- const { log, helpers } = fakeHelpers();
350
- const items = await new Locator("li", stub.connector, "ctx", helpers).all();
351
- await items[1]!.isEnabled();
352
- assert.ok(log.includes("ensure"));
353
- const probe = stub.sent.find(({ method }) => method === "script.callFunction")!;
354
- assert.deepEqual((probe.params as { target: unknown }).target, { context: "ctx", sandbox: "samurai" });
355
- });
@@ -1,292 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import Page from "../browser/page.js";
4
- import { remote, stubConnector, type StubResponse } from "../testing/stub-connector.js";
5
- import { BiDiError } from "../transport/bidi-error.js";
6
- import { ActionTimeoutError } from "./action-timeout-error.js";
7
- import Locator, { InvalidSelectorError, UnsupportedOperationError } from "./locator.js";
8
- import { cssSelector, textSelector, xpathSelector } from "./selector.js";
9
-
10
- type Sent = Array<{ method: string; params: unknown }>;
11
- interface LocateParams {
12
- context: string;
13
- locator: unknown;
14
- maxNodeCount?: number;
15
- startNodes?: Array<{ sharedId: string }>;
16
- }
17
- const locateCommands = (sent: Sent) =>
18
- sent.filter(({ method }) => method === "browsingContext.locateNodes").map(({ params }) => params as LocateParams);
19
-
20
- const READY: StubResponse = remote({
21
- attached: true,
22
- visible: true,
23
- enabled: true,
24
- editable: true,
25
- box: { x: 0, y: 0, width: 10, height: 10 },
26
- hitTarget: "self",
27
- });
28
-
29
- function chainWith(steps: ConstructorParameters<typeof Locator>[0], ...responses: StubResponse[]) {
30
- const stub = stubConnector(...(responses.length ? responses : [remote(null)]));
31
- return { locator: new Locator(steps, stub.connector, "ctx"), ...stub };
32
- }
33
-
34
- describe("chained locators", () => {
35
- it("search inside the previous step's matches, with scoped xpaths", async () => {
36
- const { locator, sent, nodeCounts } = chainWith("//form");
37
- nodeCounts(1, 2);
38
- const child = locator.locator("input").getByCss("span.x");
39
- assert.equal(child.selector, "//form >> .//input >> css=span.x");
40
- assert.equal(await child.count(), 2);
41
- const [first, second, third] = locateCommands(sent);
42
- assert.deepEqual(first!.locator, { type: "xpath", value: "//form" });
43
- assert.equal(first!.startNodes, undefined);
44
- assert.deepEqual(second!.locator, { type: "xpath", value: ".//input" });
45
- assert.deepEqual(second!.startNodes, [{ sharedId: "stub-node-0" }]);
46
- assert.deepEqual(third!.locator, { type: "css", value: "span.x" });
47
- assert.deepEqual(third!.startNodes, [{ sharedId: "stub-node-0" }, { sharedId: "stub-node-1" }]);
48
- });
49
-
50
- it("make // relative to the parent match", async () => {
51
- const { locator, sent } = chainWith("//form");
52
- await locator.locator("//input").count();
53
- assert.deepEqual(locateCommands(sent)[1]!.locator, { type: "xpath", value: ".//input" });
54
- });
55
-
56
- it("stop at the first empty step", async () => {
57
- const { locator, sent, nodeCounts } = chainWith("//form");
58
- nodeCounts(0);
59
- assert.equal(await locator.getByCss("input").count(), 0);
60
- assert.equal(locateCommands(sent).length, 1);
61
- });
62
-
63
- it("limit only the final step to one node when a single element is needed", async () => {
64
- const { locator, sent } = chainWith("//form", remote("x"));
65
- await locator.getByCss("input").textContent();
66
- const [first, second] = locateCommands(sent);
67
- assert.equal(first!.maxNodeCount, undefined);
68
- assert.equal(second!.maxNodeCount, 1);
69
- });
70
-
71
- it("send the element found last to the page function", async () => {
72
- const { locator, calls } = chainWith("//form", remote("x"));
73
- await locator.getByCss("input").textContent();
74
- assert.deepEqual(calls[0]!.args[0], { sharedId: "stub-node-0" });
75
- });
76
- });
77
-
78
- describe("selector kinds", () => {
79
- it("getByCss, getByText and getByRole send the matching BiDi locators", async () => {
80
- const stub = stubConnector(remote(null));
81
- const page = new Page(stub.connector, "ctx");
82
- await page.getByCss("form.login input").count();
83
- await page.getByText("Sign in").count();
84
- await page.getByText("in", { match: "partial", ignoreCase: true }).count();
85
- await page.getByRole("button", { name: "Submit" }).count();
86
- await page.getByRole("link").count();
87
- assert.deepEqual(
88
- locateCommands(stub.sent).map((command) => command.locator),
89
- [
90
- { type: "css", value: "form.login input" },
91
- { type: "innerText", value: "Sign in", matchType: "full", ignoreCase: false },
92
- { type: "innerText", value: "in", matchType: "partial", ignoreCase: true },
93
- { type: "accessibility", value: { role: "button", name: "Submit" } },
94
- { type: "accessibility", value: { role: "link" } },
95
- ],
96
- );
97
- });
98
-
99
- it("are described in messages", () => {
100
- const stub = stubConnector(remote(null));
101
- const page = new Page(stub.connector, "ctx");
102
- assert.equal(page.getByCss("a.b").selector, "css=a.b");
103
- assert.equal(page.getByText("Hi").selector, 'text="Hi"');
104
- assert.equal(page.getByRole("button", { name: "Go" }).selector, 'role=button[name="Go"]');
105
- assert.equal(page.locator("h1").getByRole("link").selector, "//h1 >> role=link");
106
- });
107
-
108
- it("reject empty arguments when built", () => {
109
- const stub = stubConnector(remote(null));
110
- const page = new Page(stub.connector, "ctx");
111
- assert.throws(() => page.getByCss(""), TypeError);
112
- assert.throws(() => page.getByText(""), TypeError);
113
- assert.throws(() => page.getByRole(""), TypeError);
114
- });
115
-
116
- it("actions report the described selector when they time out", async () => {
117
- const { locator, nodeCounts } = chainWith([cssSelector("form"), textSelector("Save")]);
118
- nodeCounts(0);
119
- await assert.rejects(locator.click({ timeout: 100 }), (err) => {
120
- assert.ok(err instanceof ActionTimeoutError);
121
- assert.match(err.message, /click\(\): css=form >> text="Save" was not attached within 100ms/);
122
- return true;
123
- });
124
- });
125
- });
126
-
127
- describe("all() on non-xpath locators", () => {
128
- it("returns n-th locators that re-resolve the whole chain", async () => {
129
- const { locator, nodeCounts, sent } = chainWith([cssSelector("li")], READY);
130
- nodeCounts(3);
131
- const items = await locator.all();
132
- assert.deepEqual(
133
- items.map((item) => item.selector),
134
- ["css=li >> nth=0", "css=li >> nth=1", "css=li >> nth=2"],
135
- );
136
- nodeCounts(3, 3);
137
- await items[2]!.isEnabled();
138
- const last = locateCommands(sent).pop()!;
139
- assert.deepEqual(last.locator, { type: "css", value: "li" });
140
- assert.equal(last.maxNodeCount, undefined, "the step before nth must not be limited");
141
- });
142
-
143
- it("an n-th locator whose index no longer exists is not attached", async () => {
144
- const { locator, nodeCounts } = chainWith([cssSelector("li")], READY);
145
- nodeCounts(3);
146
- const items = await locator.all();
147
- nodeCounts(1);
148
- assert.equal(await items[2]!.isEnabled(), false);
149
- });
150
-
151
- it("chained xpath locators use n-th too, xpath-only ones keep positional xpaths", async () => {
152
- const { locator, nodeCounts } = chainWith("//ul");
153
- nodeCounts(1, 2);
154
- const items = await locator.locator("li").all();
155
- assert.deepEqual(
156
- items.map((item) => item.selector),
157
- ["//ul >> .//li >> nth=0", "//ul >> .//li >> nth=1"],
158
- );
159
- });
160
- });
161
-
162
- describe("elements that are not there", () => {
163
- it("reads answer null or false without running a script", async () => {
164
- const { locator, nodeCounts, expressions } = chainWith([cssSelector("p")]);
165
- nodeCounts(0);
166
- assert.equal(await locator.textContent(), null);
167
- assert.equal(await locator.inputValue(), null);
168
- assert.equal(await locator.getAttribute("href"), null);
169
- assert.equal(await locator.isVisible(), false);
170
- assert.equal(await locator.isEnabled(), false);
171
- assert.equal(await locator.isEditable(), false);
172
- assert.equal(await locator.count(), 0);
173
- assert.equal(expressions.length, 0);
174
- });
175
-
176
- it("getBoundingClientRect and focus need the element", async () => {
177
- const { locator, nodeCounts } = chainWith([cssSelector("p")]);
178
- nodeCounts(0);
179
- await assert.rejects(locator.getBoundingClientRect(), /Failed to locate element css=p/);
180
- await assert.rejects(locator.focus({ force: true, timeout: 50 }), ActionTimeoutError);
181
- });
182
-
183
- it("a node that went away between the search and the read counts as missing", async () => {
184
- const { locator } = chainWith([cssSelector("p")], { error: "no such node" });
185
- assert.equal(await locator.textContent(), null);
186
- assert.equal(await locator.isVisible(), false);
187
- });
188
-
189
- it("a node that went away between the search and the probe is not attached", async () => {
190
- const { locator } = chainWith([cssSelector("p")], { error: "no such node" });
191
- assert.equal(await locator.isEnabled(), false);
192
- });
193
-
194
- it("other script errors from a read still surface", async () => {
195
- const { locator } = chainWith([cssSelector("p")], { error: "unknown error" });
196
- await assert.rejects(locator.textContent(), BiDiError);
197
- });
198
- });
199
-
200
- describe("locate errors", () => {
201
- it("an invalid selector is thrown immediately with the selector in the message", async () => {
202
- const { locator, failLocate, sent } = chainWith([cssSelector("a[")]);
203
- failLocate(new BiDiError("browsingContext.locateNodes", "invalid selector", "bad css"));
204
- await assert.rejects(locator.click({ timeout: 2000 }), (err) => {
205
- assert.ok(err instanceof InvalidSelectorError);
206
- assert.equal(err.selector, "css=a[");
207
- assert.match(err.message, /Invalid selector css=a\[: .*bad css/);
208
- return true;
209
- });
210
- assert.equal(locateCommands(sent).length, 1, "not retried");
211
- });
212
-
213
- it("an unsupported locator type says so", async () => {
214
- const { locator, failLocate } = chainWith([xpathSelector("//a")]);
215
- failLocate(new BiDiError("browsingContext.locateNodes", "unsupported operation", "no accessibility"));
216
- await assert.rejects(locator.count(), UnsupportedOperationError);
217
- });
218
-
219
- it("other locate failures are rethrown as they are", async () => {
220
- const { locator, failLocate } = chainWith("//a");
221
- failLocate(new BiDiError("browsingContext.locateNodes", "no such frame", "gone"));
222
- await assert.rejects(locator.count(), { code: "no such frame" });
223
- });
224
-
225
- it("expect-style reads rethrow an invalid selector without waiting for the timeout", async () => {
226
- const { locator, failLocate } = chainWith([cssSelector("a[")]);
227
- failLocate(new BiDiError("browsingContext.locateNodes", "invalid selector", "bad css"));
228
- const started = Date.now();
229
- await assert.rejects(locator.waitFor({ timeout: 3000 }), InvalidSelectorError);
230
- assert.ok(Date.now() - started < 1000);
231
- });
232
- });
233
-
234
- describe("text locators in a browser without the innerText locator", () => {
235
- const nodesOf = (...ids: string[]): StubResponse => ({
236
- type: "array",
237
- value: ids.map((sharedId) => ({ type: "node", sharedId }) as never),
238
- });
239
-
240
- it("try the native locator once, then search in the page", async () => {
241
- const { locator, unsupportedLocators, sent, calls } = chainWith([textSelector("Sign in")], nodesOf("t1", "t2"));
242
- unsupportedLocators("innerText");
243
- assert.equal(await locator.count(), 2);
244
- assert.equal(locateCommands(sent).length, 1, "the native attempt");
245
- assert.equal(calls.length, 1);
246
- assert.deepEqual(calls[0]!.args, [[], "Sign in", "full", false]);
247
- assert.match(calls[0]!.functionDeclaration, /^\(starts, value, match, ignoreCase\) =>/);
248
- });
249
-
250
- it("remember the answer for the connection, across locators", async () => {
251
- const stub = stubConnector(nodesOf("t1"));
252
- stub.unsupportedLocators("innerText");
253
- const page = new Page(stub.connector, "ctx");
254
- await page.getByText("a").count();
255
- await page.getByText("b", { match: "partial", ignoreCase: true }).count();
256
- assert.equal(locateCommands(stub.sent).length, 1, "only the first locator asked the browser");
257
- assert.deepEqual(stub.calls[1]!.args, [[], "b", "partial", true]);
258
- });
259
-
260
- it("pass the parent's matches as start nodes and honour the limit", async () => {
261
- const stub = stubConnector(nodesOf("t1", "t2", "t3"), remote("hello"));
262
- stub.unsupportedLocators("innerText");
263
- const parent = new Locator([xpathSelector("//form"), textSelector("x")], stub.connector, "ctx");
264
- assert.equal(await parent.textContent(), "hello");
265
- // the text step ran in the page with the form as its start node; only one element is needed
266
- assert.deepEqual(stub.calls[0]!.args[0], [{ sharedId: "stub-node-0" }]);
267
- assert.deepEqual(stub.calls[1]!.args[0], { sharedId: "t1" });
268
- });
269
-
270
- it("do not affect css, xpath or role locators", async () => {
271
- const stub = stubConnector(remote(null));
272
- stub.unsupportedLocators("innerText");
273
- const page = new Page(stub.connector, "ctx");
274
- await page.getByCss("a").count();
275
- await page.getByRole("button").count();
276
- assert.equal(locateCommands(stub.sent).length, 2);
277
- assert.equal(stub.calls.length, 0);
278
- });
279
-
280
- it("an unsupported role locator is reported, not searched for elsewhere", async () => {
281
- const stub = stubConnector(remote(null));
282
- stub.unsupportedLocators("accessibility");
283
- const page = new Page(stub.connector, "ctx");
284
- await assert.rejects(page.getByRole("button").count(), UnsupportedOperationError);
285
- });
286
-
287
- it("a text search that throws in the page reads as a locate failure", async () => {
288
- const { locator, unsupportedLocators } = chainWith([textSelector("x")], { exception: "TypeError: boom" });
289
- unsupportedLocators("innerText");
290
- await assert.rejects(locator.count(), /Failed to locate element text="x"\. Details: TypeError: boom/);
291
- });
292
- });