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

Commit 0345fc7

Browse files
Samuel-StOkobenguyent
andauthored
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|nosuchfile|cannotfind/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)

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