diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index 9a07e63f..ef356ba1 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -55,12 +55,20 @@ class ElementHandle extends JSHandle { } async _scrollIntoViewIfNeeded() { - const error = await this.executionContext().evaluate(element => { + const error = await this.executionContext().evaluate(async element => { if (!element.isConnected) return 'Node is detached from document'; if (element.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement'; - element.scrollIntoViewIfNeeded(); + const visibleRatio = await new Promise(resolve => { + const observer = new IntersectionObserver(entries => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + if (visibleRatio !== 1.0) + element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); return false; }, this); if (error) @@ -70,8 +78,7 @@ class ElementHandle extends JSHandle { /** * @return {!Promise<{x: number, y: number}>} */ - async _visibleCenter() { - await this._scrollIntoViewIfNeeded(); + async _boundingBoxCenter() { const box = await this._assertBoundingBox(); return { x: box.x + box.width / 2, @@ -102,7 +109,8 @@ class ElementHandle extends JSHandle { } async hover() { - const {x, y} = await this._visibleCenter(); + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._boundingBoxCenter(); await this._page.mouse.move(x, y); } @@ -110,7 +118,8 @@ class ElementHandle extends JSHandle { * @param {!Object=} options */ async click(options = {}) { - const {x, y} = await this._visibleCenter(); + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._boundingBoxCenter(); await this._page.mouse.click(x, y, options); } @@ -125,7 +134,8 @@ class ElementHandle extends JSHandle { } async tap() { - const {x, y} = await this._visibleCenter(); + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._boundingBoxCenter(); await this._page.touchscreen.tap(x, y); } @@ -222,9 +232,7 @@ class ElementHandle extends JSHandle { needsViewportReset = true; } - await this.executionContext().evaluate(function(element) { - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - }, this); + await this._scrollIntoViewIfNeeded(); boundingBox = await this._assertBoundingBox(); diff --git a/test/assets/offscreenbuttons.html b/test/assets/offscreenbuttons.html new file mode 100644 index 00000000..27192336 --- /dev/null +++ b/test/assets/offscreenbuttons.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/test/input.spec.js b/test/input.spec.js index 5a228e99..7fdc2e56 100644 --- a/test/input.spec.js +++ b/test/input.spec.js @@ -29,6 +29,29 @@ module.exports.addTests = function({testRunner, expect, DeviceDescriptors}) { expect(await page.evaluate(() => result)).toBe('Clicked'); }); + it('should click offscreen buttons', async({page, server}) => { + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages = []; + page.on('console', msg => messages.push(msg.text())); + for (let i = 0; i < 10; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => window.scrollTo(0, 0)); + await page.click(`#btn${i}`); + } + expect(messages).toEqual([ + 'button #0 clicked', + 'button #1 clicked', + 'button #2 clicked', + 'button #3 clicked', + 'button #4 clicked', + 'button #5 clicked', + 'button #6 clicked', + 'button #7 clicked', + 'button #8 clicked', + 'button #9 clicked' + ]); + }); + it('should click on checkbox input and toggle', async({page, server}) => { await page.goto(server.PREFIX + '/input/checkbox.html'); expect(await page.evaluate(() => result.check)).toBe(null);