Skip to Content
  • +1 844-335-0400
Vayner Systems
  • Sign in
  • Contact Us
  • Home
  • Services
    • Software
    • Hardware
  • About
  • Success Stories
  • Downloads
  • Blog
  • Contact us
Vayner Systems
      • Home
      • Services
        • Software
        • Hardware
      • About
      • Success Stories
      • Downloads
      • Blog
      • Contact us
    • +1 844-335-0400
    • Sign in
    • Contact Us

    Improve code readability by implementing custom fixtures in Playwright

  • All Blogs
  • Product Development
  • Improve code readability by implementing custom fixtures in Playwright
  • June 24, 2025 by
    Improve code readability by implementing custom fixtures in Playwright
    Viacheslav Driuchyn

    Modernizing your test automation from outdated frameworks like Protractor or Selenium is a smart move, but without readable and reusable code, even the best test tools can become maintenance nightmares.

    In our journey of migrating from Protractor to Playwright, we quickly realized the importance of simplifying repetitive setup logic across dozens of test specs. Our test suites combined UI interactions with API validations, executed in a specific sequence to maintain event order and test data integrity.

    This post demonstrates how introducing custom fixtures in Playwright dramatically improved our test readability and helped us adhere to DRY (Don't Repeat Yourself) principles - making our Playwright tests easier to maintain, scale, and understand.

    💡 At Vayner Systems, we help teams transition from stale frameworks and build modern, scalable QA foundations with Playwright. Contact us to elevate your test automation with clean architecture and best practices.

    The problem: repeated setup logic across 30+ Specs

    Here’s a simplified version of what our original test.beforeAll setup looked like:

    test.beforeAll(async ({ browser }) => { page = await browser.newPage(); splash = new SplashPage(page); sidebar = new SidebarPage(page); // ... initialize 10+ more page objects await page.goto(env.users['apiUser10'].tokenUrl!); await splash.waitForSyncAndOfflineReady(); await splash.openCreateProjectForm(); await sidebar.createProject(projectName, fullName); });

    And then test.beforeEach to init API client, and test.afterAll to close the page:

    test.beforeEach(({ request }) => { api = new ApiService(request); }); test.afterAll(async () => { await page.close(); });

    This pattern repeated across multiple test files, creating unnecessary complexity and duplication. We decided to introduce custom fixtures to encapsulate this behavior.


    Phase 1: Create a loginAndCreateProject fixture

    We introduced a fixture that encapsulates all initialization steps: navigation, synchronization, project creation, and returning all required page objects.

    interface LoginFixture { loginAndCreateProject(params: LoginInput): Promise<AppPages>; } export const test = base.extend<LoginFixture>({ loginAndCreateProject: async ({ browser }, use) => { const loginAndCreateProject = async ({ projectName, user }: LoginInput): Promise<AppPages> => { const page = await browser.newPage(); const splash = new SplashPage(page); const sidebar = new SidebarPage(page); await splash.goTo(env.users[user].tokenUrl!, true); await splash.waitForSyncAndOfflineReady(); await splash.openCreateProjectForm(); await sidebar.createProject(projectName, fullName); return { page, sidebar, // All necessary page objects... }; }; await use(loginAndCreateProject); } });


    Fixture Usage in Tests

    test.beforeAll(async ({ loginAndCreateProject }) => { ({ page, menu, bucket2, ... } = await loginAndCreateProject({ projectName: projectName, user: 'apiUser10', })); });


    //... test spec code


    test.afterAll(async() => {

    ​ await page.close();

    });

    Better? Yes. But we still had an afterAll block outside the fixture and parameters passed explicitly to the fixture, creating some tight coupling.


    Phase 2: Parameterize and Finalize the Fixture

    We refactored further by:

    • Moving user and project parameters to a shared testCaseInput fixture
    • Managing the browser lifecycle via worker-scoped browser fixture
    • Moving teardown logic into the fixture itself
    • Removing the need for beforeAll by handling setup in the first test

    export const test = base.extend<LoginFixture & TestCaseInput>({ testCaseInput: [ { projectName: 'defaultName', userId: 'defaultUser', }, { auto: true } ], browser: [ async ({}, use) => { const browser = await chromium.launch(); await use(browser); await browser.close(); }, { scope: 'worker' } ], loginAndCreateProject: async ({ browser, testCaseInput }, use) => { const loginAndCreateProject = async (): Promise<AppPages> => { console.log(`Logging in with user ${testCaseInput.userId}`); const page = await browser.newPage(); const splash = new SplashPage(page); const sidebar = new SidebarPage(page); await splash.goTo(url); await splash.waitForSyncAndOfflineReady(); await splash.openCreateProjectForm(); await sidebar.createProject(testCaseInput.projectName, fullName); return { page, sidebar, // Other page objects }; }; await use(loginAndCreateProject); } });


    In the test spec file:

    //pass custom spec related values to the fixture:

    test.use({ testCaseInput: { projectName: 'Auto QA Project', userId: 'apiUser10', } });


    //passed custom fixture to first test in spec & removed before all test('Initial setup', async ({ loginAndCreateProject }) => { ({ menu, bucket2, ... } = await loginAndCreateProject()); const project = await api.findProjectByName(projectName); await menu.openBucket2(); });

    This is now much cleaner than the original version, just compare samples below. No more noisy setup logic or duplicated teardown calls. Each spec remains focused on behavior, not configuration.

    Initial version:

    After code refactoring & using custom fixtures:


    Conclusion

    By introducing custom fixtures in Playwright, we achieved better code organization, reduced duplication, and drastically improved the readability and maintainability of our test suites. Clean fixtures mean faster onboarding, simpler debugging, and more robust automation.

    Looking to scale your automation setup or modernize your stack? Contact Vayner Systems to get expert help with custom software and QA solutions that combine speed, quality, and clarity.

    in Product Development
    How to add Type checking and ESLint to Playwright project

    Don't waste your time!
    Start Your Journey


    Start Now

    Focus on what matters. We'll do the rest.

    Vayner Systems sees the potential and the passion behind innovative companies that rely on small teams and big ideas. It's where many of the largest companies started and, it's where we can build relationships with young new companies that need the right tools to thrive.

    Home  | Blog | About | Contact us


    Vayner Systems
    35585 Curtis Blvd Unit B 
    Eastlake OH 44095 
    United States

    • +1 844-335-0400
    • info@vaynersystems.com
    Follow us