A better global setup in Playwright reusing login with project dependencies
Tags: testing, e2e, auth, playwright
Published: 2023-03-15 19:11:53

Global setup is being used by many teams and companies to login to an application and then use this setup for tests that need to be in an authenticated state; however, it is lacking some important features. When you use global setup, you don't see a trace for the setup part of your tests and the setup doesn't appear in the HTML report. This can make debugging difficult. It's also not possible to use fixtures in global setup.

In order to fix this issue, project dependencies were created.

## What are project dependencies?

[Project dependencies](https://playwright.dev/docs/next/test-projects#dependencies) are a better way of doing global setup. To add a dependency, so that one project depends on another project, create a separate project in the Playwright config for your setup tests, where each test in this project will be a step in the setup routine.

Every time you run a test from the basic project, it will first run the tests from the setup project.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'basic',
      dependencies: ['setup'],
    },
  ],
});
```

By using project dependencies, the HTML report will show the setup tests, the [trace viewer](https://playwright.dev/next/trace-viewer-intro) will record traces of the setup, and you can use the inspector to inspect the DOM snapshot of the trace of your setup tests and you can also use fixtures.

## Running sequence

Tests in the 'setup' project will run first and then the tests in the 'chromium', 'webkit', and 'firefox' projects will run in parallel once all tests in setup have completed.

![chrome, firefox and safari project that depend on a setup project](https://user-images.githubusercontent.com/13063165/224719295-9afa89e4-d0f6-4ec4-b529-90c50aea9feb.png)

### What happens if a dependency fails?

In the following example, you will see an 'e2e tests' project that depends on both the 'Browser Login' project and the 'Database' project. The 'Browser Login' project and the Database project will run in parallel. However, as the 'Database' project failed, the 'e2e tests' project will never run as it depends on both the 'Browser Login' and the 'Database' projects to pass.

![e2e tests project that depends on browser login project and database project](https://user-images.githubusercontent.com/13063165/224721074-7b7d1c2b-9360-4438-812f-6acb3b95bc65.png)

## Project Dependency example

Playwright runs tests in isolated environments called [Browser Contexts](https://playwright.dev/docs/browser-contexts). Every test runs independently from any other test. This means that each test has its own local storage, session storage, cookies, etc. However, tests can use the [storage state](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state), which contains cookies and a local storage snapshot, from other tests so as to run tests in a logged in state.

Let's create an [example of how to use Project dependencies](https://github.com/debs-obrien/playwright-project-dependencies) to have a global setup that logs into Wikipedia and saves the storage state so that all tests from the e2e project start running in this logged in state.

First, install Playwright using the [CLI](https://playwright.dev/docs/intro) or [VS Code extension](https://playwright.dev/docs/getting-started-vscode). You can then modify the config file, create your login test, and an e2e test that starts from a logged in state by using storage state.

### Configuring the setup project

Start by creating a basic [`playwright.config.ts` file](https://playwright.dev/docs/next/test-configuration#basic-configuration) or modifying the one that has already been created for us. The options needed are the `testDir` option which is the name of the directory where you want to store your tests and the `projects` options which is what projects you want to run.

A [project](https://playwright.dev/docs/api/class-testproject) is a logical group of tests that run using the same configuration. The first project you need is one called 'setup' and by using [`testMatch`](https://playwright.dev/docs/next/test-configuration#filtering-tests) you can filter any files that end in `setup.ts` and only these tests will be run when running the 'setup' project.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
  ],
});
```

### Create a login test

Next, create a `login.setup.ts`. To make it easier to see that this is a setup test, you can import `test as setup` and then instead of using the word `test` you can use the word `setup` when writing your test.

The aim is to create a test that logs into Wikipedia and ensures that the user is in a logged in state. You can use Playwright's test generator either from the [VS Code extension](https://playwright.dev/docs/getting-started-vscode#generating-tests) or using the [CLI to open the Playwright Inspector](https://playwright.dev/docs/codegen-intro), and generate the code by clicking on the 'Log in' button and filling out the username and password. You can then add the assertion to ensure that once logged in you can see the 'Personal Tools' option.

If you don't already have a username or password, you can quickly create an account and then use your own credentials to see that the tests work.

```js
// login.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('do login', async ({ page }) => {
  await page.goto('https://en.wikipedia.org');
  await page.getByRole('link', { name: 'Log in' }).click();
  await page.getByPlaceholder('Enter your username').fill('your_username');
  await page.getByPlaceholder('Enter your password').fill('your_password');
  await page.getByRole('button', { name: 'Log in' }).click();

  await expect(page.getByRole('button', { name: 'Personal tools' })).toBeVisible();
});
```

### Using env variables

In order for your username and password to be secure, you can store them as `.env` variables and access them in your tests through `process.env.USERNAME!` and `process.env.PASSWORD!`.

```js
// login.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('do login', async ({ page }) => {
  await page.goto('https://en.wikipedia.org');
  await page.getByRole('link', { name: 'Log in' }).click();
  await page.getByPlaceholder('Enter your username').fill(process.env.USERNAME!);
  await page.getByPlaceholder('Enter your password').fill(process.env.PASSWORD!);
  await page.getByRole('button', { name: 'Log in' }).click();

  await expect(page.getByRole('button', { name: 'Personal tools' })).toBeVisible();
});
```

Don't forget to install the `dotenv` package from npm.

```bash
npm i dotenv
```

Once the package is installed, import it in your Playwright config with `require('dotenv').config();` so you have access to the `.env` variables.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
require('dotenv').config();

export default defineConfig({
  testDir: './tests',

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'e2e tests',
      dependencies: ['setup'],
    },
  ],
});
```

Next, create a `.env` file and add the username and password. Make sure to add this file to the `.gitignore` so that your secrets don't end up on CI.

```bash
USERNAME: your_username
PASSWORD: your_password
```

You can use GitHub secrets when working with CI. Create your repository secrets in the settings of your repo and then add the env variables to the [GitHub actions workflow](https://playwright.dev/docs/next/ci-intro) already created when installing Playwright.

```yml
env: 
  USERNAME: ${{secrets.USERNAME}}
  PASSWORD: ${{secrets.PASSWORD}}
```

### Create a e2e tests project

Create a project called `e2e tests logged in`. This project will depend on the 'setup' project and will match any test files that end in `loggedin.spec.ts`.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
require('dotenv').config();

export default defineConfig({
  testDir: './tests',

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'e2e tests logged in',
      testMatch: '**/*loggedin.spec.ts',
      dependencies: ['setup'],
    },
  ],
});
```

### Adding storage state

The setup project will write storage state into an 'auth.json' file in a `.auth` folder inside the playwright folder. This exports a const of `STORAGE_STATE` to share the location of the storage file between projects.

Next, you need to tell your test to use the `STORAGE_STATE` variable you have created as the value of its `storageStage`. This returns the storage state for the browser context and contains the current cookies and a local storage snapshot.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import path from 'path';
require('dotenv').config();

export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
  testDir: './tests',

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'e2e tests logged in',
      testMatch: '**/*loggedin.spec.ts',
      dependencies: ['setup'],
      use: {
        storageState: STORAGE_STATE,
      },
    },
  ],
});
```

At the moment, there is nothing saved in the `STORAGE_STATE` so the next step is to populate the context with the storage state after login actions have been performed. By doing this you only have to log in once and the credentials will be stored in the `STORAGE_STATE` file, meaning you don't need to log in again for every test. Start by importing the `STORAGE_STATE` from the Playwright config file and then use this as the path to save your storage state to.

```js
// login.setup.ts
import { test as setup, expect } from '@playwright/test';
import { STORAGE_STATE } from '../playwright.config';

setup('do login', async ({ page }) => {
  await page.goto('https://en.wikipedia.org');
  await page.getByRole('link', { name: 'Log in' }).click();
  await page.getByPlaceholder('Enter your username').fill('TestingLogin');
  await page.getByPlaceholder('Enter your password').fill('e2etests');
  await page.getByRole('button', { name: 'Log in' }).click();

  await expect(page.getByRole('button', { name: 'Personal tools' })).toBeVisible();

  await page.context().storageState({ path: STORAGE_STATE });
});
```

### Create a e2e test

Create some e2e tests that continue testing the application from a logged in state. When running all tests in the file the setup will only be run once and the second test will start already authenticated because of the specified `storageState` in the config.

```js
// e2e-loggedin.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('https://en.wikipedia.org');
});

test('menu', async ({ page }) => {
  await page.getByRole('link', { name: 'TestingLogin' }).click();
  await expect(page.getByRole('heading', { name: /TestingLogin/i })).toBeVisible();
  await page.getByRole('link', { name: /alerts/i  }).click();
  await page.getByText('Alerts', { exact: true }).click();
  await page.getByRole('button', { name: /notice/i  }).click();
  await page.getByText('Notices').click();
  await page.getByRole('link', { name: /watchlist/i  }).click();
})

test('logs user out', async ({ page }) => {
  await page.getByRole('button', { name: /Personal tools/i }).check();
  await page.getByRole('link', { name:  /Log out/i }).click();
  await expect(page.getByRole('heading', { name: /Log out/i })).toBeVisible();
  await expect(page.getByRole('link', { name: 'Log in', exact: true })).toBeVisible();
})
```

### Configure the baseURL

When using the same URL for both the 'setup' and 'e2e tests', you can configure the `baseURL` in the `playwright.config.ts` file. Setting a `baseURL` means you can just use `page.goto('/')` in your tests which is quicker to write, less prone to typos and it makes it easier to manage should the baseURL change in the future.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import path from 'path';
require('dotenv').config();

export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
  testDir: './tests',

  use: {
    baseURL: 'https://en.wikipedia.org',
  },

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'e2e tests logged in',
      dependencies: ['setup'],
      use: {
        storageState: STORAGE_STATE,
      },
    },
  ],
});
```

You can then use a `/` instead of 'https://en.wikipedia.org' for the `page.goto` in all your tests going forward including the 'setup' test that you created.

```js
// e2e-loggedin.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('/');
});

//...
```

### Configure the HTML Reporter

If you don't already have this setup in your Playwright config, then the next step is to add the HTML reporter to the `playwright.config.ts` file in order to setup the HTML reports for your tests. You can also add the `retries` for CI, set `fullyParallel` to true and set the `trace` to be recorded on the first retry of a failed test.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import path from 'path';
require('dotenv').config();

export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
  testDir: './tests',
  // Configure the reporter
  reporter: ['html'],
  // Retry on CI only
  retries: process.env.CI ? 2 : 0,
  // Run tests in files in parallel
  fullyParallel: true,

  use: {
    baseURL: 'https://en.wikipedia.org',
    // run traces on the first retry of a failed test
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'e2e tests logged in',
      dependencies: ['setup'],
      use: {
        storageState: STORAGE_STATE,
      },
    },
  ],
});
```

You can continue to add more projects to the config to add tests that don't require the user to be logged in. Using the testing filters of [`testIgnore`](https://playwright.dev/docs/next/test-configuration#filtering-tests) you can ignore all the setup tests and logged in tests when running the tests in this project.

```js
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import path from 'path';
require('dotenv').config();

export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
  testDir: './tests',
  // Configure the reporter
  reporter: ['html'],
  // Retry on CI only
  retries: process.env.CI ? 2 : 0,
  // Run tests in files in parallel
  fullyParallel: true,

  use: {
    baseURL: 'https://en.wikipedia.org',
    // run traces on the first retry of a failed test
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    {
      name: 'e2e tests logged in',
      dependencies: ['setup'],
      use: {
        storageState: STORAGE_STATE,
      },
    },
    {
      name: 'e2e tests',
      testIgnore: ['**/*loggedin.spec.ts', '**/*.setup.ts'],
    },
  ],
});
```

When you don't specify a browser, tests will be run on Chromium by default. You can of course run these tests on different browsers and devices and setup more projects. To learn more about [Projects](https://playwright.dev/docs/next/test-projects) and [Browsers](https://playwright.dev/docs/browsers#configure-browsers) check out the Playwright docs.

### Viewing the HTML report and trace viewer

Now you can run your tests from the CLI with the flag `--trace on` in order to see a full report of your tests including the 'setup' and 'e2e tests logged in' and also see a trace of each of them.

```bash
npx playwright test --trace on
```

After running the tests, you should see in the CLI that 2 tests have passed and you can now run the command to open the HTML report.

```js
npx playwright show-report
```

This command opens up the Playwright HTML report where you can see your two projects, the 'setup' project containing the login test and the 'e2e tests logged in' project containing the menu test and the log out test.

![html report showing both tests](https://user-images.githubusercontent.com/13063165/225038909-cfd92503-2d59-4c73-b513-b127452c3d02.png)

You can open the report for each of the tests including the 'setup test' and walk through each step of the test which is really helpful for debugging.

![html report showing steps of login test](https://user-images.githubusercontent.com/13063165/225039275-9d3e11fa-2771-4d7a-8c29-87b3089766dd.png)

To enhance the debugging experience even further, when you use the `--trace on` flag, you have a fully recorded trace of all your tests including the setup test. You can open the trace and view the timeline or go through every action, see the network requests, the console, the test source code and use dev tools to inspect the DOM snapshots.

![trace viewer showing trace of the login test](https://user-images.githubusercontent.com/13063165/225040918-f5316acc-e092-4e09-9da0-2d826dced162.png)

By clicking on the pop out button above the DOM snapshot you get a full view of your trace where you can easily inspect the code with Dev tools and debug your global setup should you need to.

![pop out to inspect trace and use dev tools](https://user-images.githubusercontent.com/13063165/225042112-dacb2ae4-3835-4365-ba71-d0cb01b84b76.png) 

## Conclusion

Using Project dependencies makes it so much easier to have a global setup with out of the box HTML reports and traces of your setup. 
- Check out the [Playwright docs](https://playwright.dev/docs/api/class-testproject#test-project-dependencies) to learn more
- Check out the [1.31 release video](https://youtu.be/PI50YAPTAs4) which goes though a similar demo.
- Clone the [repository of this demo](https://github.com/debs-obrien/playwright-project-dependencies)