diff --git a/src/server/dom.ts b/src/server/dom.ts index ef84753916..966ea55020 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -276,7 +276,19 @@ export class ElementHandle extends js.JSHandle { options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { let retry = 0; // We progressively wait longer between retries, up to 500ms. - const waitTime = [0, 20, 100, 500]; + const waitTime = [0, 20, 100, 100, 500]; + + // By default, we scroll with protocol method to reveal the action point. + // However, that might not work to scroll from under position:sticky elements + // that overlay the target element. To fight this, we cycle through different + // scroll alignments. This works in most scenarios. + const scrollOptions: (ScrollIntoViewOptions | undefined)[] = [ + undefined, + { block: 'end', inline: 'end' }, + { block: 'center', inline: 'center' }, + { block: 'start', inline: 'start' }, + ]; + while (progress.isRunning()) { if (retry) { progress.log(`retrying ${actionName} action, attempt #${retry}`); @@ -288,7 +300,8 @@ export class ElementHandle extends js.JSHandle { } else { progress.log(`attempting ${actionName} action`); } - const result = await this._performPointerAction(progress, actionName, waitForEnabled, action, options); + const forceScrollOptions = scrollOptions[retry % scrollOptions.length]; + const result = await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options); ++retry; if (result === 'error:notvisible') { if (options.force) @@ -313,7 +326,7 @@ export class ElementHandle extends js.JSHandle { return 'done'; } - async _performPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | { hitTargetDescription: string } | 'done'> { + async _performPointerAction(progress: Progress, actionName: string, waitForEnabled: boolean, action: (point: types.Point) => Promise, forceScrollOptions: ScrollIntoViewOptions | undefined, 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(); @@ -327,9 +340,16 @@ export class ElementHandle extends js.JSHandle { progress.log(' scrolling into view if needed'); progress.throwIfAborted(); // Avoid action that has side-effects. - const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined); - if (scrolled !== 'done') - return scrolled; + if (forceScrollOptions) { + await this._evaluateInUtility(([injected, node, options]) => { + if (node.nodeType === 1 /* Node.ELEMENT_NODE */) + (node as Node as Element).scrollIntoView(options); + }, forceScrollOptions); + } else { + const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined); + if (scrolled !== 'done') + return scrolled; + } progress.log(' done scrolling'); const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint(); diff --git a/test/page-click.spec.ts b/test/page-click.spec.ts index 3b3ff68d9e..ebfd53d8ed 100644 --- a/test/page-click.spec.ts +++ b/test/page-click.spec.ts @@ -338,6 +338,43 @@ it('should click the button with fixed position inside an iframe', (test, { brow expect(await frame.evaluate(() => window['result'])).toBe('Clicked'); }); +it('should click the button behind sticky header', async ({page}) => { + await page.setViewportSize({ width: 500, height: 240 }); + await page.setContent(` + + +
+ +
    +
  1. hi1
  2. hi2
  3. hi3
  4. hi4
  5. hi5
  6. hi6
  7. hi7
  8. hi8
  9. +
  10. hi9
  11. +
  12. hi10
  13. hi11
  14. hi12
  15. hi13
  16. hi14
  17. +
+ `); + await page.$eval('#li14', e => e.scrollIntoView()); + await page.click('#target'); + expect(await page.evaluate(() => window['__clicked'])).toBe(true); +}); + it('should click the button with deviceScaleFactor set', async ({browser, server}) => { const context = await browser.newContext({ viewport: { width: 400, height: 400 }, deviceScaleFactor: 5 }); const page = await context.newPage();