Skip to content

Commit 0345fc7

Browse files
feat: Add support for Playwright storageState configuration (#5192)
* feat: add support for Playwright storageState configuration and helper methods * doc : run docs --------- Co-authored-by: kobenguyent <kobenguyent@gmail.com>
1 parent 2505ac7 commit 0345fc7

File tree

4 files changed

+301
-3
lines changed

4 files changed

+301
-3
lines changed

docs/helpers/Playwright.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ Type: [object][6]
8282
* `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
8383
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][49].
8484
* `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}"]`) } }`
85+
* `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object)
86+
passed directly to `browser.newContext`.
87+
If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
88+
those cookies are used instead and the configured `storageState` is ignored (no merge).
89+
May include session cookies, auth tokens, localStorage and (if captured with
90+
`grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
8591

8692

8793

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

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

1342+
### grabStorageState
1343+
1344+
Grab the current storage state (cookies, localStorage, etc.) via Playwright's `browserContext.storageState()`.
1345+
Returns the raw object that Playwright provides.
1346+
1347+
Security: The returned object can contain authentication tokens, session cookies
1348+
and (when `indexedDB: true` is used) data that may include user PII. Treat it as a secret.
1349+
Avoid committing it to source control and prefer storing it in a protected secrets store / CI artifact vault.
1350+
1351+
#### Parameters
1352+
1353+
* `options` **[object][6]?**
1354+
1355+
* `options.indexedDB` **[boolean][26]?** set to true to include IndexedDB in snapshot (Playwright >=1.51)```js
1356+
// basic usage
1357+
const state = await I.grabStorageState();
1358+
require('fs').writeFileSync('authState.json', JSON.stringify(state));
1359+
1360+
// include IndexedDB when using Firebase Auth, etc.
1361+
const stateWithIDB = await I.grabStorageState({ indexedDB: true });
1362+
```
1363+
13361364
### grabTextFrom
13371365
13381366
Retrieves a text from an element located by CSS or XPath and returns it to test.

docs/playwright.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,3 +664,48 @@ Playwright can be added to GitHub Actions using [official action](https://github
664664
- name: run CodeceptJS tests
665665
run: npx codeceptjs run
666666
```
667+
668+
## Reusing Auth State (storageState) <Badge text="Since 3.7.5" type="warning"/>
669+
670+
Use Playwright's native `storageState` to start tests already authenticated.
671+
Pass either a JSON file path or a state object to the Playwright helper; CodeceptJS forwards it directly to Playwright (no pre-checks).
672+
673+
**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.
674+
675+
Reference: https://playwright.dev/docs/auth#reuse-authentication-state
676+
677+
**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.
678+
679+
Minimal examples:
680+
681+
```js
682+
// File path
683+
helpers: { Playwright: { url: 'http://localhost', browser: 'chromium', storageState: 'authState.json' } }
684+
685+
// Inline object
686+
const state = require('./authState.json');
687+
helpers: { Playwright: { url: 'http://localhost', browser: 'chromium', storageState: state } }
688+
```
689+
690+
Scenario with explicit cookies (bypasses configured storageState):
691+
692+
```js
693+
const authCookies = [{ name: 'session', value: 'abc123', domain: 'localhost', path: '/', httpOnly: true, secure: false, sameSite: 'Lax' }]
694+
Scenario('Dashboard (authenticated)', { cookies: authCookies }, ({ I }) => {
695+
I.amOnPage('/dashboard')
696+
I.see('Welcome')
697+
})
698+
```
699+
700+
Helper snippet:
701+
702+
```js
703+
// Grab current state as object
704+
const state = await I.grabStorageState()
705+
// Persist manually (sensitive file!)
706+
require('fs').writeFileSync('authState.json', JSON.stringify(state))
707+
708+
// Include IndexedDB (Playwright >= 1.51) if your app relies on it (e.g. Firebase Auth persistence)
709+
const stateWithIDB = await I.grabStorageState({ indexedDB: true })
710+
require('fs').writeFileSync('authState-with-idb.json', JSON.stringify(stateWithIDB))
711+
```

lib/helper/Playwright.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ const pathSeparator = path.sep
9898
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
9999
* @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).
100100
* @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).
101-
* @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}\"]\`) } }`
101+
* @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}"]`) } }`
102+
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
103+
* passed directly to `browser.newContext`.
104+
* If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
105+
* those cookies are used instead and the configured `storageState` is ignored (no merge).
106+
* May include session cookies, auth tokens, localStorage and (if captured with
107+
* `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
102108
*/
103109
const config = {}
104110

@@ -360,6 +366,11 @@ class Playwright extends Helper {
360366
// override defaults with config
361367
this._setConfig(config)
362368

369+
// pass storageState directly (string path or object) and let Playwright handle errors/missing file
370+
if (typeof config.storageState !== 'undefined') {
371+
this.storageState = config.storageState
372+
}
373+
363374
}
364375

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

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

590602
// load pre-saved cookies
591603
if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }
592-
593-
if (this.storageState) contextOptions.storageState = this.storageState
604+
else if (this.storageState) contextOptions.storageState = this.storageState
594605
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
595606
if (this.options.locale) contextOptions.locale = this.options.locale
596607
if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
@@ -2162,6 +2173,30 @@ class Playwright extends Helper {
21622173
if (cookie[0]) return cookie[0]
21632174
}
21642175

2176+
/**
2177+
* Grab the current storage state (cookies, localStorage, etc.) via Playwright's `browserContext.storageState()`.
2178+
* Returns the raw object that Playwright provides.
2179+
*
2180+
* Security: The returned object can contain authentication tokens, session cookies
2181+
* and (when `indexedDB: true` is used) data that may include user PII. Treat it as a secret.
2182+
* Avoid committing it to source control and prefer storing it in a protected secrets store / CI artifact vault.
2183+
*
2184+
* @param {object} [options]
2185+
* @param {boolean} [options.indexedDB] set to true to include IndexedDB in snapshot (Playwright >=1.51)
2186+
*
2187+
* ```js
2188+
* // basic usage
2189+
* const state = await I.grabStorageState();
2190+
* require('fs').writeFileSync('authState.json', JSON.stringify(state));
2191+
*
2192+
* // include IndexedDB when using Firebase Auth, etc.
2193+
* const stateWithIDB = await I.grabStorageState({ indexedDB: true });
2194+
* ```
2195+
*/
2196+
async grabStorageState(options = {}) {
2197+
return this.browserContext.storageState(options)
2198+
}
2199+
21652200
/**
21662201
* {{> clearCookie }}
21672202
*/

test/helper/Playwright_test.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,3 +1980,193 @@ describe('using data-testid attribute', () => {
19801980
assert.equal(webElements.length, 1)
19811981
})
19821982
})
1983+
1984+
// Tests for storageState configuration & helper behavior
1985+
describe('Playwright - storageState object ', function () {
1986+
let I
1987+
1988+
before(() => {
1989+
global.codecept_dir = path.join(__dirname, '/../data')
1990+
1991+
// Provide a storageState object (cookie + localStorage) to seed the context
1992+
I = new Playwright({
1993+
url: siteUrl,
1994+
browser: 'chromium',
1995+
restart: true,
1996+
show: false,
1997+
waitForTimeout: 5000,
1998+
waitForAction: 200,
1999+
storageState: {
2000+
cookies: [
2001+
{
2002+
name: 'auth',
2003+
value: '123',
2004+
domain: 'localhost',
2005+
path: '/',
2006+
httpOnly: false,
2007+
secure: false,
2008+
sameSite: 'Lax',
2009+
},
2010+
],
2011+
origins: [
2012+
{
2013+
origin: siteUrl,
2014+
localStorage: [{ name: 'ls_key', value: 'ls_val' }],
2015+
},
2016+
],
2017+
},
2018+
})
2019+
I._init()
2020+
return I._beforeSuite()
2021+
})
2022+
2023+
afterEach(async () => {
2024+
return I._after()
2025+
})
2026+
2027+
it('should apply config storageState (cookies & localStorage)', async () => {
2028+
await I._before()
2029+
await I.amOnPage('/')
2030+
const cookies = await I.grabCookie()
2031+
const names = cookies.map(c => c.name)
2032+
expect(names).to.include('auth')
2033+
const authCookie = cookies.find(c => c.name === 'auth')
2034+
expect(authCookie && authCookie.value).to.equal('123')
2035+
const lsVal = await I.executeScript(() => localStorage.getItem('ls_key'))
2036+
assert.equal(lsVal, 'ls_val')
2037+
})
2038+
2039+
it('should allow Scenario cookies to override config storageState', async () => {
2040+
const test = {
2041+
title: 'override cookies scenario',
2042+
opts: {
2043+
cookies: [
2044+
{
2045+
name: 'override',
2046+
value: '2',
2047+
domain: 'localhost',
2048+
path: '/',
2049+
},
2050+
],
2051+
},
2052+
}
2053+
await I._before(test)
2054+
await I.amOnPage('/')
2055+
const cookies = await I.grabCookie()
2056+
const names = cookies.map(c => c.name)
2057+
expect(names).to.include('override')
2058+
expect(names).to.not.include('auth') // original config cookie ignored for this Scenario
2059+
const overrideCookie = cookies.find(c => c.name === 'override')
2060+
expect(overrideCookie && overrideCookie.value).to.equal('2')
2061+
})
2062+
2063+
it('grabStorageState should return current state', async () => {
2064+
await I._before()
2065+
await I.amOnPage('/')
2066+
const state = await I.grabStorageState()
2067+
expect(state.cookies).to.be.an('array')
2068+
const names = state.cookies.map(c => c.name)
2069+
expect(names).to.include('auth')
2070+
expect(state.origins).to.be.an('array')
2071+
const originEntry = state.origins.find(o => o.origin === siteUrl)
2072+
expect(originEntry).to.exist
2073+
if (originEntry && originEntry.localStorage) {
2074+
const lsNames = originEntry.localStorage.map(e => e.name)
2075+
expect(lsNames).to.include('ls_key')
2076+
}
2077+
// With IndexedDB flag (will include same base data; presence suffices)
2078+
const stateIdx = await I.grabStorageState({ indexedDB: true })
2079+
expect(stateIdx).to.be.ok
2080+
})
2081+
})
2082+
2083+
// Additional tests for storageState file path usage and error conditions
2084+
describe('Playwright - storageState file path', function () {
2085+
this.timeout(15000)
2086+
it('should load storageState from a JSON file path', async () => {
2087+
const tmpPath = path.join(__dirname, '../data/output/tmp-auth-state.json')
2088+
const fileState = {
2089+
cookies: [{ name: 'filecookie', value: 'f1', domain: 'localhost', path: '/' }],
2090+
origins: [{ origin: siteUrl, localStorage: [{ name: 'from_file', value: 'yes' }] }],
2091+
}
2092+
fs.mkdirSync(path.dirname(tmpPath), { recursive: true })
2093+
fs.writeFileSync(tmpPath, JSON.stringify(fileState, null, 2))
2094+
2095+
let I = new Playwright({
2096+
url: siteUrl,
2097+
browser: 'chromium',
2098+
restart: true,
2099+
show: false,
2100+
storageState: tmpPath,
2101+
})
2102+
I._init()
2103+
await I._beforeSuite()
2104+
await I._before()
2105+
await I.amOnPage('/')
2106+
const cookies = await I.grabCookie()
2107+
const names = cookies.map(c => c.name)
2108+
expect(names).to.include('filecookie')
2109+
const lsVal = await I.executeScript(() => localStorage.getItem('from_file'))
2110+
expect(lsVal).to.equal('yes')
2111+
await I._after()
2112+
})
2113+
2114+
it('should allow Scenario cookies to override file-based storageState', async () => {
2115+
const tmpPath = path.join(__dirname, '../data/output/tmp-auth-state-override.json')
2116+
const fileState = {
2117+
cookies: [{ name: 'basecookie', value: 'b1', domain: 'localhost', path: '/' }],
2118+
origins: [{ origin: siteUrl, localStorage: [{ name: 'persist', value: 'keep' }] }],
2119+
}
2120+
fs.mkdirSync(path.dirname(tmpPath), { recursive: true })
2121+
fs.writeFileSync(tmpPath, JSON.stringify(fileState, null, 2))
2122+
2123+
let I = new Playwright({
2124+
url: siteUrl,
2125+
browser: 'chromium',
2126+
restart: true,
2127+
show: false,
2128+
storageState: tmpPath,
2129+
})
2130+
I._init()
2131+
await I._beforeSuite()
2132+
const test = {
2133+
title: 'override cookies with file-based storageState',
2134+
opts: {
2135+
cookies: [{ name: 'override_from_file', value: 'ov1', domain: 'localhost', path: '/' }],
2136+
},
2137+
}
2138+
await I._before(test)
2139+
await I.amOnPage('/')
2140+
const cookies = await I.grabCookie()
2141+
const names = cookies.map(c => c.name)
2142+
expect(names).to.include('override_from_file')
2143+
expect(names).to.not.include('basecookie')
2144+
const overrideCookie = cookies.find(c => c.name === 'override_from_file')
2145+
expect(overrideCookie && overrideCookie.value).to.equal('ov1')
2146+
await I._after()
2147+
})
2148+
2149+
it('should throw when storageState file path does not exist', async () => {
2150+
const badPath = path.join(__dirname, '../data/output/missing-auth-state.json')
2151+
let I = new Playwright({
2152+
url: siteUrl,
2153+
browser: 'chromium',
2154+
restart: true,
2155+
show: false,
2156+
storageState: badPath,
2157+
})
2158+
I._init()
2159+
await I._beforeSuite()
2160+
let threw = false
2161+
try {
2162+
await I._before()
2163+
} catch (e) {
2164+
threw = true
2165+
expect(e.message).to.match(/ENOENT|no such file|cannot find/i)
2166+
}
2167+
expect(threw, 'expected missing storageState path to throw').to.be.true
2168+
try {
2169+
await I._after()
2170+
} catch (_) {}
2171+
})
2172+
})

0 commit comments

Comments
 (0)