feat: implement Element.waitForSelector (#7825)

Co-authored-by: Johan Bay <jobay@google.com>
Co-authored-by: Mathias Bynens <mathias@qiwi.be>
This commit is contained in:
Alex Rudenko 2021-12-09 12:51:14 +01:00 committed by GitHub
parent a604646f44
commit c03429444d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 144 additions and 15 deletions

View File

@ -336,6 +336,7 @@
* [elementHandle.toString()](#elementhandletostring) * [elementHandle.toString()](#elementhandletostring)
* [elementHandle.type(text[, options])](#elementhandletypetext-options) * [elementHandle.type(text[, options])](#elementhandletypetext-options)
* [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) * [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
* [elementHandle.waitForSelector(selector[, options])](#elementhandlewaitforselectorselector-options)
- [class: HTTPRequest](#class-httprequest) - [class: HTTPRequest](#class-httprequest)
* [httpRequest.abort([errorCode], [priority])](#httprequestaborterrorcode-priority) * [httpRequest.abort([errorCode], [priority])](#httprequestaborterrorcode-priority)
* [httpRequest.abortErrorReason()](#httprequestaborterrorreason) * [httpRequest.abortErrorReason()](#httprequestaborterrorreason)
@ -4872,6 +4873,19 @@ await elementHandle.press('Enter');
This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
#### elementHandle.waitForSelector(selector[, options])
- `selector` <[string]> A [selector] 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 selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM.
Wait for an element matching `selector` to appear within the `elementHandle`s subtree. If the `selector` already matches an element at the moment of calling the method, the promise returned by the method resolves immediately. If the selector doesnt appear after `timeout` milliseconds of waiting, the promise rejects.
This method does not work across navigations or if the element is detached from DOM.
### class: HTTPRequest ### class: HTTPRequest
Whenever the page sends a request, such as for a network resource, the following events are emitted by Puppeteer's page: Whenever the page sends a request, such as for a network resource, the following events are emitted by Puppeteer's page:

View File

@ -58,6 +58,7 @@ export interface WaitForSelectorOptions {
visible?: boolean; visible?: boolean;
hidden?: boolean; hidden?: boolean;
timeout?: number; timeout?: number;
root?: ElementHandle;
} }
/** /**
@ -631,13 +632,14 @@ export class DOMWorld {
waitForHidden ? ' to be hidden' : '' waitForHidden ? ' to be hidden' : ''
}`; }`;
async function predicate( async function predicate(
root: Element | Document,
selector: string, selector: string,
waitForVisible: boolean, waitForVisible: boolean,
waitForHidden: boolean waitForHidden: boolean
): Promise<Node | null | boolean> { ): Promise<Node | null | boolean> {
const node = predicateQueryHandler const node = predicateQueryHandler
? ((await predicateQueryHandler(document, selector)) as Element) ? ((await predicateQueryHandler(root, selector)) as Element)
: document.querySelector(selector); : root.querySelector(selector);
return checkWaitForOptions(node, waitForVisible, waitForHidden); return checkWaitForOptions(node, waitForVisible, waitForHidden);
} }
const waitTaskOptions: WaitTaskOptions = { const waitTaskOptions: WaitTaskOptions = {
@ -648,6 +650,7 @@ export class DOMWorld {
timeout, timeout,
args: [selector, waitForVisible, waitForHidden], args: [selector, waitForVisible, waitForHidden],
binding, binding,
root: options.root,
}; };
const waitTask = new WaitTask(waitTaskOptions); const waitTask = new WaitTask(waitTaskOptions);
const jsHandle = await waitTask.promise; const jsHandle = await waitTask.promise;
@ -671,13 +674,14 @@ export class DOMWorld {
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`; const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`;
function predicate( function predicate(
root: Element | Document,
xpath: string, xpath: string,
waitForVisible: boolean, waitForVisible: boolean,
waitForHidden: boolean waitForHidden: boolean
): Node | null | boolean { ): Node | null | boolean {
const node = document.evaluate( const node = document.evaluate(
xpath, xpath,
document, root,
null, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, XPathResult.FIRST_ORDERED_NODE_TYPE,
null null
@ -691,6 +695,7 @@ export class DOMWorld {
polling, polling,
timeout, timeout,
args: [xpath, waitForVisible, waitForHidden], args: [xpath, waitForVisible, waitForHidden],
root: options.root,
}; };
const waitTask = new WaitTask(waitTaskOptions); const waitTask = new WaitTask(waitTaskOptions);
const jsHandle = await waitTask.promise; const jsHandle = await waitTask.promise;
@ -737,6 +742,7 @@ export interface WaitTaskOptions {
timeout: number; timeout: number;
binding?: PageBinding; binding?: PageBinding;
args: SerializableOrJSHandle[]; args: SerializableOrJSHandle[];
root?: ElementHandle;
} }
/** /**
@ -755,6 +761,7 @@ export class WaitTask {
_reject: (x: Error) => void; _reject: (x: Error) => void;
_timeoutTimer?: NodeJS.Timeout; _timeoutTimer?: NodeJS.Timeout;
_terminated = false; _terminated = false;
_root: ElementHandle;
constructor(options: WaitTaskOptions) { constructor(options: WaitTaskOptions) {
if (helper.isString(options.polling)) if (helper.isString(options.polling))
@ -777,6 +784,7 @@ export class WaitTask {
this._domWorld = options.domWorld; this._domWorld = options.domWorld;
this._polling = options.polling; this._polling = options.polling;
this._timeout = options.timeout; this._timeout = options.timeout;
this._root = options.root;
this._predicateBody = getPredicateBody(options.predicateBody); this._predicateBody = getPredicateBody(options.predicateBody);
this._args = options.args; this._args = options.args;
this._binding = options.binding; this._binding = options.binding;
@ -823,13 +831,24 @@ export class WaitTask {
} }
if (this._terminated || runCount !== this._runCount) return; if (this._terminated || runCount !== this._runCount) return;
try { try {
success = await context.evaluateHandle( if (this._root) {
waitForPredicatePageFunction, success = await this._root.evaluateHandle(
this._predicateBody, waitForPredicatePageFunction,
this._polling, this._predicateBody,
this._timeout, this._polling,
...this._args this._timeout,
); ...this._args
);
} else {
success = await context.evaluateHandle(
waitForPredicatePageFunction,
null,
this._predicateBody,
this._polling,
this._timeout,
...this._args
);
}
} catch (error_) { } catch (error_) {
error = error_; error = error_;
} }
@ -890,11 +909,13 @@ export class WaitTask {
} }
async function waitForPredicatePageFunction( async function waitForPredicatePageFunction(
root: Element | Document | null,
predicateBody: string, predicateBody: string,
polling: string, polling: string,
timeout: number, timeout: number,
...args: unknown[] ...args: unknown[]
): Promise<unknown> { ): Promise<unknown> {
root = root || document;
const predicate = new Function('...args', predicateBody); const predicate = new Function('...args', predicateBody);
let timedOut = false; let timedOut = false;
if (timeout) setTimeout(() => (timedOut = true), timeout); if (timeout) setTimeout(() => (timedOut = true), timeout);
@ -906,7 +927,7 @@ async function waitForPredicatePageFunction(
* @returns {!Promise<*>} * @returns {!Promise<*>}
*/ */
async function pollMutation(): Promise<unknown> { async function pollMutation(): Promise<unknown> {
const success = await predicate(...args); const success = await predicate(root, ...args);
if (success) return Promise.resolve(success); if (success) return Promise.resolve(success);
let fulfill; let fulfill;
@ -916,13 +937,13 @@ async function waitForPredicatePageFunction(
observer.disconnect(); observer.disconnect();
fulfill(); fulfill();
} }
const success = await predicate(...args); const success = await predicate(root, ...args);
if (success) { if (success) {
observer.disconnect(); observer.disconnect();
fulfill(success); fulfill(success);
} }
}); });
observer.observe(document, { observer.observe(root, {
childList: true, childList: true,
subtree: true, subtree: true,
attributes: true, attributes: true,
@ -941,7 +962,7 @@ async function waitForPredicatePageFunction(
fulfill(); fulfill();
return; return;
} }
const success = await predicate(...args); const success = await predicate(root, ...args);
if (success) fulfill(success); if (success) fulfill(success);
else requestAnimationFrame(onRaf); else requestAnimationFrame(onRaf);
} }
@ -958,7 +979,7 @@ async function waitForPredicatePageFunction(
fulfill(); fulfill();
return; return;
} }
const success = await predicate(...args); const success = await predicate(root, ...args);
if (success) fulfill(success); if (success) fulfill(success);
else setTimeout(onTimeout, pollInterval); else setTimeout(onTimeout, pollInterval);
} }

View File

@ -351,6 +351,50 @@ export class ElementHandle<
this._frameManager = frameManager; this._frameManager = frameManager;
} }
/**
* Wait for the `selector` to appear within the element. If at the moment of calling the
* method the `selector` already exists, the method will return immediately. If
* the `selector` doesn't appear after the `timeout` milliseconds of waiting, the
* function will throw.
*
* This method does not work across navigations or if the element is detached from DOM.
*
* @param selector - A
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
* of an element to wait for
* @param options - Optional waiting parameters
* @returns Promise which resolves when element specified by selector string
* is added to DOM. Resolves to `null` if waiting for hidden: `true` and
* selector is not found in DOM.
* @remarks
* The optional parameters in `options` are:
*
* - `visible`: wait for the selected 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`: wait for the selected 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`: maximum time to wait 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.
*/
waitForSelector(
selector: string,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
} = {}
): Promise<ElementHandle | null> {
return this._context._world.waitForSelector(selector, {
...options,
root: this,
});
}
asElement(): ElementHandle<ElementType> | null { asElement(): ElementHandle<ElementType> | null {
return this; return this;
} }

View File

@ -257,6 +257,29 @@ describe('ElementHandle specs', function () {
}); });
}); });
describe('Element.waitForSelector', () => {
it('should wait correctly with waitForSelector on an element', async () => {
const { page } = getTestState();
const waitFor = page.waitForSelector('.foo');
// Set the page content after the waitFor has been started.
await page.setContent(
'<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
);
let element = await waitFor;
expect(element).toBeDefined();
const innerWaitFor = element.waitForSelector('.bar');
await element.evaluate((el) => {
el.innerHTML = '<div class="bar">bar1</div>';
});
element = await innerWaitFor;
expect(element).toBeDefined();
expect(
await element.evaluate((el: HTMLElement) => el.innerText)
).toStrictEqual('bar1');
});
});
describe('ElementHandle.hover', function () { describe('ElementHandle.hover', function () {
it('should work', async () => { it('should work', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -419,6 +442,33 @@ describe('ElementHandle specs', function () {
expect(element).toBeDefined(); expect(element).toBeDefined();
}); });
it('should wait correctly with waitForSelector on an element', async () => {
const { page, puppeteer } = getTestState();
puppeteer.registerCustomQueryHandler('getByClass', {
queryOne: (element, selector) => element.querySelector(`.${selector}`),
});
const waitFor = page.waitForSelector('getByClass/foo');
// Set the page content after the waitFor has been started.
await page.setContent(
'<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
);
let element = await waitFor;
expect(element).toBeDefined();
const innerWaitFor = element.waitForSelector('getByClass/bar');
await element.evaluate((el) => {
el.innerHTML = '<div class="bar">bar1</div>';
});
element = await innerWaitFor;
expect(element).toBeDefined();
expect(
await element.evaluate((el: HTMLElement) => el.innerText)
).toStrictEqual('bar1');
});
it('should wait correctly with waitFor', async () => { it('should wait correctly with waitFor', async () => {
/* page.waitFor is deprecated so we silence the warning to avoid test noise */ /* page.waitFor is deprecated so we silence the warning to avoid test noise */
sinon.stub(console, 'warn').callsFake(() => {}); sinon.stub(console, 'warn').callsFake(() => {});