mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 03:39:48 +03:00
feat(click): provide preview of the element intercepting pointer events (#3449)
This commit is contained in:
parent
85c93e91a7
commit
69e1e713ef
10
src/dom.ts
10
src/dom.ts
@ -306,10 +306,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||||||
progress.logger.info(' element is outside of the viewport');
|
progress.logger.info(' element is outside of the viewport');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (result === 'error:nothittarget') {
|
if (typeof result === 'object' && 'hitTargetDescription' in result) {
|
||||||
if (options.force)
|
if (options.force)
|
||||||
throw new Error('Element does not receive pointer events');
|
throw new Error(`Element does not receive pointer events, ${result.hitTargetDescription} intercepts them`);
|
||||||
progress.logger.info(' element does not receive pointer events');
|
progress.logger.info(` ${result.hitTargetDescription} intercepts pointer events`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -317,7 +317,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||||||
return 'done';
|
return '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'> {
|
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' | { hitTargetDescription: string } | '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();
|
||||||
@ -685,7 +685,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | 'error:nothittarget' | 'done'> {
|
async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> {
|
||||||
const frame = await this.ownerFrame();
|
const frame = await this.ownerFrame();
|
||||||
if (frame && frame.parentFrame()) {
|
if (frame && frame.parentFrame()) {
|
||||||
const element = await frame.frameElement();
|
const element = await frame.frameElement();
|
||||||
|
@ -479,15 +479,36 @@ export default class InjectedScript {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkHitTargetAt(node: Node, point: types.Point): 'error:notconnected' | 'error:nothittarget' | 'done' {
|
checkHitTargetAt(node: Node, point: types.Point): 'error:notconnected' | 'done' | { hitTargetDescription: string } {
|
||||||
let element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
let element: Element | null | undefined = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||||
if (!element || !element.isConnected)
|
if (!element || !element.isConnected)
|
||||||
return 'error:notconnected';
|
return 'error:notconnected';
|
||||||
element = element.closest('button, [role=button]') || element;
|
element = element.closest('button, [role=button]') || element;
|
||||||
let hitElement = this.deepElementFromPoint(document, point.x, point.y);
|
let hitElement = this.deepElementFromPoint(document, point.x, point.y);
|
||||||
while (hitElement && hitElement !== element)
|
const hitParents: Element[] = [];
|
||||||
|
while (hitElement && hitElement !== element) {
|
||||||
|
hitParents.push(hitElement);
|
||||||
hitElement = this._parentElementOrShadowHost(hitElement);
|
hitElement = this._parentElementOrShadowHost(hitElement);
|
||||||
return hitElement === element ? 'done' : 'error:nothittarget';
|
}
|
||||||
|
if (hitElement === element)
|
||||||
|
return 'done';
|
||||||
|
const hitTargetDescription = this.previewNode(hitParents[0]);
|
||||||
|
// Root is the topmost element in the hitTarget's chain that is not in the
|
||||||
|
// element's chain. For example, it might be a dialog element that overlays
|
||||||
|
// the target.
|
||||||
|
let rootHitTargetDescription: string | undefined;
|
||||||
|
while (element) {
|
||||||
|
const index = hitParents.indexOf(element);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (index > 1)
|
||||||
|
rootHitTargetDescription = this.previewNode(hitParents[index - 1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
element = this._parentElementOrShadowHost(element);
|
||||||
|
}
|
||||||
|
if (rootHitTargetDescription)
|
||||||
|
return { hitTargetDescription: `${hitTargetDescription} from ${rootHitTargetDescription} subtree` };
|
||||||
|
return { hitTargetDescription };
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchEvent(node: Node, type: string, eventInit: Object) {
|
dispatchEvent(node: Node, type: string, eventInit: Object) {
|
||||||
|
@ -31,7 +31,7 @@ it.skip(WIRE)('should fail when element jumps during hit testing', async({page,
|
|||||||
expect(clicked).toBe(false);
|
expect(clicked).toBe(false);
|
||||||
expect(await page.evaluate('window.clicked')).toBe(undefined);
|
expect(await page.evaluate('window.clicked')).toBe(undefined);
|
||||||
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
|
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
|
||||||
expect(error.message).toContain('element does not receive pointer events');
|
expect(error.message).toContain('<body>…</body> intercepts pointer events');
|
||||||
expect(error.message).toContain('retrying click action');
|
expect(error.message).toContain('retrying click action');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ it('should timeout waiting for hit target', async({page, server}) => {
|
|||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
document.body.style.position = 'relative';
|
document.body.style.position = 'relative';
|
||||||
const blocker = document.createElement('div');
|
const blocker = document.createElement('div');
|
||||||
|
blocker.id = 'blocker';
|
||||||
blocker.style.position = 'absolute';
|
blocker.style.position = 'absolute';
|
||||||
blocker.style.width = '400px';
|
blocker.style.width = '400px';
|
||||||
blocker.style.height = '20px';
|
blocker.style.height = '20px';
|
||||||
@ -50,6 +51,36 @@ it('should timeout waiting for hit target', async({page, server}) => {
|
|||||||
});
|
});
|
||||||
const error = await button.click({ timeout: 5000 }).catch(e => e);
|
const error = await button.click({ timeout: 5000 }).catch(e => e);
|
||||||
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
|
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
|
||||||
expect(error.message).toContain('element does not receive pointer events');
|
expect(error.message).toContain('<div id="blocker"></div> intercepts pointer events');
|
||||||
|
expect(error.message).toContain('retrying click action');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report wrong hit target subtree', async({page, server}) => {
|
||||||
|
await page.goto(server.PREFIX + '/input/button.html');
|
||||||
|
const button = await page.$('button');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.body.style.position = 'relative';
|
||||||
|
|
||||||
|
const blocker = document.createElement('div');
|
||||||
|
blocker.id = 'blocker';
|
||||||
|
blocker.style.position = 'absolute';
|
||||||
|
blocker.style.width = '400px';
|
||||||
|
blocker.style.height = '20px';
|
||||||
|
blocker.style.left = '0';
|
||||||
|
blocker.style.top = '0';
|
||||||
|
document.body.appendChild(blocker);
|
||||||
|
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.id = 'inner';
|
||||||
|
inner.style.position = 'absolute';
|
||||||
|
inner.style.left = '0';
|
||||||
|
inner.style.top = '0';
|
||||||
|
inner.style.right = '0';
|
||||||
|
inner.style.bottom = '0';
|
||||||
|
blocker.appendChild(inner);
|
||||||
|
});
|
||||||
|
const error = await button.click({ timeout: 5000 }).catch(e => e);
|
||||||
|
expect(error.message).toContain('elementHandle.click: Timeout 5000ms exceeded.');
|
||||||
|
expect(error.message).toContain('<div id="inner"></div> from <div id="blocker">…</div> subtree intercepts pointer events');
|
||||||
expect(error.message).toContain('retrying click action');
|
expect(error.message).toContain('retrying click action');
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user