page.localStorage — First-Class Web Storage
Stop stringifying functions for page.evaluate. Playwright 1.61 exposes page.localStorage and page.sessionStorage as real objects with getItem, setItem, items, and clear.
📖 The Story: The Token That Wouldn’t Set
A QA engineer needed to seed an auth token into localStorage before navigating to a dashboard. The code looked simple: await page.evaluate((t) => localStorage.setItem('token', t), 'abc123'). It worked — most of the time.
But every few runs, the test failed. The dashboard redirected back to login because the token wasn’t there. The engineer added a await page.waitForTimeout(100) after the evaluate, which fixed it most of the time, but occasionally the test still failed.
The root cause: page.evaluate runs in the page context, but the page hadn’t finished loading when the evaluate fired. The localStorage call silently failed because the page’s origin wasn’t established yet. The engineer needed to add a waitForLoadState before the evaluate — a non-obvious dependency that broke every new test.
The WebStorage API in 1.61 makes this a non-issue. page.localStorage.setItem() waits for the page to be ready, then sets the item. No timing hacks.
🎯 Why It Matters
Sync-Looking
page.localStorage.setItem('token', 'abc') — clean, readable, no stringified functions.
Typed
Full TypeScript support. getItem returns string | null, items() returns a record.
Auto-Waiting
Waits for the page’s origin to be ready. No more waitForTimeout hacks.
Full Surface
getItem, setItem, removeItem, clear, items() — the same API you know from the browser.
🌍 Real World Example
A team seeds 12 localStorage keys before every test (theme, language, feature flags, A/B variant, auth token, etc.). Before 1.61, each key required a separate page.evaluate call with a stringified function, and the test setup was 24 lines of brittle boilerplate. With page.localStorage.setItem(), it’s 12 clean lines, fully typed, and the auto-waiting means no test ever fails because localStorage was set before the origin was ready.
🧒 Explain Like I'm 10
Imagine you want to put a note inside a box. Before, you had to write a letter that said “please put this note in the box”, give the letter to a messenger, and hope the messenger found the box.
Now, you just walk up to the box and put the note in yourself. No messenger, no letter, no hoping.
🎓 Professional Explanation
Web Storage (localStorage and sessionStorage) is the browser’s key-value store scoped to a page’s origin. Setting storage from a Playwright test previously required page.evaluate with a stringified function — async, hard to type, and sensitive to page load timing.
Playwright 1.61 introduces page.localStorage and page.sessionStorage as first-class objects. They expose getItem(), setItem(), removeItem(), clear(), and items() — the same surface as the browser’s Storage interface, but called from your test with full TypeScript support and Playwright’s auto-waiting built in.
📊 The Flow
WebStorage API vs page.evaluate
⚙️ The WebStorage API
The API lives on page.localStorage and page.sessionStorage. Both expose the same methods. All calls are async (return Promises) but look synchronous — you await them like any other Playwright action.
import { test } from class="tk-str">'@playwright/test'; test(class="tk-str">'seed auth token before navigation', async ({ page }) => { await page.goto(class="tk-str">'https:class="tk-com">//app.example.com'); await page.localStorage.setItem(class="tk-str">'token', class="tk-str">'abc123'); await page.localStorage.setItem(class="tk-str">'theme', class="tk-str">'dark'); await page.localStorage.setItem(class="tk-str">'featureFlag_newUI', class="tk-str">'true'); await page.goto(class="tk-str">'https:class="tk-com">//app.example.com/dashboard'); class=class="tk-str">"tk-com">// Dashboard reads token from localStorage — no login redirect });
setItem(key, value) sets a single key. Playwright auto-waits for the page origin to be ready, so you don’t need waitForLoadState before the call.
class=class="tk-str">"tk-com">// Read values back test(class="tk-str">'read localStorage', async ({ page }) => { await page.goto(class="tk-str">'https:class="tk-com">//app.example.com'); await page.localStorage.setItem(class="tk-str">'token', class="tk-str">'abc123'); const token = await page.localStorage.getItem(class="tk-str">'token'); console.log(token); class=class="tk-str">"tk-com">// class="tk-str">'abc123' const missing = await page.localStorage.getItem(class="tk-str">'nonexistent'); console.log(missing); class=class="tk-str">"tk-com">// null class=class="tk-str">"tk-com">// Get all key-value pairs as a record const all = await page.localStorage.items(); console.log(all); class=class="tk-str">"tk-com">// { token: class="tk-str">'abc123', theme: class="tk-str">'dark', ... } });
getItem(key) returns string | null. items() returns a Record<string, string> of all key-value pairs — useful for snapshotting the full storage state.
class=class="tk-str">"tk-com">// Remove and clear test(class="tk-str">'cleanup storage between tests', async ({ page }) => { await page.goto(class="tk-str">'https:class="tk-com">//app.example.com'); class=class="tk-str">"tk-com">// Remove a single key await page.localStorage.removeItem(class="tk-str">'token'); class=class="tk-str">"tk-com">// Clear all localStorage for the current origin await page.localStorage.clear(); class=class="tk-str">"tk-com">// sessionStorage has the same API await page.sessionStorage.setItem(class="tk-str">'cartId', class="tk-str">'cart-456'); const cartId = await page.sessionStorage.getItem(class="tk-str">'cartId'); await page.sessionStorage.clear(); });
removeItem(key) deletes one key. clear() wipes all storage for the current origin. sessionStorage has the identical API but is scoped to the tab’s session.
🔄 Before vs After
Before: page.evaluate with stringified function (1.60 and earlier)
class=class="tk-str">"tk-com">// Async, brittle, sensitive to page load timing await page.evaluate((token) => { window.localStorage.setItem(class="tk-str">'token', token); window.localStorage.setItem(class="tk-str">'theme', class="tk-str">'dark'); }, class="tk-str">'abc123'); class=class="tk-str">"tk-com">// Sometimes fails silently if the page origin isn't ready yet
The evaluate runs in the page context. If the page hasn’t finished loading, localStorage.setItem may silently fail because the origin isn’t established. Engineers add waitForTimeout hacks to work around it.
After: page.localStorage (1.61+)
import { test } from class="tk-str">'@playwright/test'; test(class="tk-str">'seed auth token', async ({ page }) => { await page.goto(class="tk-str">'https:class="tk-com">//app.example.com'); await page.localStorage.setItem(class="tk-str">'token', class="tk-str">'abc123'); await page.localStorage.setItem(class="tk-str">'theme', class="tk-str">'dark'); await page.goto(class="tk-str">'https:class="tk-com">//app.example.com/dashboard'); class=class="tk-str">"tk-com">// Dashboard reads token — no redirect });
Clean, typed, auto-waiting. No stringified functions, no timing hacks. The same API surface as the browser’s localStorage, but called from your test.
Storage is scoped to the page’s current origin. If you navigate to a different origin, you get a fresh storage scope — the keys you set on app.example.com are not visible on api.example.com. If you need to seed storage for multiple origins, set it after each goto that changes the origin.
⚠️ Common Mistakes
Mistake 1: Setting storage before goto
If you call page.localStorage.setItem() before page.goto(), there’s no page yet — the call will fail or set storage on about:blank. Always navigate to the origin first, then set storage.
Mistake 2: Forgetting storage is origin-scoped
localStorage is scoped to the origin. Setting it on app.example.com doesn’t affect api.example.com. If your test navigates across origins, set storage for each origin separately.
Mistake 3: Treating items() as synchronous
page.localStorage.items() returns a Promise. You must await it. Writing const items = page.localStorage.items() without await gives you a Promise object, not the storage record.
✅ Best Practices
- Always goto first — the page must be on the target origin before you set storage.
- Use items() for snapshots — useful for asserting the full storage state in one call.
- Clear storage in afterEach to prevent leakage between tests.
- Pair with storageState for auth — seed once, save, load into future contexts.
- Prefer setItem over evaluate — it’s typed, auto-waiting, and easier to read.
🏗️ Senior Engineer Deep Dive
When to use WebStorage vs storageState
page.localStorage.setItem() sets storage on the current page — useful mid-test when you need to inject state after navigation. storageState saves the entire storage (plus cookies) to a file that can be loaded into a fresh context — useful for auth setup tests. Use WebStorage for in-test mutations, use storageState for cross-test persistence.
Why auto-waiting matters here
The browser’s localStorage is only available after the page’s origin is established. With page.evaluate, you had to manually waitForLoadState or risk a silent failure. The WebStorage API handles this internally — it waits for the origin to be ready before executing the storage call. This eliminates an entire class of flaky tests.
💼 Interview Questions
Show Answer
Show Answer
Show Answer
Show Answer
🏋️ Practical Exercise
Seed Storage the Clean Way
- Write a test that navigates to
https://example.com - Set three localStorage keys using
page.localStorage.setItem() - Read them back with
page.localStorage.items()and log the result - Clear storage with
page.localStorage.clear() - Verify items() returns an empty object
🚀 Mini Project
🏗️ The Auth Seed Pattern
- Write a setup test that logs in via the UI and saves storageState to
auth.json - In your main tests, configure
use: { storageState: 'auth.json' } - Write a test that needs to override the theme mid-test — use
page.localStorage.setItem('theme', 'dark') - Assert the UI re-renders with the dark theme
- Compare the readability to the old page.evaluate approach
📝 Quiz
Test your understanding. Click an option to check your answer.
❓ FAQ
Q: Can I use WebStorage to set cookies?
A: No. Cookies and Web Storage are different APIs. Use context.addCookies() for cookies, page.localStorage / page.sessionStorage for Web Storage. The storageState API saves and restores both.
Q: Does WebStorage work with IndexedDB?
A: No. IndexedDB is a separate, more complex async database. Playwright does not have a first-class IndexedDB API — you still use page.evaluate for IndexedDB operations.
Q: Can I set storage on a popup or iframe?
A: Yes, but you need a reference to the page/frame. page.localStorage sets storage on the main frame’s origin. For a popup, use popup.localStorage. For an iframe, use frame.evaluate() until a first-class frame.localStorage API ships.
📦 Summary
🎯 Key Takeaways
- page.localStorage / sessionStorage expose a typed, auto-waiting WebStorage API.
- Always goto first — the page must be on the target origin before setting storage.
- items() returns all key-value pairs as a Record—useful for snapshotting.
- Storage is origin-scoped — setting it on one origin doesn’t affect another.
- Use storageState for cross-test persistence, WebStorage for in-test mutations.
Comments
Comments
Post a Comment