diff --git a/docs/api.md b/docs/api.md
index 6292c46f51..4ebe20e65c 100644
--- a/docs/api.md
+++ b/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)
- [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(`
`);
+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.
diff --git a/src/dom.ts b/src/dom.ts
index 82528a92af..1f2c2d34ae 100644
--- a/src/dom.ts
+++ b/src/dom.ts
@@ -637,6 +637,36 @@ export class ElementHandle extends js.JSHandle {
return result;
}
+ async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise | 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;
+ return handle._adoptTo(await this._context.frame._mainContext());
+ }, this._page._timeoutSettings.timeout(options), 'elementHandle.waitForSelector');
+ }
+
+ async _adoptTo(context: FrameExecutionContext): Promise> {
+ 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 = (injectedScript: js.JSHandle) => Promise>>;
-export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden'): SchedulableTask {
- return injectedScript => injectedScript.evaluateHandle((injected, { parsed, state }) => {
+export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | 'detached' | 'visible' | 'hidden', root?: ElementHandle): SchedulableTask {
+ 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 {
diff --git a/src/frames.ts b/src/frames.ts
index fa5cdf3c58..2c88073fbc 100644
--- a/src/frames.ts
+++ b/src/frames.ts
@@ -577,13 +577,7 @@ export class Frame {
return null;
}
const handle = result.asElement() as dom.ElementHandle;
- 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'));
}
diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts
index 47095bdd3f..81069c5521 100644
--- a/src/rpc/channels.ts
+++ b/src/rpc/channels.ts
@@ -1620,6 +1620,7 @@ export interface ElementHandleChannel extends JSHandleChannel {
textContent(params?: ElementHandleTextContentParams): Promise;
type(params: ElementHandleTypeParams): Promise;
uncheck(params: ElementHandleUncheckParams): Promise;
+ waitForSelector(params: ElementHandleWaitForSelectorParams): Promise;
}
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 = {
diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts
index a6ed20c038..43856a16b3 100644
--- a/src/rpc/client/elementHandle.ts
+++ b/src/rpc/client/elementHandle.ts
@@ -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 extends JSHandle {
return parseResult(result.value);
});
}
+
+ async waitForSelector(selector: string, options: ElementHandleWaitForSelectorOptions = {}): Promise | null> {
+ return this._wrapApiCall('elementHandle.waitForSelector', async () => {
+ const result = await this._elementChannel.waitForSelector({ selector, ...options });
+ return ElementHandle.fromNullable(result.element) as ElementHandle | null;
+ });
+ }
}
export function convertSelectOptionValues(values: string | ElementHandle | SelectOption | string[] | ElementHandle[] | SelectOption[] | null): { elements?: ElementHandleChannel[], options?: SelectOption[] } {
diff --git a/src/rpc/protocol.yml b/src/rpc/protocol.yml
index 1a25f6cef2..ffc24f2345 100644
--- a/src/rpc/protocol.yml
+++ b/src/rpc/protocol.yml
@@ -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:
diff --git a/src/rpc/server/elementHandlerDispatcher.ts b/src/rpc/server/elementHandlerDispatcher.ts
index 9a61f8e073..a13f62344f 100644
--- a/src/rpc/server/elementHandlerDispatcher.ts
+++ b/src/rpc/server/elementHandlerDispatcher.ts
@@ -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 {
diff --git a/src/rpc/validator.ts b/src/rpc/validator.ts
index 1815ac1683..47c7c7266b 100644
--- a/src/rpc/validator.ts
+++ b/src/rpc/validator.ts
@@ -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),
diff --git a/test/wait-for-selector.spec.ts b/test/wait-for-selector.spec.ts
index d6f2d56e5a..be7c6d1ce9 100644
--- a/test/wait-for-selector.spec.ts
+++ b/test/wait-for-selector.spec.ts
@@ -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(`extratarget
`);
+ 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(``);
+ const div = await page.$('div');
+ const promise = div.waitForSelector('span', { state: 'attached' });
+ await div.evaluate(div => div.innerHTML = 'target');
+ const span = await promise;
+ expect(await span.evaluate(e => e.textContent)).toBe('target');
+});
+
+it('elementHandle.waitForSelector should timeout', async({page}) => {
+ await page.setContent(``);
+ 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(``);
+ 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];
diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts
index af675ce796..490a9c8445 100644
--- a/utils/generate_types/overrides.d.ts
+++ b/utils/generate_types/overrides.d.ts
@@ -46,9 +46,12 @@ type ElementHandleForTag = ElementHandle<
type HTMLOrSVGElement = SVGElement | HTMLElement;
type HTMLOrSVGElementHandle = ElementHandle;
-type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
+type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
state: 'visible'|'attached';
-}
+};
+type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelectorOptions & {
+ state: 'visible'|'attached';
+};
export interface Page {
evaluate(pageFunction: PageFunction, arg: Arg): Promise;
@@ -76,8 +79,8 @@ export interface Page {
waitForFunction(pageFunction: PageFunction, arg: Arg, options?: PageWaitForFunctionOptions): Promise>;
waitForFunction(pageFunction: PageFunction, arg?: any, options?: PageWaitForFunctionOptions): Promise>;
- waitForSelector(selector: K, options?: WaitForSelectorOptionsNotHidden): Promise>;
- waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise;
+ waitForSelector(selector: K, options?: PageWaitForSelectorOptionsNotHidden): Promise>;
+ waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise;
waitForSelector(selector: K, options: PageWaitForSelectorOptions): Promise | null>;
waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise;
}
@@ -108,8 +111,8 @@ export interface Frame {
waitForFunction(pageFunction: PageFunction, arg: Arg, options?: PageWaitForFunctionOptions): Promise>;
waitForFunction(pageFunction: PageFunction, arg?: any, options?: PageWaitForFunctionOptions): Promise>;
- waitForSelector(selector: K, options?: WaitForSelectorOptionsNotHidden): Promise>;
- waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise;
+ waitForSelector(selector: K, options?: PageWaitForSelectorOptionsNotHidden): Promise>;
+ waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise;
waitForSelector(selector: K, options: PageWaitForSelectorOptions): Promise | null>;
waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise;
}
@@ -149,6 +152,11 @@ export interface ElementHandle extends JSHandle {
$$eval(selector: string, pageFunction: PageFunctionOn, arg: Arg): Promise;
$$eval(selector: K, pageFunction: PageFunctionOn, arg?: any): Promise;
$$eval(selector: string, pageFunction: PageFunctionOn, arg?: any): Promise;
+
+ waitForSelector(selector: K, options?: ElementHandleWaitForSelectorOptionsNotHidden): Promise>;
+ waitForSelector(selector: string, options?: ElementHandleWaitForSelectorOptionsNotHidden): Promise;
+ waitForSelector(selector: K, options: ElementHandleWaitForSelectorOptions): Promise | null>;
+ waitForSelector(selector: string, options: ElementHandleWaitForSelectorOptions): Promise;
}
export interface BrowserType {