It was 2 a.m. when the CI alert fired. A front-end login test case had suddenly started failing — it was perfectly fine yesterday. Bleary-eyed, I opened the Allure report and saw the failure screenshot: login succeeded, the token was saved into LocalStorage, but after a page reload, the token was gone and the user got kicked back to the login page.
That makes no sense, I thought. The front-end colleague swore that localStorage is persistent. How could it disappear after a refresh? Even weirder, the flow worked flawlessly when I tested it manually in the browser, yet the automation script reproduced the issue consistently. At that moment I knew: I wasn't going to bed anytime soon.
Breaking Down the Problem
Our scenario is quite typical: after a user logs in, the front end writes a JWT via localStorage.setItem('token', xxx) and then navigates to the home page. To verify that "the user stays logged in after a page refresh," we wrote the following Playwright test:
- Simulate login → assert that a token exists in
localStorage page.reload()- Assert again that the token is still there and that the page doesn’t redirect to
/login
Manual testing worked perfectly, yet the automation failed most of the time: after reload, localStorage was cleared. I initially suspected the front end was wiping it during a reload. I combed through the source code — no cleanup logic. Then I wondered if Playwright’s page.reload behaved oddly, so I swapped it with page.goto — still broken.
The root cause gradually surfaced: localStorage persistence is tied to the browser storage directory, and we were launching Playwright with the default launch parameters, without specifying a persistent directory. This means that every browser.new_context() gave us a temporary profile. Data wouldn’t get lost when you call page.reload inside the same context. But our test framework used @pytest.fixture(scope="function") to create a brand-new context for each test case. In cross-context scenarios — such as recreating a context to simulate closing and reopening the browser — localStorage was gone.
To make matters worse, CI container environments naturally create a fresh browser instance per job, and the behavior of new_context can differ from a developer’s local machine (where caching and recycling mechanisms may kick in). This led to tests that occasionally passed locally but always failed in CI. Here lies the reason why "the conventional approach doesn't work": directly asserting on localStorage only verifies the data living inside the current session’s memory, not actual "disk persistence."
Solution Design
I outlined several possible verification paths:
-
Option A: Read localStorage directly via
page.evaluate– Verifies the current session but cannot guarantee that the data has been flushed to disk and can survive a “close browser and reopen” scenario. Pass. -
Option B: Selenium’s
local_storageinterface – Selenium 4 supports it, but it requires extra configuration for the Chrome user data directory. The maintenance cost is high, and since we’ve already fully migrated to Playwright, there’s no reason to go backwards. -
Option C: Playwright’s
storageStatesnapshot + reload – Playwright providescontext.storage_state()to export the entire storage state (cookies + LocalStorage and other origin storage), and allows you to hydrate it back via thestorage_stateparameter when creating a new context. This precisely simulates the data recovery process of “disk persistence → next browser session.”
So the final choice: Combine Playwright + pytest with the storageState mechanism, and design a fixture to truly verify localStorage persistence. This not only tests the front-end logic but also incidentally validates the correctness of storage recovery.
Core Implementation
1. A reusable helper: save and restore storage state
This code addresses the problem of “accurately simulating closing and reopening the browser while preserving localStorage.”
import json
import os
from pathlib import Path
from playwright.sync_api import BrowserContext
def save_and_reload_storage(context: BrowserContext, filepath: str = "state.json") -> BrowserContext:
"""
Serialize the current context's storageState to a file,
then close the original context and create a brand new one
using the saved state. This simulates closing the browser
and opening it again.
"""
# Save the complete storage state (cookies + localStorage)
state = context.storage_state()
Path(filepath).write_text(json.dumps(state), encoding="utf-8")
# Close the original context, releasing resources
page = context.pages[0] if context.pages else None
if page:
page.close()
context.close()
# Rebuild context from the saved state
browser = context.browser
# Important: use the same browser instance, otherwise a new variable is introduced
new_context = browser.new_context(storage_state=filepath)
return new_context
Key points: The storage_state parameter must point to a file path (Playwright reads it internally); you can also pass a dict, but a file is easier to audit in logs. Also, be sure to close the old context first before creating the new one to guarantee a fully fresh session simulation.
2. pytest fixture: providing independent context and verification tools for tests
This code addresses “managing the Playwright lifecycle uniformly inside pytest and offering persistent verification capabilities.”
import pytest
from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page
@pytest.fixture(scope="function")
def persisted_context() -> BrowserContext:
"""
Each test case receives a BrowserContext equipped with
persistent verification capabilities.
Inside the test, you can simulate reloading state via reload_state()
to model
Top comments (0)