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:
parent
a604646f44
commit
c03429444d
14
docs/api.md
14
docs/api.md
@ -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 doesn’t 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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(() => {});
|
||||
|
Loading…
Reference in New Issue
Block a user