Search code examples
javascriptplaywrightplaywright-test

How to use Playwright fixture to modify or add custom command?


I want a custom Playwright command for page.goto called page.gotoDOM that automatically passes the waitUntil: 'domcontentloaded' parameter so I don't have to code it in every test.

I have followed the instructions for fixtures and tried various combinations without success.

My code so far:

// @ts-check
const base = require('@playwright/test');
const { test, expect } = require('@playwright/test');
const describe = test.describe;

const testDOM = base.test.extend({
    pageDOM: async ({ host, page }, use) => {
    await page.goto(host, { waitUntil: 'domcontentloaded'});
    await use(page);
  },
})
describe('Google', () => {
  test('Search box', async ({ page }) => {
    await page.goto('https://google.com/', { waitUntil: 'domcontentloaded'});
    await expect(page.getByRole('combobox', { name: 'Search' })).toBeVisible();
  });
  testDOM('Search box using testDOM', async ({ page }) => {
    await testDOM('https://google.com/'); //  <-- Now I don't need to pass the waitUntil parameter
    await expect(page.getByRole('combobox', { name: 'Search' })).toBeVisible();
  });
})

Right now I want to get it working in one file, ultimately available for all files once that is done.

Currently the code above is complaining that testDOM requires more arguments but so far my attempts to correct that have failed.


Solution

  • There are many ways to do helper functions in Playwright. See Playwright: how to create a static utility method? which is highly relevant here.

    For this case, I'd keep it simple and just inline page.goto(url, {waitUntil: "domcontentloaded"}) everywhere that uses that option. This really isn't much code and it seems premature to abstract it away--it's clear to anyone familiar with Playwright what it does on sight, isn't that verbose, and likely won't require much maintenance.

    But if "domcontentloaded" is too long to look at over and over, you could make the config object a global:

    // could be in external 'constants'/config file, or at top of a single test script
    const domLoaded = {waitUntil: "domcontentloaded"};
    
    // use throughout your test scripts:
    await page.goto(url, domLoaded);
    await page.goto(url, {timeout: 20_000, ...domLoaded});
    

    Note that Playwright has an even faster predicate that's similar to "domcontentloaded" called "commit", which is shorter to type and can be inlined easily.

    Also note that goto generally shouldn't be needed much--I usually use one per test file in a test.beforeEach block. If you're using goto over and over at the beginning of each test, then you can avoid the repetition easily with beforeEach. Most navigations in test cases should be initiated by a user action like a click, not goto.

    Assuming none of the above meet your needs, another option is to write a plain, boring JS function that accepts page as the first argument. This is a straightforward and unclever approach and seems like a reasonable solution for this case.

    util.js:

    const gotoDOM = (page, url, opts={}) =>
      page.goto(url, {...opts, waitUntil: "domcontentloaded"});
    
    export {gotoDOM};
    

    foo.test.js:

    import {expect, test} from "@playwright/test"; // ^1.42.1
    import {gotoDOM} from "./util";
    
    test("gotoDOM works", async ({page}) => {
      await gotoDOM(page, "https://www.example.com");
      await expect(page.getByText("Example Domain")).toBeVisible();
    });
    

    This lets you pass in an optional options argument like {timeout: 20_000} as the last argument, just as in normal Playwright. When writing abstractions, I like to try to stick to the design of the library I'm using so it's less surprising, so I would not want a signature like await gotoDOM({ page, url: 'https://google.com' }) as shown in this other answer, especially if I need to pass an options object through my wrapper into Puppeteer's API.

    If you don't like the page argument, you can use a closure pattern, discussed at length in the linked answer:

    util.js

    const helpersFor = page => {
      return {
        gotoDOM(url, opts={}) {
          return page.goto(url, {...opts, waitUntil: "domcontentloaded"});
        },
        // ... more helpers ...
      };
    };
    
    export default helpersFor;
    

    foo.test.js:

    import {expect, test} from "@playwright/test";
    import helpersFor from "./util";
    
    let gotoDOM;
    test.beforeEach(({page}) => {
      ({gotoDOM} = helpersFor(page));
    });
    
    test("gotoDOM works", async ({page}) => {
      await gotoDOM("https://www.example.com");
      await expect(page.getByText("Example Domain")).toBeVisible();
    });
    

    If you only have one helper, this isn't that exciting, but it can be useful once you have a handful of them since you generally only use one page per test. I use this pattern more in POMs than in beforeEach.


    By the way, the base.test.extend pattern in OP's attempt seems best for POMs that you want to inject into each test block, but is overkill for a simple helper function like this.