diff --git a/docs/core-concepts.md b/docs/core-concepts.md
index edfe9708de3e9..7b2672efd2f13 100644
--- a/docs/core-concepts.md
+++ b/docs/core-concepts.md
@@ -168,7 +168,13 @@ await page.click('css:light=div');
Selectors using the same or different engines can be combined using the `>>` separator. For example,
```js
-await page.click('#free-month-promo >> text=Learn more');
+// Click an element with text 'Sign Up' inside of a #free-month-promo.
+await page.click('#free-month-promo >> text=Sign Up');
+```
+
+```js
+// Capture textContent of a section that contains an element with text 'Selectors'.
+const sectionText = await page.$eval('*css=section >> text=Selectors', e => e.textContent);
```
diff --git a/docs/selectors.md b/docs/selectors.md
index d5b8c0946a013..3726942cc9dce 100644
--- a/docs/selectors.md
+++ b/docs/selectors.md
@@ -22,6 +22,8 @@ document
.querySelector('span[attr=value]')
```
+Selector engine name can be prefixed with `*` to capture element that matches the particular clause instead of the last one. For example, `css=article >> text=Hello` captures the element with the text `Hello`, and `*css=article >> text=Hello` (note the `*`) captures the `article` element that contains some element with the text `Hello`.
+
For convenience, selectors in the wrong format are heuristically converted to the right format:
- Selector starting with `//` is assumed to be `xpath=selector`. Example: `page.click('//html')` is converted to `page.click('xpath=//html')`.
- Selector surrounded with quotes (either `"` or `'`) is assumed to be `text=selector`. Example: `page.click('"foo"')` is converted to `page.click('text="foo"')`.
diff --git a/src/injected/selectorEvaluator.ts b/src/injected/selectorEvaluator.ts
index b5101ab1940dc..1234552c2e8d1 100644
--- a/src/injected/selectorEvaluator.ts
+++ b/src/injected/selectorEvaluator.ts
@@ -55,22 +55,27 @@ class SelectorEvaluator {
}
private _querySelectorRecursively(root: SelectorRoot, selector: types.ParsedSelector, index: number): Element | undefined {
- const current = selector[index];
- if (index === selector.length - 1)
+ const current = selector.parts[index];
+ if (index === selector.parts.length - 1)
return this.engines.get(current.name)!.query(root, current.body);
const all = this.engines.get(current.name)!.queryAll(root, current.body);
for (const next of all) {
const result = this._querySelectorRecursively(next, selector, index + 1);
if (result)
- return result;
+ return selector.capture === index ? next : result;
}
}
querySelectorAll(selector: types.ParsedSelector, root: Node): Element[] {
if (!(root as any)['querySelectorAll'])
throw new Error('Node is not queryable.');
+ const capture = selector.capture === undefined ? selector.parts.length - 1 : selector.capture;
+ // Query all elements up to the capture.
+ const partsToQuerAll = selector.parts.slice(0, capture + 1);
+ // Check they have a descendant matching everything after the capture.
+ const partsToCheckOne = selector.parts.slice(capture + 1);
let set = new Set([ root as SelectorRoot ]);
- for (const { name, body } of selector) {
+ for (const { name, body } of partsToQuerAll) {
const newSet = new Set();
for (const prev of set) {
for (const next of this.engines.get(name)!.queryAll(prev, body)) {
@@ -81,7 +86,11 @@ class SelectorEvaluator {
}
set = newSet;
}
- return Array.from(set) as Element[];
+ const candidates = Array.from(set) as Element[];
+ if (!partsToCheckOne.length)
+ return candidates;
+ const partial = { parts: partsToCheckOne };
+ return candidates.filter(e => !!this._querySelectorRecursively(e, partial, 0));
}
}
diff --git a/src/selectors.ts b/src/selectors.ts
index aca8220143e85..218f810aba4d2 100644
--- a/src/selectors.ts
+++ b/src/selectors.ts
@@ -62,7 +62,7 @@ export class Selectors {
}
private _needsMainContext(parsed: types.ParsedSelector): boolean {
- return parsed.some(({name}) => {
+ return parsed.parts.some(({name}) => {
const custom = this._engines.get(name);
return custom ? !custom.contentScript : false;
});
@@ -188,13 +188,13 @@ export class Selectors {
let index = 0;
let quote: string | undefined;
let start = 0;
- const result: types.ParsedSelector = [];
+ const result: types.ParsedSelector = { parts: [] };
const append = () => {
const part = selector.substring(start, index).trim();
const eqIndex = part.indexOf('=');
let name: string;
let body: string;
- if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9-+:]+$/)) {
+ if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9-+:*]+$/)) {
name = part.substring(0, eqIndex).trim();
body = part.substring(eqIndex + 1);
} else if (part.length > 1 && part[0] === '"' && part[part.length - 1] === '"') {
@@ -213,9 +213,19 @@ export class Selectors {
body = part;
}
name = name.toLowerCase();
+ let capture = false;
+ if (name[0] === '*') {
+ capture = true;
+ name = name.substring(1);
+ }
if (!this._builtinEngines.has(name) && !this._engines.has(name))
- throw new Error(`Unknown engine ${name} while parsing selector ${selector}`);
- result.push({ name, body });
+ throw new Error(`Unknown engine "${name}" while parsing selector ${selector}`);
+ result.parts.push({ name, body });
+ if (capture) {
+ if (result.capture !== undefined)
+ throw new Error(`Only one of the selectors can capture using * modifier`);
+ result.capture = result.parts.length - 1;
+ }
};
while (index < selector.length) {
const c = selector[index];
diff --git a/src/types.ts b/src/types.ts
index e428dd901a88f..1ad1e0d82e974 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -157,6 +157,9 @@ export type JSCoverageOptions = {
};
export type ParsedSelector = {
- name: string,
- body: string,
-}[];
+ parts: {
+ name: string,
+ body: string,
+ }[],
+ capture?: number,
+};
diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js
index 0c39efc50a23e..a3c5356f467bf 100644
--- a/test/queryselector.spec.js
+++ b/test/queryselector.spec.js
@@ -111,6 +111,21 @@ describe('Page.$eval', function() {
const html = await page.$eval('button >> "Next"', e => e.outerHTML);
expect(html).toBe('');
});
+ it('should support * capture', async({page, server}) => {
+ await page.setContent('