fix(click): better compensate for integer click coordinates in firefox (#33467)
This commit is contained in:
Родитель
50775698ae
Коммит
43e4ba9f2f
|
@ -266,14 +266,22 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
const filtered = quads.map(quad => intersectQuadWithViewport(quad)).filter(quad => computeQuadArea(quad) > 0.99);
|
const filtered = quads.map(quad => intersectQuadWithViewport(quad)).filter(quad => computeQuadArea(quad) > 0.99);
|
||||||
if (!filtered.length)
|
if (!filtered.length)
|
||||||
return 'error:notinviewport';
|
return 'error:notinviewport';
|
||||||
// Return the middle point of the first quad.
|
if (this._page._browserContext._browser.options.name === 'firefox') {
|
||||||
const result = { x: 0, y: 0 };
|
// Firefox internally uses integer coordinates, so 8.x is converted to 8 or 9 when clicking.
|
||||||
for (const point of filtered[0]) {
|
//
|
||||||
result.x += point.x / 4;
|
// This does not work nicely for small elements. For example, 1x1 square with corners
|
||||||
result.y += point.y / 4;
|
// (8;8) and (9;9) is targeted when clicking at (8;8) but not when clicking at (9;9).
|
||||||
|
// So, clicking at (8.x;8.y) will sometimes click at (9;9) and miss the target.
|
||||||
|
//
|
||||||
|
// Therefore, we try to find an integer point within a quad to make sure we click inside the element.
|
||||||
|
for (const quad of filtered) {
|
||||||
|
const integerPoint = findIntegerPointInsideQuad(quad);
|
||||||
|
if (integerPoint)
|
||||||
|
return integerPoint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compensateHalfIntegerRoundingError(result);
|
// Return the middle point of the first quad.
|
||||||
return result;
|
return quadMiddlePoint(filtered[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _offsetPoint(offset: types.Point): Promise<types.Point | 'error:notvisible' | 'error:notconnected'> {
|
private async _offsetPoint(offset: types.Point): Promise<types.Point | 'error:notvisible' | 'error:notconnected'> {
|
||||||
|
@ -920,24 +928,47 @@ function roundPoint(point: types.Point): types.Point {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function compensateHalfIntegerRoundingError(point: types.Point) {
|
function quadMiddlePoint(quad: types.Quad): types.Point {
|
||||||
// Firefox internally uses integer coordinates, so 8.5 is converted to 9 when clicking.
|
const result = { x: 0, y: 0 };
|
||||||
//
|
for (const point of quad) {
|
||||||
// This does not work nicely for small elements. For example, 1x1 square with corners
|
result.x += point.x / 4;
|
||||||
// (8;8) and (9;9) is targeted when clicking at (8;8) but not when clicking at (9;9).
|
result.y += point.y / 4;
|
||||||
// So, clicking at (8.5;8.5) will effectively click at (9;9) and miss the target.
|
}
|
||||||
//
|
return result;
|
||||||
// Therefore, we skew half-integer values from the interval (8.49, 8.51) towards
|
}
|
||||||
// (8.47, 8.49) that is rounded towards 8. This means clicking at (8.5;8.5) will
|
|
||||||
// be replaced with (8.48;8.48) and will effectively click at (8;8).
|
function triangleArea(p1: types.Point, p2: types.Point, p3: types.Point): number {
|
||||||
//
|
return Math.abs(p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2;
|
||||||
// Other browsers use float coordinates, so this change should not matter.
|
}
|
||||||
const remainderX = point.x - Math.floor(point.x);
|
|
||||||
if (remainderX > 0.49 && remainderX < 0.51)
|
function isPointInsideQuad(point: types.Point, quad: types.Quad): boolean {
|
||||||
point.x -= 0.02;
|
const area1 = triangleArea(point, quad[0], quad[1]) + triangleArea(point, quad[1], quad[2]) + triangleArea(point, quad[2], quad[3]) + triangleArea(point, quad[3], quad[0]);
|
||||||
const remainderY = point.y - Math.floor(point.y);
|
const area2 = triangleArea(quad[0], quad[1], quad[2]) + triangleArea(quad[1], quad[2], quad[3]);
|
||||||
if (remainderY > 0.49 && remainderY < 0.51)
|
// Check that point is inside the quad.
|
||||||
point.y -= 0.02;
|
if (Math.abs(area1 - area2) > 0.1)
|
||||||
|
return false;
|
||||||
|
// Check that point is not on the right/bottom edge, because clicking
|
||||||
|
// there does not actually click the element.
|
||||||
|
return point.x < Math.max(quad[0].x, quad[1].x, quad[2].x, quad[3].x) &&
|
||||||
|
point.y < Math.max(quad[0].y, quad[1].y, quad[2].y, quad[3].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findIntegerPointInsideQuad(quad: types.Quad): types.Point | undefined {
|
||||||
|
// Try all four rounding directions of the middle point.
|
||||||
|
const point = quadMiddlePoint(quad);
|
||||||
|
point.x = Math.floor(point.x);
|
||||||
|
point.y = Math.floor(point.y);
|
||||||
|
if (isPointInsideQuad(point, quad))
|
||||||
|
return point;
|
||||||
|
point.x += 1;
|
||||||
|
if (isPointInsideQuad(point, quad))
|
||||||
|
return point;
|
||||||
|
point.y += 1;
|
||||||
|
if (isPointInsideQuad(point, quad))
|
||||||
|
return point;
|
||||||
|
point.x -= 1;
|
||||||
|
if (isPointInsideQuad(point, quad))
|
||||||
|
return point;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document';
|
export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document';
|
||||||
|
|
|
@ -95,12 +95,42 @@ it('should not throw UnhandledPromiseRejection when page closes', async ({ page,
|
||||||
]).catch(e => {});
|
]).catch(e => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should click the 1x1 div', async ({ page }) => {
|
it('should click the aligned 1x1 div', async ({ page }) => {
|
||||||
await page.setContent(`<div style="width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
await page.setContent(`<div style="width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
||||||
await page.click('div');
|
await page.click('div');
|
||||||
expect(await page.evaluate('window.__clicked')).toBe(true);
|
expect(await page.evaluate('window.__clicked')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should click the half-aligned 1x1 div', async ({ page }) => {
|
||||||
|
await page.setContent(`<div style="margin-left: 20.5px; margin-top: 11.5px; width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
||||||
|
await page.click('div');
|
||||||
|
expect(await page.evaluate('window.__clicked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should click the unaligned 1x1 div v1', async ({ page }) => {
|
||||||
|
await page.setContent(`<div style="margin-left: 20.23px; margin-top: 11.65px; width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
||||||
|
await page.click('div');
|
||||||
|
expect(await page.evaluate('window.__clicked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should click the unaligned 1x1 div v2', async ({ page }) => {
|
||||||
|
await page.setContent(`<div style="margin-left: 20.68px; margin-top: 11.13px; width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
||||||
|
await page.click('div');
|
||||||
|
expect(await page.evaluate('window.__clicked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should click the unaligned 1x1 div v3', async ({ page }) => {
|
||||||
|
await page.setContent(`<div style="margin-left: 20.68px; margin-top: 11.52px; width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
||||||
|
await page.click('div');
|
||||||
|
expect(await page.evaluate('window.__clicked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should click the unaligned 1x1 div v4', async ({ page }) => {
|
||||||
|
await page.setContent(`<div style="margin-left: 20.15px; margin-top: 11.24px; width: 1px; height: 1px;" onclick="window.__clicked = true"></div>`);
|
||||||
|
await page.click('div');
|
||||||
|
expect(await page.evaluate('window.__clicked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should click the button after navigation ', async ({ page, server }) => {
|
it('should click the button after navigation ', async ({ page, server }) => {
|
||||||
await page.goto(server.PREFIX + '/input/button.html');
|
await page.goto(server.PREFIX + '/input/button.html');
|
||||||
await page.click('button');
|
await page.click('button');
|
||||||
|
|
Загрузка…
Ссылка в новой задаче