From dec8fb789053afbfdebfef7db7ae55d9036c62f8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 14 Aug 2020 13:18:32 -0700 Subject: [PATCH] fix(hover): do not require the element to be enabled before hovering (#3445) --- docs/actionability.md | 3 ++- src/dom.ts | 15 ++++++++------- test/mouse.spec.ts | 7 +++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/actionability.md b/docs/actionability.md index bff54316e1..720f6f7242 100644 --- a/docs/actionability.md +++ b/docs/actionability.md @@ -8,7 +8,8 @@ Some actions like `page.click()` support `{force: true}` option that disable non | Actions | Performed checks | | ------ | ------- | -| `check()`
`click()`
`dblclick()`
`hover()`
`uncheck()` | [Visible]
[Stable]
[Enabled]
[Receiving Events]
[Attached] | +| `check()`
`click()`
`dblclick()`
`uncheck()` | [Visible]
[Stable]
[Enabled]
[Receiving Events]
[Attached] | +| `hover()` | [Visible]
[Stable]
[Receiving Events]
[Attached] | | `fill()` | [Visible]
[Enabled]
[Editable]
[Attached] | | `dispatchEvent()`
`focus()`
`press()`
`setInputFiles()`
`selectOption()`
`type()` | [Attached] | | `scrollIntoViewIfNeeded()`
`screenshot()` | [Visible]
[Stable]
[Attached] | diff --git a/src/dom.ts b/src/dom.ts index f715061061..82528a92af 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -287,11 +287,12 @@ export class ElementHandle extends js.JSHandle { }; } - async _retryPointerAction(progress: Progress, actionName: string, action: (point: types.Point) => Promise, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { + async _retryPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise, + options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { let first = true; while (progress.isRunning()) { progress.logger.info(`${first ? 'attempting' : 'retrying'} ${actionName} action`); - const result = await this._performPointerAction(progress, actionName, action, options); + const result = await this._performPointerAction(progress, actionName, waitForEnabled, action, options); first = false; if (result === 'error:notvisible') { if (options.force) @@ -316,12 +317,12 @@ export class ElementHandle extends js.JSHandle { return 'done'; } - async _performPointerAction(progress: Progress, actionName: string, action: (point: types.Point) => Promise, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:nothittarget' | 'done'> { + async _performPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:nothittarget' | 'done'> { const { force = false, position } = options; if ((options as any).__testHookBeforeStable) await (options as any).__testHookBeforeStable(); if (!force) { - const result = await this._waitForDisplayedAtStablePosition(progress, true /* waitForEnabled */); + const result = await this._waitForDisplayedAtStablePosition(progress, waitForEnabled); if (result !== 'done') return result; } @@ -379,7 +380,7 @@ export class ElementHandle extends js.JSHandle { } _hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'hover', point => this._page.mouse.move(point.x, point.y), options); + return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, point => this._page.mouse.move(point.x, point.y), options); } click(options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { @@ -390,7 +391,7 @@ export class ElementHandle extends js.JSHandle { } _click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'click', point => this._page.mouse.click(point.x, point.y, options), options); + return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, point => this._page.mouse.click(point.x, point.y, options), options); } dblclick(options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { @@ -401,7 +402,7 @@ export class ElementHandle extends js.JSHandle { } _dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'dblclick', point => this._page.mouse.dblclick(point.x, point.y, options), options); + return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, point => this._page.mouse.dblclick(point.x, point.y, options), options); } async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise { diff --git a/test/mouse.spec.ts b/test/mouse.spec.ts index 0d39149229..f4a7506fc4 100644 --- a/test/mouse.spec.ts +++ b/test/mouse.spec.ts @@ -107,6 +107,13 @@ it('should trigger hover state', async({page, server}) => { expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-91'); }); +it('should trigger hover state on disabled button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.$eval('#button-6', (button: HTMLButtonElement) => button.disabled = true); + await page.hover('#button-6', { timeout: 5000 }); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); +}); + it('should trigger hover state with removed window.Node', async({page, server}) => { await page.goto(server.PREFIX + '/input/scrollable.html'); await page.evaluate(() => delete window.Node);