diff --git a/docs/src/auth.md b/docs/src/auth.md index 1f20edafe3..994599d0f8 100644 --- a/docs/src/auth.md +++ b/docs/src/auth.md @@ -159,101 +159,6 @@ implement **login once and run multiple scenarios**. The lifecycle looks like: This approach will also **work in CI environments**, since it does not rely on any external state. -### Reuse authentication in Playwright Test -* langs: js - -When using [Playwright Test](./intro.md), you can log in once in the global setup -and then reuse authentication state in tests. That way all your tests are completely -isolated, yet you only waste time logging in once for the entire test suite run. - -First, introduce the global setup that would sign in once. In this example we use the `baseURL` and `storageState` options from the configuration file. - -```js js-flavor=js -// global-setup.js -const { chromium } = require('@playwright/test'); - -module.exports = async config => { - const { baseURL, storageState } = config.projects[0].use; - const browser = await chromium.launch(); - const page = await browser.newPage(); - await page.goto(baseURL); - await page.fill('input[name="user"]', 'user'); - await page.fill('input[name="password"]', 'password'); - await page.click('text=Sign in'); - await page.context().storageState({ path: storageState }); - await browser.close(); -}; -``` - -```js js-flavor=ts -// global-setup.ts -import { chromium, FullConfig } from '@playwright/test'; - -async function globalSetup(config: FullConfig) { - const { baseURL, storageState } = config.projects[0].use; - const browser = await chromium.launch(); - const page = await browser.newPage(); - await page.goto(baseURL!); - await page.fill('input[name="user"]', 'user'); - await page.fill('input[name="password"]', 'password'); - await page.click('text=Sign in'); - await page.context().storageState({ path: storageState as string }); - await browser.close(); -} - -export default globalSetup; -``` - -Next specify `globalSetup`, `baseURL` and `storageState` in the configuration file. - -```js js-flavor=js -// playwright.config.js -// @ts-check -/** @type {import('@playwright/test').PlaywrightTestConfig} */ -const config = { - globalSetup: require.resolve('./global-setup'), - use: { - baseURL: 'http://localhost:3000/', - storageState: 'state.json', - }, -}; -module.exports = config; -``` - -```js js-flavor=ts -// playwright.config.ts -import { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - globalSetup: require.resolve('./global-setup'), - use: { - baseURL: 'http://localhost:3000/', - storageState: 'state.json', - }, -}; -export default config; -``` - -Tests start already authenticated because we specify `storageState` that was populated by global setup. - -```js js-flavor=ts -import { test } from '@playwright/test'; - -test('test', async ({ page }) => { - await page.goto('/'); - // You are signed in! -}); -``` - -```js js-flavor=js -const { test } = require('@playwright/test'); - -test('test', async ({ page }) => { - await page.goto('/'); - // You are signed in! -}); -``` - ### API reference - [`method: BrowserContext.storageState`] - [`method: Browser.newContext`] diff --git a/docs/src/test-auth-js.md b/docs/src/test-auth-js.md new file mode 100644 index 0000000000..c8e5c72c07 --- /dev/null +++ b/docs/src/test-auth-js.md @@ -0,0 +1,313 @@ +--- +id: test-auth +title: "Authentication" +--- + +Tests written with Playwright execute in isolated clean-slate environments called +[browser contexts](./core-concepts.md#browser-contexts). Each test gets a brand +new page created in a brand new context. This isolation model improves reproducibility +and prevents cascading test failures. + +Below are the typical strategies for implementing the signed-in scenarios. + + + +## Sign in with beforeEach + +This is the simplest way where each test signs in inside the `beforeEach` hook. It also is the +least efficient one in case the log in process has high latencies. + +```js js-flavor=ts +import { test } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + // Runs before each test and signs in each page. + await page.goto('https://github.com/login'); + await page.click('text=Login'); + await page.fill('input[name="login"]', 'username'); + await page.fill('input[name="password"]', 'password'); + await page.click('text=Submit'); +}); + +test('first', async ({ page }) => { + // page is signed in. +}); + +test('second', async ({ page }) => { + // page is signed in. +}); +``` + +```js js-flavor=js +const { test } = require('@playwright/test'); + +test.beforeEach(async ({ page }) => { + // Runs before each test and signs in each page. + await page.goto('https://github.com/login'); + await page.click('text=Login'); + await page.fill('input[name="login"]', 'username'); + await page.fill('input[name="password"]', 'password'); + await page.click('text=Submit'); +}); + +test('first', async ({ page }) => { + // page is signed in. +}); + +test('second', async ({ page }) => { + // page is signed in. +}); +``` + +Redoing login for every test can slow down test execution. To mitigate that, reuse +existing authentication state instead. + +## Reuse signed in state + +Playwright provides a way to reuse the signed-in state in the tests. That way you can log +in only once and then skip the log in step for all of the tests. + +Create a new global setup script: + +```js js-flavor=js +// global-setup.js +const { chromium } = require('@playwright/test'); + +module.exports = async config => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto('https://github.com/login'); + await page.fill('input[name="user"]', 'user'); + await page.fill('input[name="password"]', 'password'); + await page.click('text=Sign in'); + // Save signed-in state to 'storageState.json'. + await page.context().storageState({ path: 'storageState.json' }); + await browser.close(); +}; +``` + +```js js-flavor=ts +// global-setup.ts +import { chromium, FullConfig } from '@playwright/test'; + +async function globalSetup(config: FullConfig) { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto('https://github.com/login'); + await page.fill('input[name="user"]', 'user'); + await page.fill('input[name="password"]', 'password'); + await page.click('text=Sign in'); + // Save signed-in state to 'storageState.json'. + await page.context().storageState({ path: 'storageState.json' }); + await browser.close(); +} + +export default globalSetup; +``` + +Register global setup script in the Playwright configuration file: + +```js js-flavor=ts +// playwright.config.ts +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + globalSetup: require.resolve('./global-setup'), + use: { + // Tell all tests to load signed-in state from 'storageState.json'. + storageState: 'storageState.json' + } +}; +export default config; +``` + +```js js-flavor=js +// playwright.config.js +// @ts-check +/** @type {import('@playwright/test').PlaywrightTestConfig} */ +const config = { + globalSetup: require.resolve('./global-setup'), + use: { + // Tell all tests to load signed-in state from 'storageState.json'. + storageState: 'storageState.json' + } +}; +module.exports = config; +``` + +Tests start already authenticated because we specify `storageState` that was populated by global setup. + +```js js-flavor=ts +import { test } from '@playwright/test'; + +test('test', async ({ page }) => { + // page is signed in. +}); +``` + +```js js-flavor=js +const { test } = require('@playwright/test'); + +test('test', async ({ page }) => { + // page is signed in. +}); +``` + +:::note +If you can log in once and commit the `storageState.json` into the repository, you won't need the global +setup at all, just specify the `storageState.json` in Playwright Config as above and it'll be picked up. +::: + +### Multiple signed in roles + +Sometimes you have more than one signed-in user in your end to end tests. You can achieve that via logging in for these users multiple times in globalSetup and saving that state into different files. + +```js js-flavor=js +// global-setup.js +const { chromium } = require('@playwright/test'); + +module.exports = async config => { + const browser = await chromium.launch(); + const adminPage = await browser.newPage(); + // ... log in + await adminPage.context().storageState({ path: 'adminStorageState.json' }); + + const userPage = await browser.newPage(); + // ... log in + await userPage.context().storageState({ path: 'userStorageState.json' }); + await browser.close(); +}; +``` + +```js js-flavor=ts +// global-setup.ts +import { chromium, FullConfig } from '@playwright/test'; + +async function globalSetup(config: FullConfig) { + const browser = await chromium.launch(); + const adminPage = await browser.newPage(); + // ... log in + await adminPage.context().storageState({ path: 'adminStorageState.json' }); + + const userPage = await browser.newPage(); + // ... log in + await userPage.context().storageState({ path: 'userStorageState.json' }); + await browser.close(); +} + +export default globalSetup; +``` + +After that you can specify the user to use for each test file or each test group: + +```js js-flavor=ts +import { test } from '@playwright/test'; + +test.use({ storageState: 'adminStorageState.json' }); + +test('admin test', async ({ page }) => { + // page is signed in as admin. +}); + +test.describe(() => { + test.use({ storageState: 'userStorageState.json' }); + + test('user test', async ({ page }) => { + // page is signed in as a user. + }); +}); +``` + +```js js-flavor=js +const { test } = require('@playwright/test'); + +test.use({ storageState: 'adminStorageState.json' }); + +test('admin test', async ({ page }) => { + // page is signed in as amin. +}); + +test.describe(() => { + test.use({ storageState: 'userStorageState.json' }); + + test('user test', async ({ page }) => { + // page is signed in as a user. + }); +}); +``` + +### Reuse the signed in page in multiple tests + +Although discouraged, sometimes it is necessary to sacrifice the isolation and run a number of tests +in the same page. In that case, you can log into that page once in `beforeAll` and then use that same +page in all the tests. Note that you need to run these tests serially using `test.describe.serial` in +order to achieve that: + +```js js-flavor=js +// example.spec.js +// @ts-check + +const { test } = require('@playwright/test'); + +test.describe.serial('use the same page', () => { + /** @type {import('@playwright/test').Page} */ + let page; + + test.beforeAll(async ({ browser }) => { + // Create page yourself and sign in. + page = await browser.newPage(); + await page.goto('https://github.com/login'); + await page.fill('input[name="user"]', 'user'); + await page.fill('input[name="password"]', 'password'); + await page.click('text=Sign in'); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('first test', async () => { + // page is signed in. + }); + + test('second test', async () => { + // page is signed in. + }); +}); +``` + +```js js-flavor=ts +// example.spec.ts + +import { test, Page } from '@playwright/test'; + +test.describe.serial('use the same page', () => { + let page: Page; + + test.beforeAll(async ({ browser }) => { + // Create page once and sign in. + page = await browser.newPage(); + await page.goto('https://github.com/login'); + await page.fill('input[name="user"]', 'user'); + await page.fill('input[name="password"]', 'password'); + await page.click('text=Sign in'); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('first test', async () => { + // page is signed in. + }); + + test('second test', async () => { + // page is signed in. + }); +}); +``` + +:::note +You can also use `storageState` property when you are creating the [`method: Browser.newPage`] in order to +pass it an existing logged in state. +::: diff --git a/package.json b/package.json index c9fedfeeac..7e39e5f6ae 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build-installer": "babel -s --extensions \".ts\" --out-dir lib/utils/ src/utils", "doc": "node utils/doclint/cli.js", "lint": "npm run eslint && npm run tsc && npm run doc && npm run check-deps && node utils/generate_channels.js && node utils/generate_types/ --check-clean && npm run test-types", - "flint": "concurrently \"npm run eslint\" \"npm run tsc\" \"npm run doc\" \"npm run check-deps\" \"node utils/generate_channels.js\" \"node utils/generate_types/ --check-clean\" \"npm run test-types\"", + "flint": "concurrently -s all \"npm run eslint\" \"npm run tsc\" \"npm run doc\" \"npm run check-deps\" \"node utils/generate_channels.js\" \"node utils/generate_types/ --check-clean\" \"npm run test-types\"", "clean": "rimraf lib && rimraf src/generated/", "prepare": "node install-from-github.js", "build": "node utils/build/build.js",