@itmetsam/samurai-framework 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/README.md +129 -0
  2. package/bin/samurai.mjs +31 -0
  3. package/dist/api.js +3 -0
  4. package/dist/assert/assertion-error.d.ts +23 -0
  5. package/dist/assert/assertion-error.js +45 -0
  6. package/dist/assert/expect.d.ts +55 -0
  7. package/dist/assert/expect.js +188 -0
  8. package/dist/browser/browser-context.d.ts +91 -0
  9. package/dist/browser/browser-context.js +146 -0
  10. package/dist/browser/browser-finder.d.ts +2 -0
  11. package/dist/browser/browser-finder.js +95 -0
  12. package/dist/browser/browser.d.ts +95 -0
  13. package/dist/browser/browser.js +372 -0
  14. package/dist/browser/context-tree.d.ts +23 -0
  15. package/dist/browser/context-tree.js +60 -0
  16. package/dist/browser/dialog.d.ts +37 -0
  17. package/dist/browser/dialog.js +61 -0
  18. package/dist/browser/download.d.ts +71 -0
  19. package/dist/browser/download.js +137 -0
  20. package/dist/browser/downloads-dir.d.ts +8 -0
  21. package/dist/browser/downloads-dir.js +38 -0
  22. package/dist/browser/emulation.d.ts +61 -0
  23. package/dist/browser/emulation.js +144 -0
  24. package/dist/browser/file-chooser.d.ts +39 -0
  25. package/dist/browser/file-chooser.js +86 -0
  26. package/dist/browser/file-paths.d.ts +4 -0
  27. package/dist/browser/file-paths.js +18 -0
  28. package/dist/browser/index.d.ts +4 -0
  29. package/dist/browser/index.js +4 -0
  30. package/dist/browser/navigation-error.d.ts +13 -0
  31. package/dist/browser/navigation-error.js +19 -0
  32. package/dist/browser/page-logs.d.ts +86 -0
  33. package/dist/browser/page-logs.js +149 -0
  34. package/dist/browser/page-wait.d.ts +9 -0
  35. package/dist/browser/page-wait.js +17 -0
  36. package/dist/browser/page.d.ts +370 -0
  37. package/dist/browser/page.js +1038 -0
  38. package/dist/browser/pdf.d.ts +72 -0
  39. package/dist/browser/pdf.js +37 -0
  40. package/dist/browser/screenshot.d.ts +29 -0
  41. package/dist/browser/screenshot.js +42 -0
  42. package/dist/cli/args.d.ts +35 -0
  43. package/dist/cli/args.js +69 -0
  44. package/dist/cli/cli.cli-test.d.ts +1 -0
  45. package/dist/cli/cli.cli-test.js +170 -0
  46. package/dist/cli/console-reporter.d.ts +15 -0
  47. package/dist/cli/console-reporter.js +52 -0
  48. package/dist/cli/init.d.ts +7 -0
  49. package/dist/cli/init.js +61 -0
  50. package/dist/cli/main.d.ts +12 -0
  51. package/dist/cli/main.js +156 -0
  52. package/dist/config/config.d.ts +24 -0
  53. package/dist/config/config.js +42 -0
  54. package/dist/config/default.d.ts +2 -0
  55. package/dist/config/default.js +3 -0
  56. package/dist/config/mask.d.ts +11 -0
  57. package/dist/config/mask.js +85 -0
  58. package/dist/config/run-settings.d.ts +28 -0
  59. package/dist/config/run-settings.js +61 -0
  60. package/dist/config/secrets.d.ts +10 -0
  61. package/dist/config/secrets.js +67 -0
  62. package/dist/config/variables.d.ts +11 -0
  63. package/dist/config/variables.js +26 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +15 -0
  66. package/dist/locator/action-timeout-error.d.ts +25 -0
  67. package/dist/locator/action-timeout-error.js +35 -0
  68. package/dist/locator/element-state.d.ts +44 -0
  69. package/dist/locator/element-state.js +109 -0
  70. package/dist/locator/label-locator.d.ts +13 -0
  71. package/dist/locator/label-locator.js +49 -0
  72. package/dist/locator/locator.d.ts +168 -0
  73. package/dist/locator/locator.js +591 -0
  74. package/dist/locator/selector-errors.d.ts +9 -0
  75. package/dist/locator/selector-errors.js +15 -0
  76. package/dist/locator/selector.d.ts +60 -0
  77. package/dist/locator/selector.js +113 -0
  78. package/dist/locator/text-locator.d.ts +13 -0
  79. package/dist/logger/index.d.ts +2 -0
  80. package/dist/logger/index.js +30 -0
  81. package/dist/network/data-collector.d.ts +26 -0
  82. package/dist/network/data-collector.js +77 -0
  83. package/dist/network/network-tracker.d.ts +94 -0
  84. package/dist/network/network-tracker.js +198 -0
  85. package/dist/network/network-wait.d.ts +16 -0
  86. package/dist/network/network-wait.js +24 -0
  87. package/dist/network/response.d.ts +22 -0
  88. package/dist/network/response.js +42 -0
  89. package/dist/network/router.d.ts +120 -0
  90. package/dist/network/router.js +343 -0
  91. package/dist/network/url-match.d.ts +8 -0
  92. package/dist/network/url-match.js +38 -0
  93. package/dist/recorder/apply.d.ts +3 -0
  94. package/dist/recorder/apply.js +10 -0
  95. package/dist/recorder/capture.d.ts +18 -0
  96. package/dist/recorder/capture.js +198 -0
  97. package/dist/recorder/index.d.ts +3 -0
  98. package/dist/recorder/index.js +3 -0
  99. package/dist/recorder/normalise.d.ts +68 -0
  100. package/dist/recorder/normalise.js +104 -0
  101. package/dist/recorder/ranking.d.ts +9 -0
  102. package/dist/recorder/ranking.js +21 -0
  103. package/dist/recorder/recorder.d.ts +52 -0
  104. package/dist/recorder/recorder.js +143 -0
  105. package/dist/recorder/session.d.ts +49 -0
  106. package/dist/recorder/session.js +113 -0
  107. package/dist/runner/module-type.d.ts +6 -0
  108. package/dist/runner/module-type.js +29 -0
  109. package/dist/runner/page-logs-report.d.ts +40 -0
  110. package/dist/runner/page-logs-report.js +44 -0
  111. package/dist/runner/prepare-run.d.ts +14 -0
  112. package/dist/runner/prepare-run.js +20 -0
  113. package/dist/runner/project.d.ts +17 -0
  114. package/dist/runner/project.js +33 -0
  115. package/dist/runner/registry.d.ts +7 -0
  116. package/dist/runner/registry.js +40 -0
  117. package/dist/runner/reporter.d.ts +43 -0
  118. package/dist/runner/reporter.js +83 -0
  119. package/dist/runner/run.d.ts +27 -0
  120. package/dist/runner/run.js +34 -0
  121. package/dist/runner/test-error.d.ts +3 -0
  122. package/dist/runner/test-error.js +18 -0
  123. package/dist/runner/test-runner.d.ts +44 -0
  124. package/dist/runner/test-runner.js +205 -0
  125. package/dist/script/call-function.d.ts +34 -0
  126. package/dist/script/call-function.js +71 -0
  127. package/dist/script/element-handle.d.ts +10 -0
  128. package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
  129. package/dist/script/helpers.d.ts +28 -0
  130. package/dist/script/helpers.js +50 -0
  131. package/dist/script/preload.d.ts +31 -0
  132. package/dist/script/preload.js +71 -0
  133. package/dist/script/serialize.d.ts +29 -0
  134. package/dist/script/serialize.js +207 -0
  135. package/dist/steps/build.d.ts +5 -0
  136. package/dist/steps/build.js +38 -0
  137. package/dist/steps/edit.d.ts +30 -0
  138. package/dist/steps/edit.js +80 -0
  139. package/dist/steps/emit.d.ts +6 -0
  140. package/dist/steps/emit.js +73 -0
  141. package/dist/steps/index.d.ts +5 -0
  142. package/dist/steps/index.js +4 -0
  143. package/dist/steps/model.d.ts +103 -0
  144. package/dist/steps/model.js +1 -0
  145. package/dist/steps/parse.d.ts +3 -0
  146. package/dist/steps/parse.js +272 -0
  147. package/dist/transport/bidi-connection.d.ts +46 -0
  148. package/dist/transport/bidi-connection.js +176 -0
  149. package/dist/transport/bidi-error.d.ts +10 -0
  150. package/dist/transport/bidi-error.js +10 -0
  151. package/dist/transport/subscriptions.d.ts +36 -0
  152. package/dist/transport/subscriptions.js +81 -0
  153. package/{src → dist}/types/test.d.ts +10 -4
  154. package/dist/wait/wait-until.d.ts +20 -0
  155. package/dist/wait/wait-until.js +63 -0
  156. package/package.json +44 -7
  157. package/.claude/settings.local.json +0 -7
  158. package/.env.example +0 -3
  159. package/.github/actions/setup/action.yml +0 -16
  160. package/.github/workflows/ci.yml +0 -71
  161. package/.trunk/configs/.markdownlint.yaml +0 -2
  162. package/.trunk/configs/.yamllint.yaml +0 -7
  163. package/.trunk/configs/svgo.config.mjs +0 -14
  164. package/.trunk/trunk.yaml +0 -34
  165. package/docs/superpowers/bidi-roadmap.md +0 -60
  166. package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
  167. package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
  168. package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
  169. package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
  170. package/docs/superpowers/recorder-roadmap.md +0 -52
  171. package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
  172. package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
  173. package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
  174. package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
  175. package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
  176. package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
  177. package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
  178. package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
  179. package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
  180. package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
  181. package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
  182. package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
  183. package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
  184. package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
  185. package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
  186. package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
  187. package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
  188. package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
  189. package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
  190. package/public/logo-dark.svg +0 -236
  191. package/public/logo-light.svg +0 -262
  192. package/samurai.config.ts +0 -7
  193. package/src/assert/assertion-error.test.ts +0 -55
  194. package/src/assert/assertion-error.ts +0 -62
  195. package/src/assert/expect-locator.test.ts +0 -217
  196. package/src/assert/expect.test.ts +0 -73
  197. package/src/assert/expect.ts +0 -299
  198. package/src/browser/browser-context.browser-test.ts +0 -201
  199. package/src/browser/browser-context.test.ts +0 -407
  200. package/src/browser/browser-context.ts +0 -208
  201. package/src/browser/browser-finder.ts +0 -110
  202. package/src/browser/browser.test.ts +0 -88
  203. package/src/browser/browser.ts +0 -515
  204. package/src/browser/context-tree.test.ts +0 -86
  205. package/src/browser/context-tree.ts +0 -70
  206. package/src/browser/dialog.browser-test.ts +0 -144
  207. package/src/browser/dialog.test.ts +0 -322
  208. package/src/browser/dialog.ts +0 -83
  209. package/src/browser/download.test.ts +0 -161
  210. package/src/browser/download.ts +0 -172
  211. package/src/browser/downloads-dir.test.ts +0 -75
  212. package/src/browser/downloads-dir.ts +0 -39
  213. package/src/browser/emulation.browser-test.ts +0 -186
  214. package/src/browser/emulation.test.ts +0 -200
  215. package/src/browser/emulation.ts +0 -204
  216. package/src/browser/file-chooser.test.ts +0 -142
  217. package/src/browser/file-chooser.ts +0 -99
  218. package/src/browser/file-paths.ts +0 -18
  219. package/src/browser/goto.browser-test.ts +0 -44
  220. package/src/browser/navigation-error.ts +0 -26
  221. package/src/browser/navigation.browser-test.ts +0 -172
  222. package/src/browser/page-api.browser-test.ts +0 -228
  223. package/src/browser/page-logs.browser-test.ts +0 -119
  224. package/src/browser/page-logs.test.ts +0 -212
  225. package/src/browser/page-logs.ts +0 -208
  226. package/src/browser/page-wait.ts +0 -19
  227. package/src/browser/page.test.ts +0 -1567
  228. package/src/browser/page.ts +0 -1176
  229. package/src/browser/pdf.ts +0 -62
  230. package/src/browser/screenshot.ts +0 -65
  231. package/src/browser/transfers.browser-test.ts +0 -180
  232. package/src/config/config.ts +0 -33
  233. package/src/config/default.ts +0 -5
  234. package/src/config/fixtures.test.ts +0 -137
  235. package/src/config/mask.test.ts +0 -143
  236. package/src/config/mask.ts +0 -87
  237. package/src/config/run-settings.test.ts +0 -163
  238. package/src/config/run-settings.ts +0 -114
  239. package/src/config/secrets.ts +0 -70
  240. package/src/config/variables.ts +0 -31
  241. package/src/index.ts +0 -10
  242. package/src/locator/action-timeout-error.test.ts +0 -83
  243. package/src/locator/action-timeout-error.ts +0 -72
  244. package/src/locator/element-state.test.ts +0 -181
  245. package/src/locator/element-state.ts +0 -159
  246. package/src/locator/locate-nodes.browser-test.ts +0 -169
  247. package/src/locator/locator-actions.test.ts +0 -355
  248. package/src/locator/locator-chain.test.ts +0 -292
  249. package/src/locator/locator.browser-test.ts +0 -137
  250. package/src/locator/locator.test.ts +0 -179
  251. package/src/locator/locator.ts +0 -656
  252. package/src/locator/selector-errors.ts +0 -18
  253. package/src/locator/selector.test.ts +0 -124
  254. package/src/locator/selector.ts +0 -107
  255. package/src/locator/set-files.browser-test.ts +0 -141
  256. package/src/locator/set-files.test.ts +0 -191
  257. package/src/locator/text-locator.test.ts +0 -118
  258. package/src/logger/index.test.ts +0 -28
  259. package/src/logger/index.ts +0 -40
  260. package/src/network/data-collector.test.ts +0 -111
  261. package/src/network/data-collector.ts +0 -92
  262. package/src/network/network-data.browser-test.ts +0 -192
  263. package/src/network/network-tracker.browser-test.ts +0 -151
  264. package/src/network/network-tracker.test.ts +0 -307
  265. package/src/network/network-tracker.ts +0 -286
  266. package/src/network/network-wait.ts +0 -28
  267. package/src/network/response.test.ts +0 -93
  268. package/src/network/response.ts +0 -58
  269. package/src/network/router.browser-test.ts +0 -260
  270. package/src/network/router.test.ts +0 -528
  271. package/src/network/router.ts +0 -420
  272. package/src/network/url-match.test.ts +0 -69
  273. package/src/network/url-match.ts +0 -41
  274. package/src/runner/page-logs-report.test.ts +0 -144
  275. package/src/runner/page-logs-report.ts +0 -82
  276. package/src/runner/prepare-run.test.ts +0 -33
  277. package/src/runner/prepare-run.ts +0 -36
  278. package/src/runner/registry.test.ts +0 -59
  279. package/src/runner/registry.ts +0 -46
  280. package/src/runner/reporter.test.ts +0 -73
  281. package/src/runner/reporter.ts +0 -109
  282. package/src/runner/test-error.test.ts +0 -39
  283. package/src/runner/test-error.ts +0 -20
  284. package/src/runner/test-runner.ts +0 -184
  285. package/src/script/call-function.browser-test.ts +0 -112
  286. package/src/script/call-function.test.ts +0 -174
  287. package/src/script/call-function.ts +0 -113
  288. package/src/script/helpers.test.ts +0 -116
  289. package/src/script/helpers.ts +0 -68
  290. package/src/script/preload.browser-test.ts +0 -144
  291. package/src/script/preload.test.ts +0 -180
  292. package/src/script/preload.ts +0 -102
  293. package/src/script/serialize.test.ts +0 -182
  294. package/src/script/serialize.ts +0 -203
  295. package/src/testing/browser-fixture.ts +0 -18
  296. package/src/testing/fake-websocket.ts +0 -72
  297. package/src/testing/stub-connector.ts +0 -154
  298. package/src/tests/index.spec.ts +0 -30
  299. package/src/transport/bidi-connection.test.ts +0 -155
  300. package/src/transport/bidi-connection.ts +0 -256
  301. package/src/transport/bidi-error.ts +0 -23
  302. package/src/transport/foundation.browser-test.ts +0 -73
  303. package/src/transport/subscriptions.test.ts +0 -140
  304. package/src/transport/subscriptions.ts +0 -110
  305. package/src/types/bidi-commands.test-d.ts +0 -55
  306. package/src/wait/wait-until.test.ts +0 -117
  307. package/src/wait/wait-until.ts +0 -86
  308. package/tsconfig.json +0 -44
  309. /package/{src/api.ts → dist/api.d.ts} +0 -0
  310. /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
  311. /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
  312. /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
  313. /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
  314. /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
  315. /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
  316. /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
  317. /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
  318. /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
  319. /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
  320. /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
  321. /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
  322. /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
  323. /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
  324. /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
  325. /package/{src → dist}/types/bidi.d.ts +0 -0
  326. /package/{src → dist}/types/browser.d.ts +0 -0
  327. /package/{src → dist}/types/config.d.ts +0 -0
  328. /package/{src → dist}/types/env.d.ts +0 -0
@@ -0,0 +1,591 @@
1
+ import { BiDiError } from "../transport/bidi-error.js";
2
+ import { callFunction, ScriptError } from "../script/call-function.js";
3
+ import { CALL_PROBE, HELPER_SANDBOX, HELPERS_MISSING, } from "../script/helpers.js";
4
+ import { ElementHandle } from "../script/element-handle.js";
5
+ import { takeScreenshot, } from "../browser/screenshot.js";
6
+ import { assertIsFile, resolveFiles } from "../browser/file-paths.js";
7
+ import { resolveTimeout, waitUntil, WaitTimeoutError, } from "../wait/wait-until.js";
8
+ import { ActionTimeoutError } from "./action-timeout-error.js";
9
+ import { allPass, DETACHED_STATE, evaluateChecks, parseElementState, PROBE_ELEMENT, } from "./element-state.js";
10
+ import { InvalidSelectorError, UnsupportedOperationError, } from "./selector-errors.js";
11
+ import { LABEL_LOCATE } from "./label-locator.js";
12
+ import { TEXT_LOCATE } from "./text-locator.js";
13
+ import { cssSelector, describeChain, describeSelector, labelSelector, roleSelector, testIdSelector, textSelector, toBiDiLocator, xpathSelector, } from "./selector.js";
14
+ export { InvalidSelectorError, UnsupportedOperationError, } from "./selector-errors.js";
15
+ /** Locator types a connection's browser rejected, so they are not tried again on every poll */
16
+ const unsupportedByConnector = new WeakMap();
17
+ function unsupportedTypes(connector) {
18
+ let types = unsupportedByConnector.get(connector);
19
+ if (!types)
20
+ unsupportedByConnector.set(connector, (types = new Set()));
21
+ return types;
22
+ }
23
+ /** What `setInputFiles` needs to know about the element before it sends files */
24
+ const FILE_INPUT_INFO = `(el) => ({
25
+ tag: el.tagName.toLowerCase(),
26
+ type: (el.getAttribute("type") || "").toLowerCase(),
27
+ multiple: Boolean(el.multiple),
28
+ })`;
29
+ const FILE_INPUT_CHECKS = ["attached", "enabled"];
30
+ const SCREENSHOT_CHECKS = ["attached", "visible"];
31
+ const CLICK_CHECKS = [
32
+ "attached",
33
+ "visible",
34
+ "stable",
35
+ "enabled",
36
+ "hit target",
37
+ ];
38
+ const FILL_CHECKS = [
39
+ "attached",
40
+ "visible",
41
+ "enabled",
42
+ "editable",
43
+ "hit target",
44
+ ];
45
+ const ATTACHED_ONLY = ["attached"];
46
+ /** Unicode code point WebDriver uses for the Backspace key */
47
+ const BACKSPACE = "";
48
+ export default class Locator {
49
+ /**
50
+ * `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
51
+ * `helpers` is the page's helper realm. With it the actionability probe is installed once per
52
+ * document and called by name; without it (a locator built by hand) the probe source is sent on every poll.
53
+ * `alternatives` are fallback locators tried in order whenever this chain matches nothing (see `withFallbacks`).
54
+ */
55
+ constructor(selector, biDiConnector, contextId, helpers, alternatives = []) {
56
+ this.chain =
57
+ typeof selector === "string" ? [xpathSelector(selector)] : selector;
58
+ this.biDiConnector = biDiConnector;
59
+ this.contextId = contextId;
60
+ this.helpers = helpers;
61
+ this.alternatives = alternatives;
62
+ }
63
+ /**
64
+ * The description of this locator used in error messages, e.g. `//form >> role=button[name="Save"]`;
65
+ * fallbacks follow as `or …`
66
+ */
67
+ get selector() {
68
+ return [
69
+ describeChain(this.chain),
70
+ ...this.alternatives.map((alt) => alt.selector),
71
+ ].join(" or ");
72
+ }
73
+ /** Steps that keep the fallbacks: each fallback gets the same step, so "inside A or B" stays meaningful */
74
+ child(step) {
75
+ return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers, this.alternatives.map((alt) => alt.child(step)));
76
+ }
77
+ /**
78
+ * A locator that tries this one first and, whenever it matches nothing, each of `fallbacks` in order;
79
+ * the first one with a match wins. For steps with several ways to find the same element (a test id,
80
+ * then role and name, then text), most stable first. `matchedBy` says which one matched last, so a
81
+ * self-healing layer can tell the primary locator has gone stale.
82
+ * @example
83
+ * page.getByTestId("save").withFallbacks(page.getByRole("button", { name: "Save" }), page.getByText("Save"));
84
+ */
85
+ withFallbacks(...fallbacks) {
86
+ return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [...this.alternatives, ...fallbacks]);
87
+ }
88
+ /**
89
+ * Which locator found elements the last time this one searched: index 0 is the primary, 1 the first
90
+ * fallback, and so on. `undefined` before any search or when the last search found nothing.
91
+ */
92
+ get matchedBy() {
93
+ return this.matched;
94
+ }
95
+ /** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches */
96
+ getByLabel(text, options) {
97
+ return this.child(labelSelector(text, options));
98
+ }
99
+ /** Elements whose `data-testid` attribute equals `testId`, inside this locator's matches */
100
+ getByTestId(testId) {
101
+ return this.child(testIdSelector(testId));
102
+ }
103
+ /** Elements matching `xpath` inside this locator's matches (relative and `//` paths search inside them) */
104
+ locator(xpath) {
105
+ return this.child(xpathSelector(xpath));
106
+ }
107
+ /** Elements matching the CSS selector inside this locator's matches */
108
+ getByCss(css) {
109
+ return this.child(cssSelector(css));
110
+ }
111
+ /** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` */
112
+ getByText(text, options) {
113
+ return this.child(textSelector(text, options));
114
+ }
115
+ /** Elements with the ARIA role (and accessible name, when given), inside this locator's matches */
116
+ getByRole(role, options) {
117
+ return this.child(roleSelector(role, options));
118
+ }
119
+ /** One locator per element currently matching, in document order */
120
+ async all() {
121
+ if (this.alternatives.length > 0) {
122
+ // Pin to whichever of the primary and the fallbacks matches, so each result is one element
123
+ const first = await this.firstMatching();
124
+ return first ? first.all() : [];
125
+ }
126
+ const count = await this.count();
127
+ const only = this.chain.length === 1 ? this.chain[0] : undefined;
128
+ return Array.from({ length: count }, (_, index) => only?.kind === "xpath"
129
+ ? // A positional xpath keeps working without a second search for the whole chain
130
+ new Locator(`(${describeSelector(only, false)})[${index + 1}]`, this.biDiConnector, this.contextId, this.helpers)
131
+ : this.child({ kind: "nth", index }));
132
+ }
133
+ /**
134
+ * Finds the elements the chain matches, in document order. Every step searches inside the previous
135
+ * step's matches; `max` limits the final result. Nothing is cached: the element that matches "first"
136
+ * can change between polls, and a removed node stays resolvable in the browser.
137
+ */
138
+ async resolve(max) {
139
+ this.matched = undefined;
140
+ const own = await this.resolveChain(max);
141
+ if (own.length > 0) {
142
+ this.matched = { index: 0, selector: describeChain(this.chain) };
143
+ return own;
144
+ }
145
+ for (const [index, alternative] of this.alternatives.entries()) {
146
+ const found = await alternative.resolve(max);
147
+ if (found.length > 0) {
148
+ this.matched = { index: index + 1, selector: alternative.selector };
149
+ return found;
150
+ }
151
+ }
152
+ return [];
153
+ }
154
+ /** The primary or the first fallback that currently matches, without its own fallbacks */
155
+ async firstMatching() {
156
+ if ((await this.resolveChain(1)).length > 0)
157
+ return this.withoutFallbacks();
158
+ for (const alternative of this.alternatives) {
159
+ const found = await alternative.firstMatching();
160
+ if (found)
161
+ return found;
162
+ }
163
+ return undefined;
164
+ }
165
+ withoutFallbacks() {
166
+ return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers);
167
+ }
168
+ async resolveChain(max) {
169
+ let nodes;
170
+ for (const [index, selector] of this.chain.entries()) {
171
+ if (selector.kind === "nth") {
172
+ const picked = nodes?.[selector.index];
173
+ nodes = picked ? [picked] : [];
174
+ }
175
+ else {
176
+ nodes = await this.locateStep(selector, index > 0, nodes, index === this.chain.length - 1 ? max : undefined);
177
+ }
178
+ if (nodes.length === 0)
179
+ return [];
180
+ }
181
+ return nodes ?? [];
182
+ }
183
+ async resolveOne() {
184
+ return (await this.resolve(1))[0] ?? null;
185
+ }
186
+ async locateStep(selector, scoped, startNodes, max) {
187
+ if (selector.kind === "label")
188
+ return this.locateInPage(LABEL_LOCATE, selector, startNodes, max);
189
+ if (selector.kind === "text" &&
190
+ unsupportedTypes(this.biDiConnector).has("innerText")) {
191
+ return this.locateByText(selector, startNodes, max);
192
+ }
193
+ try {
194
+ return await this.locateNatively(selector, scoped, startNodes, max);
195
+ }
196
+ catch (err) {
197
+ if (err instanceof BiDiError && err.code === "invalid selector") {
198
+ throw new InvalidSelectorError(describeSelector(selector, scoped), err.message);
199
+ }
200
+ if (err instanceof BiDiError && err.code === "unsupported operation") {
201
+ if (selector.kind === "text") {
202
+ // The browser has no innerText locator (Firefox): remember that and search in the page instead
203
+ unsupportedTypes(this.biDiConnector).add("innerText");
204
+ return this.locateByText(selector, startNodes, max);
205
+ }
206
+ throw new UnsupportedOperationError(`${describeSelector(selector, scoped)}: this kind of locator is not supported by the browser (${err.message})`);
207
+ }
208
+ throw err;
209
+ }
210
+ }
211
+ async locateNatively(selector, scoped, startNodes, max) {
212
+ const { nodes } = await this.biDiConnector.send("browsingContext.locateNodes", {
213
+ context: this.contextId,
214
+ locator: toBiDiLocator(selector, scoped),
215
+ ...(max !== undefined && { maxNodeCount: max }),
216
+ // Only the references are needed; skip serializing the matched subtrees
217
+ serializationOptions: { maxDomDepth: 0 },
218
+ ...(startNodes && {
219
+ startNodes: startNodes.map(({ sharedId }) => ({ sharedId })),
220
+ }),
221
+ });
222
+ const found = new Map();
223
+ for (const node of nodes) {
224
+ if (node.sharedId !== undefined && !found.has(node.sharedId)) {
225
+ found.set(node.sharedId, new ElementHandle(node.sharedId, node.handle));
226
+ }
227
+ }
228
+ return [...found.values()];
229
+ }
230
+ /** Text search done in the page, for browsers without BiDi's innerText locator */
231
+ locateByText(selector, startNodes, max) {
232
+ return this.locateInPage(TEXT_LOCATE, selector, startNodes, max);
233
+ }
234
+ /** Runs a page-side search `(starts, value, match, ignoreCase) => elements` */
235
+ async locateInPage(fn, selector, startNodes, max) {
236
+ const found = await this.call(fn, [
237
+ startNodes ?? [],
238
+ selector.value,
239
+ selector.match,
240
+ selector.ignoreCase,
241
+ ]);
242
+ return max === undefined ? found : found.slice(0, max);
243
+ }
244
+ /** Calls `fn` in the page; a throwing function becomes a "Failed to locate element" error */
245
+ async call(fn, args, sandbox) {
246
+ try {
247
+ return await callFunction(this.biDiConnector, this.contextId, fn, args, {
248
+ awaitPromise: false,
249
+ ...(sandbox !== undefined && { sandbox }),
250
+ });
251
+ }
252
+ catch (err) {
253
+ if (err instanceof ScriptError) {
254
+ throw new Error(`Failed to locate element ${this.selector}. Details: ${err.text}`);
255
+ }
256
+ throw err;
257
+ }
258
+ }
259
+ /**
260
+ * Calls a function on the first matching element: `body` sees it as `el`, followed by `params`.
261
+ * Answers `whenMissing` when nothing matches, or the element disappeared meanwhile.
262
+ */
263
+ async callOnElement(whenMissing, body, params = [], args = []) {
264
+ const el = await this.resolveOne();
265
+ if (!el)
266
+ return whenMissing;
267
+ try {
268
+ return await this.call(`(el${params.map((param) => `, ${param}`).join("")}) => { ${body} }`, [el, ...args]);
269
+ }
270
+ catch (err) {
271
+ if (err instanceof BiDiError && err.code === "no such node")
272
+ return whenMissing;
273
+ throw err;
274
+ }
275
+ }
276
+ /** Like `callOnElement`, for operations that cannot go on without the element */
277
+ async callOnExistingElement(body) {
278
+ const missing = Symbol("missing");
279
+ const result = await this.callOnElement(missing, body);
280
+ if (result === missing) {
281
+ throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
282
+ }
283
+ return result;
284
+ }
285
+ /** Reads the element's actionability state: one search, one probe */
286
+ async probeState(options) {
287
+ const el = await this.resolveOne();
288
+ if (!el)
289
+ return DETACHED_STATE;
290
+ try {
291
+ return parseElementState(await this.callProbe(el, options));
292
+ }
293
+ catch (err) {
294
+ // The document changed between the search and the probe
295
+ if (err instanceof BiDiError && err.code === "no such node")
296
+ return DETACHED_STATE;
297
+ throw err;
298
+ }
299
+ }
300
+ async callProbe(el, options) {
301
+ const args = [el, { ...options }];
302
+ if (!this.helpers)
303
+ return this.call(PROBE_ELEMENT, args);
304
+ await this.helpers.ensureInstalled();
305
+ let result = await this.call(CALL_PROBE, args, HELPER_SANDBOX);
306
+ if (result === HELPERS_MISSING) {
307
+ // A document got past the registration (e.g. created while it was being set up): install again once
308
+ await this.helpers.reinstall();
309
+ result = await this.call(CALL_PROBE, args, HELPER_SANDBOX);
310
+ if (result === HELPERS_MISSING) {
311
+ throw new ScriptError("the samurai helpers are missing in the page after reinstalling", "probeElement");
312
+ }
313
+ }
314
+ return result;
315
+ }
316
+ /**
317
+ * Probes until every required check passes and returns that state.
318
+ * Throws ActionTimeoutError describing the last probe on timeout.
319
+ */
320
+ async waitForActionable(action, checks, probe, options) {
321
+ const timeout = await resolveTimeout(options?.timeout);
322
+ // Stability compares two probes; with timeout 0 there is only one
323
+ const required = options?.force
324
+ ? ATTACHED_ONLY
325
+ : timeout === 0
326
+ ? checks.filter((check) => check !== "stable")
327
+ : checks;
328
+ let lastChecks;
329
+ try {
330
+ return await waitUntil(() => this.probeState(probe), (current, previous) => {
331
+ lastChecks = evaluateChecks(required, current, previous);
332
+ return allPass(lastChecks);
333
+ }, { timeout });
334
+ }
335
+ catch (err) {
336
+ if (!(err instanceof WaitTimeoutError))
337
+ throw err;
338
+ const last = err.last;
339
+ throw new ActionTimeoutError({
340
+ action,
341
+ selector: this.selector,
342
+ timeout,
343
+ reason: !last
344
+ ? "unreadable"
345
+ : last.attached
346
+ ? "not-actionable"
347
+ : "not-attached",
348
+ checks: lastChecks,
349
+ coveredBy: lastChecks?.["hit target"] === "fail"
350
+ ? (last?.hitTarget ?? undefined)
351
+ : undefined,
352
+ });
353
+ }
354
+ }
355
+ /** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
356
+ async click(options) {
357
+ const state = await this.waitForActionable("click", CLICK_CHECKS, { scroll: true, hitTest: true }, options);
358
+ // "attached" is always required, so the box is present
359
+ await this.clickRect(state.box);
360
+ }
361
+ /** Simulate a click event on the center off a page element.
362
+ * This is mostly used internally to focus input elements when using the `fill()` method
363
+ * @example
364
+ * const container = page.locator("div[@id=['clickable-element']");
365
+ * const rect = await container.getBoundingClientRect();
366
+ * await container.clickRect(rect);
367
+ */
368
+ async clickRect(rect) {
369
+ await this.biDiConnector.send("input.performActions", {
370
+ context: this.contextId,
371
+ actions: [
372
+ {
373
+ type: "pointer",
374
+ id: "samurai-pointer",
375
+ actions: [
376
+ {
377
+ type: "pointerMove",
378
+ x: rect.x + rect.width / 2,
379
+ y: rect.y + rect.height / 2,
380
+ },
381
+ {
382
+ type: "pointerDown",
383
+ button: 0,
384
+ },
385
+ {
386
+ type: "pointerUp",
387
+ button: 0,
388
+ },
389
+ ],
390
+ },
391
+ ],
392
+ });
393
+ }
394
+ /**
395
+ * Sets the files of an `<input type=file>` (an empty list clears them); the browser fires `input` and `change`.
396
+ * Paths are resolved against the working directory and must exist on the machine the browser runs on.
397
+ * Waits for the input to be attached and enabled; it need not be visible, file inputs are often hidden behind a label.
398
+ * @example
399
+ * await page.locator("input[@type='file']").setInputFiles("./fixtures/avatar.png");
400
+ * await page.locator("input[@type='file']").setInputFiles([]);
401
+ */
402
+ async setInputFiles(files, options) {
403
+ const paths = resolveFiles(files);
404
+ for (const file of paths)
405
+ await assertIsFile(file, "setInputFiles");
406
+ await this.waitForActionable("setInputFiles", FILE_INPUT_CHECKS, { scroll: false, hitTest: false }, options?.timeout === undefined ? undefined : { timeout: options.timeout });
407
+ const missing = () => new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
408
+ const el = await this.resolveOne();
409
+ if (!el)
410
+ throw missing();
411
+ let info;
412
+ try {
413
+ info = await this.call(FILE_INPUT_INFO, [el]);
414
+ }
415
+ catch (err) {
416
+ if (err instanceof BiDiError && err.code === "no such node")
417
+ throw missing();
418
+ throw err;
419
+ }
420
+ if (info.tag !== "input" || info.type !== "file") {
421
+ const found = info.tag === "input"
422
+ ? `<input type="${info.type || "text"}">`
423
+ : `<${info.tag}>`;
424
+ throw new Error(`setInputFiles(): ${this.selector} is not an <input type=file> (found ${found})`);
425
+ }
426
+ if (paths.length > 1 && !info.multiple) {
427
+ throw new Error(`setInputFiles(): ${this.selector} does not accept multiple files`);
428
+ }
429
+ await this.biDiConnector.send("input.setFiles", {
430
+ context: this.contextId,
431
+ element: { sharedId: el.sharedId },
432
+ files: paths,
433
+ });
434
+ }
435
+ /**
436
+ * Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
437
+ * visible. Returns the image (also written to `options.path`).
438
+ * @example
439
+ * await page.locator("div[@id='chart']").screenshot({ path: "out/chart.png" });
440
+ */
441
+ async screenshot(options = {}) {
442
+ const { timeout, ...capture } = options;
443
+ await this.waitForActionable("screenshot", SCREENSHOT_CHECKS, { scroll: true, hitTest: false }, timeout === undefined ? undefined : { timeout });
444
+ const el = await this.resolveOne();
445
+ if (!el) {
446
+ throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
447
+ }
448
+ return takeScreenshot(this.biDiConnector, this.contextId, capture, {
449
+ sharedId: el.sharedId,
450
+ });
451
+ }
452
+ /** Waits until the element is attached, then focuses it */
453
+ async focus(options) {
454
+ await this.waitForActionable("focus", ATTACHED_ONLY, { scroll: false, hitTest: false }, options);
455
+ await this.callOnExistingElement(`el.focus();`);
456
+ }
457
+ /** Waits until the element reaches `state` (default "visible"); does not scroll */
458
+ async waitFor({ state = "visible", timeout, } = {}) {
459
+ const resolved = await resolveTimeout(timeout);
460
+ const reached = {
461
+ attached: (current) => current.attached,
462
+ detached: (current) => !current.attached,
463
+ visible: (current) => current.attached && current.visible,
464
+ hidden: (current) => !current.attached || !current.visible,
465
+ };
466
+ try {
467
+ await waitUntil(() => this.probeState({ scroll: false, hitTest: false }), reached[state], {
468
+ timeout: resolved,
469
+ });
470
+ }
471
+ catch (err) {
472
+ if (!(err instanceof WaitTimeoutError))
473
+ throw err;
474
+ throw new ActionTimeoutError({
475
+ action: "waitFor",
476
+ selector: this.selector,
477
+ timeout: resolved,
478
+ reason: err.last === undefined ? "unreadable" : "wrong-state",
479
+ state,
480
+ });
481
+ }
482
+ }
483
+ /** Returns the browser's getBoundingClientRect result for the given element */
484
+ async getBoundingClientRect() {
485
+ const rect = await this.callOnExistingElement(`
486
+ const { x, y, width, height, top, right, bottom, left } = el.getBoundingClientRect();
487
+ return { x, y, width, height, top, right, bottom, left };
488
+ `);
489
+ if (typeof rect !== "object" || rect === null) {
490
+ throw new Error(`Expected to receive an object but instead received "${describeType(rect)}"`);
491
+ }
492
+ return rect;
493
+ }
494
+ stringOrNull(value) {
495
+ if (typeof value === "string")
496
+ return value;
497
+ if (value === null)
498
+ return null;
499
+ throw new Error(`Expected a string or null but received "${describeType(value)}"`);
500
+ }
501
+ /** Whether the element exists, has a non-empty box and is not hidden by CSS */
502
+ async isVisible() {
503
+ const result = await this.callOnElement(false, `
504
+ const rect = el.getBoundingClientRect();
505
+ const style = getComputedStyle(el);
506
+ return rect.width > 0 && rect.height > 0 &&
507
+ style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
508
+ `);
509
+ return result === true;
510
+ }
511
+ /** Trimmed text content, or null when the element is missing */
512
+ async textContent() {
513
+ return this.stringOrNull(await this.callOnElement(null, `return el.textContent.trim();`));
514
+ }
515
+ /** Value of an input/textarea/select, or null when missing */
516
+ async inputValue() {
517
+ return this.stringOrNull(await this.callOnElement(null, `return typeof el.value === "string" ? el.value : null;`));
518
+ }
519
+ /** Attribute value, or null when the element or attribute is missing */
520
+ async getAttribute(name) {
521
+ return this.stringOrNull(await this.callOnElement(null, `return el.getAttribute(name);`, ["name"], [name]));
522
+ }
523
+ /** References to the elements currently matching, in document order. Internal: for tooling that identifies elements */
524
+ async elements() {
525
+ return this.resolve();
526
+ }
527
+ /** Number of elements currently matching */
528
+ async count() {
529
+ return (await this.resolve()).length;
530
+ }
531
+ /** Whether the element exists and is not disabled; does not wait */
532
+ async isEnabled() {
533
+ const state = await this.probeState({ scroll: false, hitTest: false });
534
+ return state.attached && state.enabled;
535
+ }
536
+ /** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
537
+ async isEditable() {
538
+ const state = await this.probeState({ scroll: false, hitTest: false });
539
+ return state.attached && state.editable;
540
+ }
541
+ /** Wait for certain time (ms). Default: 300ms */
542
+ async wait(duration = 300) {
543
+ await this.biDiConnector.send("input.performActions", {
544
+ context: this.contextId,
545
+ actions: [
546
+ {
547
+ type: "none",
548
+ id: "samurai-sleep",
549
+ actions: [
550
+ {
551
+ type: "pause",
552
+ duration,
553
+ },
554
+ ],
555
+ },
556
+ ],
557
+ });
558
+ }
559
+ /** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
560
+ async fill(value, options) {
561
+ const state = await this.waitForActionable("fill", FILL_CHECKS, { scroll: true, hitTest: true }, options);
562
+ // Typing over the selected existing value replaces it; Backspace clears it for an empty value
563
+ const keys = value === "" ? BACKSPACE : value;
564
+ const actions = Array.from(keys).flatMap((char) => [
565
+ {
566
+ type: "keyDown",
567
+ value: char,
568
+ },
569
+ {
570
+ type: "keyUp",
571
+ value: char,
572
+ },
573
+ ]);
574
+ await this.clickRect(state.box);
575
+ await this.wait();
576
+ await this.callOnElement(undefined, `if (typeof el.select === "function") el.select();`);
577
+ await this.biDiConnector.send("input.performActions", {
578
+ context: this.contextId,
579
+ actions: [
580
+ {
581
+ actions,
582
+ type: "key",
583
+ id: "samurai-keyboard",
584
+ },
585
+ ],
586
+ });
587
+ }
588
+ }
589
+ function describeType(value) {
590
+ return value === null ? "null" : typeof value;
591
+ }
@@ -0,0 +1,9 @@
1
+ /** The browser rejected a selector (bad css or xpath); retrying cannot help */
2
+ export declare class InvalidSelectorError extends Error {
3
+ readonly selector: string;
4
+ constructor(selector: string, reason: string);
5
+ }
6
+ /** The browser does not implement this kind of locator */
7
+ export declare class UnsupportedOperationError extends Error {
8
+ constructor(message: string);
9
+ }
@@ -0,0 +1,15 @@
1
+ /** The browser rejected a selector (bad css or xpath); retrying cannot help */
2
+ export class InvalidSelectorError extends Error {
3
+ constructor(selector, reason) {
4
+ super(`Invalid selector ${selector}: ${reason}`);
5
+ this.name = "InvalidSelectorError";
6
+ this.selector = selector;
7
+ }
8
+ }
9
+ /** The browser does not implement this kind of locator */
10
+ export class UnsupportedOperationError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "UnsupportedOperationError";
14
+ }
15
+ }
@@ -0,0 +1,60 @@
1
+ import type { NodeLocator } from "../types/bidi-modules/browsing-context.js";
2
+ /** One step of a locator. A locator is a chain of them, each searching inside the previous step's matches */
3
+ export type Selector = {
4
+ kind: "xpath";
5
+ value: string;
6
+ } | {
7
+ kind: "css";
8
+ value: string;
9
+ } | {
10
+ kind: "text";
11
+ value: string;
12
+ match: "full" | "partial";
13
+ ignoreCase: boolean;
14
+ } | {
15
+ kind: "role";
16
+ role?: string;
17
+ name?: string;
18
+ }
19
+ /** Form controls named by a `<label>`, `aria-labelledby` or `aria-label` whose text matches */
20
+ | {
21
+ kind: "label";
22
+ value: string;
23
+ match: "full" | "partial";
24
+ ignoreCase: boolean;
25
+ }
26
+ /** Elements whose `data-testid` attribute equals `value` */
27
+ | {
28
+ kind: "testid";
29
+ value: string;
30
+ }
31
+ /** Picks the n-th (0-based) of everything the chain so far matches; `all()` builds these */
32
+ | {
33
+ kind: "nth";
34
+ index: number;
35
+ };
36
+ export interface TextOptions {
37
+ /** @default "full" */
38
+ match?: "full" | "partial";
39
+ /** @default false */
40
+ ignoreCase?: boolean;
41
+ }
42
+ export declare function xpathSelector(value: string): Selector;
43
+ export declare function cssSelector(value: string): Selector;
44
+ export declare function textSelector(value: string, { match, ignoreCase }?: TextOptions): Selector;
45
+ export declare function labelSelector(value: string, { match, ignoreCase }?: TextOptions): Selector;
46
+ export declare function testIdSelector(value: string): Selector;
47
+ export declare function roleSelector(role: string, { name }?: {
48
+ name?: string;
49
+ }): Selector;
50
+ /**
51
+ * Normalises an xpath. Relative xpaths get a leading `//` (`.//` when `scoped`, i.e. searching inside
52
+ * a parent match). Absolute (`/…`), grouped (`(…)`) and context (`./…`) paths are kept; in a scoped
53
+ * position `//…` is also made relative, so `parent.locator("//x")` means "x inside parent".
54
+ */
55
+ export declare function normalizeXpath(xpath: string, scoped: boolean): string;
56
+ /** The BiDi locator for a step. `scoped` is true for every step after the first */
57
+ export declare function toBiDiLocator(selector: Selector, scoped: boolean): NodeLocator;
58
+ /** A short name for error messages */
59
+ export declare function describeSelector(selector: Selector, scoped: boolean): string;
60
+ export declare function describeChain(chain: readonly Selector[]): string;