chore: migrate most actions to Progress (#2439)

This commit is contained in:
Dmitry Gozman 2020-06-03 09:14:53 -07:00 committed by GitHub
parent abfd278461
commit f188b0a174
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 162 additions and 136 deletions

View file

@ -27,7 +27,7 @@ import * as js from './javascript';
import { Page } from './page';
import { selectors } from './selectors';
import * as types from './types';
import { NotConnectedError, TimeoutError } from './errors';
import { NotConnectedError } from './errors';
import { Log, logError } from './logger';
import { Progress } from './progress';
@ -240,43 +240,41 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
};
}
async _retryPointerAction(actionName: string, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
this._page._log(inputLog, `elementHandle.${actionName}()`);
const deadline = this._page._timeoutSettings.computeDeadline(options);
while (!helper.isPastDeadline(deadline)) {
const result = await this._performPointerAction(actionName, action, deadline, options);
async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
progress.log(inputLog, progress.apiName);
while (!progress.isCanceled()) {
const result = await this._performPointerAction(progress, action, options);
if (result === 'done')
return;
}
throw new TimeoutError(`waiting for element to receive pointer events failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
}
async _performPointerAction(actionName: string, action: (point: types.Point) => Promise<void>, deadline: number, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<'done' | 'retry'> {
async _performPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'done' | 'retry'> {
const { force = false, position } = options;
if (!force)
await this._waitForDisplayedAtStablePositionAndEnabled(deadline);
await this._waitForDisplayedAtStablePositionAndEnabled(progress);
this._page._log(inputLog, 'scrolling into view if needed...');
progress.log(inputLog, 'scrolling into view if needed...');
const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
if (scrolled === 'invisible') {
if (force)
throw new Error('Element is not visible');
this._page._log(inputLog, '...element is not visible, retrying input action');
progress.log(inputLog, '...element is not visible, retrying input action');
return 'retry';
}
this._page._log(inputLog, '...done scrolling');
progress.log(inputLog, '...done scrolling');
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
if (maybePoint === 'invisible') {
if (force)
throw new Error('Element is not visible');
this._page._log(inputLog, 'element is not visibile, retrying input action');
progress.log(inputLog, 'element is not visibile, retrying input action');
return 'retry';
}
if (maybePoint === 'outsideviewport') {
if (force)
throw new Error('Element is outside of the viewport');
this._page._log(inputLog, 'element is outside of the viewport, retrying input action');
progress.log(inputLog, 'element is outside of the viewport, retrying input action');
return 'retry';
}
const point = roundPoint(maybePoint);
@ -284,41 +282,53 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (!force) {
if ((options as any).__testHookBeforeHitTarget)
await (options as any).__testHookBeforeHitTarget();
this._page._log(inputLog, `checking that element receives pointer events at (${point.x},${point.y})...`);
progress.log(inputLog, `checking that element receives pointer events at (${point.x},${point.y})...`);
const matchesHitTarget = await this._checkHitTargetAt(point);
if (!matchesHitTarget) {
this._page._log(inputLog, '...element does not receive pointer events, retrying input action');
progress.log(inputLog, '...element does not receive pointer events, retrying input action');
return 'retry';
}
this._page._log(inputLog, `...element does receive pointer events, continuing input action`);
progress.log(inputLog, `...element does receive pointer events, continuing input action`);
}
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
this._page._log(inputLog, `performing "${actionName}" action...`);
progress.log(inputLog, `performing ${progress.apiName} action...`);
await action(point);
this._page._log(inputLog, `... "${actionName}" action done`);
this._page._log(inputLog, 'waiting for scheduled navigations to finish...');
progress.log(inputLog, `...${progress.apiName} action done`);
progress.log(inputLog, 'waiting for scheduled navigations to finish...');
if (restoreModifiers)
await this._page.keyboard._ensureModifiers(restoreModifiers);
}, deadline, options, true);
this._page._log(inputLog, '...navigations have finished');
}, progress.deadline, options, true);
progress.log(inputLog, '...navigations have finished');
return 'done';
}
hover(options?: PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
return this._retryPointerAction('hover', point => this._page.mouse.move(point.x, point.y), options);
hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._hover(progress, options), options, this._page, this._page._timeoutSettings);
}
click(options?: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
return this._retryPointerAction('click', point => this._page.mouse.click(point.x, point.y, options), options);
_hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
return this._retryPointerAction(progress, point => this._page.mouse.move(point.x, point.y), options);
}
dblclick(options?: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
return this._retryPointerAction('dblclick', point => this._page.mouse.dblclick(point.x, point.y, options), options);
click(options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._click(progress, options), options, this._page, this._page._timeoutSettings);
}
_click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
return this._retryPointerAction(progress, point => this._page.mouse.click(point.x, point.y, options), options);
}
dblclick(options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._dblclick(progress, options), options, this._page, this._page._timeoutSettings);
}
_dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
return this._retryPointerAction(progress, point => this._page.mouse.dblclick(point.x, point.y, options), options);
}
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options?: types.NavigatingActionWaitOptions): Promise<string[]> {
@ -346,28 +356,27 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, deadline, options);
}
async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise<void> {
this._page._log(inputLog, `elementHandle.fill(${value})`);
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._fill(progress, value, options), options, this._page, this._page._timeoutSettings);
}
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<void> {
progress.log(inputLog, `elementHandle.fill("${value}")`);
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const deadline = this._page._timeoutSettings.computeDeadline(options);
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
const poll = await this._evaluateHandleInUtility(({ injected, node }, { value }) => {
return injected.waitForEnabledAndFill(node, value);
}, { value });
try {
const filledPromise = poll.evaluate(poll => poll.result);
const injectedResult = await helper.waitWithDeadline(filledPromise, 'element to be visible and enabled', deadline, 'pw:input');
const needsInput = handleInjectedResult(injectedResult);
if (needsInput) {
if (value)
await this._page.keyboard.insertText(value);
else
await this._page.keyboard.press('Delete');
}
} finally {
poll.evaluate(poll => poll.cancel()).catch(e => {}).then(() => poll.dispose());
new InjectedScriptPollHandler(progress, poll);
const injectedResult = await poll.evaluate(poll => poll.result);
const needsInput = handleInjectedResult(injectedResult);
if (needsInput) {
if (value)
await this._page.keyboard.insertText(value);
else
await this._page.keyboard.press('Delete');
}
}, deadline, options, true);
}, progress.deadline, options, true);
}
async selectText(): Promise<void> {
@ -436,20 +445,24 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, deadline, options, true);
}
async check(options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
this._page._log(inputLog, `elementHandle.check()`);
await this._setChecked(true, options);
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(async progress => {
progress.log(inputLog, `elementHandle.check()`);
await this._setChecked(progress, true, options);
}, options, this._page, this._page._timeoutSettings);
}
async uncheck(options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
this._page._log(inputLog, `elementHandle.uncheck()`);
await this._setChecked(false, options);
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(async progress => {
progress.log(inputLog, `elementHandle.uncheck()`);
await this._setChecked(progress, false, options);
}, options, this._page, this._page._timeoutSettings);
}
private async _setChecked(state: boolean, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
if (await this._evaluateInUtility(({ injected, node }) => injected.isCheckboxChecked(node), {}) === state)
return;
await this.click(options);
await this._click(progress, options);
if (await this._evaluateInUtility(({ injected, node }) => injected.isCheckboxChecked(node), {}) !== state)
throw new Error('Unable to click checkbox');
}
@ -490,20 +503,16 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return result;
}
async _waitForDisplayedAtStablePositionAndEnabled(deadline: number): Promise<void> {
this._page._log(inputLog, 'waiting for element to be displayed, enabled and not moving...');
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<void> {
progress.log(inputLog, 'waiting for element to be displayed, enabled and not moving...');
const rafCount = this._page._delegate.rafCountForStablePosition();
const poll = await this._evaluateHandleInUtility(({ injected, node }, { rafCount }) => {
return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount);
}, { rafCount });
try {
const stablePromise = poll.evaluate(poll => poll.result);
const injectedResult = await helper.waitWithDeadline(stablePromise, 'element to be displayed and not moving', deadline, 'pw:input');
handleInjectedResult(injectedResult);
} finally {
poll.evaluate(poll => poll.cancel()).catch(e => {}).then(() => poll.dispose());
}
this._page._log(inputLog, '...element is displayed and does not move');
new InjectedScriptPollHandler(progress, poll);
const injectedResult = await poll.evaluate(poll => poll.result);
handleInjectedResult(injectedResult);
progress.log(inputLog, '...element is displayed and does not move');
}
async _checkHitTargetAt(point: types.Point): Promise<boolean> {

View file

@ -105,6 +105,7 @@ export class FrameManager {
}
}
// TODO: take progress parameter.
async waitForSignalsCreatedBy<T>(action: () => Promise<T>, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise<T> {
if (options.noWaitAfter)
return action();
@ -697,107 +698,94 @@ export class Frame {
}
private async _retryWithSelectorIfNotConnected<R>(
actionName: string,
selector: string, options: types.TimeoutOptions,
action: (handle: dom.ElementHandle<Element>, deadline: number) => Promise<R>): Promise<R> {
const deadline = this._page._timeoutSettings.computeDeadline(options);
this._page._log(dom.inputLog, `(page|frame).${actionName}("${selector}")`);
while (!helper.isPastDeadline(deadline)) {
try {
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
this._page._log(dom.inputLog, `waiting for the selector "${selector}"`);
const handle = await Progress.runCancelableTask(
progress => this._scheduleRerunnableTask(progress, world, task),
options, this._page, this._page._timeoutSettings);
this._page._log(dom.inputLog, `...got element for the selector`);
const element = handle.asElement() as dom.ElementHandle<Element>;
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R>): Promise<R> {
return Progress.runCancelableTask(async progress => {
progress.log(dom.inputLog, `${progress.apiName}("${selector}")`);
while (!progress.isCanceled()) {
try {
return await action(element, deadline);
} finally {
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
progress.log(dom.inputLog, `waiting for the selector "${selector}"`);
const handle = await this._scheduleRerunnableTask(progress, world, task);
progress.log(dom.inputLog, `...got element for the selector`);
const element = handle.asElement() as dom.ElementHandle<Element>;
progress.cleanupWhenCanceled(() => element.dispose());
const result = await action(progress, element);
element.dispose();
return result;
} catch (e) {
if (!(e instanceof NotConnectedError))
throw e;
progress.log(dom.inputLog, 'element was detached from the DOM, retrying');
}
} catch (e) {
if (!(e instanceof NotConnectedError))
throw e;
this._page._log(dom.inputLog, 'Element was detached from the DOM, retrying');
}
}
throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log.`);
return undefined as any;
}, options, this._page, this._page._timeoutSettings);
}
async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('click', selector, options,
(handle, deadline) => handle.click(helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options));
}
async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('dblclick', selector, options,
(handle, deadline) => handle.dblclick(helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options));
}
async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('fill', selector, options,
(handle, deadline) => handle.fill(value, helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options,
(progress, handle) => handle._fill(progress, value, options));
}
async focus(selector: string, options: types.TimeoutOptions = {}) {
await this._retryWithSelectorIfNotConnected('focus', selector, options,
(handle, deadline) => handle.focus());
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.focus());
}
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> {
return await this._retryWithSelectorIfNotConnected('textContent', selector, options,
(handle, deadline) => handle.textContent());
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent());
}
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
return await this._retryWithSelectorIfNotConnected('innerText', selector, options,
(handle, deadline) => handle.innerText());
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText());
}
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
return await this._retryWithSelectorIfNotConnected('innerHTML', selector, options,
(handle, deadline) => handle.innerHTML());
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML());
}
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
return await this._retryWithSelectorIfNotConnected('getAttribute', selector, options,
(handle, deadline) => handle.getAttribute(name) as Promise<string>);
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name));
}
async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('hover', selector, options,
(handle, deadline) => handle.hover(helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options));
}
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return await this._retryWithSelectorIfNotConnected('selectOption', selector, options,
(handle, deadline) => handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, deadline)));
return await this._retryWithSelectorIfNotConnected(selector, options,
(progress, handle) => handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
}
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
await this._retryWithSelectorIfNotConnected('setInputFiles', selector, options,
(handle, deadline) => handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options,
(progress, handle) => handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
}
async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('type', selector, options,
(handle, deadline) => handle.type(text, helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options,
(progress, handle) => handle.type(text, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
}
async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('press', selector, options,
(handle, deadline) => handle.press(key, helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options,
(progress, handle) => handle.press(key, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
}
async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('check', selector, options,
(handle, deadline) => handle.check(helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options));
}
async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected('uncheck', selector, options,
(handle, deadline) => handle.uncheck(helper.optionsWithUpdatedTimeout(options, deadline)));
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options));
}
async waitForTimeout(timeout: number) {

View file

@ -148,7 +148,7 @@ describe('Page.click', function() {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.display = 'none');
await page.click('button', { force: true }).catch(e => error = e);
expect(error.message).toBe('Element is not visible');
expect(error.message).toContain('Element is not visible');
expect(await page.evaluate(() => result)).toBe('Was not clicked');
});
it('should waitFor display:none to be gone', async({page, server}) => {
@ -180,16 +180,16 @@ describe('Page.click', function() {
it('should timeout waiting for display:none to be gone', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.display = 'none');
const error = await page.click('button', { timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
expect(error.message).toContain('DEBUG=pw:input');
const error = await page.click('button', { timeout: 5000 }).catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during page.click.');
expect(error.message).toContain('waiting for element to be displayed, enabled and not moving...');
});
it('should timeout waiting for visbility:hidden to be gone', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.visibility = 'hidden');
const error = await page.click('button', { timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
expect(error.message).toContain('DEBUG=pw:input');
const error = await page.click('button', { timeout: 5000 }).catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during page.click.');
expect(error.message).toContain('waiting for element to be displayed, enabled and not moving...');
});
it('should waitFor visible when parent is hidden', async({page, server}) => {
let done = false;
@ -429,9 +429,9 @@ describe('Page.click', function() {
button.style.transition = 'margin 5s linear 0s';
button.style.marginLeft = '200px';
});
const error = await button.click({ timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
expect(error.message).toContain('DEBUG=pw:input');
const error = await button.click({ timeout: 5000 }).catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during elementHandle.click.');
expect(error.message).toContain('waiting for element to be displayed, enabled and not moving...');
});
it('should wait for becoming hit target', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
@ -477,9 +477,9 @@ describe('Page.click', function() {
blocker.style.top = '0';
document.body.appendChild(blocker);
});
const error = await button.click({ timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
expect(error.message).toContain('DEBUG=pw:input');
const error = await button.click({ timeout: 5000 }).catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during elementHandle.click.');
expect(error.message).toContain('...element does not receive pointer events, retrying input action');
});
it('should fail when obscured and not waiting for hit target', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
@ -689,7 +689,7 @@ describe('Page.click', function() {
await handle.evaluate(button => button.className = 'animated');
const error = await promise;
expect(await page.evaluate(() => window.clicked)).toBe(undefined);
expect(error.message).toBe('Element is outside of the viewport');
expect(error.message).toContain('Element is outside of the viewport');
});
it('should fail when element jumps during hit testing', async({page, server}) => {
await page.setContent('<button>Click me</button>');
@ -699,12 +699,12 @@ describe('Page.click', function() {
const margin = parseInt(document.querySelector('button').style.marginLeft || 0) + 100;
document.querySelector('button').style.marginLeft = margin + 'px';
});
const promise = handle.click({ timeout: 1000, __testHookBeforeHitTarget }).then(() => clicked = true).catch(e => e);
const promise = handle.click({ timeout: 5000, __testHookBeforeHitTarget }).then(() => clicked = true).catch(e => e);
const error = await promise;
expect(clicked).toBe(false);
expect(await page.evaluate(() => window.clicked)).toBe(undefined);
expect(error.message).toContain('timeout exceeded');
expect(error.message).toContain('DEBUG=pw:input');
expect(error.message).toContain('Timeout 5000ms exceeded during elementHandle.click.');
expect(error.message).toContain('...element does not receive pointer events, retrying input action');
});
it('should dispatch microtasks in order', async({page, server}) => {
await page.setContent(`

View file

@ -259,20 +259,34 @@ describe('ElementHandle.click', function() {
const button = await page.$('button');
await page.evaluate(button => button.style.display = 'none', button);
const error = await button.click({ force: true }).catch(err => err);
expect(error.message).toBe('Element is not visible');
expect(error.message).toContain('Element is not visible');
});
it('should throw for recursively hidden nodes with force', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.parentElement.style.display = 'none', button);
const error = await button.click({ force: true }).catch(err => err);
expect(error.message).toBe('Element is not visible');
expect(error.message).toContain('Element is not visible');
});
it('should throw for <br> elements with force', async({page, server}) => {
await page.setContent('hello<br>goodbye');
const br = await page.$('br');
const error = await br.click({ force: true }).catch(err => err);
expect(error.message).toBe('Element is outside of the viewport');
expect(error.message).toContain('Element is outside of the viewport');
});
it('should double click the button', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => {
window.double = false;
const button = document.querySelector('button');
button.addEventListener('dblclick', event => {
window.double = true;
});
});
const button = await page.$('button');
await button.dblclick();
expect(await page.evaluate('double')).toBe(true);
expect(await page.evaluate('result')).toBe('Clicked');
});
});
@ -387,3 +401,18 @@ describe('ElementHandle convenience API', function() {
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
});
});
describe('ElementHandle.check', () => {
it('should check the box', async({page}) => {
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
const input = await page.$('input');
await input.check();
expect(await page.evaluate(() => checkbox.checked)).toBe(true);
});
it('should uncheck the box', async({page}) => {
await page.setContent(`<input id='checkbox' type='checkbox' checked></input>`);
const input = await page.$('input');
await input.uncheck();
expect(await page.evaluate(() => checkbox.checked)).toBe(false);
});
});

View file

@ -1054,7 +1054,7 @@ describe('Page.fill', function() {
it.skip(WEBKIT)('should throw on incorrect date', async({page, server}) => {
await page.setContent('<input type=date>');
const error = await page.fill('input', '2020-13-05').catch(e => e);
expect(error.message).toBe('Malformed date "2020-13-05"');
expect(error.message).toContain('Malformed date "2020-13-05"');
});
it('should fill time input', async({page, server}) => {
await page.setContent('<input type=time>');
@ -1064,7 +1064,7 @@ describe('Page.fill', function() {
it.skip(WEBKIT)('should throw on incorrect time', async({page, server}) => {
await page.setContent('<input type=time>');
const error = await page.fill('input', '25:05').catch(e => e);
expect(error.message).toBe('Malformed time "25:05"');
expect(error.message).toContain('Malformed time "25:05"');
});
it('should fill datetime-local input', async({page, server}) => {
await page.setContent('<input type=datetime-local>');
@ -1074,7 +1074,7 @@ describe('Page.fill', function() {
it.skip(WEBKIT || FFOX)('should throw on incorrect datetime-local', async({page, server}) => {
await page.setContent('<input type=datetime-local>');
const error = await page.fill('input', 'abc').catch(e => e);
expect(error.message).toBe('Malformed datetime-local "abc"');
expect(error.message).toContain('Malformed datetime-local "abc"');
});
it('should fill contenteditable', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');