microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
66.83k stars 3.66k forks source link

[Feature]: OR operator support for frameLocator #32735

Closed qk5 closed 1 month ago

qk5 commented 1 month ago

🚀 Feature Request

The or operator currently does not work with frameLocator.

Example

let frameLoc = page.frameLocator('iframe1').or(page.frameLocator('iframe2));

I also tried a workaround by using: owner() let frameLoc = page.frameLocator('iframe1').owner().or(page.frameLocator('iframe2).owner());

And while adding owner() does not give me any syntax error, when running the code, playwright failed to find my locator.

Example: let button = frameLoc.getByRole('button', { name: 'Submit' }); await expect(button).toBeVisible();

Motivation

It will be nice to support frameLocators with the or operator

mxschmitt commented 1 month ago

You can do this, would that work for you?

const frameLoc = page.locator('iframe1').or(page.locator('iframe2')).contentFrame()

contentFrame 'enters' the iframe.

qk5 commented 1 month ago

You can do this, would that work for you?

const frameLoc = page.locator('iframe1').or(page.locator('iframe2')).contentFrame()

contentFrame 'enters' the iframe.

I tried your method by adding contentFrame() at end, but still not working for me.

mxschmitt commented 1 month ago

I tried to repro with the following, but it seems to work for me:

it('should do it', async ({ page, server }) => {
  page.on('console', m => console.log(m.text()));
  await page.route('**/frame.html*', route => route.fulfill({
    body: `
    <div></div>
    <script>
      const div = document.querySelector('div');
      div.innerText = new URL(location.href).searchParams.get('text');
    </script>
  `,
    contentType: 'text/html',
  }));
  await page.setContent(`
    <iframe name='frame1' src="${server.PREFIX}/frame.html?text=frame1"></iframe>
    <iframe name='frame2' src="${server.PREFIX}/frame.html?text=frame2"></iframe>
  `);
  // in order to fix strict mode violation errors, do ')).contentFrame(); -> ')).first().contentFrame();
  const frameLoc = page.locator('[name=frame1]').or(page.locator('[name=frame2-not-there]')).contentFrame();
  console.log(await frameLoc.locator('body').innerText());
});

would it be possible to share some code/repro with us?

qk5 commented 1 month ago

I tried to repro with the following, but it seems to work for me:

it('should do it', async ({ page, server }) => {
  page.on('console', m => console.log(m.text()));
  await page.route('**/frame.html*', route => route.fulfill({
    body: `
    <div></div>
    <script>
      const div = document.querySelector('div');
      div.innerText = new URL(location.href).searchParams.get('text');
    </script>
  `,
    contentType: 'text/html',
  }));
  await page.setContent(`
    <iframe name='frame1' src="${server.PREFIX}/frame.html?text=frame1"></iframe>
    <iframe name='frame2' src="${server.PREFIX}/frame.html?text=frame2"></iframe>
  `);
  // in order to fix strict mode violation errors, do ')).contentFrame(); -> ')).first().contentFrame();
  const frameLoc = page.locator('[name=frame1]').or(page.locator('[name=frame2-not-there]')).contentFrame();
  console.log(await frameLoc.locator('body').innerText());
});

would it be possible to share some code/repro with us?

Sure, here is my sample page:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Main Page</title>
    <style>
        iframe {
            width: 100%;
            height: 500px;
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <iframe src="mainFrame.html" name="mainFrame"></iframe>
</body>
</html>

mainFrame.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Main Frame</title>
    <style>
        #container {
            display: flex;
            justify-content: space-around;
            height: 100%;
        }
        iframe {
            width: 45%;
            height: 100%;
            border: 1px solid blue;
        }
    </style>
</head>
<body>
    <div id="container">
        <iframe src="frame1.html" name="iframe1"></iframe>
        <iframe src="frame2.html" name="iframe2"></iframe>
    </div>
</body>
</html>

frame1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Frame 1</title>
</head>
<body>
    <label>frame1</label>
    <br>
    <button id="button1">Button 1</button>
</body>
</html>

frame2.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Frame 2</title>
</head>
<body>
    <label>frame2</label>
    <br>
    <button id="button2">Button 2</button>
</body>
</html>

And here is my code snippet:

  let _mainFrame = page.locator('iframe[name="mainFrame"]'); 

  // convert mainFrame to contentFrame() because it has locators I want to use in my script
  let mainFrame = _mainFrame.contentFrame();

  let frameLoc = _mainFrame.locator('iframe[name="iframe1"]')
                  .or(_mainFrame.locator('iframe[name="iframe2"]')).contentFrame();

  let text1 = frameLoc.getByText('frame1');
  let text2 = frameLoc.getByText('frame2');

  await expect(text1).toBeVisible();
  await expect(text2).toBeVisible();

And when I tried to run my code above, playwright timeout when trying to look for the text1 locator.

Any help is appreciated, thank you.

mxschmitt commented 1 month ago

In your example it doesn't look like an 'or' to me, since both of the frames are always there. So you can just do:

  await page.goto('http://127.0.0.1:8080/');
  const mainFrame = page.locator('iframe[name="mainFrame"]').contentFrame();

  const text1 =  mainFrame.locator('iframe[name="iframe1"]').contentFrame().getByText('frame1');
  const text2 =  mainFrame.locator('iframe[name="iframe2"]').contentFrame().getByText('frame2');

  await expect(text1).toBeVisible();
  await expect(text2).toBeVisible();

Would that work for you?

Is there an error message or just a timeout when its failing with text1?

qk5 commented 1 month ago

In your example it doesn't look like an 'or' to me, since both of the frames are always there. So you can just do:

  await page.goto('http://127.0.0.1:8080/');
  const mainFrame = page.locator('iframe[name="mainFrame"]').contentFrame();

  const text1 =  mainFrame.locator('iframe[name="iframe1"]').contentFrame().getByText('frame1');
  const text2 =  mainFrame.locator('iframe[name="iframe2"]').contentFrame().getByText('frame2');

  await expect(text1).toBeVisible();
  await expect(text2).toBeVisible();

Would that work for you?

Is there an error message or just a timeout when its failing with text1?

Thank you for getting back to me.

The example I provided above was a simplify version of my company's internal site, just to demonstrate that if you run above code, you will get a timeout error.

So, about my company's internal website I am trying to automate, the landing page will take me to a page similar to the index.html page (from above). When I land on index.html, it will have a mainframe, and within this mainframe, it may have iframe1, iframe2, follow by different iframe ids (up to 6 different iframes). These iframes within the mainframe are external services, where we have no control of.

For example: Based on my understanding of my internal site, these are the possible iframe combinations:

mainframe > iframe1
mainframe > iframe2
mainframe > iframe1 > iframe3
mainframe > iframe2 > iframe4 > iframe6
mainframe > iframe1 > iframe5 > iframe6

And because I almost always see iframe1 or iframe2 after mainframe, that's why I want to use the OR operator to bypass for a workaround. Please let me know if you need more information, thanks.

While we on this topic, may I also ask if it is possible to see locators within layers of iframes highlighted in Web Browser when focusing on the locator code in VS Code? I can see locators getting highlighted when there is one iframe, but not locator within 2 or more layers of iframes.

yury-s commented 1 month ago

For example: Based on my understanding of my internal site, these are the possible iframe combinations:

You can select one of the frames using Locator.or as Max described above, e.g.:

import { expect, test } from "@playwright/test";

test("Test", async ({ page }) => {
  const iframe1 = page.locator('#mainframe').contentFrame().locator('#iframe1');
  const iframe2 = page.locator('#mainframe').contentFrame().locator('#iframe2');
  const aFrame = iframe1.or(iframe2).contentFrame();
  await page.setContent(`<iframe id='mainframe' srcdoc='<iframe id="iframe2" srcdoc="<div>foo</div>"></iframe>'></iframe>`)
  await expect(aFrame.locator('div')).toHaveText('foo');
});
qk5 commented 1 month ago

For example: Based on my understanding of my internal site, these are the possible iframe combinations:

You can select one of the frames using Locator.or as Max described above, e.g.:

import { expect, test } from "@playwright/test";

test("Test", async ({ page }) => {
  const iframe1 = page.locator('#mainframe').contentFrame().locator('#iframe1');
  const iframe2 = page.locator('#mainframe').contentFrame().locator('#iframe2');
  const aFrame = iframe1.or(iframe2).contentFrame();
  await page.setContent(`<iframe id='mainframe' srcdoc='<iframe id="iframe2" srcdoc="<div>foo</div>"></iframe>'></iframe>`)
  await expect(aFrame.locator('div')).toHaveText('foo');
});

Thank you for getting back to me.

I am sorry to bring this up again, but it looks like you are not trying the exact html code examples I provided above.

I am going to provide my sample html files again (this time with a simplified version without frame2.html), I am also posting the sample code you provided from above (just minor changes), and you will see the timeout error I am getting.

Here is my updated version from the code you provided above:

  const _mainFrame = page.locator('iframe[name="mainFrame"]');
  const mainFrame = _mainFrame.contentFrame();

  const iframe1 = mainFrame.locator('iframe[name="iframe1"]');
  const iframe2 = mainFrame.locator('iframe[name="iframe2"]');

  let frameLoc = iframe1.or(iframe2).contentFrame();
  await expect(frameLoc.locator('div')).toHaveText('frame1');

This is the simplified version of my html codes from above, without the frame2.html file

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Main Page</title>
    <style>
        iframe {
            width: 100%;
            height: 500px;
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <iframe src="mainFrame.html" name="mainFrame"></iframe>
</body>
</html>

mainFrame.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Main Frame</title>
    <style>
        #container {
            display: flex;
            justify-content: space-around;
            height: 100%;
        }
        iframe {
            width: 45%;
            height: 100%;
            border: 1px solid blue;
        }
    </style>
</head>
<body>
    <div id="container">
        <iframe src="frame1.html" name="iframe1"></iframe>
    </div>
</body>
</html>

frame1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Frame 1</title>
</head>
<body>
    <label>frame1</label>
    <br>
    <button id="button1">Button 1</button>
</body>
</html>

Any help is appreciated, thank you.

qk5 commented 1 month ago

And here is a video recording of my run with code and error message.

iframe_issue.webm

qk5 commented 1 month ago

Hi @yury-s and @mxschmitt

I just realized this issue was closed, and I would like to bring this up again because I have provided video recording above to demonstrate that both your solutions are not working for me.

So, instead of using your own examples, could you please create local HTML files using the exact sample codes I provided above and try again?

In addition, I would also like to know if it is possible to see locators within layers of iframes highlighted in Web Browser when focusing on the locator code in VS Code? I can see locators getting highlighted when there is one iframe, but not locators within 2 or more layers of iframes.

Thank you again for both of your time, any help is appreciated.

DetachHead commented 2 weeks ago

related: #12201

would be nice if either of these issues could be re-opened. imo dealing with frames is quite annoying and it would be great if they could just be automatically handled instead

qk5 commented 2 weeks ago

related: #12201

would be nice if either of these issues could be re-opened. imo dealing with frames is quite annoying and it would be great if they could just be automatically handled instead

@DetachHead Just letting you know I have filed a ticket to highlight iframe locators, and the playwright team are working on it, looks like we will have this feature in the next release.

https://github.com/microsoft/playwright/issues/33146

And also, I agreed with you that it will be better if they can consolidate both page and iframe locators together like you suggested in your ticket.

I am not sure if the playwright team still follow on closed ticket or not. If you want, I can open a new ticket if I don't hear from them by end of this week because the issue I demonstrated in this ticket is still not resolved, and you may comment in my new ticket. Or you may open a new ticket, and I can comment there as well.