Rayrun
← Back to Discord Forum

POMs as fixtures

I see people creating fixtures for all the pages and use the pages as fixtures. Why adding one more layer and complexity adding all the pages in a base page and use them as a fixture? Why not use regular POM and call the pages in the tests? Is there any benefit in this?

This thread is trying to answer question "What are the benefits of using POMs as fixtures, and how can fixtures be organized in multiple files? What is the difference between using POM pages as fixtures and regular POM with hooks?"

20 replies
refactoreric

Hi, the @playwright/test runner is encouraging/enforcing test isolation, so each test (even within the same spec) get their own browser context and page (window/tab), with fresh cookies, local storage, etc.

Since each test gets its own page object, you cannot create your POMs somewhere at the top. You have to do that in the tests themselves. That can get a bit noisy. So people define the POMs as fixtures and 'inject' them into the test.

I think when the test deals with various pages, this can quickly become noisy by itself, like

// Use all those pages.
});

Instead you could create a WebshopPages class, exposing a property for each of those pages, needed for a webshop scenario.

await pages.catalog.find('clean code');
   await pages.catalog.openResult(1);
   await pages.product.buy();
   await pages.checkout.asReturningCustomer();
   await pages.login.authenticate();
   await pages.checkout.confirmPurchase();
});

To be honest I'm still a bit 'searching' how to design the tests cleanly, with little noise.

The other thing is when I have serial tests in which I call API's, doing assertations, etc. I have issues with the context, pages etc. I have tests in which I call the pages in the tests and they work out of the box, when I do the same tests but have the pages as fixtures. I need to describe the tests and have all the tests stored inside the describe.

refactoreric

Hmm I don't recognize your problem. Maybe if you share some relevant code snippets it will be clearer. And what are the symptoms, how exactly does it 'not work' if the tests are not in a describe block?

Well. I use test.describe.configure({ mode: 'serial' }); to run all the tests in the spec file one after another.

  1. When I call the pages in the tests and i run the spec file, all the tests run in the same context and in the same page.
  2. When I use the pages as fixtures, after the first test is done I get browserContext.close and the context closes after the first test, so the second test fails.

Anything that is different between the 2 tests is that in the 1st I call the pages in the tests (default POM) and in the 2nd I call the pages as fixtures. I have basePages fixture where I make fixture for every page.

This is the first spec that works (in this one I call the pages in the tests)

test.describe.configure({ mode: 'serial' });
let page: Page;

test.beforeAll(async ({ browser }) => {
    page = await browser.newPage();
});

test.afterAll(async () => {
    await page.close();
});

test('Signup', async () => {
    const registration = new signupPage(page);
    await registration.goToSignupUrl();
    await registration.enterEmail(loginData.adminEmail);
    await registration.enterPassword(loginData.password);
    await registration.clickSignupButton();
    await expect(page).toHaveURL(/.*home/);
});

test('Simulate form submission', async ({ request }) => {
    const userID = await getUserID(page);
    await simulateFormSubmission(request, userID);
    await page.reload();
});

test('Add user info', async () => {
    const addInfo = new addInfoPage(page);
    await addInfo.clickAddInfoButton();
    await addInfo.enterName(userData.name);
    await addInfo.enterNumber(userData.number);
    await addInfo.enterBirthdate(userData.birthdate);
    await addInfo.clickAddUserInfo();
});

And this is the second spec where I use the pages as fixtures in the tests

test.describe.configure({ mode: 'serial' });
let page: Page;

test.beforeAll(async ({ browser }) => {
    page = await browser.newPage();
});

test.afterAll(async () => {
    await page.close();
});

test('Signup', async ({ registration }) => {
    await registration.goToSignupUrl();
    await registration.enterEmail(loginData.adminEmail);
    await registration.enterPassword(loginData.password);
    await registration.clickSignupButton();
    await expect(page).toHaveURL(/.*home/);
});

test('Simulate form submission', async ({ request }) => {
    const userID = await getUserID(page);
    await simulateFormSubmission(request, userID);
    await page.reload();
});

test('Add user info', async ({ addInfo }) => {
    await addInfo.clickAddInfoButton();
    await addInfo.enterName(userData.name);
    await addInfo.enterNumber(userData.number);
    await addInfo.enterBirthdate(userData.birthdate);
    await addInfo.clickAddUserInfo();
});

and I have basePage fixture

type pages = {
    registration: signupPage;
    addInfo: addInfoPage;
}

export const test = basePage.extend<pages>({
    registration: async ({ page }, use) => {
        await use(new signupPage(page))
    },

    addInfo: async ({ page }, use) => {
        await use(new addInfoPage(page))
    }
})
refactoreric

Hi ah now I understand what you meant. This is how test fixtures work. Each test will run in an isolated browser context and page. I believe using a shared context and page from the beforeAll is discouraged.

If you have a sequence of steps, instead of making them separate tests, you could make them a single test consisting of multiple test.step calls. https://playwright.dev/docs/api/class-test#test-step

On the other hand, if the only dependency would be authentication (I don't think it is in your case), and you don't want to log in for each test, then you could use storageState: https://playwright.dev/docs/auth

Thank you so much! I used the test.step and it works like a charm 🙂

But still I don't like the idea of adding all the pages in the basePage file because in time the file will just grow larger. I was thinking about what you've mentioned in the previous message about

test('buy an item', async ({ webshopPages : pages }) => {
   await pages.catalog.find('clean code');
   await pages.catalog.openResult(1);
   await pages.product.buy();
   await pages.checkout.asReturningCustomer();
   await pages.login.authenticate();
   await pages.checkout.confirmPurchase();
});

Is there any way to group pages in the basePage so I can import them as one in the tests? I don't understand the implementation in your example

Also, what would happen if I want to have multiple fixtures in a separate files, and not have all fixtures in a single file. I would need to import 'test' from the last fixture I extended it. Isn't this going to make it more difficult to use?

Would it be possible to create new fixture file myPagesFixture.ts and add the pages there

myPagesFixture.ts

import { test as pagesTest } from '@playwright/test';
import { signupPage } from '../pages/signupPage'
import { addInfoPage } from '../pages/addInfoPage'

type pages = {
    registration: signupPage;
    addInfo: addInfoPage;
}

export const test = pagesTest.extend<pages>({
    registration: async ({ page }, use) => {
        await use(new signupPage(page))
    },

    addInfo: async ({ page }, use) => {
        await use(new addInfoPage(page))
    }
})

and then use the basePage.ts file only for importing the other fixtures from different files. This will later allow us to import test in all spec files only from basePage.ts.

basePage.ts

import { test as baseTest } from '@playwright/test';
import { test as pagesTest } from './myPagesFixture'


export const test = baseTest.extend({
    ...pagesTest
    // Or something like this?
});

export { expect, Page, BrowserContext } from '@playwright/test';

What do you think about this approach?

@refactoreric: > I believe using a shared context and page from the beforeAll is discouraged Yep, I am trying to grasp my head around each test is independent. From GUI where you have to get from point A to point D in test, it seems that steps would be repeated a lot if each test was iindependent.

I believe using a shared context and page from the beforeAll is discouraged

Yep, I am trying to grasp my head around each test is independent. From GUI where you have to get from point A to point D in test, it seems that steps would be repeated a lot if each test was iindependent.

Found this discussion and solved the latest problem I had with splitting the fixtures in multiple files https://github.com/microsoft/playwright/issues/22867

the framework i've built for our team is an abstracted POM npm package flow. For example:

To reduce duplicated code use across different product teams I have an npm package called "login-page-modules". This package contains a src folder with class files that build out the playwright POM structure and locators including methods specific to the page that can be used by anyone that installs the package. So a simple login test using this structure would look like this in code:

import { LoginPage } from '@login-page-modules';
import { test, expect } from '@playwright/test';

test.describe('SSO LoginPage tests', async () => {
  // not using authenticated state
  test.use({ storageState: undefined });
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto(testEnv);
  });

  test('login', async ({ page }) => {
    // method in LoginPage has expects to check that login is successful otherwise it returns errors.
    await loginPage.login(username, password);
  });

});

So basically anyone that needs to login via our base login page can pull in this npm package and utilize the POM structure for their stuff.

@bshostak Cool approach, but then why not just use regular POM and have everything in one place since you're again using hooks and you are creating new instance from the page in the test?

I am thinking more of the fixtures approach, but kinda liking more the regular POM

@refactoreric What do you think of that approach of splitting the fixtures into multiple files and use the basePage just for exposing them to the tests?

I kinda like the approach of using regular POM with hooks and create fixtures that I'll pass to the hooks for env. set up and tear down. Not use the POM pages as fixtures because kinda complicates things 🙃

refactoreric

Hmm what do you mean with 'POM with hooks'?

I mean, regular use of hooks 🙂

@umskip not sure what you mean. We have multiple codebases that need a way to bring in the same code. It is all in one place. Creating instance of specific page in test file/helper file, etc in specific codebase is what isolates it to being used when needed.

Well, in that case it makes sense 🙂

TwitterGitHubLinkedIn
AboutQuestionsDiscord ForumBrowser ExtensionTagsQA Jobs

Rayrun is a community for QA engineers. I am constantly looking for new ways to add value to people learning Playwright and other browser automation frameworks. If you have feedback, email luc@ray.run.