Categories
Playwright

Reusable Authentication Across Playwright Tests

Most web apps require user authentication which requires the user to login at the start of an e2e test. A very basic example is:

import { test, expect } from '@playwright/test'

test.describe.parallel('Unauthenticated tests', () => {
  test('can view as guest', async ({ page }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Hello Please Sign In')).toBeVisible()
  })
})

test.describe.parallel('Authenticated tests', () => {
  test('can view as admin', async ({ page }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await page.type('#firstname', 'Admin')
    await page.type('#surname', 'User')
    await page.click('#ok')
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=AdminUser')).toBeVisible()
  })

  test('can view as standard user', async ({ page }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await page.type('#firstname', 'Standard')
    await page.type('#surname', 'Person')
    await page.click('#ok')
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=StandardPerson')).toBeVisible()
  })
})

Whilst it’s easy to move the common code which authenticates (sets the cookies/tokens) into a login function that uses Playwright to visit a login page which is called from each test, Playwright offers something much better in that it can save browser storage state and re-use it. The idea is to login once and re-use the resulting browser state for every test that requires that role to work.

If the cookies/tokens don’t expire, you can capture them once, commit them to your code repository and simply re-use them:

First to capture:

  test('can view as admin', async ({ page, context }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await page.type('#firstname', 'Admin')
    await page.type('#surname', 'User')
    await page.click('#ok')
    await context.storageState({ path: 'storage/admin.json' });
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=AdminUser')).toBeVisible()
  })

  test('can view as standard user', async ({ page, context }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await page.type('#firstname', 'Standard')
    await page.type('#surname', 'Person')
    await page.click('#ok')
    await context.storageState({ path: 'storage/user.json' });
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=StandardPerson')).toBeVisible()
  })

And then to re-use the captured files:

import { test, expect } from '@playwright/test'

test.describe.parallel('Unauthenticated tests', () => {
  test('can view as guest', async ({ page }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Hello Please Sign In')).toBeVisible()
  })
})

test.describe.parallel('Administrator tests', () => {
  test.use({storageState: './storage/admin.json'})
  test('can view as admin', async ({ page, context }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=AdminUser')).toBeVisible()
  })
})

test.describe.parallel('User tests', () => {
  test.use({storageState: './storage/user.json'})
  test('can view as standard user', async ({ page, context }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=StandardPerson')).toBeVisible()
  })
})

But what if like most apps your authentication cookies/tokens do expire?

Fortunately you can dynamically create the session state once per test run – in the global hooks – then simply refer to those same local storage files in each test.

You can create a file like global-setup.ts which generates our storage state files once per test run:

// global-setup.ts
import { Browser, chromium, FullConfig } from '@playwright/test'

async function globalSetup (config: FullConfig) {
  const browser = await chromium.launch()
  await saveStorage(browser, 'Standard', 'Person', 'storage/user.json')
  await saveStorage(browser, 'Admin', 'User', 'storage/admin.json')
  await browser.close()
}

async function saveStorage (browser: Browser, firstName: string, lastName: string, saveStoragePath: string) {
  const page = await browser.newPage()
  await page.goto('http://webdriverjsdemo.github.io/auth/')
  await page.type('#firstname', firstName)
  await page.type('#surname', lastName)
  await page.click('#ok')
  await page.context().storageState({ path: saveStoragePath })
}

export default globalSetup

which is referenced in playwright.config.ts

// playwright.config.ts
module.exports = {
  globalSetup: require.resolve('./global-setup'),
  reporter: [['list'], ['html']],
  retries: 0,
  use: {
    headless: true,
    screenshot: 'only-on-failure',
    video: 'retry-with-video',
    trace: 'on-first-retry'
  }
}

Once you have this set up our tests remain the same but the local storage values are captured and set once per test run:

test.describe.parallel('Admin tests', () => {
  test.use({ storageState: './storage/admin.json' })
  test('can view as admin', async ({ page, context }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=AdminUser')).toBeVisible()
  })
})

test.describe.parallel('User tests', () => {
  test.use({ storageState: './storage/user.json' })
  test('can view as standard user', async ({ page, context }) => {
    await page.goto('http://webdriverjsdemo.github.io/auth/')
    await expect(page.locator('text=Welcome name=StandardPerson')).toBeVisible()
  })
})

You can call test.use({ storageState: './storage/user.json' }) for a file or a test.describe block, so if all your tests in your test file use the same authentication role place it outside a test.describe block, otherwise place it within a test.describe block of tests that use the same authentication role. Different roles? Use different test.describe blocks with different test.use calls to different files in each.

What do you think of Playwright’s ability to capture and use browser storage state?

Leave a Reply

Your email address will not be published. Required fields are marked *