feature(waitfor): waitFor visible or any by default (#284)

This commit is contained in:
Dmitry Gozman 2019-12-18 14:28:16 -08:00 committed by GitHub
parent 1c3ff0bd52
commit 9afd35d3a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 143 additions and 111 deletions

View File

@ -32,7 +32,7 @@
* [browser.browserContexts()](#browserbrowsercontexts)
* [browser.chromium](#browserchromium)
* [browser.close()](#browserclose)
* [browser.defaultContext()](#browserdefaultContext())
* [browser.defaultContext()](#browserdefaultcontext)
* [browser.disconnect()](#browserdisconnect)
* [browser.isConnected()](#browserisconnected)
* [browser.newContext(options)](#browsernewcontextoptions)

View File

@ -26,7 +26,6 @@ import { Target } from './Target';
import { Protocol } from './protocol';
import { Chromium } from './features/chromium';
import { FrameManager } from './FrameManager';
import * as events from '../events';
import * as network from '../network';
import { Permissions } from './features/permissions';
import { Overrides } from './features/overrides';

View File

@ -22,7 +22,6 @@ import { Events } from './events';
import { Events as CommonEvents } from '../events';
import { Permissions } from './features/permissions';
import { Page } from '../page';
import * as types from '../types';
import { FrameManager } from './FrameManager';
import { Firefox } from './features/firefox';
import * as network from '../network';

View File

@ -53,7 +53,7 @@ export type GotoResult = {
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean | 'visible' | 'hidden' | 'any' };
export type WaitForOptions = types.TimeoutOptions & { waitFor?: types.Visibility | 'nowait' };
export class FrameManager {
private _page: Page;
@ -364,8 +364,30 @@ export class Frame {
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string, options?: WaitForOptions): Promise<dom.ElementHandle<Element> | null> {
return this._optionallyWaitForSelector('main', selector, options, true /* returnNull */);
async $(selector: string): Promise<dom.ElementHandle<Element> | null> {
const utilityContext = await this._utilityContext();
const mainContext = await this._mainContext();
const handle = await utilityContext._$(selector);
if (handle && handle._context !== mainContext) {
const adopted = this._page._delegate.adoptElementHandle(handle, mainContext);
await handle.dispose();
return adopted;
}
return handle;
}
async waitForSelector(selector: string, options?: types.TimeoutOptions & { waitFor?: types.Visibility }): Promise<dom.ElementHandle<Element> | null> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'any' } = (options || {});
if ((waitFor as any) === 'nowait')
throw new Error('waitForSelector does not support "nowait"');
const handle = await this._waitForSelectorInUtilityContext(selector, waitFor as types.Visibility, timeout);
const mainContext = await this._mainContext();
if (handle && handle._context !== mainContext) {
const adopted = this._page._delegate.adoptElementHandle(handle, mainContext);
await handle.dispose();
return adopted;
}
return handle;
}
async $x(expression: string): Promise<dom.ElementHandle<Element>[]> {
@ -587,43 +609,43 @@ export class Frame {
}
async click(selector: string, options?: WaitForOptions & ClickOptions) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.click(options);
await handle.dispose();
}
async dblclick(selector: string, options?: WaitForOptions & MultiClickOptions) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.dblclick(options);
await handle.dispose();
}
async tripleclick(selector: string, options?: WaitForOptions & MultiClickOptions) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.tripleclick(options);
await handle.dispose();
}
async fill(selector: string, value: string, options?: WaitForOptions) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.fill(value);
await handle.dispose();
}
async focus(selector: string, options?: WaitForOptions) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.focus();
await handle.dispose();
}
async hover(selector: string, options?: WaitForOptions & PointerActionOptions) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.hover(options);
await handle.dispose();
}
async select(selector: string, value: string | dom.ElementHandle | SelectOption | string[] | dom.ElementHandle[] | SelectOption[] | undefined, options?: WaitForOptions): Promise<string[]> {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'any');
const toDispose: Promise<dom.ElementHandle>[] = [];
const values = value === undefined ? [] : Array.isArray(value) ? value : [value];
const context = await this._utilityContext();
@ -642,14 +664,14 @@ export class Frame {
}
async type(selector: string, text: string, options?: WaitForOptions & { delay?: number }) {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options, 'visible');
await handle.type(text, options);
await handle.dispose();
}
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle | null> {
if (helper.isString(selectorOrFunctionOrTimeout))
return this.$(selectorOrFunctionOrTimeout as string, { waitFor: true, ...options }) as any;
return this.waitForSelector(selectorOrFunctionOrTimeout as string, options) as any;
if (helper.isNumber(selectorOrFunctionOrTimeout))
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout as number));
if (typeof selectorOrFunctionOrTimeout === 'function')
@ -657,35 +679,36 @@ export class Frame {
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
}
private async _optionallyWaitForSelector(contextType: ContextType, selector: string, options: WaitForOptions = {}, returnNull?: boolean): Promise<dom.ElementHandle<Element> | null> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = undefined } = options;
private async _optionallyWaitForSelectorInUtilityContext(selector: string, options: WaitForOptions | undefined, defaultWaitFor: types.Visibility): Promise<dom.ElementHandle<Element> | null> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = defaultWaitFor } = (options || {});
let handle: dom.ElementHandle<Element> | null;
if (waitFor) {
let visibility: types.Visibility = 'any';
if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any')
visibility = waitFor;
else if (waitFor === true)
visibility = 'any';
else
throw new Error(`Unsupported waitFor option "${waitFor}"`);
const task = dom.waitForSelectorTask(selector, visibility, timeout);
const result = await this._scheduleRerunnableTask(task, contextType, timeout, `selector "${selectorToString(selector, visibility)}"`);
if (!result.asElement()) {
await result.dispose();
if (returnNull)
return null;
throw new Error('No node found for selector: ' + selectorToString(selector, visibility));
}
handle = result.asElement() as dom.ElementHandle<Element>;
if (waitFor !== 'nowait') {
handle = await this._waitForSelectorInUtilityContext(selector, waitFor, timeout);
if (!handle)
throw new Error('No node found for selector: ' + selectorToString(selector, waitFor));
} else {
const context = await this._context(contextType);
const context = await this._context('utility');
handle = await context._$(selector);
if (!returnNull)
assert(handle, 'No node found for selector: ' + selector);
assert(handle, 'No node found for selector: ' + selector);
}
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 waitFor option "${waitFor}"`);
const task = dom.waitForSelectorTask(selector, visibility, timeout);
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, visibility)}"`);
if (!result.asElement()) {
await result.dispose();
return null;
}
return result.asElement() as dom.ElementHandle<Element>;
}
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
options = { timeout: this._page._timeoutSettings.timeout(), ...options };
const task = dom.waitForFunctionTask(pageFunction, options, ...args);

View File

@ -162,8 +162,12 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string, options?: frames.WaitForOptions): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$(selector, options);
async $(selector: string): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$(selector);
}
async waitForSelector(selector: string, options?: types.TimeoutOptions & { waitFor?: types.Visibility }): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().waitForSelector(selector, options);
}
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string> {

View File

@ -19,7 +19,6 @@ import * as childProcess from 'child_process';
import { EventEmitter } from 'events';
import { helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network';
import * as types from '../types';
import { Connection, ConnectionEvents, TargetSession } from './Connection';
import { Page } from '../page';
import { Target } from './Target';

View File

@ -281,8 +281,6 @@ export class FrameManager implements PageDelegate {
await this._setEmulateMedia(this._session, mediaType, mediaColorScheme);
}
async setViewport(viewport: types.Viewport): Promise<void> {
if (viewport.isMobile || viewport.isLandscape || viewport.hasTouch)
throw new Error('Not implemented');

View File

@ -120,7 +120,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p
const browser = await playwright.launch(defaultBrowserOptions);
const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.chromium.wsEndpoint()});
const page = await remote.newPage();
const watchdog = page.$('div', { waitFor: true, timeout: 60000 }).catch(e => e);
const watchdog = page.waitForSelector('div', { timeout: 60000 }).catch(e => e);
remote.disconnect();
const error = await watchdog;
expect(error.message).toContain('Protocol error');

View File

@ -99,7 +99,7 @@ module.exports.addTests = function({testRunner, expect, playwright, defaultBrows
document.body.appendChild(frame);
return new Promise(x => frame.onload = x);
});
await page.$('iframe[src="https://google.com/"]', { waitFor: true });
await page.waitForSelector('iframe[src="https://google.com/"]');
const urls = page.frames().map(frame => frame.url()).sort();
expect(urls).toEqual([
server.EMPTY_PAGE,

View File

@ -128,7 +128,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
it('should waitFor visible when already visible', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.click('button', { waitFor: 'visible' });
await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should waitFor hidden when already hidden', async({page, server}) => {
@ -155,7 +155,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
let done = false;
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.display = 'none');
const clicked = page.click('button', { waitFor: 'visible' }).then(() => done = true);
const clicked = page.click('button').then(() => done = true);
for (let i = 0; i < 5; i++)
await page.evaluate('1'); // Do a round trip.
expect(done).toBe(false);
@ -207,7 +207,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
it('should fail to click a missing button', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
let error = null;
await page.click('button.does-not-exist').catch(e => error = e);
await page.click('button.does-not-exist', { waitFor: 'nowait' }).catch(e => error = e);
expect(error.message).toBe('No node found for selector: button.does-not-exist');
});
// @see https://github.com/GoogleChrome/puppeteer/issues/161

View File

@ -600,7 +600,8 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
const result = await page.content();
expect(result).toBe(expectedOutput);
});
it('should not confuse with previous navigation', async({page, server}) => {
it.skip(FFOX || WEBKIT)('should not confuse with previous navigation', async({page, server}) => {
// TODO: ffox and webkit lack 'init' lifecycle event.
const imgPath = '/img.png';
let imgResponse = null;
server.setRoute(imgPath, (req, res) => imgResponse = res);
@ -1101,15 +1102,15 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
await page.fill('textarea', 123).catch(e => error = e);
expect(error.message).toContain('Value must be string.');
});
it('should respect selector visibilty', async({page, server}) => {
it('should wait for visible visibilty', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill('input', 'some value', { waitFor: 'visible' });
await page.fill('input', 'some value');
expect(await page.evaluate(() => result)).toBe('some value');
await page.goto(server.PREFIX + '/input/textarea.html');
await page.$eval('input', i => i.style.display = 'none');
await Promise.all([
page.fill('input', 'some value', { waitFor: 'visible' }),
page.fill('input', 'some value'),
page.$eval('input', i => i.style.display = 'block'),
]);
expect(await page.evaluate(() => result)).toBe('some value');
@ -1128,12 +1129,12 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
it('should throw on hidden and invisible elements', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.$eval('input', i => i.style.display = 'none');
const invisibleError = await page.fill('input', 'some value').catch(e => e);
const invisibleError = await page.fill('input', 'some value', { waitFor: 'nowait' }).catch(e => e);
expect(invisibleError.message).toBe('Element is not visible');
await page.goto(server.PREFIX + '/input/textarea.html');
await page.$eval('input', i => i.style.visibility = 'hidden');
const hiddenError = await page.fill('input', 'some value').catch(e => e);
const hiddenError = await page.fill('input', 'some value', { waitFor: 'nowait' }).catch(e => e);
expect(hiddenError.message).toBe('Element is hidden');
});
it('should be able to fill the body', async({page}) => {

View File

@ -162,25 +162,6 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
const element = await page.$('non-existing-element');
expect(element).toBe(null);
});
it('should return null for non-existing element with waitFor:false', async({page, server}) => {
const element = await page.$('non-existing-element', { waitFor: false });
expect(element).toBe(null);
});
it('should query existing element with waitFor:false', async({page, server}) => {
await page.setContent('<section>test</section>');
const element = await page.$('css=section', { waitFor: false });
expect(element).toBeTruthy();
});
it('should throw for unknown waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
const error = await page.$('section', { waitFor: 'foo' }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option');
});
it('should throw for numeric waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
const error = await page.$('section', { waitFor: 123 }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option');
});
it('should auto-detect xpath selector', async({page, server}) => {
await page.setContent('<section>test</section>');
const element = await page.$('//html/body/section');
@ -203,14 +184,14 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
});
it('should respect waitFor visibility', async({page, server}) => {
await page.setContent('<section id="testAttribute">43543</section>');
expect(await page.$('css=section', { waitFor: 'visible'})).toBeTruthy();
expect(await page.$('css=section', { waitFor: 'any'})).toBeTruthy();
expect(await page.$('css=section')).toBeTruthy();
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.$('css=section', { waitFor: 'hidden'})).toBeTruthy();
expect(await page.$('css=section', { waitFor: 'any'})).toBeTruthy();
expect(await page.$('css=section')).toBeTruthy();
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();
});
});

View File

@ -205,21 +205,21 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
});
});
describe('Frame.$ waitFor', function() {
describe('Frame.waitForSelector', function() {
const addElement = tag => document.body.appendChild(document.createElement(tag));
it('should immediately resolve promise if node exists', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame = page.mainFrame();
await frame.$('*', { waitFor: true });
await frame.waitForSelector('*');
await frame.evaluate(addElement, 'div');
await frame.$('div', { waitFor: true });
await frame.waitForSelector('div');
});
it.skip(FFOX)('should work with removed MutationObserver', async({page, server}) => {
await page.evaluate(() => delete window.MutationObserver);
const [handle] = await Promise.all([
page.$('.zombo', { waitFor: true }),
page.waitForSelector('.zombo'),
page.setContent(`<div class='zombo'>anything</div>`),
]);
expect(await page.evaluate(x => x.textContent, handle)).toBe('anything');
@ -228,7 +228,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
it('should resolve promise when node is added', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame = page.mainFrame();
const watchdog = frame.$('div', { waitFor: true });
const watchdog = frame.waitForSelector('div');
await frame.evaluate(addElement, 'br');
await frame.evaluate(addElement, 'div');
const eHandle = await watchdog;
@ -238,7 +238,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
it('should work when node is added through innerHTML', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const watchdog = page.$('h3 div', { waitFor: true });
const watchdog = page.waitForSelector('h3 div');
await page.evaluate(addElement, 'span');
await page.evaluate(() => document.querySelector('span').innerHTML = '<h3><div></div></h3>');
await watchdog;
@ -248,7 +248,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const otherFrame = page.frames()[1];
const watchdog = page.$('div', { waitFor: true });
const watchdog = page.waitForSelector('div');
await otherFrame.evaluate(addElement, 'div');
await page.evaluate(addElement, 'div');
const eHandle = await watchdog;
@ -260,7 +260,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
const frame1 = page.frames()[1];
const frame2 = page.frames()[2];
const waitForSelectorPromise = frame2.$('div', { waitFor: true });
const waitForSelectorPromise = frame2.waitForSelector('div');
await frame1.evaluate(addElement, 'div');
await frame2.evaluate(addElement, 'div');
const eHandle = await waitForSelectorPromise;
@ -271,7 +271,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = page.frames()[1];
let waitError = null;
const waitPromise = frame.$('.box', { waitFor: true }).catch(e => waitError = e);
const waitPromise = frame.waitForSelector('.box').catch(e => waitError = e);
await utils.detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
@ -279,7 +279,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
});
it('should survive cross-process navigation', async({page, server}) => {
let boxFound = false;
const waitForSelector = page.$('.box', { waitFor: true }).then(() => boxFound = true);
const waitForSelector = page.waitForSelector('.box').then(() => boxFound = true);
await page.goto(server.EMPTY_PAGE);
expect(boxFound).toBe(false);
await page.reload();
@ -290,7 +290,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
});
it('should wait for visible', async({page, server}) => {
let divFound = false;
const waitForSelector = page.$('div', { waitFor: 'visible' }).then(() => divFound = true);
const waitForSelector = page.waitForSelector('div').then(() => divFound = true);
await page.setContent(`<div style='display: none; visibility: hidden;'>1</div>`);
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('display'));
@ -301,7 +301,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
});
it('should wait for visible recursively', async({page, server}) => {
let divVisible = false;
const waitForSelector = page.$('div#inner', { waitFor: 'visible' }).then(() => divVisible = true);
const waitForSelector = page.waitForSelector('div#inner', { waitFor: 'visible' }).then(() => divVisible = true);
await page.setContent(`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`);
expect(divVisible).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('display'));
@ -313,8 +313,8 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
it('hidden should wait for visibility: hidden', async({page, server}) => {
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForSelector = page.$('div', { waitFor: 'hidden' }).then(() => divHidden = true);
await page.$('div', { waitFor: true }); // do a round trip
const waitForSelector = page.waitForSelector('div', { waitFor: 'hidden' }).then(() => divHidden = true);
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('visibility', 'hidden'));
expect(await waitForSelector).toBe(true);
@ -323,8 +323,8 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
it('hidden should wait for display: none', async({page, server}) => {
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForSelector = page.$('div', { waitFor: 'hidden' }).then(() => divHidden = true);
await page.$('div', { waitFor: true }); // do a round trip
const waitForSelector = page.waitForSelector('div', { waitFor: 'hidden' }).then(() => divHidden = true);
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none'));
expect(await waitForSelector).toBe(true);
@ -333,20 +333,20 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
it('hidden should wait for removal', async({page, server}) => {
await page.setContent(`<div></div>`);
let divRemoved = false;
const waitForSelector = page.$('div', { waitFor: 'hidden' }).then(() => divRemoved = true);
await page.$('div', { waitFor: true }); // do a round trip
const waitForSelector = page.waitForSelector('div', { waitFor: 'hidden' }).then(() => divRemoved = true);
await page.waitForSelector('div'); // do a round trip
expect(divRemoved).toBe(false);
await page.evaluate(() => document.querySelector('div').remove());
expect(await waitForSelector).toBe(true);
expect(divRemoved).toBe(true);
});
it('should return null if waiting to hide non-existing element', async({page, server}) => {
const handle = await page.$('non-existing', { waitFor: 'hidden' });
const handle = await page.waitForSelector('non-existing', { waitFor: 'hidden' });
expect(handle).toBe(null);
});
it('should respect timeout', async({page, server}) => {
let error = null;
await page.$('div', { waitFor: true, timeout: 10 }).catch(e => error = e);
await page.waitForSelector('div', { timeout: 10 }).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "div" failed: timeout');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
@ -354,34 +354,62 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
it('should have an error message specifically for awaiting an element to be hidden', async({page, server}) => {
await page.setContent(`<div></div>`);
let error = null;
await page.$('div', { waitFor: 'hidden', timeout: 10 }).catch(e => error = e);
await page.waitForSelector('div', { waitFor: 'hidden', timeout: 10 }).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "[hidden] div" failed: timeout');
});
it('should respond to node attribute mutation', async({page, server}) => {
let divFound = false;
const waitForSelector = page.$('.zombo', { waitFor: true }).then(() => divFound = true);
const waitForSelector = page.waitForSelector('.zombo').then(() => divFound = true);
await page.setContent(`<div class='notZombo'></div>`);
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').className = 'zombo');
expect(await waitForSelector).toBe(true);
});
it('should return the element handle', async({page, server}) => {
const waitForSelector = page.$('.zombo', { waitFor: true });
const waitForSelector = page.waitForSelector('.zombo');
await page.setContent(`<div class='zombo'>anything</div>`);
expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything');
});
it('should have correct stack trace for timeout', async({page, server}) => {
let error;
await page.$('.zombo', { waitFor: true, timeout: 10 }).catch(e => error = e);
await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e);
expect(error.stack).toContain('waittask.spec.js');
});
it('should throw for waitFor nowait', async({page, server}) => {
let error;
try {
await page.waitForSelector('non-existing-element', { waitFor: 'nowait' });
} catch (e) {
error = e;
}
expect(error.message).toBe('waitForSelector does not support "nowait"');
});
it('should throw for unknown waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { waitFor: 'foo' }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option');
});
it('should throw for numeric waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { waitFor: 123 }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option');
});
it('should throw for true waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { waitFor: true }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option');
});
it('should throw for false waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { waitFor: false }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option');
});
it('should support >> selector syntax', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame = page.mainFrame();
const watchdog = frame.$('css=div >> css=span', { waitFor: true });
const watchdog = frame.waitForSelector('css=div >> css=span');
await frame.evaluate(addElement, 'br');
await frame.evaluate(addElement, 'div');
await frame.evaluate(() => document.querySelector('div').appendChild(document.createElement('span')));
@ -391,17 +419,17 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
});
});
describe('Frame.$ waitFor xpath', function() {
describe('Frame.waitForSelector xpath', function() {
const addElement = tag => document.body.appendChild(document.createElement(tag));
it('should support some fancy xpath', async({page, server}) => {
await page.setContent(`<p>red herring</p><p>hello world </p>`);
const waitForXPath = page.$('//p[normalize-space(.)="hello world"]', { waitFor: true });
const waitForXPath = page.waitForSelector('//p[normalize-space(.)="hello world"]');
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('hello world ');
});
it('should respect timeout', async({page}) => {
let error = null;
await page.$('//div', { waitFor: true, timeout: 10 }).catch(e => error = e);
await page.waitForSelector('//div', { timeout: 10 }).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "//div" failed: timeout');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
@ -411,7 +439,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
const frame1 = page.frames()[1];
const frame2 = page.frames()[2];
const waitForXPathPromise = frame2.$('//div', { waitFor: true });
const waitForXPathPromise = frame2.waitForSelector('//div');
await frame1.evaluate(addElement, 'div');
await frame2.evaluate(addElement, 'div');
const eHandle = await waitForXPathPromise;
@ -421,20 +449,20 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = page.frames()[1];
let waitError = null;
const waitPromise = frame.$('//*[@class="box"]', { waitFor: true }).catch(e => waitError = e);
const waitPromise = frame.waitForSelector('//*[@class="box"]').catch(e => waitError = e);
await utils.detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
});
it('should return the element handle', async({page, server}) => {
const waitForXPath = page.$('//*[@class="zombo"]', { waitFor: true });
const waitForXPath = page.waitForSelector('//*[@class="zombo"]');
await page.setContent(`<div class='zombo'>anything</div>`);
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('anything');
});
it('should allow you to select an element with single slash', async({page, server}) => {
await page.setContent(`<div>some text</div>`);
const waitForXPath = page.$('//html/body/div', { waitFor: true });
const waitForXPath = page.waitForSelector('//html/body/div');
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('some text');
});
});