Skip to content

Commit

Permalink
feat: add waitForXPath to ElementHandle (#8329)
Browse files Browse the repository at this point in the history
  • Loading branch information
OrKoN committed May 10, 2022
1 parent 1e82d98 commit 7eaadaf
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
39 changes: 39 additions & 0 deletions docs/api.md
Expand Up @@ -342,6 +342,7 @@
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
* [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
* [elementHandle.waitForSelector(selector[, options])](#elementhandlewaitforselectorselector-options)
* [elementHandle.waitForXPath(xpath[, options])](#elementhandlewaitforxpathxpath-options)
- [class: HTTPRequest](#class-httprequest)
* [httpRequest.abort([errorCode], [priority])](#httprequestaborterrorcode-priority)
* [httpRequest.abortErrorReason()](#httprequestaborterrorreason)
Expand Down Expand Up @@ -4902,6 +4903,44 @@ Wait for an element matching `selector` to appear within the `elementHandle`’s

This method does not work across navigations or if the element is detached from DOM.

#### elementHandle.waitForXPath(xpath[, options])

- `xpath` <[string]> A [xpath] of an element to wait for
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]<?[ElementHandle]>> Promise which resolves when element specified by xpath string is added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is not found in DOM.

Wait for the `xpath` to appear 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 navigations:

```js
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();
})();
```

### class: HTTPRequest

Whenever the page sends a request, such as for a network resource, the following events are emitted by Puppeteer's page:
Expand Down
78 changes: 78 additions & 0 deletions src/common/JSHandle.ts
Expand Up @@ -416,6 +416,84 @@ export class ElementHandle<
return result;
}

/**
* 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
* ```js
* 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();
* })();
* ```
* @param xpath - A
* {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
* element to wait for
* @param options - Optional waiting parameters
* @returns Promise which resolves when element specified by xpath string is
* added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
* not found in DOM.
* @remarks
* The optional Argument `options` have properties:
*
* - `visible`: A boolean to wait for element to be present in DOM and to be
* visible, i.e. to not have `display: none` or `visibility: hidden` CSS
* properties. Defaults to `false`.
*
* - `hidden`: A boolean wait for element to not be found in the DOM or to be
* hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
* Defaults to `false`.
*
* - `timeout`: A number which is maximum time to wait for in milliseconds.
* Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
* value can be changed by using the {@link Page.setDefaultTimeout} method.
*/
async waitForXPath(
xpath: string,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
} = {}
): Promise<ElementHandle | null> {
const frame = this._context.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;
const mainExecutionContext = await frame._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}

asElement(): ElementHandle<ElementType> | null {
return this;
}
Expand Down
28 changes: 28 additions & 0 deletions test/elementhandle.spec.ts
Expand Up @@ -280,6 +280,34 @@ describe('ElementHandle specs', function () {
});
});

describe('Element.waitForXPath', () => {
it('should wait correctly with waitForXPath on an element', async () => {
const { page } = getTestState();
// Set the page content after the waitFor has been started.
await page.setContent(
`<div id=el1>
el1
<div id=el2>
el2
</div>
</div>
<div id=el3>
el3
</div>`
);

const el2 = await page.waitForSelector('#el1');

expect(
await (await el2.waitForXPath('//div')).evaluate((el) => el.id)
).toStrictEqual('el2');

expect(
await (await el2.waitForXPath('.//div')).evaluate((el) => el.id)
).toStrictEqual('el2');
});
});

describe('ElementHandle.hover', function () {
it('should work', async () => {
const { page, server } = getTestState();
Expand Down

0 comments on commit 7eaadaf

Please sign in to comment.