Pelicer
Pelicer

Reputation: 1584

Playwright before each for all spec files

I am very new to Playwright. Due to my test suites, I need to login into my application before running each test. Inside a single spec file that is easy, I can simply call test.beforeEach. My issue is: I need to before the login before each test of each spec file.

test.describe('Test', () => {
    //I need to make the code inside this beforeEach a exported 
    //function to call inside the before each of every spec file I have
    test.beforeEach(async ({ page }) => {
        await page.goto('/login');
        await page.click('text=Log in with Google account');
        await page.fill('id=identifierId', LoginAutomationCredentials.USER);
        await page.click('button[jsname="LgbsSe"]');
        await page.fill('input[type="password"]', LoginAutomationCredentials.PASSWORD);
        await page.click('button[jsname="LgbsSe"]');
        const otp = authenticator.generateToken(LoginAutomationCredentials.TOKEN);
        await page.fill('id=totpPin', otp);
        await page.click('button[jsname="LgbsSe"]');
    });

    it('Some description', async ({ page }) => {
        await page.goto('/foo');
        const dateFilter = await page.inputValue('input[placeholder="2011/03/02"]');
        expect(dateFilter).toBe('2021/12/07');
    });
});

I tried simply taking that code and and making it a function inside a separate .ts file and then importing it, but I figured the context is needed in order to do this. This is probably something every tester that uses playwright knows and uses regularly, however, I did not find anything on the subject.

How can I avoid copying the entire code of beforeEach and pasting it to all my spec files? How can I make it a function and call it whenever I want?

Upvotes: 16

Views: 39043

Answers (4)

ggorlen
ggorlen

Reputation: 57259

Global setup and teardown is an option for pre-test logins.

Here's a sketch to show what this might look like on your code, based on the docs (untested).

global-setup.ts:

import {chromium, FullConfig} from "@playwright/test";

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // do your login:
  await page.goto("/login");
  await page.click("text=Log in with Google account");
  await page.fill(
    "id=identifierId",
    LoginAutomationCredentials.USER
  );
  await page.click('button[jsname="LgbsSe"]');
  await page.fill(
    'input[type="password"]',
    LoginAutomationCredentials.PASSWORD
  );
  await page.click('button[jsname="LgbsSe"]');
  const otp = authenticator.generateToken(
    LoginAutomationCredentials.TOKEN
  );
  await page.fill("id=totpPin", otp);
  await page.click('button[jsname="LgbsSe"]');

  // see below for further discussion
  // const { baseURL, storageState } = config.projects[0].use;
  // await page.context().storageState({ path: storageState as string });
  // await browser.close();
}

export default globalSetup;

playwright.config.ts:

import {defineConfig} from "@playwright/test";

export default defineConfig({
  globalSetup: require.resolve("./global-setup"),
  use: {
    baseURL: "http://localhost:3000/",
    storageState: "state.json",
  },
});

tests/foo.spec.js:

test.describe("Test", () => {
  it("Some description", async ({page}) => {
    await page.goto("/foo");
    const dateFilter = await page.inputValue(
      'input[placeholder="2011/03/02"]'
    );
    expect(dateFilter).toBe("2021/12/07");
  });
});

Now, in the original Playwright example, their globalSetup function writes to storage state:

// ...
  await page.getByText('Sign in').click();
  await page.context().storageState({ path: storageState as string });
  await browser.close();
// ...

But your code doesn't. You might need to do so as the last line of your globalSetup function, then call await browser.close() to save and carry those tokens between one browser session and the next.

They're pulling this storageState string from const { baseURL, storageState } = config.projects[0].use; so that it doesn't need to be hardcoded in multiple places. See their docs for a full example.


Once you have that working and you've written your state.json file, you may want to skip running future logins if the tokens are still valid. You can do so with the following modifications to global-setup.ts:

import fs from "node:fs/promises";

// ...

async function exists(path: string): Promise<boolean> {
  return !!(await fs.stat(path).catch(() => false));
}

async function globalSetup(config: FullConfig) {
  const {storageState} = config.projects[0].use;

  if (await exists(storageState)) {
    return;
  }

  const browser = await chromium.launch();
  const page = await browser.newPage();

  // do your login:
  await page.goto("/login");

  // ...
}

// ...

Most likely, state.json should be added to your project's .gitignore.

Upvotes: 2

pbaranski
pbaranski

Reputation: 25062

There two aproaches:

Use project dependency

Form Playwright 1.31 with project dependency you can set test what will be executed before any other tests and pass i.e. browser storage between tests.

See working and tested example (entering google search page and accepting policies, and then entering this page again with cookies):

playwright.config.ts:

import { PlaywrightTestConfig } from "@playwright/test";
import path from "path";

//here you save session
export const STORAGE_STATE = path.join(__dirname, 'login.json')
    
const config: PlaywrightTestConfig = {
  timeout: 10 * 1000,
  expect: {
    timeout: 3 * 1000,
  },
  testDir: './tests',
  use:{
    baseURL: 'https://google.com',
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
  },
  // here we set main project and dependent one
  projects: [
    {
      name: 'login',
      grep: /@login/
    },
    {
      name: 'depend e2e',
      grep: /@e2e/,
      dependencies: ['login'],
      use: {
        storageState: STORAGE_STATE
      }
    }

  ]
}; 

export default config;

tests/example.spec.ts:

import { test} from '@playwright/test';
import { STORAGE_STATE } from '../playwright.config';
    
test('login to service @login', async({page}) => {
  await page.goto('/');
  // below example is to reject cookies from google
  await page.waitForLoadState('domcontentloaded');
  await page.keyboard.press('Tab');
  await page.keyboard.press('Enter');
  await page.getByRole('menuitem', { name: "English (United States)" }).press('Enter');
  await page.getByRole('button', { name: 'Reject all' }).click();
  
  // Save storage:
  await page.context().storageState({path: STORAGE_STATE})
})

test('logged in test @e2e', async ({page}) => {
  await page.goto('/');
  await page.waitForLoadState('domcontentloaded');
  // BOOM - you are in!
  // Screenshot shows that settings regarding cookies were saved
  await page.screenshot({ path: 'screenshot.png' })
})

Video with example: https://www.youtube.com/watch?v=PI50YAPTAs4&t=286s

In above example I'm using tags to identify tests: https://playwright.dev/docs/test-annotations#tag-tests

Use fixtures

Let test entering google search page and accepting policies

In file fixtures.ts:

import { test as base } from "@playwright/test";

export const test = base.extend({
  page: async ({ baseURL, page }, use) => {
    // We have a few cases where we need our app to know it's running in Playwright.
    // This is inspired by Cypress that auto-injects window.Cypress.
    await page.addInitScript(() => {
      (window as any).Playwright = true;
    });

   await page.goto("/");
   // below example is to reject cookies from google
   await page.waitForLoadState("domcontentloaded");
   await page.keyboard.press("Tab");
   await page.keyboard.press("Enter");
   await page
     .getByRole("menuitem", { name: "English (United States)" })
     .press("Enter");
   await page.getByRole("button", { name: "Reject all" }).click();

    use(page);
  },
});
export { expect } from "@playwright/test";

And then in test use new test object instead importing it form "@playwright/test"

import { test, expect } from "../fixture";

test("logged in test @e2e", async ({ page }) => {
  await page.goto("/");
  await page.waitForLoadState("domcontentloaded");
  // BOOM - you are in!
  // Screenshot shows that it works
  await page.screenshot({ path: "screenshot.png" });
});

Inspired by: https://github.com/microsoft/playwright/issues/9468#issuecomment-1403214587

If you need example with global request intercepting see here: https://stackoverflow.com/a/76234592/1266040

Upvotes: 9

Jason Kohles
Jason Kohles

Reputation: 814

Fixtures is the right way, but there is a better option than having to remember to use login instead of page when you want to login. This is how I do it...

First I have this in playwright/src/index.ts where I setup all the fixtures for my project:

import { test as base_test, expect } from 'playwright_test';

type TestFixtures = {
  user: string;
};

export const test = base_test.extend<TestFixtures>( {
  user : '[email protected]',
  async context( { user, context, request }, use ) {
    // This makes a REST request to the backend to get a JWT token
    // and then stores that token in the browsers localStorage,
    // but you could replace this with whatever makes sense for auth
    // in your app
    if ( user ) {
      const content = await getAuthScript( user, request );
      await context.addInitScript( { content } );
    }
    await use( context );
  },
} );

/**
 * This makes a REST request to the backend to get a JWT token
 * and then stores that token in the browsers localStorage,
 * but you could replace this with whatever makes sense for auth
 * in your app.
 */
async function getAuthScript( user, request ) {
  const res = await request.post( '/api/test/auth', { data : { user } } );
  const { token } = await res.json();
  return `window.localStorage.setItem( 'jwt-token', "${token}" );`;
}

export { test, expect }

I also make sure that playwright/tsconfig.json includes this:

{
  "extends": "../path/to/base/tsconfig.json",
  "compilerOptions": {
    "noEmit": true,
    "paths": {
      "~": [ "./src" ],
      "~*": [ "./src/*" ]
    },
    "baseUrl": ".",
    "rootUrl": ".",
  },
  "include": [
    "src/**/*.ts",
    "test/**/*.ts"
  ],
}

Now every test will automatically login as [email protected], but if you need a test to login as a different user all you need to do in that test file is:

import { test, expect } from '~';

test.use( { user : '[email protected]' } );
test( 'can login as somebody-else', async ( { page } ) => {
  // Run your tests here...
} );

Upvotes: 6

Inkssz
Inkssz

Reputation: 164

Use fixtures.

fixture.js:

const base = require('@playwright/test')
const newTest = base.test.extend({
    login: async({page}, use) => {
        await login();
        await use(page); //runs test here
        //logic after test
    }
})
exports.newTest = newTest
exports.expect = newTest.expect

Then in your tests:

const {newTest} = require('fixture.js')
newTest('mytest', async ({login}) => {
    //test logic
    login.goto(''); // using login here since I pass it as page in the fixture.
})

Upvotes: 14

Related Questions