fix(hover): do not require the element to be enabled before hovering (#3445)

This commit is contained in:
Dmitry Gozman 2020-08-14 13:18:32 -07:00 committed by GitHub
parent c1de95f91f
commit dec8fb7890
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 17 additions and 8 deletions

View file

@ -8,7 +8,8 @@ Some actions like `page.click()` support `{force: true}` option that disable non
| Actions | Performed checks | | Actions | Performed checks |
| ------ | ------- | | ------ | ------- |
| `check()`<br>`click()`<br>`dblclick()`<br>`hover()`<br>`uncheck()` | [Visible]<br>[Stable]<br>[Enabled]<br>[Receiving Events]<br>[Attached] | | `check()`<br>`click()`<br>`dblclick()`<br>`uncheck()` | [Visible]<br>[Stable]<br>[Enabled]<br>[Receiving Events]<br>[Attached] |
| `hover()` | [Visible]<br>[Stable]<br>[Receiving Events]<br>[Attached] |
| `fill()` | [Visible]<br>[Enabled]<br>[Editable]<br>[Attached] | | `fill()` | [Visible]<br>[Enabled]<br>[Editable]<br>[Attached] |
| `dispatchEvent()`<br>`focus()`<br>`press()`<br>`setInputFiles()`<br>`selectOption()`<br>`type()` | [Attached] | | `dispatchEvent()`<br>`focus()`<br>`press()`<br>`setInputFiles()`<br>`selectOption()`<br>`type()` | [Attached] |
| `scrollIntoViewIfNeeded()`<br>`screenshot()` | [Visible]<br>[Stable]<br>[Attached] | | `scrollIntoViewIfNeeded()`<br>`screenshot()` | [Visible]<br>[Stable]<br>[Attached] |

View file

@ -287,11 +287,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}; };
} }
async _retryPointerAction(progress: Progress, actionName: string, action: (point: types.Point) => Promise<void>, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { async _retryPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise<void>,
options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
let first = true; let first = true;
while (progress.isRunning()) { while (progress.isRunning()) {
progress.logger.info(`${first ? 'attempting' : 'retrying'} ${actionName} action`); 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; first = false;
if (result === 'error:notvisible') { if (result === 'error:notvisible') {
if (options.force) if (options.force)
@ -316,12 +317,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return 'done'; return 'done';
} }
async _performPointerAction(progress: Progress, actionName: string, action: (point: types.Point) => Promise<void>, 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<void>, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:nothittarget' | 'done'> {
const { force = false, position } = options; const { force = false, position } = options;
if ((options as any).__testHookBeforeStable) if ((options as any).__testHookBeforeStable)
await (options as any).__testHookBeforeStable(); await (options as any).__testHookBeforeStable();
if (!force) { if (!force) {
const result = await this._waitForDisplayedAtStablePosition(progress, true /* waitForEnabled */); const result = await this._waitForDisplayedAtStablePosition(progress, waitForEnabled);
if (result !== 'done') if (result !== 'done')
return result; return result;
} }
@ -379,7 +380,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
_hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> { _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<void> { click(options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
@ -390,7 +391,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
_click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { _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<void> { dblclick(options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
@ -401,7 +402,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
_dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { _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<string[]> { async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {

View file

@ -107,6 +107,13 @@ it('should trigger hover state', async({page, server}) => {
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-91'); 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}) => { it('should trigger hover state with removed window.Node', async({page, server}) => {
await page.goto(server.PREFIX + '/input/scrollable.html'); await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => delete window.Node); await page.evaluate(() => delete window.Node);