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

Page Object Model [Question] #1604

Closed
tomern opened this issue Mar 31, 2020 · 33 comments
Closed

Page Object Model [Question] #1604

tomern opened this issue Mar 31, 2020 · 33 comments

Comments

@tomern
Copy link

tomern commented Mar 31, 2020

Hi, I would like to know how can you implement POM design pattern with playwright.
Thanks.

@arjunattam
Copy link
Contributor

Hi @tomern, you can use the page object in Playwright and wrap it in a Page Object Model. For example:

class CheckoutPage {
  constructor(page) {
    this.page = page;
  }
  
  async clickSubmit() {
    return this.page.click('text=Submit');
  }
}

Feel free to reopen if you have a follow-up question.

@randomactions
Copy link

Arjun, thanks for your response!

How do I use functions declared in one page object in another page object?

@arjunattam
Copy link
Contributor

Hi @randomactions, the best approach would depend on your use-case. Couple of approaches to consider:

  • Use class inheritance and create base page classes that define common methods
  • Define utility methods separately and import them inside page objects

@randomactions
Copy link

randomactions commented Jul 8, 2020

Thanks, Arjun!

@tymfear
Copy link

tymfear commented Jul 8, 2020

@randomactions I would strongly recommend against usage of inheritance - that may lead to circular dependencies.
The better approach would be a set of helper methods combined with Page Element pattern.
E.g.

// Page element class
class ConfirmDialog {
    constructor(page) {
       this.page = page;
  }

  async confirm() {
     return this.page.click('text=Confirm')
  }
}
// Page object class
class CheckoutPage {
  constructor(page) {
    this.page = page;
    this.confirmDialog = new ConfirmDialog(page);
  }
  
  async clickSubmit() {
    return this.page.click('text=Submit');
  }
}
//test 
const checkoutPage = new CheckoutPage(page);
await checkoutPage.clickSubmit();
await checkoutPage.confirmDialog.confirm();

@randomactions
Copy link

@tymfear thank you!

@vrknetha
Copy link

vrknetha commented Jul 8, 2020

import {Page, BrowserContext} from 'playwright';

export default class FacebookUtilities {
	page: Page;
	context: BrowserContext;
	constructor(page: Page, context: BrowserContext) {
		this.page = page;
		this.context = context;
	}

	TxtBxLogin = `xpath=//input[@id='email']`;
	TxtBxPassword = `xpath=//input[@id='pass']`;
	BtnLogin = `xpath=//button[@id='loginbutton']`;
	BtnProfileCancel = `xpath=//button[@id='u_0_0']`;
	BtnProfileContinue = `xpath=//button[@name='__CONFIRM__']`;
	BtnGroupsNotNow = `xpath=//button[@name='__SKIP__']`;
	BtnGroupsOk = `xpath=//button[@name='__CONFIRM__']`;
	LinkChooseWhatToAllow = `xpath=//a[@id='u_0_19']`;

	//Login
	login = async (email: string, password: string) => {
		await this.page.fill(this.TxtBxLogin, email);
		await this.page.fill(this.TxtBxPassword, password);
		await this.page.click(this.BtnLogin);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	//Profile Page
	profilePopupClickCancel = async () => {
		await this.page.waitForSelector(this.BtnProfileCancel);
		await this.page.click(this.BtnProfileCancel);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	profilePopupClickContinue = async () => {
		await this.page.waitForSelector(this.BtnProfileContinue);
		await this.page.click(this.BtnProfileContinue);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	//Groups Page
	groupsPageClickOnNotNow = async () => {
		await this.page.waitForSelector(this.BtnGroupsNotNow);
		await this.page.click(this.BtnGroupsNotNow);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	groupsPageClickOnOk = async () => {
		await this.page.waitForSelector(this.BtnGroupsOk);
		await this.page.click(this.BtnGroupsOk);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

This is how I have implemented. This would expose the selectors too just in case if you want to assert them in a page.

@randomactions
Copy link

Thank you for all the answers!

How would this all look like with TypeScript? Did anyone tried doing it in TS?

@SalmonMode
Copy link

I ended up here after a random google search, but thought I would add my 2 cents for anyone else that comes across this, in case it helps.

I made my own page object framework a while back in Python, and intend to port it over to JS/TS when I get a good amount of time and figure out a nice, idiomatic way to do it in those languages (I used descriptors in Python, and JS has a heavy focus on async stuff, so it doesn't translate perfectly).

The term "Page Object Model" was, historically, very overloaded and misused, and led a ton of people to less-than-effective and complex implementations. The "Model" part of that is also meant to indicate that the term represents a design pattern, so an individual implementation would adhere to/follow that model, and the implementation would just be a "page object". To help clear this up, new terminology was established (beyond just the model vs object thing), and now, "page component objects" are an explicit thing in the "Page Object Model".

My framework is neither a model, nor an object (although it does have objects, and adheres to the Page Object Model), i.e. it is a framework that you can use to build individual page component objects and page objects around. The terminology can help a lot when trying to figure something out.

These components are basically just objects, where each one:

  1. Knows how to locate/retrieve the web element it represents (and contains that logic internally so you don't have to deal with it once the object is made)
  2. Provides methods that let you leverage the actual element's functionality (e.g. filling out and submitting a login form)
  3. Allows references to other components for other elements inside it (e.g. a login form component having username and password components)

(Keep in mind, that neither the page object nor the component objects should have any idea that they're running in a test. The goal is to separate concerns, not conflate them)

This is called "object composition", and helps immensely by compartmentalizing logic, making things reusable, and avoiding circular dependencies, like @tymfear mentioned.

You could make each component/page ad hoc, having all the code contained therein, with no inheritance, but I strongly recommend against it. Instead, if you can't find an already existing framework, I recommend making your own. Mine took me a while to make, but that's only because I wanted to be super fancy with it. But really, something just as effective, albeit less fancy shouldn't take much time. I'm thinking something like this:

class Page {
  constructor(driver) {
    this.driver = driver;
  }
  async waitUntil(callback, timeout) {
    let endTime = new Date()).getTime() + timeout;
    let result = await callback(this).catch( err => false);
    while (!result && new Date()).getTime() < endTime){
      new Promise(resolve => setTimeout(resolve, 500));
      result = await callback(this).catch( err => false);
    }
    if (result) {
      return result;
    }
    throw new TimeoutError();
  }
}

class PageComponent {
  constructor(driver, parent) {
    this.driver = driver;
    this.parent = parent;
  }
  async getElement() {
    return await this.driver.findElement(this._locator);
  }
  async sendKeys(keys) {
    await this.getElement().sendKeys(keys);
  }
  async waitUntil(callback, timeout) {
    let endTime = new Date()).getTime() + timeout;
    let result = await callback(this).catch( err => false);
    while (!result && new Date()).getTime() < endTime){
      new Promise(resolve => setTimeout(resolve, 500));
      result = await callback(this).catch( err => false);
    }
    if (result) {
      return result;
    }
    throw new TimeoutError();
  }
}

and then you could have:

class UsernameField extends PageComponent {
  _locator = By.CSS("#username");
}
class PasswordField extends PageComponent {
  _locator = By.CSS("#username");
}
class LoginForm extends PageComponent {
  _locator = By.CSS("form#login");
  constructor(driver, parent) {
    super(driver, parent);
    this.username = new UsernameField(this.driver, this);
    this.password = new PasswordField(this.driver, this);
  }
  async fillOut(user) {
    await this.username.sendKeys(user.username);
    await this.password.sendKeys(user.password);
  }
  await submit() {
    await this.getElement().submit();
  }
}

class LoginPage extends Page {
  constructor(driver) {
    super(driver);
    this.loginForm = new LoginForm(driver, this);
    // make responsible for knowing how to wait for the DOM to finish updating when the page is first loaded
    this.waitUntil(page => await page.loginForm.getElement().isDisplayed());
  }
  async login(user) {
    await this.loginForm.fillOut(user);
    await this.loginForm.submit();
  }
}

I have no idea if that'll work as is, but hopefully that gets the idea across.

With this approachh, Playwright, Puppeteer, Selenium, whatever; it doesn't really matter. The browser-driving agent is just that: an interface that your page objects can use to tell the browser what to do/get. In regards to implementing page objects, the difference between the agents is like the difference between a brick and a rock when it comes to hammering a nail; it really just affects the particulars of how exactly you smack the nail with it.

In this case, it's just a few pieces of the framework that would need to be adjusted depending on the API of the particular API you're using. My examples used my rough recollection of Selenium's JS API, but as an example for getting the associated element, for Playwright, instead of return await this.driver.findElement(this._locator), it would be return await this.page.$(this._locator) (the locator would also need to change for Playwright's needs, but you get the idea).

Hopefully this helps!

@chilikasha
Copy link

hi all,

I would to use page object model and using playwright docs I added (just an example):

class LoginPage {
    constructor(page) {
        this.page = page
    }
    async doSmth() {
        await this.page.waitForSelector('selector')
    }
}
module.exports = { LoginPage }

and in a test file I want to import LoginPage once and use it across several tests (using it , mocha runner).
but what only works for me is doing in each test/hook:

const loginPage = new LoginPage(page)
await loginPage.doSmth()

@arjunattam maybe you could advice smth.
thanks in advance

@SalmonMode
Copy link

@akapitoha if you're looking to run multiple assertions against a page when it's in a particular state, without having any of those assertions prevent any of the others from running should they fail, then you can bundle all your its together in a describe block, and use a before to set everything up, instead of a beforeEach. Then you can have as many assertions as you want by putting them in their own its. But be careful not to make any state changing calls in those its, as that can make your tests problematic as they'll be dependent on executing in a very specific order, and won't be able to be run on their own.

@arjunattam
Copy link
Contributor

Thanks @akapitoha. @SalmonMode's suggestion is excellent, and I would recommend this approach for Mocha.

We are experimenting with a different approach in playwright-test with which you can provide your tests with a loginPage. This is called fixtures -- full syntax here. We are collecting feedback on playwright-test and working towards making it stable.

// In fixtures.ts
builder.loginPage.init(async ({page}, runTest) => {
  // get built-in page and wrap with POM
  const loginPage = new LoginPage(page);

  // pass this to your test
  runTest(loginPage);
}); 
// In your test
it('should login', async ({ loginPage }) => {
  // use loginPage in your test function
});

@chilikasha
Copy link

@SalmonMode @arjunattam thanks a lot for your quick replies.
guess I already try to implement suggestion with describe block (if I understood it correctly), like

const { LibraryPage } = require("../pages/libraryPage.js")

for (const browserType of ["chromium", "webkit"]) {
  describe("login", async () => {
    let browser
    let page
    const loginPage = new LoginPage(page)

    before(async () => {
      browser = await playwright[browserType].launch({ headless: true })
      page = await browser.newPage()
      await page.setDefaultTimeout(15000)
      await page.setDefaultNavigationTimeout(30000)
    })

    it(`(${browserType}): test 1`, async () => {
      await loginPage.navigate()
    })

    it(`(${browserType}): test 2`, async () => {
      await loginPage.navigate()
    })
  })
}

in both tests I try to call a function from PO, but get TypeError: Cannot read property 'goto' of undefined
(goto is used in navigate()

@SalmonMode
Copy link

I'm guessing loginPage.navigate() is a state changing call, so I wouldn't use it in the it. I'd have to see the rest of the code to know what's fully going on though.

@chilikasha
Copy link

chilikasha commented Nov 7, 2020

it just opens a login page

class LoginPage {
	constructor(page) {
		this.page = page
	}
	async navigate() {
		await this.page.goto("login_page_url")
	}
}
module.exports = { LoginPage }

@SalmonMode
Copy link

Ohhh, I see what's going on. It's undefined, because you passed in undefined when you first made the page object. My bad.

Try instantiating your page object in the before right after you assign a value to page.

@chilikasha
Copy link

yeah, works now across all its after adding loginPage = new LoginPage(page) in before hook. thank you so much!

@d4niloArantes
Copy link

I was looking for page object models pattern when I found this discussion. There is an implementation on the Playwright documentation here: https://playwright.dev/docs/pom

@DanielStoica85
Copy link

import {Page, BrowserContext} from 'playwright';

export default class FacebookUtilities {
	page: Page;
	context: BrowserContext;
	constructor(page: Page, context: BrowserContext) {
		this.page = page;
		this.context = context;
	}

	TxtBxLogin = `xpath=//input[@id='email']`;
	TxtBxPassword = `xpath=//input[@id='pass']`;
	BtnLogin = `xpath=//button[@id='loginbutton']`;
	BtnProfileCancel = `xpath=//button[@id='u_0_0']`;
	BtnProfileContinue = `xpath=//button[@name='__CONFIRM__']`;
	BtnGroupsNotNow = `xpath=//button[@name='__SKIP__']`;
	BtnGroupsOk = `xpath=//button[@name='__CONFIRM__']`;
	LinkChooseWhatToAllow = `xpath=//a[@id='u_0_19']`;

	//Login
	login = async (email: string, password: string) => {
		await this.page.fill(this.TxtBxLogin, email);
		await this.page.fill(this.TxtBxPassword, password);
		await this.page.click(this.BtnLogin);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	//Profile Page
	profilePopupClickCancel = async () => {
		await this.page.waitForSelector(this.BtnProfileCancel);
		await this.page.click(this.BtnProfileCancel);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	profilePopupClickContinue = async () => {
		await this.page.waitForSelector(this.BtnProfileContinue);
		await this.page.click(this.BtnProfileContinue);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	//Groups Page
	groupsPageClickOnNotNow = async () => {
		await this.page.waitForSelector(this.BtnGroupsNotNow);
		await this.page.click(this.BtnGroupsNotNow);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

	groupsPageClickOnOk = async () => {
		await this.page.waitForSelector(this.BtnGroupsOk);
		await this.page.click(this.BtnGroupsOk);
		await this.page.waitForNavigation({waitUntil: `domcontentloaded`});
	};

This is how I have implemented. This would expose the selectors too just in case if you want to assert them in a page.

How do you make assertions on those selectors?

@navedanjum
Copy link

navedanjum commented Dec 3, 2021

One can also extend playwright fixture for initialization and then use it directly in tests as fixtures

import { test as baseTest } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { ModelPage } from '../pages/ModelPage';

const test = baseTest.extend<{
  loginPage: LoginPage;
  modelPage: ModelPage;

}>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  modelPage: async ({ page }, use) => {
    await use(new ModelPage(page));
  },
});

export default test;

test.describe('Login to App', () => {
  test('Login invalid', async ({ loginPage }) => {
    await loginPage.goToLoginPage();
    await loginPage.login('XXXX@test.com ', 'XXXX');
    const isErrorMsgDisplayed = await loginPage.isMessageDisplayed('Invalid username or password.');
    await expect(isErrorMsgDisplayed).toBeTruthy();
  });

// Can use directly page fixture or any fixture on top of page from Pages constructor class
test('Login invalid', async ({ loginPage, page  }) => {
     // Do your stuff on loginPage and directly on any page in current context

});

Note: I must add that , example provided on the playwright doc - seems little off and incorrect especially putting all the locators in constructor since most of locators could be left undefined for reactive apps Ref: https://playwright.dev/docs/test-pom (works only in case of static locators).

@nicojs
Copy link
Contributor

nicojs commented Jan 4, 2022

Hi 🙋‍♂️, an interesting discussion you have here 🤗. Pitching in with my findings after a few weeks of playwright experience.

General PageObject rules

  1. All interaction with the page should be done via page objects. I.e.: No selectors in your .spec files. No page internals should leak to the .spec files.
  2. All assertions should be done in your tests. I.e. no expect... in your page objects.
  3. Page objects can contain other page objects, but should always work with a designated host (i.e. a Locator or a Page)

Playwright additional rules

  1. Favor locators over element handles.
    use page.locator over page.$ or page.$$. Locators are less brittle because of automatic waiting. No need for custom "wait for" logic
  2. Favor await expect(locator)... over expect(await locator....)....
    This is in conflict with the first general PageObject rule a bit, as it leaks page details to the expectations. For example: await expect(page.header).toContainText('Title') leaks the detail that the header contains the title as text content. But the advantage outways the downside here: your tests will be far less brittle, because of automatic waiting.

Example:

I generally create 2 small abstract base classes: Page and PageObject. A Page wraps a Playwright Page object. A PageObject wraps a Playwright Locator.

import { Page as PlaywrightPage, Locator } from 'playwright';

export abstract class PageObject {
  constructor(public readonly host: Locator) {}
}
export abstract class Page {
  constructor(public readonly page: PlaywrightPage) {}
  async close() {
    await this.page.close();
  }
}

Then I use that to create Page classes. Each Page class can contain locators and PageObjects. Both Pages and PageObjects can contain methods to help with interaction. For example:

export class Homepage extends Page {
  public readonly h1 = this.page.locator('h1');
  public readonly accordion = new Accordion(this.page.locator('.accordion'));
}

An example of the Accordion page object can be:

export class Accordion extends PageObject {
  public getCard(cardHeader: string): AccordionCardPageObject {
    const cardHost = this.host.locator(
      `:has(.card-header:has-text("${cardHeader}"))`
    );
    return new AccordionCard(cardHost);
  }
}
export class AccordionCard extends PageObject {
  public readonly body = this.host.locator(".card-body");
  public readonly header = this.host.locator(".card-header");
  
  public async activate(): Promise<void> {
    await this.host.locator(".card-header button").click();
  }
}

An example of a test:

test.describe('Homepage', () => {

  let sut: Homepage;

  test.beforeEach(async ({ page: p }) => {
    sut = new Homepage(p);
    await sut.navigate();
  });

  test('should have title "Welcome to the page"', async () => {
    await expect(sut.page).toHaveTitle('Welcome to the page');
  });

  test('should show an explanation "Welcome"', async () => {
    const card = sut.accordion.getCard("Welcome");
    await expect(card.body).toBeVisible();
  });
});

@digpulls
Copy link

digpulls commented May 7, 2022

PlaywrightPage

Hi, @nicojs thanks for sharing this.
Could you share where PlaywrightPage type is coming from in your example?

@nicojs
Copy link
Contributor

nicojs commented May 7, 2022

Could you share where PlaywrightPage type is coming from in your example?

Ah sorry, it wasn't clear before. It's the playwright Page class:

import { Page as PlaywrightPage, Locator } from 'playwright';

I've updated the example.

@vit-ganich
Copy link

@nicojs, thanks a lot for the example, it's really useful.
Have you ever considered creating a kind of default PageObject and using it for every page element without a specific element type?
For example, Accordion for accordions, and Default for buttons, textboxes, etc. Just for the consistency.

Thanks!

@nicojs
Copy link
Contributor

nicojs commented May 28, 2022

I'm not sure I follow you. Do you mean 1 singleton instance of a page object?

@jordan-paz
Copy link

jordan-paz commented Jul 7, 2022

@nicojs Thank you so much for sharing this, it has been incredibly helpful 🙏 . If I got this right, the main benefit of instantiating a PageObject with a Locator rather than a PlaywrightPage is to restrict the scope of the PageObject's selectors to the PageObject's subtree, i.e., selectors will only have visibility into the PageObject's subtree and therefore don't have to be uber specific. Is there anything else I'm missing?

@nicojs
Copy link
Contributor

nicojs commented Jul 7, 2022

That's exactly right @jordan-paz 🙏

@gajus
Copy link

gajus commented Aug 23, 2022

You can always use Proxy for this, e.g.

import { test as base, type Page, type Locator } from '@playwright/test';

type MyPage = Page & {
  findByRole: (role: string, name: string) => Locator;
};

export const test = base.extend<{ page: MyPage }>(
  {
    page: ({ page }, use) => {
      use(
        new Proxy(page, {
          get(target, property, receiver) {
            if (property === 'findByRole') {
              return (role: string, name: string) => {
                return target.locator(`role=${role}[name="${name}"]`);
              };
            }

            return Reflect.get(target, property, receiver);
          },
        })
      );
    },
  }
);

and now in tests I can do things like:

await expect(page.findByRole('link', 'Explore Other Projects')).toBeVisible();

@stkevintan
Copy link

stkevintan commented Sep 6, 2022

Hi, Thanks @nicojs , I've use the locator like what you did. but I found that the locators will be unusable after the page navigated.
since the call page.locator('xxx') is a shorthand for page.mainFrame().locator('xxx') . the main frame will be changed after page navigated. so the locator will point to the old (disposed) frame, which cannot be use again.

The only remedy I came over is to turn the property to a getter like:

class PageA extends PageObject {
-     public readonly body = this.host.locator(".card-body");
+    public get body() {
+        return this.host.locator('.card-body');
+    }
}

But this approach seems a bit tedious, do we have any better alternative ?

@NimMM90
Copy link

NimMM90 commented Dec 8, 2022

But this approach seems a bit tedious, do we have any better alternative ?

Given that ChatGPT is doing it's thing, I tried asking this prompt and it actually collated some stuff out of the docs that forms
an alternative:

One way to avoid this issue is to use the page.mainFrame().locator() method instead of the page.locator() shorthand. This method returns a selector that is bound to the page's main frame, but it does not dispose of the main frame when the page navigates, allowing the selector to continue to be used.

Another way to handle this issue is to use the Selector method's withAttribute and withText options to create selectors that are not tied to a specific frame. These options allow you to create selectors that match elements based on their attributes and text content, rather than their location in the page's DOM.

Now if that is actually cleaner than just making a nice set of getters, not sure. I feel like that's more of a stylistic choice . Given that this is old information, and any "swap to main frame" code would add boilerplate, I doubt there's much to be done here

@godon019
Copy link

@nicojs thanks for the detailed example. but why having assertions in POM is not recommended?

@Tornadoes
Copy link

Tornadoes commented Oct 25, 2023

@nicojs thanks for the detailed example. but why having assertions in POM is not recommended?

If a QA member is not familiar with the code, it will take longer to understand what the assertions are doing.
Also, wrapping one liners like an assertion in the POM class doesn't make too much sense.

More information: https://www.reddit.com/r/QualityAssurance/comments/1248csz/comment/je0dl6j/?utm_source=share&utm_medium=web2x&context=3

@squareloop1
Copy link

squareloop1 commented Jan 22, 2024

Hi 🙋‍♂️, an interesting discussion you have here 🤗. Pitching in with my findings after a few weeks of playwright experience.

General PageObject rules

  1. All interaction with the page should be done via page objects. I.e.: No selectors in your .spec files. No page internals should leak to the .spec files.
  2. All assertions should be done in your tests. I.e. no expect... in your page objects.
  3. Page objects can contain other page objects, but should always work with a designated host (i.e. a Locator or a Page)

Playwright additional rules

  1. Favor locators over element handles.
    use page.locator over page.$ or page.$$. Locators are less brittle because of automatic waiting. No need for custom "wait for" logic
  2. Favor await expect(locator)... over expect(await locator....)....
    This is in conflict with the first general PageObject rule a bit, as it leaks page details to the expectations. For example: await expect(page.header).toContainText('Title') leaks the detail that the header contains the title as text content. But the advantage outways the downside here: your tests will be far less brittle, because of automatic waiting.

Example:

I generally create 2 small abstract base classes: Page and PageObject. A Page wraps a Playwright Page object. A PageObject wraps a Playwright Locator.

import { Page as PlaywrightPage, Locator } from 'playwright';

export abstract class PageObject {
  constructor(public readonly host: Locator) {}
}
export abstract class Page {
  constructor(public readonly page: PlaywrightPage) {}
  async close() {
    await this.page.close();
  }
}

Then I use that to create Page classes. Each Page class can contain locators and PageObjects. Both Pages and PageObjects can contain methods to help with interaction. For example:

export class Homepage extends Page {
  public readonly h1 = this.page.locator('h1');
  public readonly accordion = new Accordion(this.page.locator('.accordion'));
}

An example of the Accordion page object can be:

export class Accordion extends PageObject {
  public getCard(cardHeader: string): AccordionCardPageObject {
    const cardHost = this.host.locator(
      `:has(.card-header:has-text("${cardHeader}"))`
    );
    return new AccordionCard(cardHost);
  }
}
export class AccordionCard extends PageObject {
  public readonly body = this.host.locator(".card-body");
  public readonly header = this.host.locator(".card-header");
  
  public async activate(): Promise<void> {
    await this.host.locator(".card-header button").click();
  }
}

An example of a test:

test.describe('Homepage', () => {

  let sut: Homepage;

  test.beforeEach(async ({ page: p }) => {
    sut = new Homepage(p);
    await sut.navigate();
  });

  test('should have title "Welcome to the page"', async () => {
    await expect(sut.page).toHaveTitle('Welcome to the page');
  });

  test('should show an explanation "Welcome"', async () => {
    const card = sut.accordion.getCard("Welcome");
    await expect(card.body).toBeVisible();
  });
});

Trying to implement this as it would make life alot easier, but I always get a Error: Locators must belong to the same frame. if I try to add a PageObject to a page. Is it because my page is injected as a fixture in my test?

I've also tried the solution with getters mentioned by @stkevintan but that didnt seem to work either. Seems like its mainly an issue with chained locators while a page is working fine?

// Edit: Ok, seems like the issue was something else. I had a literal string in a or() locator instead of a real locator which seem to have caused this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests