diff --git a/docs/api.md b/docs/api.md index 23f5c80b82fff..edfad50db9571 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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) @@ -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]> 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: diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 9e7ec6d56693f..99c338ea81c9c 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -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 { + 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 | null { return this; } diff --git a/test/elementhandle.spec.ts b/test/elementhandle.spec.ts index 742392d44c043..e46e6ca0d0e2a 100644 --- a/test/elementhandle.spec.ts +++ b/test/elementhandle.spec.ts @@ -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( + `
+ el1 +
+ el2 +
+
+
+ el3 +
` + ); + + 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();