Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: ElementHandle.click() in cross-origin iframes is off target in non-headless mode #7849

Closed
Mr0grog opened this issue Dec 15, 2021 · 10 comments
Labels

Comments

@Mr0grog
Copy link

Mr0grog commented Dec 15, 2021

Bug description

This is most likely an issue in Chromium, but I figured I’d file here first in case folks here know whether it’s actually intended behavior that Puppeteer needs to deal with or if I’m misunderstanding the issue.

In Puppeteer 12.0.0 (Chromium 97.0.4692.0), the DOM.getContentQuads and DOM.getBoxModel protocol methods (and maybe others) seem to behave differently in headless vs. non-headless mode: in headless mode; they return coordinates relative to the main frame’s viewport (as they used to in both modes), but in non-headless mode, they return coordinates relative to the frame the object they are called with is in.

This leads to a situation where click() works in headless mode, but not in non-headless mode (because it’s clicking in the wrong place), and where methods like clickablePoint() or boundingBox() return incorrect coordinates in non-headless mode.

Steps to reproduce the problem:

  1. Load a page structured like:

    <html>
      <body>
        <iframe id="frame" width="300" height="300" style="position: absolute; left: 50px, top: 50px;">
          # contentDocument
            <html>
              <body>
                <div id="inner-content" style="position: absolute; left: 50px, top: 50px;">Hello</div>
              </body>
            </html>
        </iframe>
      </body>
    </html>
    
  2. Try to get the clickable point of #inner-content:

    const frameElement = await page.$("#frame");
    const frame = await frameElement.contentFrame();
    const innerContent = await frame.$("#inner-content");
    const clickPoint = await innerContent.clickablePoint({ x: 0, y: 0 });
    console.log("Point:", clickpoint);
    

Doing the above in headless mode and non-headless mode should log the same coordinate, but does not.

Puppeteer version

12.0.0

Node.js version

16.13.0

npm version

8.1.3

What operating system are you seeing the problem on?

macOS

Relevant log output

No response

@Mr0grog Mr0grog added the bug label Dec 15, 2021
@Mr0grog
Copy link
Author

Mr0grog commented Dec 15, 2021

I was able to work around this by calculating the correct position using .evaluate(), but this is kind of complicated:

const getBoundsInFrame = (node) => node.getBoundingClientRect().toJSON();
const innerBox = await innerContent.evaluate(getBoundsInFrame);
const frameBox = await frameElement.evaluate(getBoundsInFrame);
const clickPoint = { x: frameBox.x + innerBox.x, y: frameBox.y + innerBox.y };
page.click("body", { offset: clickPoint });

(Obviously there’s some work in figuring out what frame(s) to check in order to make that generic.)

@OrKoN
Copy link
Collaborator

OrKoN commented Dec 22, 2021

I am unable to reproduce it. Was it an out of process iframe?

@OrKoN
Copy link
Collaborator

OrKoN commented Dec 22, 2021

This is my script. Do you have an exact script that reproduces the problem?

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: true, // or false
  });
  const page = await browser.newPage();
  await page.goto('bug-click.html');
  const frameElement = await page.$("#frame");
  const frame = await frameElement.contentFrame();
  const innerContent = await frame.$("#inner-content");
  const clickPoint = await innerContent.clickablePoint({ x: 0, y: 0 });
  console.log("Point:", clickPoint);
  await browser.close();
})();
<html>
  <body>
    <iframe id="frame" width="300" height="300" style="position: absolute; left: 50px, top: 50px;">
    </iframe>
    <script>
    	document.getElementById('frame').src = "data:text/html;charset=utf-8," + escape(`<html>
          <body>
            <div id="inner-content" style="position: absolute; left: 50px; top: 50px;">Hello</div>
          </body>
        </html>`);
    </script>
  </body>
</html>

@Mr0grog
Copy link
Author

Mr0grog commented Dec 22, 2021

Was it an out of process iframe?

The iframe where I encountered this is a different domain, so I think it would normally be, but the script I was dealing with uses --disable-features=site-per-process, which I was thinking should prevent it from being out of process. It’s possible I might be misunderstanding that, though.

Do you have an exact script that reproduces the problem?

Not one that is straightforward (I hit this in a reasonably complex legacy tool I’m now maintaining that deals with Airtable). I’ll try to create a complete test case sometime in the next week or so.

@Mr0grog
Copy link
Author

Mr0grog commented Dec 23, 2021

Yep, if the frame is from a different origin (and probably therefore a separate process), that triggers the bug. Here’s a modified version of your script that works:

<!-- bug-click.html -->
<html>
  <body>
    <iframe id="frame" width="300" height="300" style="position: absolute; left: 50px; top: 50px;" src="http://example.com/">
    </iframe>
  </body>
</html>
// bug-click.js
const puppeteer = require('puppeteer');

async function getClickablePoint(headless) {
  const browser = await puppeteer.launch({ headless });
  const page = await browser.newPage();
  await page.goto(`file://${__dirname}/bug-click.html`);
  const frameElement = await page.$("#frame");
  const frame = await frameElement.contentFrame();
  const innerContent = await frame.$("a");
  const clickPoint = await innerContent.clickablePoint({ x: 0, y: 0 });
  await browser.close();
  return clickPoint;
}

async function testBug() {
  const clickableHeadless = await getClickablePoint(true);
  const clickableVisible = await getClickablePoint(false);
  console.log("These two values should be the same:")
  console.log("Clickable Point (Headless):", clickableHeadless);
  console.log("Clickable Point (Visible): ", clickableVisible);
}

testBug();

And I see this output:

$ node pptr-test/bug-click.js
These two values should be the same:
Clickable Point (Headless): { x: 84, y: 326.875 }
Clickable Point (Visible):  { x: 32, y: 274.875 }

It also doesn’t appear to matter if args: ['--disable-features=site-per-process'] was passed to puppeteer.launch.

@Mr0grog Mr0grog changed the title [Bug]: ElementHandle.click() in an iframe is off target in non-headless mode [Bug]: ElementHandle.click() in cross-origin iframes is off target in non-headless mode Jan 3, 2022
@OrKoN
Copy link
Collaborator

OrKoN commented Jan 14, 2022

So my current understanding is:

  1. headless is likely to have the iframe in the same process therefore the bounds received from the renderer are correct.
  2. OOPIF iframes support was only added in v11 so the headful case would not work before v11. Unfortunately, we didn't account for the click position calculations.

@Mr0grog
Copy link
Author

Mr0grog commented Jan 14, 2022

OOPIF iframes support was only added in v11 so the headful case would not work before v11. Unfortunately, we didn't account for the click position calculations.

Oh, interesting! I thought I had tried this in 11 (I worked my way back from 13 to see when it broke), but I may need to check again. Thanks for helping me understand and clarify the issue! Looking forward to seeing that PR land. 😄 🙏

@Mr0grog
Copy link
Author

Mr0grog commented Jan 17, 2022

The values for clickablePoint, boundingBox, and boxModel are still off in v13.1.0, and it appears to be because the borders of the iframe aren’t accounted for. From the above script:

$ node bug-click.js
These two values should be the same:
Clickable Point (Headless): { x: 84, y: 332.875 }
Clickable Point (Visible):  { x: 82, y: 330.875 }

@OrKoN
Copy link
Collaborator

OrKoN commented Jan 18, 2022

@Mr0grog thanks for double checking. I opened a CL to switch to using content box that should account for paddings/margins/borders properly.

@Mr0grog
Copy link
Author

Mr0grog commented Jan 18, 2022

No problem. Just tried 13.1.1 with that fix, and it’s working great for me — thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment