fix(click): account for detached elements and iframe overlays (#10206)

This commit is contained in:
Dmitry Gozman 2021-11-10 12:14:06 -08:00 committed by GitHub
parent 1e38ec5fa4
commit 9ec3e7cd52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 25 additions and 3 deletions

View File

@ -413,7 +413,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._finishPointerActionDetectLayoutShift(progress, actionName, point, options, action); return this._finishPointerActionDetectLayoutShift(progress, actionName, point, options, action);
} }
private async _finishPointerAction(progress: Progress, actionName: string, point: types.Point, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions, action: (point: types.Point) => Promise<void>) { private async _finishPointerAction(progress: Progress, actionName: string, point: types.Point, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions, action: (point: types.Point) => Promise<void>): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> {
if (!options.force) { if (!options.force) {
if ((options as any).__testHookBeforeHitTarget) if ((options as any).__testHookBeforeHitTarget)
await (options as any).__testHookBeforeHitTarget(); await (options as any).__testHookBeforeHitTarget();
@ -451,7 +451,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return 'done'; return 'done';
} }
private async _finishPointerActionDetectLayoutShift(progress: Progress, actionName: string, point: types.Point, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions, action: (point: types.Point) => Promise<void>) { private async _finishPointerActionDetectLayoutShift(progress: Progress, actionName: string, point: types.Point, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions, action: (point: types.Point) => Promise<void>): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> {
await progress.beforeInputAction(this); await progress.beforeInputAction(this);
let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined; let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined;

View File

@ -728,6 +728,11 @@ export class InjectedScript {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
// Element was detached during the action, for example in some event handler.
// If events before that were correctly pointing to it, consider this a valid scenario.
if (!element.isConnected)
return;
// Determine the event point. Note that Firefox does not always have window.TouchEvent. // Determine the event point. Note that Firefox does not always have window.TouchEvent.
const point = (!!window.TouchEvent && (event instanceof window.TouchEvent)) ? event.touches[0] : (event as MouseEvent | PointerEvent); const point = (!!window.TouchEvent && (event instanceof window.TouchEvent)) ? event.touches[0] : (event as MouseEvent | PointerEvent);
if (!!point && (result === undefined || result === 'done')) { if (!!point && (result === undefined || result === 'done')) {
@ -745,7 +750,11 @@ export class InjectedScript {
const stop = () => { const stop = () => {
if (this._hitTargetInterceptor === listener) if (this._hitTargetInterceptor === listener)
this._hitTargetInterceptor = undefined; this._hitTargetInterceptor = undefined;
return result!; // If we did not get any events, consider things working. Possible causes:
// - JavaScript is disabled (webkit-only).
// - Some <iframe> overlays the element from another frame.
// - Hovering a disabled control prevents any events from firing.
return result || 'done';
}; };
// Note: this removes previous listener, just in case there are two concurrent clicks // Note: this removes previous listener, just in case there are two concurrent clicks

View File

@ -68,6 +68,19 @@ it('should block click when mousedown succeeds but mouseup fails', async ({ page
]); ]);
}); });
it('should click when element detaches in mousedown', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
button.addEventListener('mousedown', () => {
(window as any).result = 'Mousedown';
button.remove();
});
});
await page.click('button', { timeout: 1000 });
expect(await page.evaluate('result')).toBe('Mousedown');
});
it('should not block programmatic events', async ({ page, server }) => { it('should not block programmatic events', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => { await page.$eval('button', button => {