mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +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');
|
||||
continue;
|
||||
}
|
||||
if (result === 'error:nothittarget') {
|
||||
if (typeof result === 'object' && 'hitTargetDescription' in result) {
|
||||
if (options.force)
|
||||
throw new Error('Element does not receive pointer events');
|
||||
progress.logger.info(' element does not receive pointer events');
|
||||
throw new Error(`Element does not receive pointer events, ${result.hitTargetDescription} intercepts them`);
|
||||
progress.logger.info(` ${result.hitTargetDescription} intercepts pointer events`);
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
@ -317,7 +317,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
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;
|
||||
if ((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;
|
||||
}
|
||||
|
||||
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();
|
||||
if (frame && frame.parentFrame()) {
|
||||
const element = await frame.frameElement();
|
||||
|
@ -479,15 +479,36 @@ export default class InjectedScript {
|
||||
});
|
||||
}
|
||||
|
||||
checkHitTargetAt(node: Node, point: types.Point): 'error:notconnected' | 'error:nothittarget' | 'done' {
|
||||
let element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||
checkHitTargetAt(node: Node, point: types.Point): 'error:notconnected' | 'done' | { hitTargetDescription: string } {
|
||||
let element: Element | null | undefined = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||
if (!element || !element.isConnected)
|
||||
return 'error:notconnected';
|
||||
element = element.closest('button, [role=button]') || element;
|
||||
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);
|
||||
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) {
|
||||
|
@ -31,7 +31,7 @@ it.skip(WIRE)('should fail when element jumps during hit testing', async({page,
|
||||
expect(clicked).toBe(false);
|
||||
expect(await page.evaluate('window.clicked')).toBe(undefined);
|
||||
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');
|
||||
});
|
||||
|
||||
@ -41,6 +41,7 @@ it('should timeout waiting for hit target', async({page, server}) => {
|
||||
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';
|
||||
@ -50,6 +51,36 @@ it('should timeout waiting for hit target', async({page, server}) => {
|
||||
});
|
||||
const error = await button.click({ timeout: 5000 }).catch(e => e);
|
||||
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');
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user