diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 49ea276fbc..63a1ba099c 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -484,6 +484,17 @@ export class CRPage implements PageDelegate { return {x, y, width, height}; } + async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise { + await this._client.send('DOM.scrollIntoViewIfNeeded', { + objectId: toRemoteObject(handle).objectId, + rect, + }).catch(e => { + if (e instanceof Error && e.message.includes('Node does not have a layout object')) + e.message = 'Node is either not visible or not an HTMLElement'; + throw e; + }); + } + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._client.send('DOM.getContentQuads', { objectId: toRemoteObject(handle).objectId diff --git a/src/dom.ts b/src/dom.ts index 5a581ae945..ab2c3a05fa 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -160,58 +160,12 @@ export class ElementHandle extends js.JSHandle { return this._page._delegate.getContentFrame(this); } - async scrollIntoViewIfNeeded() { - const error = await this._evaluateInUtility(async (node: Node, pageJavascriptEnabled: boolean) => { - if (!node.isConnected) - return 'Node is detached from document'; - if (node.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; - const element = node as Element; - // force-scroll if page's javascript is disabled. - if (!pageJavascriptEnabled) { - // @ts-ignore because only Chromium still supports 'instant' - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - return false; - } - const visibleRatio = await new Promise(resolve => { - const observer = new IntersectionObserver(entries => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); - // Firefox doesn't call IntersectionObserver callback unless - // there are rafs. - requestAnimationFrame(() => {}); - }); - if (visibleRatio !== 1.0) { - // @ts-ignore because only Chromium still supports 'instant' - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - } - return false; - }, !!this._page.context()._options.javaScriptEnabled); - if (error) - throw new Error(error); + async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise { + await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect); } - private async _ensurePointerActionPoint(relativePoint?: types.Point): Promise { - await this.scrollIntoViewIfNeeded(); - if (!relativePoint) - return this._clickablePoint(); - let r = await this._viewportPointAndScroll(relativePoint); - if (r.scrollX || r.scrollY) { - const error = await this._evaluateInUtility((element, scrollX, scrollY) => { - if (!element.ownerDocument || !element.ownerDocument.defaultView) - return 'Node does not have a containing window'; - element.ownerDocument.defaultView.scrollBy(scrollX, scrollY); - return false; - }, r.scrollX, r.scrollY); - if (error) - throw new Error(error); - r = await this._viewportPointAndScroll(relativePoint); - if (r.scrollX || r.scrollY) - throw new Error('Failed to scroll relative point into viewport'); - } - return r.point; + async scrollIntoViewIfNeeded() { + await this._scrollRectIntoViewIfNeeded(); } private async _clickablePoint(): Promise { @@ -253,7 +207,7 @@ export class ElementHandle extends js.JSHandle { return result; } - private async _viewportPointAndScroll(relativePoint: types.Point): Promise<{point: types.Point, scrollX: number, scrollY: number}> { + private async _relativePoint(relativePoint: types.Point): Promise { const [box, border] = await Promise.all([ this.boundingBox(), this._evaluateInUtility((node: Node) => { @@ -273,23 +227,13 @@ export class ElementHandle extends js.JSHandle { point.x += border.x; point.y += border.y; } - const metrics = await this._page._delegate.layoutViewport(); - // Give 20 extra pixels to avoid any issues on viewport edge. - let scrollX = 0; - if (point.x < 20) - scrollX = point.x - 20; - if (point.x > metrics.width - 20) - scrollX = point.x - metrics.width + 20; - let scrollY = 0; - if (point.y < 20) - scrollY = point.y - 20; - if (point.y > metrics.height - 20) - scrollY = point.y - metrics.height + 20; - return { point, scrollX, scrollY }; + return point; } async _performPointerAction(action: (point: types.Point) => Promise, options?: input.PointerActionOptions): Promise { - const point = await this._ensurePointerActionPoint(options ? options.relativePoint : undefined); + const relativePoint = options ? options.relativePoint : undefined; + await this._scrollRectIntoViewIfNeeded(relativePoint ? { x: relativePoint.x, y: relativePoint.y, width: 0, height: 0 } : undefined); + const point = relativePoint ? await this._relativePoint(relativePoint) : await this._clickablePoint(); let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 674146e1de..96e58f6e20 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -417,6 +417,14 @@ export class FFPage implements PageDelegate { return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } + async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise { + await this._session.send('Page.scrollIntoViewIfNeeded', { + frameId: handle._context.frame._id, + objectId: toRemoteObject(handle).objectId!, + rect, + }); + } + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._session.send('Page.getContentQuads', { frameId: handle._context.frame._id, diff --git a/src/page.ts b/src/page.ts index 394eac87c6..d1a4fc6db3 100644 --- a/src/page.ts +++ b/src/page.ts @@ -69,6 +69,7 @@ export interface PageDelegate { setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise; getBoundingBox(handle: dom.ElementHandle): Promise; getFrameElement(frame: frames.Frame): Promise; + scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise; getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>; pdf?: (options?: types.PDFOptions) => Promise; diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index a75e7f5ba6..2b8ee55719 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -545,6 +545,17 @@ export class WKPage implements PageDelegate { return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } + async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise { + await this._session.send('DOM.scrollIntoViewIfNeeded', { + objectId: toRemoteObject(handle).objectId!, + rect, + }).catch(e => { + if (e instanceof Error && e.message.includes('Node does not have a layout object')) + e.message = 'Node is either not visible or not an HTMLElement'; + throw e; + }); + } + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._session.send('DOM.getContentQuads', { objectId: toRemoteObject(handle).objectId! diff --git a/test/click.spec.js b/test/click.spec.js index f60f5ac365..6b9e4f7f8c 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -286,6 +286,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await frame.evaluate(() => window.result)).toBe('Clicked'); }); // @see https://github.com/GoogleChrome/puppeteer/issues/4110 + // @see https://bugs.chromium.org/p/chromium/issues/detail?id=986390 + // @see https://chromium-review.googlesource.com/c/chromium/src/+/1742784 xit('should click the button with fixed position inside an iframe', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await page.setViewportSize({width: 500, height: 500}); @@ -326,7 +328,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 12 * 2 + 20 : 20); expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 12 * 2 + 10 : 10); }); - it('should click a very large button with relative point', async({page, server}) => { + it.skip(FFOX)('should click a very large button with relative point', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', button => button.style.borderWidth = '8px'); await page.$eval('button', button => button.style.height = button.style.width = '2000px'); @@ -336,7 +338,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 1900 + 8 : 1900); expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910); }); - xit('should click a button in scrolling container with relative point', async({page, server}) => { + it.skip(FFOX)('should click a button in scrolling container with relative point', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', button => { const container = document.createElement('div'); @@ -347,11 +349,13 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI container.appendChild(button); button.style.height = '2000px'; button.style.width = '2000px'; + button.style.borderWidth = '8px'; }); await page.click('button', { relativePoint: { x: 1900, y: 1910 } }); expect(await page.evaluate(() => window.result)).toBe('Clicked'); - expect(await page.evaluate(() => offsetX)).toBe(1900); - expect(await page.evaluate(() => offsetY)).toBe(1910); + // Safari reports border-relative offsetX/offsetY. + expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 1900 + 8 : 1900); + expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910); }); it('should update modifiers correctly', async({page, server}) => { @@ -370,7 +374,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('button'); expect(await page.evaluate(() => shiftKey)).toBe(false); }); - it.skip(CHROMIUM)('should click an offscreen element when scroll-behavior is smooth', async({page}) => { + it('should click an offscreen element when scroll-behavior is smooth', async({page}) => { await page.setContent(`
@@ -379,7 +383,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('button'); expect(await page.evaluate('window.clicked')).toBe(true); }); - it.skip(true)('should click on an animated button', async({page}) => { + xit('should click on an animated button', async({page}) => { const buttonSize = 50; const containerWidth = 500; const transition = 500; diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 65620ba44e..4a8efb9de2 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -201,9 +201,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) it('should work for TextNodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild); - let error = null; - await buttonTextNode.click().catch(err => error = err); - expect(error.message).toBe('Node is not of type HTMLElement'); + await buttonTextNode.click(); + expect(await page.evaluate(() => result)).toBe('Clicked'); }); it('should throw for detached nodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -211,7 +210,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) await page.evaluate(button => button.remove(), button); let error = null; await button.click().catch(err => error = err); - expect(error.message).toBe('Node is detached from document'); + expect(error.message).toContain('Node is detached from document'); }); it('should throw for hidden nodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html');