Skip to content

Commit

Permalink
feat: use an xpath query handler (#8730)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrandolf committed Aug 4, 2022
1 parent 49193cb commit 5cf9b4d
Show file tree
Hide file tree
Showing 17 changed files with 426 additions and 383 deletions.
114 changes: 57 additions & 57 deletions docs/api/index.md

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions docs/api/puppeteer.customqueryhandler.md
Expand Up @@ -4,8 +4,6 @@ sidebar_label: CustomQueryHandler

# CustomQueryHandler interface

Contains two functions `queryOne` and `queryAll` that can be [registered](./puppeteer.registercustomqueryhandler.md) as alternative querying strategies. The functions `queryOne` and `queryAll` are executed in the page context. `queryOne` should take an `Element` and a selector string as argument and return a single `Element` or `null` if no element is found. `queryAll` takes the same arguments but should instead return a `NodeListOf<Element>` or `Array<Element>` with all the elements that match the given query selector.

**Signature:**

```typescript
Expand All @@ -14,7 +12,7 @@ export interface CustomQueryHandler

## Properties

| Property | Modifiers | Type | Description |
| ------------------------------------------------------- | --------- | ---------------------------------------------------- | ----------------- |
| [queryAll?](./puppeteer.customqueryhandler.queryall.md) | | (element: Node, selector: string) =&gt; Node\[\] | <i>(Optional)</i> |
| [queryOne?](./puppeteer.customqueryhandler.queryone.md) | | (element: Node, selector: string) =&gt; Node \| null | <i>(Optional)</i> |
| Property | Modifiers | Type | Description |
| ------------------------------------------------------- | --------- | ------------------------------------------------- | ----------------- |
| [queryAll?](./puppeteer.customqueryhandler.queryall.md) | | (node: Node, selector: string) =&gt; Node\[\] | <i>(Optional)</i> |
| [queryOne?](./puppeteer.customqueryhandler.queryone.md) | | (node: Node, selector: string) =&gt; Node \| null | <i>(Optional)</i> |
2 changes: 1 addition & 1 deletion docs/api/puppeteer.customqueryhandler.queryall.md
Expand Up @@ -8,6 +8,6 @@ sidebar_label: CustomQueryHandler.queryAll

```typescript
interface CustomQueryHandler {
queryAll?: (element: Node, selector: string) => Node[];
queryAll?: (node: Node, selector: string) => Node[];
}
```
2 changes: 1 addition & 1 deletion docs/api/puppeteer.customqueryhandler.queryone.md
Expand Up @@ -8,6 +8,6 @@ sidebar_label: CustomQueryHandler.queryOne

```typescript
interface CustomQueryHandler {
queryOne?: (element: Node, selector: string) => Node | null;
queryOne?: (node: Node, selector: string) => Node | null;
}
```
6 changes: 5 additions & 1 deletion docs/api/puppeteer.elementhandle._x.md
Expand Up @@ -4,7 +4,11 @@ sidebar_label: ElementHandle.$x

# ElementHandle.$x() method

The method evaluates the XPath expression relative to the elementHandle. If there are no such elements, the method will resolve to an empty array.
> Warning: This API is now obsolete.
>
> Use [ElementHandle.$$()](./puppeteer.elementhandle.__.md) with the `xpath` prefix.
>
> The method evaluates the XPath expression relative to the elementHandle. If there are no such elements, the method will resolve to an empty array.
**Signature:**

Expand Down
80 changes: 29 additions & 51 deletions docs/api/puppeteer.elementhandle.md

Large diffs are not rendered by default.

54 changes: 29 additions & 25 deletions docs/api/puppeteer.elementhandle.waitforxpath.md
Expand Up @@ -4,31 +4,35 @@ sidebar_label: ElementHandle.waitForXPath

# ElementHandle.waitForXPath() method

Wait for the `xpath` within the element. If at the moment of calling the method the `xpath` already exists, the method will return immediately. If the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the function will throw.

If `xpath` starts with `//` instead of `.//`, the dot will be appended automatically.

This method works across navigation

```ts
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
let currentURL;
page
.waitForXPath('//img')
.then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of [
'https://example.com',
'https://google.com',
'https://bbc.com',
]) {
await page.goto(currentURL);
}
await browser.close();
})();
```
> Warning: This API is now obsolete.
>
> Use [ElementHandle.waitForSelector()](./puppeteer.elementhandle.waitforselector.md) with the `xpath` prefix.
>
> Wait for the `xpath` within the element. If at the moment of calling the method the `xpath` already exists, the method will return immediately. If the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
>
> If `xpath` starts with `//` instead of `.//`, the dot will be appended automatically.
>
> This method works across navigation
>
> ```ts
> const puppeteer = require('puppeteer');
> (async () => {
> const browser = await puppeteer.launch();
> const page = await browser.newPage();
> let currentURL;
> page
> .waitForXPath('//img')
> .then(() => console.log('First URL with image: ' + currentURL));
> for (currentURL of [
> 'https://example.com',
> 'https://google.com',
> 'https://bbc.com',
> ]) {
> await page.goto(currentURL);
> }
> await browser.close();
> })();
> ```
**Signature:**

Expand Down
14 changes: 8 additions & 6 deletions docs/api/puppeteer.frame.waitforxpath.md
Expand Up @@ -4,6 +4,14 @@ sidebar_label: Frame.waitForXPath

# Frame.waitForXPath() method

> Warning: This API is now obsolete.
>
> Use [Frame.waitForSelector()](./puppeteer.frame.waitforselector.md) with the `xpath` prefix.
>
> Wait for the `xpath` to appear in page. If at the moment of calling the method the `xpath` already exists, the method will return immediately. If the xpath doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
>
> For a code example, see the example for [Frame.waitForSelector()](./puppeteer.frame.waitforselector.md). That function behaves identically other than taking a CSS selector rather than an XPath.
**Signature:**

```typescript
Expand All @@ -25,9 +33,3 @@ class Frame {
**Returns:**

Promise&lt;[ElementHandle](./puppeteer.elementhandle.md)&lt;Node&gt; \| null&gt;

## Remarks

Wait for the `xpath` to appear in page. If at the moment of calling the method the `xpath` already exists, the method will return immediately. If the xpath doesn't appear after the `timeout` milliseconds of waiting, the function will throw.

For a code example, see the example for [Frame.waitForSelector()](./puppeteer.frame.waitforselector.md). That function behaves identically other than taking a CSS selector rather than an XPath.
2 changes: 1 addition & 1 deletion src/common/AriaQueryHandler.ts
Expand Up @@ -114,7 +114,7 @@ const waitFor = async (
return (await domWorld._waitForSelectorInPage(
(_: Element, selector: string) => {
return (
globalThis as any as unknown as {
globalThis as unknown as {
ariaQuerySelector(selector: string): void;
}
).ariaQuerySelector(selector);
Expand Down
46 changes: 0 additions & 46 deletions src/common/DOMWorld.ts
Expand Up @@ -717,52 +717,6 @@ export class DOMWorld {
return elementHandle;
}

async waitForXPath(
xpath: string,
options: WaitForSelectorOptions
): Promise<ElementHandle<Node> | null> {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this.#timeoutSettings.timeout(),
} = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`;
function predicate(
root: Element | Document,
xpath: string,
waitForVisible: boolean,
waitForHidden: boolean
): Node | null | boolean {
const node = document.evaluate(
xpath,
root,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
return checkWaitForOptions(node, waitForVisible, waitForHidden);
}
const waitTaskOptions: WaitTaskOptions = {
domWorld: this,
predicateBody: makePredicateString(predicate),
predicateAcceptsContextElement: true,
title,
polling,
timeout,
args: [xpath, waitForVisible, waitForHidden],
root: options.root,
};
const waitTask = new WaitTask(waitTaskOptions);
const jsHandle = await waitTask.promise;
const elementHandle = jsHandle.asElement();
if (!elementHandle) {
await jsHandle.dispose();
return null;
}
return elementHandle;
}

waitForFunction(
pageFunction: Function | string,
options: {polling?: string | number; timeout?: number} = {},
Expand Down
54 changes: 10 additions & 44 deletions src/common/ElementHandle.ts
Expand Up @@ -140,6 +140,8 @@ export class ElementHandle<
}

/**
* @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath` prefix.
*
* Wait for the `xpath` within the element. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If
* the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
Expand Down Expand Up @@ -197,27 +199,10 @@ export class ElementHandle<
timeout?: number;
} = {}
): Promise<ElementHandle<Node> | null> {
const frame = this._context.frame();
assert(frame);
const secondaryContext = await frame._secondaryWorld.executionContext();
const adoptedRoot = await secondaryContext._adoptElementHandle(this);
xpath = xpath.startsWith('//') ? '.' + xpath : xpath;
if (!xpath.startsWith('.//')) {
await adoptedRoot.dispose();
throw new Error('Unsupported xpath expression: ' + xpath);
}
const handle = await frame._secondaryWorld.waitForXPath(xpath, {
...options,
root: adoptedRoot,
});
await adoptedRoot.dispose();
if (!handle) {
return null;
if (xpath.startsWith('//')) {
xpath = `.${xpath}`;
}
const mainExecutionContext = await frame._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
return this.waitForSelector(`xpath/${xpath}`, options);
}

override asElement(): ElementHandle<ElementType> | null {
Expand Down Expand Up @@ -964,36 +949,17 @@ export class ElementHandle<
}

/**
* @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix.
*
* The method evaluates the XPath expression relative to the elementHandle.
* If there are no such elements, the method will resolve to an empty array.
* @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
const arrayHandle = await this.evaluateHandle((element, expression) => {
const doc = element.ownerDocument || document;
const iterator = doc.evaluate(
expression,
element,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array = [];
let item;
while ((item = iterator.iterateNext())) {
array.push(item);
}
return array;
}, expression);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle) {
result.push(elementHandle);
}
if (expression.startsWith('//')) {
expression = `.${expression}`;
}
return result;
return this.$$(`xpath/${expression}`);
}

/**
Expand Down
13 changes: 5 additions & 8 deletions src/common/FrameManager.ts
Expand Up @@ -1374,7 +1374,8 @@ export class Frame {
}

/**
* @remarks
* @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix.
*
* Wait for the `xpath` to appear in page. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If
* the xpath doesn't appear after the `timeout` milliseconds of waiting, the
Expand All @@ -1392,14 +1393,10 @@ export class Frame {
xpath: string,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<Node> | null> {
const handle = await this._secondaryWorld.waitForXPath(xpath, options);
if (!handle) {
return null;
if (xpath.startsWith('//')) {
xpath = `.${xpath}`;
}
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
return this.waitForSelector(`xpath/${xpath}`, options);
}

/**
Expand Down

0 comments on commit 5cf9b4d

Please sign in to comment.