Two user interactions using parallel pages in Playwright

Does your application have frequent interactions between two users? Sometimes functional testing of these interactions can be difficult and time consuming when running actions sequentially. Luckily, some modern test automation frameworks incorporate multi page handling which make these scenarios easier and quicker. Let's take a look of how to do this in Playwright.

In this example, we will be testing the Testery application's insight notification feature. The read and unread status is not shared across users, so when one user marks a new message as read, it should stay unread for all other users.

insights.feature:

@multi-user
Scenario: When User A marks message as read/unread it doesn't change for User B
    Given both users are logged in and on desired account
    And all insights are marked 'unread'
    When user navigates to Insights page
    And user marks messages as 'read'
    Then no 'unread' messages exist
    When user B navigates to Insights page
    Then no 'read' messages exist for user B

Let's spin up a new page to run in parallel with the main page, reducing unnecessary logins, logouts, and navigations. We can also utilize Playwright fixtures to launch the new page and have it ready to take commands.

fixtures.ts:

import { test as base, createBdd } from 'playwright-bdd'
import { type Page } from '@playwright/test'

interface MyFixtures {
  pageTwo: Page
}

export const test = base.extend<MyFixtures>({
  pageTwo: async ({ context }, use) => {
    const pageTwo: Page = await context.newPage()
    await use(pageTwo)
  }
})

export const { Given, When, Then, After } = createBdd(test)

Now we can reference the fixture in our steps to interact with the second page.

steps.ts:

import { expect } from '@playwright/test'
import { Given, When, Then } from '../fixtures'
import { LoginPage } from '../pages/login.page'
import { ACCOUNT, USERNAME_ONE, USERNAME_TWO, USERPASS_ONE, USERPASS_TWO, WEB_APP_URL } from '../testdata'

Given('both users are logged in and on desired account', async ({ context, page, pageTwo }) => {
  // example of unique tasks on pages
  const loginPageA = new LoginPage(page)
  const loginPageB = new LoginPage(pageTwo)

  await page.goto(WEB_APP_URL)
  await pageTwo.goto(WEB_APP_URL)
  await loginPageA.doLogin(USERNAME_ONE, USERPASS_ONE)
  await loginPageB.doLogin(USERNAME_TWO, USERPASS_TWO)

  // example of same tasks on pages
  const allPages = context.pages()
  for (const page of allPages) {
    const url: string = page.url()
    const currentProject: string = url.split('/')[3]
    if (currentProject !== ACCOUNT) {
      await page.locator('div.user-menu').click()
      await page.getByRole('option', { name: ACCOUNT }).click()
    }
    await page.waitForLoadState()
  }
})

Both pages are open and ready to receive commands. As we continue with the normal user flow, at any point, we can also do some navigation for User B.

💡
When running the steps in the playwright UI, you'll notice two tracks of pages being shown in the timeline now.

For this simple scenario, we only have to navigate to the page and verify some elements for user B.

...
  When user B navigates to Insights page
  Then no 'read' messages exist for user B

steps.ts:

When('user B navigates to Insights page', async ({ pageTwo }) => {
  const leftSideMenu = new LeftSideMenu(pageTwo)
  const insightsPage = new InsightsPage(pageTwo)
  await leftSideMenu.dashboardTab.click()
  await leftSideMenu.insightsTab.click()
  await insightsPage.pageHeader.waitFor()
})

Then('no {string} messages exist for user B', async ({ pageTwo }, state: string) => {
  const insightsPage = new InsightsPage(pageTwo)
  if (state === 'read') {
    const hasReadMessage = await insightsPage.doesReadMessageExist()
    expect(hasReadMessage).toBe(false)
  } else if (state === 'unread') {
    const hasUnreadMessage = await insightsPage.doesUnreadMessageExist()
    expect(hasUnreadMessage).toBe(false)
  } else { throw new Error(`${state} is not supported`) }
})

This scenario is complete, but with these tools you can go even further. For example, if you want even more optimization try making flexible steps which take in which user you want as a step parameter and use page or pageTwo based on the user. This will help reduce duplicate code if running the same steps for different users. Or, if you wanted to verify a third user, that is now possible as well by adding another similar fixture.

Next time you need to automate two or more user interactions, please try it out.

Happy Testing!