From f188b0a17479454f636715411eb8b2b4706d3bbf Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 3 Jun 2020 09:14:53 -0700 Subject: [PATCH] chore: migrate most actions to Progress (#2439) --- src/dom.ts | 133 ++++++++++++++++++++----------------- src/frames.ts | 90 +++++++++++-------------- test/click.spec.js | 34 +++++----- test/elementhandle.spec.js | 35 +++++++++- test/page.spec.js | 6 +- 5 files changed, 162 insertions(+), 136 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index 9241b084a0..5e12036fba 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -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 extends js.JSHandle { }; } - async _retryPointerAction(actionName: string, action: (point: types.Point) => Promise, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { - 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, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + 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, deadline: number, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<'done' | 'retry'> { + async _performPointerAction(progress: Progress, action: (point: types.Point) => Promise, 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 extends js.JSHandle { 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 { - return this._retryPointerAction('hover', point => this._page.mouse.move(point.x, point.y), options); + hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise { + return Progress.runCancelableTask(progress => this._hover(progress, options), options, this._page, this._page._timeoutSettings); } - click(options?: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { - return this._retryPointerAction('click', point => this._page.mouse.click(point.x, point.y, options), options); + _hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise { + return this._retryPointerAction(progress, point => this._page.mouse.move(point.x, point.y), options); } - dblclick(options?: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { - return this._retryPointerAction('dblclick', point => this._page.mouse.dblclick(point.x, point.y, options), options); + click(options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + return Progress.runCancelableTask(progress => this._click(progress, options), options, this._page, this._page._timeoutSettings); + } + + _click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + return this._retryPointerAction(progress, point => this._page.mouse.click(point.x, point.y, options), options); + } + + dblclick(options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + return Progress.runCancelableTask(progress => this._dblclick(progress, options), options, this._page, this._page._timeoutSettings); + } + + _dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + 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 { @@ -346,28 +356,27 @@ export class ElementHandle extends js.JSHandle { }, deadline, options); } - async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise { - this._page._log(inputLog, `elementHandle.fill(${value})`); + async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise { + 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 { + 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 { @@ -436,20 +445,24 @@ export class ElementHandle extends js.JSHandle { }, 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 extends js.JSHandle { return result; } - async _waitForDisplayedAtStablePositionAndEnabled(deadline: number): Promise { - this._page._log(inputLog, 'waiting for element to be displayed, enabled and not moving...'); + async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise { + 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 { diff --git a/src/frames.ts b/src/frames.ts index 04fd1c87c8..38d21c52a1 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -105,6 +105,7 @@ export class FrameManager { } } + // TODO: take progress parameter. async waitForSignalsCreatedBy(action: () => Promise, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise { if (options.noWaitAfter) return action(); @@ -697,107 +698,94 @@ export class Frame { } private async _retryWithSelectorIfNotConnected( - actionName: string, selector: string, options: types.TimeoutOptions, - action: (handle: dom.ElementHandle, deadline: number) => Promise): Promise { - 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; + action: (progress: Progress, handle: dom.ElementHandle) => Promise): Promise { + 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; + 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 { - 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 { - 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 { - 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 { - return await this._retryWithSelectorIfNotConnected('getAttribute', selector, options, - (handle, deadline) => handle.getAttribute(name) as Promise); + 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 { - 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 { - 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) { diff --git a/test/click.spec.js b/test/click.spec.js index 54f4449819..349f2748b6 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -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(''); @@ -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(` diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 4a2bab10dd..b26a2d3197 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -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
elements with force', async({page, server}) => { await page.setContent('hello
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(``); + 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(``); + const input = await page.$('input'); + await input.uncheck(); + expect(await page.evaluate(() => checkbox.checked)).toBe(false); + }); +}); diff --git a/test/page.spec.js b/test/page.spec.js index 9a3db634aa..60ed0c21f4 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -1054,7 +1054,7 @@ describe('Page.fill', function() { it.skip(WEBKIT)('should throw on incorrect date', async({page, server}) => { await page.setContent(''); 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(''); @@ -1064,7 +1064,7 @@ describe('Page.fill', function() { it.skip(WEBKIT)('should throw on incorrect time', async({page, server}) => { await page.setContent(''); 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(''); @@ -1074,7 +1074,7 @@ describe('Page.fill', function() { it.skip(WEBKIT || FFOX)('should throw on incorrect datetime-local', async({page, server}) => { await page.setContent(''); 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');