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.
- package/CHANGELOG.md +6 -5
- package/README.md +3 -2
- package/dist/cli/pageSemantics.d.ts +3 -39
- package/dist/cli/pageSemantics.d.ts.map +1 -1
- package/dist/cli/pageSemantics.js +3 -191
- package/dist/cli/pageSemantics.js.map +1 -1
- package/dist/cli/selector.d.ts +3 -3
- package/dist/cli/selector.d.ts.map +1 -1
- package/dist/cli/selector.js +6 -9
- package/dist/cli/selector.js.map +1 -1
- package/dist/cli/snapshot.d.ts +5 -6
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +59 -18
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/a11y.d.ts +6 -2
- package/dist/lib/a11y.d.ts.map +1 -1
- package/dist/lib/a11y.js +3 -1
- package/dist/lib/a11y.js.map +1 -1
- package/dist/lib/bidi/evaluate.d.ts +11 -0
- package/dist/lib/bidi/evaluate.d.ts.map +1 -0
- package/dist/lib/bidi/evaluate.js +25 -0
- package/dist/lib/bidi/evaluate.js.map +1 -0
- package/dist/lib/bidi/types.d.ts +2 -0
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +1 -1
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +46 -35
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +4 -6
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +5 -7
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +65 -27
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +47 -20
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/clearFastPath.d.ts.map +1 -1
- package/dist/lib/clearFastPath.js +2 -0
- package/dist/lib/clearFastPath.js.map +1 -1
- package/dist/lib/clickFastPath.d.ts.map +1 -1
- package/dist/lib/clickFastPath.js +2 -0
- package/dist/lib/clickFastPath.js.map +1 -1
- package/dist/lib/driver.d.ts +19 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +111 -4
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/elementHandle.d.ts +13 -10
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +119 -50
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +5 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +5 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/expect.d.ts +10 -0
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +76 -49
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/fillFastPath.d.ts.map +1 -1
- package/dist/lib/fillFastPath.js +2 -0
- package/dist/lib/fillFastPath.js.map +1 -1
- package/dist/lib/frame.d.ts.map +1 -1
- package/dist/lib/frame.js +29 -17
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/locator.d.ts +21 -54
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +165 -208
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +23 -13
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/pageSemantics.d.ts +42 -0
- package/dist/lib/pageSemantics.d.ts.map +1 -0
- package/dist/lib/pageSemantics.js +216 -0
- package/dist/lib/pageSemantics.js.map +1 -0
- package/dist/lib/query.d.ts +68 -0
- package/dist/lib/query.d.ts.map +1 -0
- package/dist/lib/query.js +590 -0
- package/dist/lib/query.js.map +1 -0
- package/dist/lib/shadowRoot.d.ts +21 -0
- package/dist/lib/shadowRoot.d.ts.map +1 -0
- package/dist/lib/shadowRoot.js +29 -0
- package/dist/lib/shadowRoot.js.map +1 -0
- package/dist/lib/shadowRootLocator.d.ts +31 -0
- package/dist/lib/shadowRootLocator.d.ts.map +1 -0
- package/dist/lib/shadowRootLocator.js +55 -0
- package/dist/lib/shadowRootLocator.js.map +1 -0
- package/dist/lib/timing.d.ts +2 -2
- package/dist/lib/timing.js +2 -2
- package/docs/accessibility.md +9 -1
- package/docs/api-reference.md +4 -1
- package/docs/browser-api.md +3 -3
- package/docs/browser-context.md +3 -3
- package/docs/cli.md +10 -7
- package/docs/error-codes.md +3 -1
- package/docs/getting-started.md +1 -1
- package/docs/mcp.md +9 -5
- package/docs/public/examples/iframe-child.html +12 -1
- package/docs/public/examples/shadow-dom.html +137 -0
- package/docs/recipes/login-once-reuse-session.md +1 -1
- package/docs/selectors.md +61 -0
- package/docs/session-management.md +17 -10
- package/package.json +3 -3
- 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"}
|
package/dist/lib/timing.d.ts
CHANGED
|
@@ -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.
|
|
121
|
-
*
|
|
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;
|
package/dist/lib/timing.js
CHANGED
|
@@ -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.
|
|
125
|
-
*
|
|
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;
|
package/docs/accessibility.md
CHANGED
|
@@ -130,7 +130,12 @@ interface A11yViolation {
|
|
|
130
130
|
impact: 'minor' | 'moderate' | 'serious' | 'critical';
|
|
131
131
|
description: string;
|
|
132
132
|
helpUrl: string;
|
|
133
|
-
nodes: Array<{
|
|
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.
|
package/docs/api-reference.md
CHANGED
|
@@ -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: **
|
|
191
|
+
Total exports: **97**.
|
package/docs/browser-api.md
CHANGED
|
@@ -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: '
|
|
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('
|
|
388
|
+
await browser.saveState('.auth/session.json');
|
|
389
389
|
|
|
390
390
|
// Load session from file
|
|
391
|
-
await browser.loadState('
|
|
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());
|
package/docs/browser-context.md
CHANGED
|
@@ -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
|
|
143
|
-
|
|
144
|
-
|
|
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,
|
|
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`
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
package/docs/error-codes.md
CHANGED
|
@@ -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
|
|
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. |
|
package/docs/getting-started.md
CHANGED
|
@@ -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: '
|
|
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
|
|
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.**
|
|
192
|
-
|
|
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
|
|
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
|
-
-
|
|
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 |
|