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

View File

@ -351,6 +351,50 @@ export class ElementHandle<
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 {
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 () {
it('should work', async () => {
const { page, server } = getTestState();
@ -419,6 +442,33 @@ describe('ElementHandle specs', function () {
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 () => {
/* page.waitFor is deprecated so we silence the warning to avoid test noise */
sinon.stub(console, 'warn').callsFake(() => {});