diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md
index d03602539..2df11ee95 100644
--- a/docs/helpers/Playwright.md
+++ b/docs/helpers/Playwright.md
@@ -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.
@@ -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.
diff --git a/docs/playwright.md b/docs/playwright.md
index f43bbc0a9..5c5d6bb3c 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -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)
+
+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))
+```
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index ff0118793..e12180d6b 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -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 = {}
@@ -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) {
@@ -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'
@@ -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
@@ -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 }}
*/
diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js
index 430798cf9..d16af3281 100644
--- a/test/helper/Playwright_test.js
+++ b/test/helper/Playwright_test.js
@@ -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 (_) {}
+ })
+})