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
Comments
Arjun, thanks for your response! How do I use functions declared in one page object in another page object? |
Hi @randomactions, the best approach would depend on your use-case. Couple of approaches to consider:
|
Thanks, Arjun! |
@randomactions I would strongly recommend against usage of inheritance - that may lead to circular dependencies. // 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(); |
@tymfear thank you! |
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. |
Thank you for all the answers! How would this all look like with TypeScript? Did anyone tried doing it in TS? |
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:
(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 Hopefully this helps! |
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 const loginPage = new LoginPage(page)
await loginPage.doSmth() @arjunattam maybe you could advice smth. |
@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 |
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 // 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
}); |
@SalmonMode @arjunattam thanks a lot for your quick replies. 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 |
I'm guessing |
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 } |
Ohhh, I see what's going on. It's undefined, because you passed in Try instantiating your page object in the |
yeah, works now across all |
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 |
How do you make assertions on those selectors? |
One can also extend playwright fixture for initialization and then use it directly in tests as fixtures
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). |
Hi 🙋♂️, an interesting discussion you have here 🤗. Pitching in with my findings after a few weeks of playwright experience. General PageObject rules
Playwright additional rules
Example:I generally create 2 small abstract base classes: 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 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 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();
});
}); |
Hi, @nicojs thanks for sharing this. |
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. |
@nicojs, thanks a lot for the example, it's really useful. Thanks! |
I'm not sure I follow you. Do you mean 1 singleton instance of a page object? |
@nicojs Thank you so much for sharing this, it has been incredibly helpful 🙏 . If I got this right, the main benefit of instantiating a |
That's exactly right @jordan-paz 🙏 |
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(); |
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. 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 ? |
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
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 |
@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. More information: https://www.reddit.com/r/QualityAssurance/comments/1248csz/comment/je0dl6j/?utm_source=share&utm_medium=web2x&context=3 |
Trying to implement this as it would make life alot easier, but I always get a 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 |
Hi, I would like to know how can you implement POM design pattern with playwright.
Thanks.
The text was updated successfully, but these errors were encountered: