api: introduce ElementHandle.waitForSelector (#3452)
This is similar to Frame.waitForSelector, but relative to the handle.
This commit is contained in:
parent
a03c761201
commit
85c93e91a7
23
docs/api.md
23
docs/api.md
|
|
@ -2748,6 +2748,7 @@ ElementHandle instances can be used as an argument in [`page.$eval()`](#pageeval
|
|||
- [elementHandle.toString()](#elementhandletostring)
|
||||
- [elementHandle.type(text[, options])](#elementhandletypetext-options)
|
||||
- [elementHandle.uncheck([options])](#elementhandleuncheckoptions)
|
||||
- [elementHandle.waitForSelector(selector[, options])](#elementhandlewaitforselectorselector-options)
|
||||
<!-- GEN:stop -->
|
||||
<!-- GEN:toc-extends-JSHandle -->
|
||||
- [jsHandle.asElement()](#jshandleaselement)
|
||||
|
|
@ -3110,6 +3111,28 @@ If the element is detached from the DOM at any moment during the action, this me
|
|||
|
||||
When all steps combined have not finished during the specified `timeout`, this method rejects with a [TimeoutError]. Passing zero timeout disables this.
|
||||
|
||||
#### elementHandle.waitForSelector(selector[, options])
|
||||
- `selector` <[string]> A selector of an element to wait for, relative to the element handle. See [working with selectors](#working-with-selectors) for more details.
|
||||
- `options` <[Object]>
|
||||
- `state` <"attached"|"detached"|"visible"|"hidden"> Defaults to `'visible'`. Can be either:
|
||||
- `'attached'` - wait for element to be present in DOM.
|
||||
- `'detached'` - wait for element to not be present in DOM.
|
||||
- `'visible'` - wait for element to have non-empty bounding box and no `visibility:hidden`. Note that element without any content or with `display:none` has an empty bounding box and is not considered visible.
|
||||
- `'hidden'` - wait for element to be either detached from DOM, or have an empty bounding box or `visibility:hidden`. This is opposite to the `'visible'` option.
|
||||
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
|
||||
- returns: <[Promise]<[null]|[ElementHandle]>> Promise that resolves when element specified by selector satisfies `state` option. Resolves to `null` if waiting for `hidden` or `detached`.
|
||||
|
||||
Wait for the `selector` relative to the element handle to satisfy `state` option (either appear/disappear from dom, or become visible/hidden). If at the moment of calling the method `selector` already satisfies the condition, the method will return immediately. If the selector doesn't satisfy the condition for the `timeout` milliseconds, the function will throw.
|
||||
|
||||
```js
|
||||
await page.setContent(`<div><span></span></div>`);
|
||||
const div = await page.$('div');
|
||||
// Waiting for the 'span' selector relative to the div.
|
||||
const span = await div.waitForSelector('span', { state: 'attached' });
|
||||
```
|
||||
|
||||
> **NOTE** This method works does not work across navigations, use [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) instead.
|
||||
|
||||
### class: JSHandle
|
||||
|
||||
JSHandle represents an in-page JavaScript object. JSHandles can be created with the [page.evaluateHandle](#pageevaluatehandlepagefunction-arg) method.
|
||||
|
|
|
|||
38
src/dom.ts
38
src/dom.ts
|
|
@ -637,6 +637,36 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
return result;
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
|
||||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
const info = selectors._parseSelector(selector);
|
||||
const task = waitForSelectorTask(info, state, this);
|
||||
return this._page._runAbortableTask(async progress => {
|
||||
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
const context = await this._context.frame._context(info.world);
|
||||
const injected = await context.injectedScript();
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, await task(injected));
|
||||
const result = await pollHandler.finishHandle();
|
||||
if (!result.asElement()) {
|
||||
result.dispose();
|
||||
return null;
|
||||
}
|
||||
const handle = result.asElement() as ElementHandle<Element>;
|
||||
return handle._adoptTo(await this._context.frame._mainContext());
|
||||
}, this._page._timeoutSettings.timeout(options), 'elementHandle.waitForSelector');
|
||||
}
|
||||
|
||||
async _adoptTo(context: FrameExecutionContext): Promise<ElementHandle<T>> {
|
||||
if (this._context !== context) {
|
||||
const adopted = await this._page._delegate.adoptElementHandle(this, context);
|
||||
this.dispose();
|
||||
return adopted;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
async _waitForDisplayedAtStablePosition(progress: Progress, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> {
|
||||
if (waitForEnabled)
|
||||
progress.logger.info(` waiting for element to be visible, enabled and not moving`);
|
||||
|
|
@ -782,12 +812,12 @@ function roundPoint(point: types.Point): types.Point {
|
|||
|
||||
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<types.InjectedScriptPoll<T>>>;
|
||||
|
||||
export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden'): SchedulableTask<Element | undefined> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, state }) => {
|
||||
export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden', root?: ElementHandle): SchedulableTask<Element | undefined> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, state, root }) => {
|
||||
let lastElement: Element | undefined;
|
||||
|
||||
return injected.pollRaf((progress, continuePolling) => {
|
||||
const element = injected.querySelector(parsed, document);
|
||||
const element = injected.querySelector(parsed, root || document);
|
||||
const visible = element ? injected.isVisible(element) : false;
|
||||
|
||||
if (lastElement !== element) {
|
||||
|
|
@ -809,7 +839,7 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' |
|
|||
return !visible ? undefined : continuePolling;
|
||||
}
|
||||
});
|
||||
}, { parsed: selector.parsed, state });
|
||||
}, { parsed: selector.parsed, state, root });
|
||||
}
|
||||
|
||||
export function dispatchEventTask(selector: SelectorInfo, type: string, eventInit: Object): SchedulableTask<undefined> {
|
||||
|
|
|
|||
|
|
@ -577,13 +577,7 @@ export class Frame {
|
|||
return null;
|
||||
}
|
||||
const handle = result.asElement() as dom.ElementHandle<Element>;
|
||||
const mainContext = await this._mainContext();
|
||||
if (handle && handle._context !== mainContext) {
|
||||
const adopted = await this._page._delegate.adoptElementHandle(handle, mainContext);
|
||||
handle.dispose();
|
||||
return adopted;
|
||||
}
|
||||
return handle;
|
||||
return handle._adoptTo(await this._mainContext());
|
||||
}, this._page._timeoutSettings.timeout(options), this._apiName('waitForSelector'));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1620,6 +1620,7 @@ export interface ElementHandleChannel extends JSHandleChannel {
|
|||
textContent(params?: ElementHandleTextContentParams): Promise<ElementHandleTextContentResult>;
|
||||
type(params: ElementHandleTypeParams): Promise<ElementHandleTypeResult>;
|
||||
uncheck(params: ElementHandleUncheckParams): Promise<ElementHandleUncheckResult>;
|
||||
waitForSelector(params: ElementHandleWaitForSelectorParams): Promise<ElementHandleWaitForSelectorResult>;
|
||||
}
|
||||
export type ElementHandleEvalOnSelectorParams = {
|
||||
selector: string,
|
||||
|
|
@ -1913,6 +1914,18 @@ export type ElementHandleUncheckOptions = {
|
|||
timeout?: number,
|
||||
};
|
||||
export type ElementHandleUncheckResult = void;
|
||||
export type ElementHandleWaitForSelectorParams = {
|
||||
selector: string,
|
||||
timeout?: number,
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden',
|
||||
};
|
||||
export type ElementHandleWaitForSelectorOptions = {
|
||||
timeout?: number,
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden',
|
||||
};
|
||||
export type ElementHandleWaitForSelectorResult = {
|
||||
element?: ElementHandleChannel,
|
||||
};
|
||||
|
||||
// ----------- Request -----------
|
||||
export type RequestInitializer = {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ElementHandleChannel, JSHandleInitializer, ElementHandleScrollIntoViewIfNeededOptions, ElementHandleHoverOptions, ElementHandleClickOptions, ElementHandleDblclickOptions, ElementHandleFillOptions, ElementHandleSetInputFilesOptions, ElementHandlePressOptions, ElementHandleCheckOptions, ElementHandleUncheckOptions, ElementHandleScreenshotOptions, ElementHandleTypeOptions, ElementHandleSelectTextOptions } from '../channels';
|
||||
import { ElementHandleChannel, JSHandleInitializer, ElementHandleScrollIntoViewIfNeededOptions, ElementHandleHoverOptions, ElementHandleClickOptions, ElementHandleDblclickOptions, ElementHandleFillOptions, ElementHandleSetInputFilesOptions, ElementHandlePressOptions, ElementHandleCheckOptions, ElementHandleUncheckOptions, ElementHandleScreenshotOptions, ElementHandleTypeOptions, ElementHandleSelectTextOptions, ElementHandleWaitForSelectorOptions } from '../channels';
|
||||
import { Frame } from './frame';
|
||||
import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
|
@ -208,6 +208,13 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
|
|||
return parseResult(result.value);
|
||||
});
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options: ElementHandleWaitForSelectorOptions = {}): Promise<ElementHandle<Element> | null> {
|
||||
return this._wrapApiCall('elementHandle.waitForSelector', async () => {
|
||||
const result = await this._elementChannel.waitForSelector({ selector, ...options });
|
||||
return ElementHandle.fromNullable(result.element) as ElementHandle<Element> | null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function convertSelectOptionValues(values: string | ElementHandle | SelectOption | string[] | ElementHandle[] | SelectOption[] | null): { elements?: ElementHandleChannel[], options?: SelectOption[] } {
|
||||
|
|
|
|||
|
|
@ -1577,6 +1577,19 @@ ElementHandle:
|
|||
noWaitAfter: boolean?
|
||||
timeout: number?
|
||||
|
||||
waitForSelector:
|
||||
parameters:
|
||||
selector: string
|
||||
timeout: number?
|
||||
state:
|
||||
type: enum?
|
||||
literals:
|
||||
- attached
|
||||
- detached
|
||||
- visible
|
||||
- hidden
|
||||
returns:
|
||||
element: ElementHandle?
|
||||
|
||||
|
||||
Request:
|
||||
|
|
|
|||
|
|
@ -148,6 +148,10 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
|
|||
async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
|
||||
return { value: serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
}
|
||||
|
||||
async waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element?: ElementHandleChannel }> {
|
||||
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._elementHandle.waitForSelector(params.selector, params)) };
|
||||
}
|
||||
}
|
||||
|
||||
export function convertSelectOptionValues(elements?: ElementHandleChannel[], options?: types.SelectOption[]): string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null {
|
||||
|
|
|
|||
|
|
@ -759,6 +759,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
noWaitAfter: tOptional(tBoolean),
|
||||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.ElementHandleWaitForSelectorParams = tObject({
|
||||
selector: tString,
|
||||
timeout: tOptional(tNumber),
|
||||
state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])),
|
||||
});
|
||||
scheme.RequestResponseParams = tOptional(tObject({}));
|
||||
scheme.RouteAbortParams = tObject({
|
||||
errorCode: tOptional(tString),
|
||||
|
|
|
|||
|
|
@ -47,6 +47,41 @@ it('should immediately resolve promise if node exists', async({page, server}) =>
|
|||
await frame.waitForSelector('div', { state: 'attached'});
|
||||
});
|
||||
|
||||
it('elementHandle.waitForSelector should immediately resolve if node exists', async({page}) => {
|
||||
await page.setContent(`<span>extra</span><div><span>target</span></div>`);
|
||||
const div = await page.$('div');
|
||||
const span = await div.waitForSelector('span', { state: 'attached' });
|
||||
expect(await span.evaluate(e => e.textContent)).toBe('target');
|
||||
});
|
||||
|
||||
it('elementHandle.waitForSelector should wait', async({page}) => {
|
||||
await page.setContent(`<div></div>`);
|
||||
const div = await page.$('div');
|
||||
const promise = div.waitForSelector('span', { state: 'attached' });
|
||||
await div.evaluate(div => div.innerHTML = '<span>target</span>');
|
||||
const span = await promise;
|
||||
expect(await span.evaluate(e => e.textContent)).toBe('target');
|
||||
});
|
||||
|
||||
it('elementHandle.waitForSelector should timeout', async({page}) => {
|
||||
await page.setContent(`<div></div>`);
|
||||
const div = await page.$('div');
|
||||
const error = await div.waitForSelector('span', { timeout: 100 }).catch(e => e);
|
||||
expect(error.message).toContain('Timeout 100ms exceeded.');
|
||||
});
|
||||
|
||||
it('elementHandle.waitForSelector should throw on navigation', async({page, server}) => {
|
||||
await page.setContent(`<div></div>`);
|
||||
const div = await page.$('div');
|
||||
const promise = div.waitForSelector('span').catch(e => e);
|
||||
// Give it some time before navigating.
|
||||
for (let i = 0; i < 10; i++)
|
||||
await page.evaluate(() => 1);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const error = await promise;
|
||||
expect(error.message).toContain('Execution context was destroyed, most likely because of a navigation');
|
||||
});
|
||||
|
||||
it('should work with removed MutationObserver', async({page, server}) => {
|
||||
await page.evaluate(() => delete window.MutationObserver);
|
||||
const [handle] = await Promise.all([
|
||||
|
|
@ -158,7 +193,7 @@ it('should work when node is added through innerHTML', async({page, server}) =>
|
|||
await watchdog;
|
||||
});
|
||||
|
||||
it('Page.$ waitFor is shortcut for main frame', async({page, server}) => {
|
||||
it('page.waitForSelector is shortcut for main frame', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
const otherFrame = page.frames()[1];
|
||||
|
|
|
|||
20
utils/generate_types/overrides.d.ts
vendored
20
utils/generate_types/overrides.d.ts
vendored
|
|
@ -46,9 +46,12 @@ type ElementHandleForTag<K extends keyof HTMLElementTagNameMap> = ElementHandle<
|
|||
type HTMLOrSVGElement = SVGElement | HTMLElement;
|
||||
type HTMLOrSVGElementHandle = ElementHandle<HTMLOrSVGElement>;
|
||||
|
||||
type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
||||
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
|
||||
state: 'visible'|'attached';
|
||||
}
|
||||
};
|
||||
type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelectorOptions & {
|
||||
state: 'visible'|'attached';
|
||||
};
|
||||
|
||||
export interface Page {
|
||||
evaluate<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg): Promise<R>;
|
||||
|
|
@ -76,8 +79,8 @@ export interface Page {
|
|||
waitForFunction<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg, options?: PageWaitForFunctionOptions): Promise<SmartHandle<R>>;
|
||||
waitForFunction<R>(pageFunction: PageFunction<void, R>, arg?: any, options?: PageWaitForFunctionOptions): Promise<SmartHandle<R>>;
|
||||
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options?: WaitForSelectorOptionsNotHidden): Promise<ElementHandleForTag<K>>;
|
||||
waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise<HTMLOrSVGElementHandle>;
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options?: PageWaitForSelectorOptionsNotHidden): Promise<ElementHandleForTag<K>>;
|
||||
waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise<HTMLOrSVGElementHandle>;
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options: PageWaitForSelectorOptions): Promise<ElementHandleForTag<K> | null>;
|
||||
waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise<null|HTMLOrSVGElementHandle>;
|
||||
}
|
||||
|
|
@ -108,8 +111,8 @@ export interface Frame {
|
|||
waitForFunction<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg, options?: PageWaitForFunctionOptions): Promise<SmartHandle<R>>;
|
||||
waitForFunction<R>(pageFunction: PageFunction<void, R>, arg?: any, options?: PageWaitForFunctionOptions): Promise<SmartHandle<R>>;
|
||||
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options?: WaitForSelectorOptionsNotHidden): Promise<ElementHandleForTag<K>>;
|
||||
waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise<HTMLOrSVGElementHandle>;
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options?: PageWaitForSelectorOptionsNotHidden): Promise<ElementHandleForTag<K>>;
|
||||
waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise<HTMLOrSVGElementHandle>;
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options: PageWaitForSelectorOptions): Promise<ElementHandleForTag<K> | null>;
|
||||
waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise<null|HTMLOrSVGElementHandle>;
|
||||
}
|
||||
|
|
@ -149,6 +152,11 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
|
|||
$$eval<R, Arg, E extends HTMLOrSVGElement = HTMLOrSVGElement>(selector: string, pageFunction: PageFunctionOn<E[], Arg, R>, arg: Arg): Promise<R>;
|
||||
$$eval<K extends keyof HTMLElementTagNameMap, R>(selector: K, pageFunction: PageFunctionOn<HTMLElementTagNameMap[K][], void, R>, arg?: any): Promise<R>;
|
||||
$$eval<R, E extends HTMLOrSVGElement = HTMLOrSVGElement>(selector: string, pageFunction: PageFunctionOn<E[], void, R>, arg?: any): Promise<R>;
|
||||
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options?: ElementHandleWaitForSelectorOptionsNotHidden): Promise<ElementHandleForTag<K>>;
|
||||
waitForSelector(selector: string, options?: ElementHandleWaitForSelectorOptionsNotHidden): Promise<HTMLOrSVGElementHandle>;
|
||||
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options: ElementHandleWaitForSelectorOptions): Promise<ElementHandleForTag<K> | null>;
|
||||
waitForSelector(selector: string, options: ElementHandleWaitForSelectorOptions): Promise<null|HTMLOrSVGElementHandle>;
|
||||
}
|
||||
|
||||
export interface BrowserType<Browser> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue