Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: waitForElement accepts waitFor attached|detached|visible|hidden #1244

Merged
merged 1 commit into from
Mar 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 67 additions & 112 deletions docs/api.md

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
return super._createHandle(remoteObject);
}

_injected(): Promise<js.JSHandle> {
_injected(): Promise<js.JSHandle<Injected>> {
const selectors = Selectors._instance();
if (this._injectedPromise && selectors._generation !== this._injectedGeneration) {
this._injectedPromise.then(handle => handle.dispose());
Expand Down Expand Up @@ -456,17 +456,22 @@ export function waitForFunctionTask(selector: string | undefined, pageFunction:
}, await context._injected(), selector, predicateBody, polling, options.timeout || 0, ...args);
}

export function waitForSelectorTask(selector: string, visibility: types.Visibility, timeout: number): Task {
return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => {
const polling = visibility === 'any' ? 'mutation' : 'raf';
export function waitForSelectorTask(selector: string, waitFor: 'attached' | 'detached' | 'visible' | 'hidden', timeout: number): Task {
return async (context: FrameExecutionContext) => context.evaluateHandle((injected, selector, waitFor, timeout) => {
const polling = (waitFor === 'attached' || waitFor === 'detached') ? 'mutation' : 'raf';
return injected.poll(polling, selector, timeout, (element: Element | undefined): Element | boolean => {
if (!element)
return visibility === 'hidden';
if (visibility === 'any')
return element;
return injected.isVisible(element) === (visibility === 'visible') ? element : false;
switch (waitFor) {
case 'attached':
return element || false;
case 'detached':
return !element;
case 'visible':
return element && injected.isVisible(element) ? element : false;
case 'hidden':
return !element || !injected.isVisible(element);
}
});
}, await context._injected(), selector, visibility, timeout);
}, await context._injected(), selector, waitFor, timeout);
}

export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => {
Expand Down
42 changes: 18 additions & 24 deletions src/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,10 @@ export class Frame {
return handle;
}

async waitForSelector(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise<dom.ElementHandle<Element> | null> {
const { timeout = this._page._timeoutSettings.timeout(), visibility = 'any' } = (options || {});
const handle = await this._waitForSelectorInUtilityContext(selector, visibility, timeout);
async waitForElement(selector: string, options?: types.WaitForElementOptions): Promise<dom.ElementHandle<Element> | null> {
if (options && (options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.waitFor?');
const handle = await this._waitForSelectorInUtilityContext(selector, options);
const mainContext = await this._mainContext();
if (handle && handle._context !== mainContext) {
const adopted = this._page._delegate.adoptElementHandle(handle, mainContext);
Expand All @@ -601,10 +602,6 @@ export class Frame {
return handle;
}

async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise<dom.ElementHandle<Element> | null> {
return this.waitForSelector(selector, options);
}

$eval: types.$Eval = async (selector, pageFunction, ...args) => {
const context = await this._mainContext();
const elementHandle = await context._$(selector);
Expand Down Expand Up @@ -875,9 +872,9 @@ export class Frame {
handle.dispose();
}

async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: types.WaitForFunctionOptions & { visibility?: types.Visibility } = {}, ...args: any[]): Promise<js.JSHandle | null> {
async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: types.WaitForFunctionOptions & types.WaitForElementOptions = {}, ...args: any[]): Promise<js.JSHandle | null> {
if (helper.isString(selectorOrFunctionOrTimeout))
return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any;
return this.waitForElement(selectorOrFunctionOrTimeout, options) as any;
if (helper.isNumber(selectorOrFunctionOrTimeout))
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
if (typeof selectorOrFunctionOrTimeout === 'function')
Expand All @@ -891,9 +888,9 @@ export class Frame {
throw new Error('waitFor option should be a boolean, got "' + (typeof waitFor) + '"');
let handle: dom.ElementHandle<Element>;
if (waitFor) {
const maybeHandle = await this._waitForSelectorInUtilityContext(selector, 'any', timeout);
const maybeHandle = await this._waitForSelectorInUtilityContext(selector, { timeout, waitFor: 'attached' });
if (!maybeHandle)
throw new Error('No node found for selector: ' + selectorToString(selector, 'any'));
throw new Error('No node found for selector: ' + selectorToString(selector, 'attached'));
handle = maybeHandle;
} else {
const context = await this._context('utility');
Expand All @@ -904,14 +901,12 @@ export class Frame {
return handle;
}

private async _waitForSelectorInUtilityContext(selector: string, waitFor: types.Visibility, timeout: number): Promise<dom.ElementHandle<Element> | null> {
let visibility: types.Visibility = 'any';
if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any')
visibility = waitFor;
else
throw new Error(`Unsupported visibility option "${waitFor}"`);
const task = dom.waitForSelectorTask(selector, visibility, timeout);
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, visibility)}"`);
private async _waitForSelectorInUtilityContext(selector: string, options?: types.WaitForElementOptions): Promise<dom.ElementHandle<Element> | null> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'attached' } = (options || {});
if (!['attached', 'detached', 'visible', 'hidden'].includes(waitFor))
throw new Error(`Unsupported waitFor option "${waitFor}"`);
const task = dom.waitForSelectorTask(selector, waitFor, timeout);
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, waitFor)}"`);
if (!result.asElement()) {
result.dispose();
return null;
Expand Down Expand Up @@ -1095,14 +1090,13 @@ function createTimeoutPromise(timeout: number): Disposable<Promise<TimeoutError>
};
}

function selectorToString(selector: string, visibility: types.Visibility): string {
function selectorToString(selector: string, waitFor: 'attached' | 'detached' | 'visible' | 'hidden'): string {
let label;
switch (visibility) {
switch (waitFor) {
case 'visible': label = '[visible] '; break;
case 'hidden': label = '[hidden] '; break;
case 'any':
case undefined:
label = ''; break;
case 'attached': label = ''; break;
case 'detached': label = '[detached]'; break;
}
return `${label}${selector}`;
}
Expand Down
10 changes: 3 additions & 7 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,8 @@ export class Page extends platform.EventEmitter {
return this.mainFrame().$(selector);
}

async waitForSelector(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().waitForSelector(selector, options);
}

async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$wait(selector, options);
async waitForElement(selector: string, options?: types.WaitForElementOptions): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().waitForElement(selector, options);
}

evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => {
Expand Down Expand Up @@ -483,7 +479,7 @@ export class Page extends platform.EventEmitter {
return this.mainFrame().uncheck(selector, options);
}

async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options?: types.WaitForFunctionOptions & { visibility?: types.Visibility }, ...args: any[]): Promise<js.JSHandle | null> {
async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options?: types.WaitForFunctionOptions & types.WaitForElementOptions, ...args: any[]): Promise<js.JSHandle | null> {
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}

Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type Quad = [ Point, Point, Point, Point ];
export type TimeoutOptions = { timeout?: number };
export type WaitForOptions = TimeoutOptions & { waitFor?: boolean };

export type Visibility = 'visible' | 'hidden' | 'any';
export type WaitForElementOptions = TimeoutOptions & { waitFor?: 'attached' | 'detached' | 'visible' | 'hidden' };

export type Polling = 'raf' | 'mutation' | number;
export type WaitForFunctionOptions = TimeoutOptions & { polling?: Polling };
Expand Down
2 changes: 1 addition & 1 deletion test/chromium/oopif.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
document.body.appendChild(frame);
return new Promise(x => frame.onload = x);
});
await page.waitForSelector('iframe[src="https://google.com/"]');
await page.waitForElement('iframe[src="https://google.com/"]');
const urls = page.frames().map(frame => frame.url()).sort();
expect(urls).toEqual([
server.EMPTY_PAGE,
Expand Down
8 changes: 4 additions & 4 deletions test/launcher.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,15 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
expect(error.message).toContain('Navigation failed because browser has disconnected!');
await browserServer.close();
});
it('should reject waitForSelector when browser closes', async({server}) => {
it('should reject waitForElement when browser closes', async({server}) => {
server.setRoute('/empty.html', () => {});
const browserServer = await playwright.launchServer({...defaultBrowserOptions });
const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
const page = await remote.newPage();
const watchdog = page.waitForSelector('div', { timeout: 60000 }).catch(e => e);
const watchdog = page.waitForElement('div', { timeout: 60000 }).catch(e => e);

// Make sure the previous waitForSelector has time to make it to the browser before we disconnect.
await page.waitForSelector('body');
// Make sure the previous waitForElement has time to make it to the browser before we disconnect.
await page.waitForElement('body');

await remote.close();
const error = await watchdog;
Expand Down
11 changes: 0 additions & 11 deletions test/queryselector.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,6 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
const element = await page.$('css=section >> css=div');
expect(element).toBeTruthy();
});
it('should respect waitFor visibility', async({page, server}) => {
await page.setContent('<section id="testAttribute">43543</section>');
expect(await page.waitForSelector('css=section', { waitFor: 'visible'})).toBeTruthy();
expect(await page.waitForSelector('css=section', { waitFor: 'any'})).toBeTruthy();
expect(await page.waitForSelector('css=section')).toBeTruthy();

await page.setContent('<section id="testAttribute" style="display: none">43543</section>');
expect(await page.waitForSelector('css=section', { waitFor: 'hidden'})).toBeTruthy();
expect(await page.waitForSelector('css=section', { waitFor: 'any'})).toBeTruthy();
expect(await page.waitForSelector('css=section')).toBeTruthy();
});
});

describe('Page.$$', function() {
Expand Down
Loading