craftdriver 1.8.0 → 1.9.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 (108) hide show
  1. package/CHANGELOG.md +6 -5
  2. package/README.md +3 -2
  3. package/dist/cli/pageSemantics.d.ts +3 -39
  4. package/dist/cli/pageSemantics.d.ts.map +1 -1
  5. package/dist/cli/pageSemantics.js +3 -191
  6. package/dist/cli/pageSemantics.js.map +1 -1
  7. package/dist/cli/selector.d.ts +3 -3
  8. package/dist/cli/selector.d.ts.map +1 -1
  9. package/dist/cli/selector.js +6 -9
  10. package/dist/cli/selector.js.map +1 -1
  11. package/dist/cli/snapshot.d.ts +5 -6
  12. package/dist/cli/snapshot.d.ts.map +1 -1
  13. package/dist/cli/snapshot.js +59 -18
  14. package/dist/cli/snapshot.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib/a11y.d.ts +6 -2
  20. package/dist/lib/a11y.d.ts.map +1 -1
  21. package/dist/lib/a11y.js +3 -1
  22. package/dist/lib/a11y.js.map +1 -1
  23. package/dist/lib/bidi/evaluate.d.ts +11 -0
  24. package/dist/lib/bidi/evaluate.d.ts.map +1 -0
  25. package/dist/lib/bidi/evaluate.js +25 -0
  26. package/dist/lib/bidi/evaluate.js.map +1 -0
  27. package/dist/lib/bidi/types.d.ts +2 -0
  28. package/dist/lib/bidi/types.d.ts.map +1 -1
  29. package/dist/lib/browser.d.ts +1 -1
  30. package/dist/lib/browser.d.ts.map +1 -1
  31. package/dist/lib/browser.js +46 -35
  32. package/dist/lib/browser.js.map +1 -1
  33. package/dist/lib/browserContext.d.ts +4 -6
  34. package/dist/lib/browserContext.d.ts.map +1 -1
  35. package/dist/lib/browserContext.js +5 -7
  36. package/dist/lib/browserContext.js.map +1 -1
  37. package/dist/lib/by.d.ts +65 -27
  38. package/dist/lib/by.d.ts.map +1 -1
  39. package/dist/lib/by.js +47 -20
  40. package/dist/lib/by.js.map +1 -1
  41. package/dist/lib/clearFastPath.d.ts.map +1 -1
  42. package/dist/lib/clearFastPath.js +2 -0
  43. package/dist/lib/clearFastPath.js.map +1 -1
  44. package/dist/lib/clickFastPath.d.ts.map +1 -1
  45. package/dist/lib/clickFastPath.js +2 -0
  46. package/dist/lib/clickFastPath.js.map +1 -1
  47. package/dist/lib/driver.d.ts +19 -0
  48. package/dist/lib/driver.d.ts.map +1 -1
  49. package/dist/lib/driver.js +111 -4
  50. package/dist/lib/driver.js.map +1 -1
  51. package/dist/lib/elementHandle.d.ts +13 -10
  52. package/dist/lib/elementHandle.d.ts.map +1 -1
  53. package/dist/lib/elementHandle.js +119 -50
  54. package/dist/lib/elementHandle.js.map +1 -1
  55. package/dist/lib/errors.d.ts +5 -1
  56. package/dist/lib/errors.d.ts.map +1 -1
  57. package/dist/lib/errors.js +5 -1
  58. package/dist/lib/errors.js.map +1 -1
  59. package/dist/lib/expect.d.ts +10 -0
  60. package/dist/lib/expect.d.ts.map +1 -1
  61. package/dist/lib/expect.js +76 -49
  62. package/dist/lib/expect.js.map +1 -1
  63. package/dist/lib/fillFastPath.d.ts.map +1 -1
  64. package/dist/lib/fillFastPath.js +2 -0
  65. package/dist/lib/fillFastPath.js.map +1 -1
  66. package/dist/lib/frame.d.ts.map +1 -1
  67. package/dist/lib/frame.js +29 -17
  68. package/dist/lib/frame.js.map +1 -1
  69. package/dist/lib/locator.d.ts +21 -54
  70. package/dist/lib/locator.d.ts.map +1 -1
  71. package/dist/lib/locator.js +165 -208
  72. package/dist/lib/locator.js.map +1 -1
  73. package/dist/lib/page.d.ts.map +1 -1
  74. package/dist/lib/page.js +23 -13
  75. package/dist/lib/page.js.map +1 -1
  76. package/dist/lib/pageSemantics.d.ts +42 -0
  77. package/dist/lib/pageSemantics.d.ts.map +1 -0
  78. package/dist/lib/pageSemantics.js +216 -0
  79. package/dist/lib/pageSemantics.js.map +1 -0
  80. package/dist/lib/query.d.ts +68 -0
  81. package/dist/lib/query.d.ts.map +1 -0
  82. package/dist/lib/query.js +590 -0
  83. package/dist/lib/query.js.map +1 -0
  84. package/dist/lib/shadowRoot.d.ts +21 -0
  85. package/dist/lib/shadowRoot.d.ts.map +1 -0
  86. package/dist/lib/shadowRoot.js +29 -0
  87. package/dist/lib/shadowRoot.js.map +1 -0
  88. package/dist/lib/shadowRootLocator.d.ts +31 -0
  89. package/dist/lib/shadowRootLocator.d.ts.map +1 -0
  90. package/dist/lib/shadowRootLocator.js +55 -0
  91. package/dist/lib/shadowRootLocator.js.map +1 -0
  92. package/dist/lib/timing.d.ts +2 -2
  93. package/dist/lib/timing.js +2 -2
  94. package/docs/accessibility.md +9 -1
  95. package/docs/api-reference.md +4 -1
  96. package/docs/browser-api.md +3 -3
  97. package/docs/browser-context.md +3 -3
  98. package/docs/cli.md +10 -7
  99. package/docs/error-codes.md +3 -1
  100. package/docs/getting-started.md +1 -1
  101. package/docs/mcp.md +9 -5
  102. package/docs/public/examples/iframe-child.html +12 -1
  103. package/docs/public/examples/shadow-dom.html +137 -0
  104. package/docs/recipes/login-once-reuse-session.md +1 -1
  105. package/docs/selectors.md +61 -0
  106. package/docs/session-management.md +17 -10
  107. package/package.json +3 -3
  108. package/skills/craftdriver/patterns.md +3 -0
@@ -0,0 +1,29 @@
1
+ /** W3C WebDriver shadow-root reference key. */
2
+ export const W3C_SHADOW_ROOT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf';
3
+ /**
4
+ * A WebDriver Classic shadow-root search context.
5
+ *
6
+ * This is intentionally internal. The public API exposes the lazy
7
+ * `ShadowRootLocator`, not a cacheable wire reference that can detach.
8
+ */
9
+ export class ClassicShadowRoot {
10
+ driver;
11
+ shadowRootId;
12
+ constructor(driver, shadowRootId) {
13
+ this.driver = driver;
14
+ this.shadowRootId = shadowRootId;
15
+ }
16
+ getId() {
17
+ return this.shadowRootId;
18
+ }
19
+ findElement(locator) {
20
+ return this.driver.findElementFromShadowRoot(this.shadowRootId, locator);
21
+ }
22
+ findElements(locator) {
23
+ return this.driver.findElementsFromShadowRoot(this.shadowRootId, locator);
24
+ }
25
+ toJSON() {
26
+ return { [W3C_SHADOW_ROOT_KEY]: this.shadowRootId };
27
+ }
28
+ }
29
+ //# sourceMappingURL=shadowRoot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadowRoot.js","sourceRoot":"","sources":["../../src/lib/shadowRoot.ts"],"names":[],"mappings":"AAIA,+CAA+C;AAC/C,MAAM,CAAC,MAAM,mBAAmB,GAAG,oCAAoC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,OAAO,iBAAiB;IAElB;IACA;IAFV,YACU,MAAc,EACd,YAAoB;QADpB,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAQ;IAC3B,CAAC;IAEJ,KAAK;QACH,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,WAAW,CAAC,OAAW;QACrB,OAAO,IAAI,CAAC,MAAM,CAAC,yBAAyB,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC3E,CAAC;IAED,YAAY,CAAC,OAAW;QACtB,OAAO,IAAI,CAAC,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;IACtD,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ import { By, type ExactLocatorOptions, type RoleLocatorOptions } from './by.js';
2
+ import type { Driver } from './driver.js';
3
+ import { ElementHandle } from './elementHandle.js';
4
+ import { Locator } from './locator.js';
5
+ import { QueryEnvironment, type SearchRootPlan } from './query.js';
6
+ /**
7
+ * Lazy, explicit search context for one open ShadowRoot.
8
+ *
9
+ * It deliberately exposes lookup methods only: a ShadowRoot is a document
10
+ * fragment, not an actionable element.
11
+ */
12
+ export declare class ShadowRootLocator {
13
+ private driver;
14
+ private root;
15
+ private getDefaultTimeout;
16
+ private environment;
17
+ constructor(driver: Driver, root: SearchRootPlan, getDefaultTimeout: () => number, environment: QueryEnvironment);
18
+ locator(selector: string | By): Locator;
19
+ find(selector: string | By): ElementHandle;
20
+ findAll(selector: string | By): Promise<ElementHandle[]>;
21
+ getByRole(role: string, options?: RoleLocatorOptions): Locator;
22
+ getByText(text: string, options?: {
23
+ exact?: boolean;
24
+ }): Locator;
25
+ getByLabel(text: string, options?: ExactLocatorOptions): Locator;
26
+ getByPlaceholder(text: string, options?: ExactLocatorOptions): Locator;
27
+ getByAltText(text: string, options?: ExactLocatorOptions): Locator;
28
+ getByTitle(text: string, options?: ExactLocatorOptions): Locator;
29
+ getByTestId(id: string): Locator;
30
+ }
31
+ //# sourceMappingURL=shadowRootLocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadowRootLocator.d.ts","sourceRoot":"","sources":["../../src/lib/shadowRootLocator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,gBAAgB,EAEhB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAEpB;;;;;GAKG;AACH,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,iBAAiB;IACzB,OAAO,CAAC,WAAW;gBAHX,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,cAAc,EACpB,iBAAiB,EAAE,MAAM,MAAM,EAC/B,WAAW,EAAE,gBAAgB;IAGvC,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,EAAE,GAAG,OAAO;IAUvC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,EAAE,GAAG,aAAa;IAUpC,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAI9D,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO;IAI9D,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO;IAI/D,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO;IAIhE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO;IAItE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO;IAIlE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO;IAIhE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAGjC"}
@@ -0,0 +1,55 @@
1
+ import { By } from './by.js';
2
+ import { ElementHandle } from './elementHandle.js';
3
+ import { Locator } from './locator.js';
4
+ import { createLocatorPlan, } from './query.js';
5
+ /**
6
+ * Lazy, explicit search context for one open ShadowRoot.
7
+ *
8
+ * It deliberately exposes lookup methods only: a ShadowRoot is a document
9
+ * fragment, not an actionable element.
10
+ */
11
+ export class ShadowRootLocator {
12
+ driver;
13
+ root;
14
+ getDefaultTimeout;
15
+ environment;
16
+ constructor(driver, root, getDefaultTimeout, environment) {
17
+ this.driver = driver;
18
+ this.root = root;
19
+ this.getDefaultTimeout = getDefaultTimeout;
20
+ this.environment = environment;
21
+ }
22
+ locator(selector) {
23
+ const by = typeof selector === 'string' ? By.css(selector) : selector;
24
+ return Locator.fromPlan(this.driver, createLocatorPlan(by, this.root), this.getDefaultTimeout, this.environment);
25
+ }
26
+ find(selector) {
27
+ const locator = this.locator(selector);
28
+ return ElementHandle.fromTarget(this.driver, { kind: 'locator', plan: locator._queryPlan() }, this.getDefaultTimeout, this.environment);
29
+ }
30
+ async findAll(selector) {
31
+ return this.locator(selector).all();
32
+ }
33
+ getByRole(role, options) {
34
+ return this.locator(By.role(role, options));
35
+ }
36
+ getByText(text, options) {
37
+ return this.locator(By.text(text, options));
38
+ }
39
+ getByLabel(text, options) {
40
+ return this.locator(By.labelText(text, options));
41
+ }
42
+ getByPlaceholder(text, options) {
43
+ return this.locator(By.placeholder(text, options));
44
+ }
45
+ getByAltText(text, options) {
46
+ return this.locator(By.altText(text, options));
47
+ }
48
+ getByTitle(text, options) {
49
+ return this.locator(By.title(text, options));
50
+ }
51
+ getByTestId(id) {
52
+ return this.locator(By.testId(id));
53
+ }
54
+ }
55
+ //# sourceMappingURL=shadowRootLocator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadowRootLocator.js","sourceRoot":"","sources":["../../src/lib/shadowRootLocator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAqD,MAAM,SAAS,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAEL,iBAAiB,GAElB,MAAM,YAAY,CAAC;AAEpB;;;;;GAKG;AACH,MAAM,OAAO,iBAAiB;IAElB;IACA;IACA;IACA;IAJV,YACU,MAAc,EACd,IAAoB,EACpB,iBAA+B,EAC/B,WAA6B;QAH7B,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAgB;QACpB,sBAAiB,GAAjB,iBAAiB,CAAc;QAC/B,gBAAW,GAAX,WAAW,CAAkB;IACpC,CAAC;IAEJ,OAAO,CAAC,QAAqB;QAC3B,MAAM,EAAE,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACtE,OAAO,OAAO,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,EACX,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAChC,IAAI,CAAC,iBAAiB,EACtB,IAAI,CAAC,WAAW,CACjB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAqB;QACxB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvC,OAAO,aAAa,CAAC,UAAU,CAC7B,IAAI,CAAC,MAAM,EACX,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,EAAE,EAC/C,IAAI,CAAC,iBAAiB,EACtB,IAAI,CAAC,WAAW,CACjB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAqB;QACjC,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAC;IACtC,CAAC;IAED,SAAS,CAAC,IAAY,EAAE,OAA4B;QAClD,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,SAAS,CAAC,IAAY,EAAE,OAA6B;QACnD,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,UAAU,CAAC,IAAY,EAAE,OAA6B;QACpD,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,gBAAgB,CAAC,IAAY,EAAE,OAA6B;QAC1D,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,YAAY,CAAC,IAAY,EAAE,OAA6B;QACtD,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,UAAU,CAAC,IAAY,EAAE,OAA6B;QACpD,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,WAAW,CAAC,EAAU;QACpB,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC;CACF"}
@@ -117,8 +117,8 @@ export declare const SESSION_CREATE_BACKOFF_STEP_MS = 500;
117
117
  * a barrier the BiDi side respects — an immediately following `{ context }`
118
118
  * script call can land while BiDi is still swapping the old realm for the new
119
119
  * one. The error is pre-execution (the script never ran, no side effects), so a
120
- * short retry that re-resolves the context is safe. See
121
- * plans/TODO-bidi-first-navigation.md (Option A).
120
+ * short retry that re-resolves the context is safe. The injected-error
121
+ * regression case lives in `tests/evaluate.test.ts`.
122
122
  */
123
123
  export declare const EVAL_REALM_RETRY_ATTEMPTS = 3;
124
124
  export declare const EVAL_REALM_RETRY_DELAY_MS = 25;
@@ -121,8 +121,8 @@ export const SESSION_CREATE_BACKOFF_STEP_MS = 500;
121
121
  * a barrier the BiDi side respects — an immediately following `{ context }`
122
122
  * script call can land while BiDi is still swapping the old realm for the new
123
123
  * one. The error is pre-execution (the script never ran, no side effects), so a
124
- * short retry that re-resolves the context is safe. See
125
- * plans/TODO-bidi-first-navigation.md (Option A).
124
+ * short retry that re-resolves the context is safe. The injected-error
125
+ * regression case lives in `tests/evaluate.test.ts`.
126
126
  */
127
127
  export const EVAL_REALM_RETRY_ATTEMPTS = 3;
128
128
  export const EVAL_REALM_RETRY_DELAY_MS = 25;
@@ -130,7 +130,12 @@ interface A11yViolation {
130
130
  impact: 'minor' | 'moderate' | 'serious' | 'critical';
131
131
  description: string;
132
132
  helpUrl: string;
133
- nodes: Array<{ target: string[]; html: string; failureSummary: string }>;
133
+ nodes: Array<{
134
+ // A nested string[] is axe's selector path through open shadow roots.
135
+ target: Array<string | [string, string, ...string[]]>;
136
+ html: string;
137
+ failureSummary: string;
138
+ }>;
134
139
  }
135
140
  ```
136
141
 
@@ -175,3 +180,6 @@ a `window` flag so subsequent audits in the same document skip the
175
180
  inject step). The audit itself uses Classic WebDriver
176
181
  `execute/async` so the underlying `axe.run()` promise is awaited
177
182
  before the result is shipped back — works with or without BiDi.
183
+ axe-core traverses open Shadow DOM itself. CraftDriver preserves axe's nested
184
+ cross-tree selector paths in `node.target`; closed roots remain outside the
185
+ audit, matching axe-core's support boundary.
@@ -30,6 +30,7 @@ Find page content with semantic locators and selector helpers.
30
30
  |---|---|---|---|
31
31
  | `By` | class | — | [selectors](selectors.md) |
32
32
  | `Locator` | class | — | [selectors](selectors.md) |
33
+ | `ShadowRootLocator` | class | Lazy, explicit search context for one open ShadowRoot. | [selectors](selectors.md) |
33
34
 
34
35
  ## Input
35
36
 
@@ -149,6 +150,8 @@ Run axe-core accessibility checks and inspect violation details.
149
150
  | `A11yImpact` | type | Severity buckets reported by axe-core. | [accessibility](accessibility.md) |
150
151
  | `A11yOptions` | type | — | [accessibility](accessibility.md) |
151
152
  | `A11yResult` | type | — | [accessibility](accessibility.md) |
153
+ | `A11yShadowSelector` | type | axe-core selector path through one or more open shadow boundaries. | [accessibility](accessibility.md) |
154
+ | `A11yTarget` | type | A light-DOM selector or axe-core's nested selector path for Shadow DOM. | [accessibility](accessibility.md) |
152
155
  | `A11yViolation` | type | — | [accessibility](accessibility.md) |
153
156
  | `A11yViolationNode` | type | — | [accessibility](accessibility.md) |
154
157
 
@@ -185,4 +188,4 @@ Exports that are public but not yet assigned to a feature group.
185
188
  | `RemoteWebDriverOptions` | type | Options for connecting to any W3C-compatible remote WebDriver endpoint — a self-hosted Selenium Grid, BrowserStack, or another cloud provider. Provider-specific detail (BrowserStack's `bstack:options`, etc.) is forwarded through `capabilities` without schema conversion. | — |
186
189
  | `SessionStateCookie` | type | — | — |
187
190
 
188
- Total exports: **94**.
191
+ Total exports: **97**.
@@ -19,7 +19,7 @@ const browser = await Browser.launch({ browserName: 'chrome' });
19
19
  // With pre-loaded session state
20
20
  const browser = await Browser.launch({
21
21
  browserName: 'chrome',
22
- storageState: './auth.json',
22
+ storageState: '.auth/session.json',
23
23
  });
24
24
  ```
25
25
 
@@ -385,10 +385,10 @@ See [Session Management](./session-management.md) for full details.
385
385
  const storage = browser.storage;
386
386
 
387
387
  // Save current session to file
388
- await browser.saveState('./session.json');
388
+ await browser.saveState('.auth/session.json');
389
389
 
390
390
  // Load session from file
391
- await browser.loadState('./session.json');
391
+ await browser.loadState('.auth/session.json');
392
392
 
393
393
  // Or use an in-memory SessionState object
394
394
  await browser.loadState(await browser.storage.getState());
@@ -80,13 +80,13 @@ await page.find('#submit').click();
80
80
  await page.expect('#welcome').toBeVisible();
81
81
 
82
82
  // Cookies + localStorage for every origin the context has visited.
83
- await ctx.saveStorageState('auth/alice.json');
83
+ await ctx.saveStorageState('.auth/alice.json');
84
84
  await browser.quit();
85
85
  ```
86
86
 
87
87
  ```typescript
88
88
  // every other test:
89
- const ctx = await browser.newContext({ storageState: 'auth/alice.json' });
89
+ const ctx = await browser.newContext({ storageState: '.auth/alice.json' });
90
90
  const page = await ctx.newPage({ url: 'https://app.example.com/dashboard' });
91
91
  // Already logged in — no form, no waiting.
92
92
  ```
@@ -135,7 +135,7 @@ Create a new isolated user context. Backed by BiDi
135
135
  const ctx = await browser.newContext({
136
136
  baseURL: 'https://staging.example.com',
137
137
  extraHTTPHeaders: { 'x-test-tenant': 'acme', 'x-staging-token': process.env.STAGING_TOKEN! },
138
- storageState: 'auth/alice.json',
138
+ storageState: '.auth/alice.json',
139
139
  });
140
140
  const page = await ctx.newPage({ url: '/dashboard' }); // resolves against baseURL
141
141
  ```
package/docs/cli.md CHANGED
@@ -139,9 +139,10 @@ Anything else is treated as a CSS selector, so attribute selectors with
139
139
 
140
140
  ## Snapshot — sanitized DOM with refs
141
141
 
142
- `craftdriver snapshot` returns one line per visible interactive element
143
- on the active page, with a stable ref (`e1`, `e2`, …) that you can
144
- use as a selector for the next command:
142
+ `craftdriver snapshot` returns visible semantic elements on the active page,
143
+ with a stable ref (`e1`, `e2`, …) that you can use as a selector for the next
144
+ command. Open Shadow DOM is traversed recursively and shown with indented
145
+ `#shadow-root (open)` boundaries; closed roots are never inspected:
145
146
 
146
147
  ```bash
147
148
  $ craftdriver snapshot
@@ -166,7 +167,7 @@ A ref names one specific element for as long as that element lives:
166
167
  - a node that survives a DOM change keeps its ref across snapshots;
167
168
  - a new node always gets a fresh number — refs are never reused, not even
168
169
  after a navigation;
169
- - if the element is removed, duplicated, or the page navigates or
170
+ - if the element is removed, or the page navigates or
170
171
  reloads, the ref fails with `STALE_REF` instead of resolving to
171
172
  whatever now sits in that position.
172
173
 
@@ -175,9 +176,11 @@ when you see `STALE_REF`; craftdriver will not guess a replacement.
175
176
  `error.detail.reason` says which case fired (`detached`,
176
177
  `document-changed`, `unknown-ref`, `ambiguous`, `no-snapshot`).
177
178
 
178
- Internally `ref=eN` resolves to a CSS attribute selector
179
- (`[data-craftdriver-ref="eN"]`) after craftdriver has checked the ref
180
- still identifies exactly one element; auto-waiting works unchanged.
179
+ Internally `ref=eN` returns the exact element from the page's identity registry,
180
+ including elements inside open shadow roots. The diagnostic
181
+ `data-craftdriver-ref` attribute is never used for lookup or uniqueness, so an
182
+ authored or cloned marker cannot redirect or invalidate a ref. Auto-waiting and
183
+ native WebDriver actions work unchanged after identity resolution.
181
184
 
182
185
  ## Tabs
183
186
 
@@ -47,8 +47,10 @@ try {
47
47
  | `EVAL_BAD_ARG` | `evaluate()` / `addInitScript()` received a non-JSON-serializable argument (function, Symbol, DOM node…). | Pass plain JSON values. |
48
48
  | `INVALID_ARGUMENT` | Caller passed an invalid value (bad enum, wrong shape, unparseable duration…). For storage-state restore this includes malformed JSON/native schema, invalid cookies/origins, or multi-origin sessionStorage. | Read the message; it lists the accepted forms. Re-save a corrupt or hand-edited state file. |
49
49
  | `UNSUPPORTED` | Feature exists but is unavailable on this browser/transport (e.g. Chromium-only over Firefox, or a BiDi-only feature with BiDi disabled). Storage-state launch/context APIs also reject non-empty sessionStorage rather than silently dropping it, and Classic launch rejects non-empty state. | Enable BiDi (`enableBiDi: true`) or switch browser. For sessionStorage or Classic, launch without state, navigate to its sole origin, then call `browser.loadState()`. |
50
+ | `NO_OPEN_SHADOW_ROOT` | A shadow host resolved, but its public `shadowRoot` getter returned `null`. This intentionally covers both an unattached root and a closed root. | Verify the host is correct and the component uses `mode: "open"`. |
51
+ | `DETACHED_SHADOW_ROOT` | A shadow root detached while resolving a query and full-plan retries could not recover. | Recreate the component or use a stable host locator; inspect `detail.queryPath` and `detail.attempts`. |
50
52
  | `STATE_INVALID` | Method called in the wrong state (e.g. `stopTrace()` without `startTrace()`). A Classic/active-page state restore also uses this when there is no active HTTP(S) origin or its origin cannot accept every requested storage entry and cookie. | Call the prerequisite first. For active-page restore, navigate to the state’s sole origin before loading it. |
51
- | `STALE_REF` | An agent-surface snapshot ref (`ref=eN`) no longer identifies exactly one live element. | Take a fresh `snapshot` and use the new ref. `error.detail.reason` says which case fired: `detached` (element removed), `document-changed` (page navigated or reloaded), `unknown-ref` (never issued for this document), `ambiguous` (duplicated, e.g. by `cloneNode`), `no-snapshot` (none taken yet). craftdriver never guesses a replacement element refs are for exploration; use a durable locator (`craftdriver locators`) in committed tests. |
53
+ | `STALE_REF` | An agent-surface snapshot ref (`ref=eN`) no longer identifies one live registry element. | Take a fresh `snapshot` and use the new ref. `error.detail.reason` says which case fired: `detached` (element removed), `document-changed` (page navigated or reloaded), `unknown-ref` (never issued for this document), `ambiguous` (corrupt/legacy identity registry), `no-snapshot` (none taken yet). Diagnostic marker attributes do not resolve refs, so cloning one cannot redirect or invalidate a live ref. |
52
54
  | `DRIVER_ERROR` | A WebDriver command returned a protocol error (non-200 response) — e.g. `stale element reference`, `element click intercepted`, `invalid selector` — or a transport-level failure. A live-context state overlay can also fail after some entries were applied. | `error.detail.webDriverError` carries the W3C JSON error code and `error.detail.webDriverMessage` the driver's message; recovery loops match on `webDriverError`. For storage restore inspect `phase` and `partialApplied`; use a fresh context when failure isolation matters. Also inspect `error.cause`. |
53
55
  | `ELECTRON_DRIVER_MISMATCH` | The resolved chromedriver can't drive the Electron app, caught **before** a session is created. | `error.detail.kind` is `'chromium-major'` (driver major ≠ the app's bundled Chromium; see `driverMajor` / `expectedChromiumMajor` / `electronVersion`) or `'arch'` (`driverArch` ≠ `runtimeArch`). The `hint` names the fix. |
54
56
  | `ELECTRON_LAUNCH_FAILED` | The Electron app process exited during session creation ("Chrome instance exited"). | `error.detail` carries the diagnosed cause (`macSigning: 'unsigned'`/`'adhoc'` on macOS, `sandboxDisabled: false` on Linux); `hint` is the top remediation; the message appends the chromedriver output tail; `error.cause` is the original driver error. |
@@ -87,7 +87,7 @@ Pick a browser with `browserName`:
87
87
  const browser = await Browser.launch({
88
88
  browserName: 'firefox', // 'chrome' | 'chromium' | 'firefox' | 'safari'
89
89
  // BiDi: restore cookies + multi-origin localStorage before first navigation.
90
- storageState: './session.json', // path or in-memory SessionState
90
+ storageState: '.auth/session.json', // path or in-memory SessionState
91
91
  });
92
92
  ```
93
93
 
package/docs/mcp.md CHANGED
@@ -182,15 +182,15 @@ Use `ref=eN` as the selector for the next call:
182
182
 
183
183
  - **A ref names one element for as long as it lives.** A surviving node keeps
184
184
  its ref across snapshots, and refs are never reused — so a ref cannot drift
185
- onto a different element. If it is removed, duplicated, or the page
185
+ onto a different element. If it is removed or the page
186
186
  navigates, the call fails `STALE_REF`; take a fresh snapshot then.
187
187
  - **Never copy refs into test code.** Convert live role/name, label, test ID,
188
188
  text, or DOM evidence into a durable selector and validate it.
189
189
  - **Token efficient.** `ref=e7` is 5 characters; `role=button[name=Sign in]`
190
190
  is 26. Over a 50-step flow that adds up.
191
- - **Auto-waiting still works.** Internally `ref=eN` resolves to a CSS
192
- attribute selector (`[data-craftdriver-ref="eN"]`); every action
193
- takes the normal visible+enabled wait path.
191
+ - **Auto-waiting still works.** `ref=eN` resolves directly to the exact element
192
+ in the page identity registry, including inside an open shadow root; every
193
+ action takes the normal visible+enabled wait path.
194
194
 
195
195
  **Invalidation rules**
196
196
 
@@ -198,13 +198,17 @@ Use `ref=eN` as the selector for the next call:
198
198
  its ref across snapshots; snapshots do **not** renumber it.
199
199
  - New elements get fresh numbers. Refs are never reused, including after a
200
200
  navigation or reload.
201
- - A ref whose element was removed or duplicated, or that was issued before
201
+ - A ref whose element was removed, or that was issued before
202
202
  the page navigated or reloaded, fails with `STALE_REF` — take a fresh
203
203
  snapshot. It never resolves to a different element.
204
204
  - `error.detail.reason` distinguishes `detached`, `document-changed`,
205
205
  `unknown-ref`, `ambiguous`, and `no-snapshot`.
206
206
  - Refs are exploration state and must never appear in committed tests.
207
207
 
208
+ Snapshots recursively enter open Shadow DOM, mark each boundary with an
209
+ indented `#shadow-root (open)` line, flatten slot assignment without duplicate
210
+ entries, and never inspect closed roots.
211
+
208
212
  ## Post-action payload
209
213
 
210
214
  Every tool returns a content array. Mutating tools (`navigate`,
@@ -38,7 +38,18 @@
38
38
  <p id="child-result"></p>
39
39
  <input id="child-input" type="text" placeholder="type here" />
40
40
  <p id="child-value"></p>
41
+ <frame-widget id="frame-widget"></frame-widget>
41
42
  <script>
43
+ customElements.define('frame-widget', class extends HTMLElement {
44
+ constructor() {
45
+ super();
46
+ const root = this.attachShadow({ mode: 'open' });
47
+ root.innerHTML = '<button id="shadow-frame-button" type="button">Shadow frame action</button>';
48
+ root.querySelector('button').addEventListener('click', () => {
49
+ document.querySelector('#child-result').textContent = 'shadow-clicked';
50
+ });
51
+ }
52
+ });
42
53
  document.getElementById('child-btn').addEventListener('click', function () {
43
54
  document.getElementById('child-result').textContent = 'clicked';
44
55
  });
@@ -48,4 +59,4 @@
48
59
  </script>
49
60
  </body>
50
61
 
51
- </html>
62
+ </html>
@@ -0,0 +1,137 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>CraftDriver Shadow DOM fixture</title>
6
+ <style>
7
+ body { font-family: sans-serif; margin: 24px; }
8
+ user-card, closed-card, no-root { display: block; margin: 12px 0; }
9
+ #status { min-height: 20px; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <h1>Shadow DOM fixture</h1>
14
+ <div id="status">idle</div>
15
+ <button id="replace-card" type="button">Replace card</button>
16
+
17
+ <!-- Open, nested, and slotted happy path -->
18
+ <user-card id="card">
19
+ <span slot="summary" id="slotted-summary" role="note">Slotted summary</span>
20
+ <img slot="summary" id="slotted-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
21
+ </user-card>
22
+
23
+ <!-- Async-host, closed-root, and missing-root cases -->
24
+ <div id="late-card-container"></div>
25
+ <closed-card id="closed-card"></closed-card>
26
+ <no-root id="no-root"></no-root>
27
+
28
+ <script>
29
+ // Nested open-root form control.
30
+ class AddressForm extends HTMLElement {
31
+ constructor() {
32
+ super();
33
+ const root = this.attachShadow({ mode: 'open' });
34
+ root.innerHTML = `
35
+ <style>:host { display:block; padding:8px; border:1px solid #aaa }</style>
36
+ <label for="city">City</label>
37
+ <input id="city" data-testid="city" placeholder="Enter city" title="Delivery city" />
38
+ <span id="save-address-label">Save address</span>
39
+ <button id="nested-save" type="button" aria-labelledby="save-address-label">Save</button>
40
+ <img id="shadow-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
41
+ `;
42
+ root.querySelector('#nested-save').addEventListener('click', () => {
43
+ document.querySelector('#status').textContent = 'saved:' + root.querySelector('#city').value;
44
+ });
45
+ setTimeout(() => {
46
+ const delayed = document.createElement('button');
47
+ delayed.id = 'delayed-shadow-action';
48
+ delayed.type = 'button';
49
+ delayed.textContent = 'Delayed shadow action';
50
+ delayed.addEventListener('click', () => {
51
+ document.querySelector('#status').textContent = 'delayed';
52
+ });
53
+ root.appendChild(delayed);
54
+ }, 250);
55
+ }
56
+ }
57
+
58
+ // Open-root host with slots, semantic content, and actions.
59
+ class UserCard extends HTMLElement {
60
+ constructor() {
61
+ super();
62
+ const root = this.attachShadow({ mode: 'open' });
63
+ root.innerHTML = `
64
+ <style>:host { display:block; padding:8px; border:1px solid #444 }</style>
65
+ <section role="region" aria-label="Account">
66
+ <slot name="summary"><span>Fallback summary</span></slot>
67
+ <slot><button id="fallback-control" type="button">Fallback action</button></slot>
68
+ <a id="profile-link" href="#profile">Profile link</a>
69
+ <img id="avatar" alt="User avatar" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
70
+ <button class="action" id="edit" type="button">Edit</button>
71
+ <button class="action" id="delete" type="button">Delete</button>
72
+ <address-form id="address"></address-form>
73
+ </section>
74
+ `;
75
+ root.querySelector('#edit').addEventListener('click', () => {
76
+ document.querySelector('#status').textContent = 'edited';
77
+ });
78
+ root.querySelector('#delete').addEventListener('click', () => {
79
+ document.querySelector('#status').textContent = 'deleted';
80
+ });
81
+ setTimeout(() => {
82
+ const delayed = document.createElement('button');
83
+ delayed.id = 'delayed-card-action';
84
+ delayed.type = 'button';
85
+ delayed.textContent = 'Delayed card action';
86
+ delayed.addEventListener('click', () => {
87
+ document.querySelector('#status').textContent = 'delayed-card';
88
+ });
89
+ root.appendChild(delayed);
90
+ }, 250);
91
+ }
92
+ }
93
+
94
+ // Closed-root privacy boundary.
95
+ class ClosedCard extends HTMLElement {
96
+ constructor() {
97
+ super();
98
+ const root = this.attachShadow({ mode: 'closed' });
99
+ root.innerHTML = `
100
+ <button id="closed-false-positive" type="button">Edit</button>
101
+ <img id="closed-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
102
+ `;
103
+ }
104
+ }
105
+
106
+ customElements.define('address-form', AddressForm);
107
+ customElements.define('user-card', UserCard);
108
+ customElements.define('closed-card', ClosedCard);
109
+
110
+ document.querySelector('#replace-card').addEventListener('click', () => {
111
+ const replacement = document.createElement('user-card');
112
+ replacement.id = 'card';
113
+ const summary = document.createElement('span');
114
+ summary.slot = 'summary';
115
+ summary.id = 'slotted-summary';
116
+ summary.setAttribute('role', 'note');
117
+ summary.textContent = 'Replacement summary';
118
+ replacement.appendChild(summary);
119
+ document.querySelector('#card').replaceWith(replacement);
120
+ });
121
+
122
+ setTimeout(() => {
123
+ const late = document.createElement('user-card');
124
+ late.id = 'late-card';
125
+ document.querySelector('#late-card-container').appendChild(late);
126
+ }, 250);
127
+
128
+ setTimeout(() => {
129
+ const delayed = document.createElement('button');
130
+ delayed.id = 'delayed-flat-action';
131
+ delayed.type = 'button';
132
+ delayed.textContent = 'Delayed flat action';
133
+ document.body.appendChild(delayed);
134
+ }, 250);
135
+ </script>
136
+ </body>
137
+ </html>
@@ -68,7 +68,7 @@ Chrome/Chromium and Firefox BiDi use the simpler launch form above.
68
68
 
69
69
  ## Notes
70
70
 
71
- - Keep generated auth files out of source control if they contain real secrets.
71
+ - Add `.auth/` to `.gitignore`; generated state contains live session cookies.
72
72
  - Regenerate auth state when the app changes its login or session behavior.
73
73
  - Use separate files such as `.auth/admin.json` and `.auth/customer.json` for different roles.
74
74
 
package/docs/selectors.md CHANGED
@@ -435,6 +435,67 @@ await card.getByRole('button', { name: 'Buy' }).click();
435
435
  await card.getByText('In stock').expect().toBeVisible();
436
436
  ```
437
437
 
438
+ ## Open Shadow DOM
439
+
440
+ CraftDriver crosses Shadow DOM with an explicit, lazy `shadowRoot()` boundary.
441
+ Call it on the locator or element handle for the shadow host, then locate within
442
+ the returned `ShadowRootLocator`:
443
+
444
+ ```typescript
445
+ const card = browser.locator('user-card#account').shadowRoot();
446
+
447
+ await card.getByRole('button', { name: 'Edit' }).click();
448
+ await card.getByLabel('Display name').fill('Alice');
449
+ await card.locator('.status').expect().toHaveText('Saved');
450
+ ```
451
+
452
+ The boundary is explicit: `browser.locator('user-card .status')` does not pierce
453
+ the root and ordinary CSS keeps its normal tree scope. Both API styles work:
454
+
455
+ ```typescript
456
+ // Lazy Locator host — re-resolves the host and root for every operation.
457
+ const lazyRoot = browser.locator('#account').shadowRoot();
458
+
459
+ // ElementHandle style — browser.find() is locator-backed and auto-waits too.
460
+ const handleRoot = browser.find('#account').shadowRoot();
461
+ await handleRoot.find(By.testId('save')).click();
462
+ ```
463
+
464
+ Nested open roots compose by crossing each boundary in order:
465
+
466
+ ```typescript
467
+ const address = browser
468
+ .locator('checkout-shell')
469
+ .shadowRoot()
470
+ .locator('address-form')
471
+ .shadowRoot();
472
+
473
+ await address.getByPlaceholder('City').fill('Sofia');
474
+ await address.getByRole('button', { name: 'Save address' }).click();
475
+ ```
476
+
477
+ `ShadowRootLocator` is a search context, not an element. It provides
478
+ `locator()`, `find()`, `findAll()`, and all `getBy*()` helpers; actions and
479
+ assertions live on the descendant `Locator`/`ElementHandle`. Filters, indexing,
480
+ auto-waiting, assertions, locator-scoped accessibility audits, `Page`, and
481
+ `Frame` contexts retain the complete root-aware query chain.
482
+
483
+ The host follows normal locator cardinality: when it matches more than one
484
+ element, use `first()`, `last()`, or `nth()` before `shadowRoot()` to select the
485
+ intended host. Descendant `count()` returns `0` for no current matches, while
486
+ actions and positive assertions auto-wait using the configured timeout.
487
+
488
+ Only roots exposed by the page's public `host.shadowRoot` getter are supported.
489
+ A missing or closed root throws `NO_OPEN_SHADOW_ROOT`; CraftDriver does not use
490
+ WebDriver as a closed-root backdoor. Detached roots restart the complete lazy
491
+ query and end with `DETACHED_SHADOW_ROOT` only when a stable root cannot be
492
+ established. Raw XPath inside a shadow root is transport-dependent; CSS and the
493
+ semantic `getBy*`/`By.*` helpers are the portable choices.
494
+
495
+ The same API is covered on Chrome, Firefox, and Safari. CraftDriver uses
496
+ WebDriver BiDi shadow primitives when the session supports them and falls back
497
+ to the standard WebDriver Classic shadow-root endpoints otherwise.
498
+
438
499
  ### Locator actions and state
439
500
 
440
501
  | Method | Description |