Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Add support for Playwright storageState configuration #5192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
kobenguyent merged 3 commits into codeceptjs:3.x from Samuel-StO:feat/storageState-playwright
Sep 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some comments aren't visible on the classic Files Changed page.

28 changes: 28 additions & 0 deletions docs/helpers/Playwright.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ Type: [object][6]
* `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][49].
* `customLocatorStrategies` **[object][6]?** custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(`[role="${selector}"]`) } }`
* `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object)
passed directly to `browser.newContext`.
If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
those cookies are used instead and the configured `storageState` is ignored (no merge).
May include session cookies, auth tokens, localStorage and (if captured with
`grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.



Expand Down Expand Up @@ -1333,6 +1339,28 @@ let pageSource = await I.grabSource();

Returns **[Promise][22]<[string][9]>** source code

### grabStorageState

Grab the current storage state (cookies, localStorage, etc.) via Playwright's `browserContext.storageState()`.
Returns the raw object that Playwright provides.

Security: The returned object can contain authentication tokens, session cookies
and (when `indexedDB: true` is used) data that may include user PII. Treat it as a secret.
Avoid committing it to source control and prefer storing it in a protected secrets store / CI artifact vault.

#### Parameters

* `options` **[object][6]?**

* `options.indexedDB` **[boolean][26]?** set to true to include IndexedDB in snapshot (Playwright >=1.51)```js
// basic usage
const state = await I.grabStorageState();
require('fs').writeFileSync('authState.json', JSON.stringify(state));

// include IndexedDB when using Firebase Auth, etc.
const stateWithIDB = await I.grabStorageState({ indexedDB: true });
```

### grabTextFrom

Retrieves a text from an element located by CSS or XPath and returns it to test.
Expand Down
45 changes: 45 additions & 0 deletions docs/playwright.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,48 @@ Playwright can be added to GitHub Actions using [official action](https://github
- name: run CodeceptJS tests
run: npx codeceptjs run
```

## Reusing Auth State (storageState) <Badge text="Since 3.7.5" type="warning"/>

Use Playwright's native `storageState` to start tests already authenticated.
Pass either a JSON file path or a state object to the Playwright helper; CodeceptJS forwards it directly to Playwright (no pre-checks).

**Sensitive**: A storage state contains session cookies, auth tokens and may contain localStorage / IndexedDB application data. Treat it like a secret: do not commit it to git, encrypt or store it in a secure CI artifact store.

Reference: https://playwright.dev/docs/auth#reuse-authentication-state

**Limitation**: If a Scenario is declared with a `cookies` option (e.g. `Scenario('My test', { cookies: [...] }, ({ I }) => { ... })`), those cookies are used to initialize the context and any helper-level `storageState` is ignored (no merge). Choose one mechanism per Scenario.

Minimal examples:

```js
// File path
helpers: { Playwright: { url: 'http://localhost', browser: 'chromium', storageState: 'authState.json' } }

// Inline object
const state = require('./authState.json');
helpers: { Playwright: { url: 'http://localhost', browser: 'chromium', storageState: state } }
```

Scenario with explicit cookies (bypasses configured storageState):

```js
const authCookies = [{ name: 'session', value: 'abc123', domain: 'localhost', path: '/', httpOnly: true, secure: false, sameSite: 'Lax' }]
Scenario('Dashboard (authenticated)', { cookies: authCookies }, ({ I }) => {
I.amOnPage('/dashboard')
I.see('Welcome')
})
```

Helper snippet:

```js
// Grab current state as object
const state = await I.grabStorageState()
// Persist manually (sensitive file!)
require('fs').writeFileSync('authState.json', JSON.stringify(state))

// Include IndexedDB (Playwright >= 1.51) if your app relies on it (e.g. Firebase Auth persistence)
const stateWithIDB = await I.grabStorageState({ indexedDB: true })
require('fs').writeFileSync('authState-with-idb.json', JSON.stringify(stateWithIDB))
```
41 changes: 38 additions & 3 deletions lib/helper/Playwright.js
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,13 @@ const pathSeparator = path.sep
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
* @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(\`[role="\${selector}\"]\`) } }`
* @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(`[role="${selector}"]`) } }`
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
* passed directly to `browser.newContext`.
* If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
* those cookies are used instead and the configured `storageState` is ignored (no merge).
* May include session cookies, auth tokens, localStorage and (if captured with
* `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
*/
const config = {}

Expand Down Expand Up @@ -360,6 +366,11 @@ class Playwright extends Helper {
// override defaults with config
this._setConfig(config)

// pass storageState directly (string path or object) and let Playwright handle errors/missing file
if (typeof config.storageState !== 'undefined') {
this.storageState = config.storageState
}

}

_validateConfig(config) {
Expand All @@ -386,6 +397,7 @@ class Playwright extends Helper {
use: { actionTimeout: 0 },
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
highlightElement: false,
storageState: undefined,
}

process.env.testIdAttribute = 'data-testid'
Expand Down Expand Up @@ -589,8 +601,7 @@ class Playwright extends Helper {

// load pre-saved cookies
if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }

if (this.storageState) contextOptions.storageState = this.storageState
else if (this.storageState) contextOptions.storageState = this.storageState
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
if (this.options.locale) contextOptions.locale = this.options.locale
if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
Expand Down Expand Up @@ -2162,6 +2173,30 @@ class Playwright extends Helper {
if (cookie[0]) return cookie[0]
}

/**
* Grab the current storage state (cookies, localStorage, etc.) via Playwright's `browserContext.storageState()`.
* Returns the raw object that Playwright provides.
*
* Security: The returned object can contain authentication tokens, session cookies
* and (when `indexedDB: true` is used) data that may include user PII. Treat it as a secret.
* Avoid committing it to source control and prefer storing it in a protected secrets store / CI artifact vault.
*
* @param {object} [options]
* @param {boolean} [options.indexedDB] set to true to include IndexedDB in snapshot (Playwright >=1.51)
*
* ```js
* // basic usage
* const state = await I.grabStorageState();
* require('fs').writeFileSync('authState.json', JSON.stringify(state));
*
* // include IndexedDB when using Firebase Auth, etc.
* const stateWithIDB = await I.grabStorageState({ indexedDB: true });
* ```
*/
async grabStorageState(options = {}) {
return this.browserContext.storageState(options)
}

/**
* {{> clearCookie }}
*/
Expand Down
190 changes: 190 additions & 0 deletions test/helper/Playwright_test.js
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1980,3 +1980,193 @@ describe('using data-testid attribute', () => {
assert.equal(webElements.length, 1)
})
})

// Tests for storageState configuration & helper behavior
describe('Playwright - storageState object ', function () {
let I

before(() => {
global.codecept_dir = path.join(__dirname, '/../data')

// Provide a storageState object (cookie + localStorage) to seed the context
I = new Playwright({
url: siteUrl,
browser: 'chromium',
restart: true,
show: false,
waitForTimeout: 5000,
waitForAction: 200,
storageState: {
cookies: [
{
name: 'auth',
value: '123',
domain: 'localhost',
path: '/',
httpOnly: false,
secure: false,
sameSite: 'Lax',
},
],
origins: [
{
origin: siteUrl,
localStorage: [{ name: 'ls_key', value: 'ls_val' }],
},
],
},
})
I._init()
return I._beforeSuite()
})

afterEach(async () => {
return I._after()
})

it('should apply config storageState (cookies & localStorage)', async () => {
await I._before()
await I.amOnPage('/')
const cookies = await I.grabCookie()
const names = cookies.map(c => c.name)
expect(names).to.include('auth')
const authCookie = cookies.find(c => c.name === 'auth')
expect(authCookie && authCookie.value).to.equal('123')
const lsVal = await I.executeScript(() => localStorage.getItem('ls_key'))
assert.equal(lsVal, 'ls_val')
})

it('should allow Scenario cookies to override config storageState', async () => {
const test = {
title: 'override cookies scenario',
opts: {
cookies: [
{
name: 'override',
value: '2',
domain: 'localhost',
path: '/',
},
],
},
}
await I._before(test)
await I.amOnPage('/')
const cookies = await I.grabCookie()
const names = cookies.map(c => c.name)
expect(names).to.include('override')
expect(names).to.not.include('auth') // original config cookie ignored for this Scenario
const overrideCookie = cookies.find(c => c.name === 'override')
expect(overrideCookie && overrideCookie.value).to.equal('2')
})

it('grabStorageState should return current state', async () => {
await I._before()
await I.amOnPage('/')
const state = await I.grabStorageState()
expect(state.cookies).to.be.an('array')
const names = state.cookies.map(c => c.name)
expect(names).to.include('auth')
expect(state.origins).to.be.an('array')
const originEntry = state.origins.find(o => o.origin === siteUrl)
expect(originEntry).to.exist
if (originEntry && originEntry.localStorage) {
const lsNames = originEntry.localStorage.map(e => e.name)
expect(lsNames).to.include('ls_key')
}
// With IndexedDB flag (will include same base data; presence suffices)
const stateIdx = await I.grabStorageState({ indexedDB: true })
expect(stateIdx).to.be.ok
})
})

// Additional tests for storageState file path usage and error conditions
describe('Playwright - storageState file path', function () {
this.timeout(15000)
it('should load storageState from a JSON file path', async () => {
const tmpPath = path.join(__dirname, '../data/output/tmp-auth-state.json')
const fileState = {
cookies: [{ name: 'filecookie', value: 'f1', domain: 'localhost', path: '/' }],
origins: [{ origin: siteUrl, localStorage: [{ name: 'from_file', value: 'yes' }] }],
}
fs.mkdirSync(path.dirname(tmpPath), { recursive: true })
fs.writeFileSync(tmpPath, JSON.stringify(fileState, null, 2))

let I = new Playwright({
url: siteUrl,
browser: 'chromium',
restart: true,
show: false,
storageState: tmpPath,
})
I._init()
await I._beforeSuite()
await I._before()
await I.amOnPage('/')
const cookies = await I.grabCookie()
const names = cookies.map(c => c.name)
expect(names).to.include('filecookie')
const lsVal = await I.executeScript(() => localStorage.getItem('from_file'))
expect(lsVal).to.equal('yes')
await I._after()
})

it('should allow Scenario cookies to override file-based storageState', async () => {
const tmpPath = path.join(__dirname, '../data/output/tmp-auth-state-override.json')
const fileState = {
cookies: [{ name: 'basecookie', value: 'b1', domain: 'localhost', path: '/' }],
origins: [{ origin: siteUrl, localStorage: [{ name: 'persist', value: 'keep' }] }],
}
fs.mkdirSync(path.dirname(tmpPath), { recursive: true })
fs.writeFileSync(tmpPath, JSON.stringify(fileState, null, 2))

let I = new Playwright({
url: siteUrl,
browser: 'chromium',
restart: true,
show: false,
storageState: tmpPath,
})
I._init()
await I._beforeSuite()
const test = {
title: 'override cookies with file-based storageState',
opts: {
cookies: [{ name: 'override_from_file', value: 'ov1', domain: 'localhost', path: '/' }],
},
}
await I._before(test)
await I.amOnPage('/')
const cookies = await I.grabCookie()
const names = cookies.map(c => c.name)
expect(names).to.include('override_from_file')
expect(names).to.not.include('basecookie')
const overrideCookie = cookies.find(c => c.name === 'override_from_file')
expect(overrideCookie && overrideCookie.value).to.equal('ov1')
await I._after()
})

it('should throw when storageState file path does not exist', async () => {
const badPath = path.join(__dirname, '../data/output/missing-auth-state.json')
let I = new Playwright({
url: siteUrl,
browser: 'chromium',
restart: true,
show: false,
storageState: badPath,
})
I._init()
await I._beforeSuite()
let threw = false
try {
await I._before()
} catch (e) {
threw = true
expect(e.message).to.match(/ENOENT|no such file|cannot find/i)
}
expect(threw, 'expected missing storageState path to throw').to.be.true
try {
await I._after()
} catch (_) {}
})
})

AltStyle によって変換されたページ (->オリジナル) /