Seeding session storage
Introduction
Section titled “Introduction”An app often reads session storage once, as it starts: a token, a feature flag, a basket. Writing the value after the
app has loaded is too late, and writing it after a first load then reloading loads the app twice. Playwright’s
addInitScript runs before the app’s first script,
but on every load until it is removed, so it would overwrite what the app writes. seed uses it for one arrival: it
stores the entries for an origin before the app’s next load there, so the app’s first script already sees them, and
then gets out of the way.
import { test } from "./fixtures";
test("a returning customer", async ({ loginPage }) => { await loginPage.sessionStorage.seed({ token: "eyJ…" }); // pending for https://shop.example await loginPage.navigation.goto(); // the app's first script already sees the token});How it works
Section titled “How it works”When the page is already on the origin, seed writes at once; the app sees the entries the next time it reads
storage, so reload or navigate if it reads only at startup. Otherwise the entries wait for the origin. POMWright adds an
init script to the page that stores them when the page’s main frame first arrives at the origin, before the app’s first
script runs, whatever takes the page there (goto, a link, a script, a form POST, a server redirect or a chain of
them) and whatever answers (the server, a service worker, a route the test registered). Once that document has run the
script, POMWright removes it:
sequenceDiagram
participant Test
participant POMWright
participant Browser
participant App as App at shop.example
Test->>POMWright: seed({ token })
POMWright->>Browser: an init script for https://shop.example
Test->>Browser: a navigation to shop.example (goto, a link, a redirect, a form)
Browser->>App: the request, as without a seed
App-->>Browser: the app's document
Note over Browser: the init script stores the entries before the app's first script
Browser-->>POMWright: the main frame committed on shop.example
POMWright->>Browser: remove the init script
The script writes only on the first arrival at the origin. If the app writes a seeded key and then reloads, or moves to another page on the same origin, before the script is removed, the script leaves the app’s value alone.
The navigation itself is never intercepted. The page the test is on is never left before it, so an origin can be seeded with data the test learns on the previous one. When the entries are applied, POMWright removes its listeners: a test that never seeds runs exactly as it would without POMWright.
Two patterns
Section titled “Two patterns”When everything is known up front, seed every origin first, then start the flow. Session storage is per tab and per origin, so each origin keeps its entries while the page is elsewhere:
import { SessionStorage } from "pomwright";import { test } from "./fixtures";
test("a consent page on the identity provider", async ({ loginPage, page }) => { const identityProvider = new SessionStorage(page, { label: "Login", origin: "https://login.example" });
await identityProvider.seed({ consent: "given" }); await loginPage.sessionStorage.seed({ token: "eyJ…" }); await loginPage.navigation.goto();});When a value is learned on the way, seed the next origin before the hop:
import { SessionStorage } from "pomwright";import { test } from "./fixtures";
test("a payment origin", async ({ checkoutPage, page }) => { const payment = new SessionStorage(page, { label: "Payment", origin: "https://pay.example" });
await checkoutPage.navigation.goto(); const orderId = await checkoutPage.orderId(); await payment.seed({ orderId }); // the page stays on shop.example await checkoutPage.pay(); // the app sends the browser to pay.example});Several seeds
Section titled “Several seeds”Await each seed before the action that starts the hop. A seed that is still registering when the page arrives at
its origin fails, because that document may have loaded without the entries. Without await, the call races the
navigation: depending on the browser and the timing, it applies or it fails. A failure that nothing awaits fails the
test as an unhandled rejection, and a test that ends before the hop fails because the seed was never applied. The
no-floating-promises lint rule of typescript-eslint
finds a missing await.
Calls for one origin merge, in the order they were made, later values winning per key. One call with every entry says the same:
import { test } from "./fixtures";
test("a returning customer in Norwegian", async ({ loginPage }) => { await loginPage.sessionStorage.seed({ token: "eyJ…", locale: "en" }); await loginPage.sessionStorage.seed({ locale: "nb" }); // the same as seed({ token: "eyJ…", locale: "nb" }) await loginPage.navigation.goto();});The calls merge the same way when they overlap, as in a Promise.all. For one origin that gains nothing, since the
calls still run one after the other. For several origins they register at the same time, instead of one after the
other. A seed can also run beside work that does not touch the page, such as an API request, as long as everything is
awaited before the hop:
import { SessionStorage } from "pomwright";import { test } from "./fixtures";
test("a consent page, both origins seeded at once", async ({ loginPage, page }) => { const identityProvider = new SessionStorage(page, { label: "Login", origin: "https://login.example" });
await Promise.all([identityProvider.seed({ consent: "given" }), loginPage.sessionStorage.seed({ token: "eyJ…" })]); await loginPage.navigation.goto();});What a seeded navigation looks like to the app
Section titled “What a seeded navigation looks like to the app”Exactly as without a seed: the same request with the same Referer and Sec-Fetch-* headers, the same method and
body, the same history entry and navigation type, the same URL fragment. page.goto returns the app’s response. With
waitUntil: "commit", the committed document is the app’s and already has the entries.
What seed cannot do
Section titled “What seed cannot do”| case | what happens |
|---|---|
| the seed is still waiting when the page closes | the test fails, naming the origin and the keys: remove the seed call, or navigate there before the test ends; do not seed in a fixture that some tests never use |
| the hop opens a new tab | a new tab has its own session storage and does not run the page’s init scripts; the seed waits on and fails the test when the page closes. Seed the new tab yourself with browserContext.addInitScript, or set the entries in it and reload it |
seed is called while the page is already loading a document on the origin, or is not awaited before the hop |
seed fails when that document may have loaded without the entries. Await the navigation first, or seed before starting it, and await the seed: see Several seeds |
| a browser without the Navigation API | seed fails at the call, before anything is registered: use set on the origin and reload instead. Every browser Playwright 1.64 ships has it |
| the browser refuses the write (the storage quota) | the app loads without the entries and the test fails |
Every one of these failures points at the line that called seed.
Several origins and frames
Section titled “Several origins and frames”Each origin’s entries stay while the page is elsewhere: seed A and B, load A, let the app hop to B and back, and A
still has what was seeded and written there. Nothing carries entries from one origin to another. A same-origin iframe
shares the page’s storage; a cross-site iframe of a seeded origin shares that origin’s storage in Chromium and Firefox,
while WebKit keeps it apart. The script writes only in the main frame, so an iframe on the seeded origin inside another
page neither receives the entries nor uses up the seed. A hop started from an iframe with top.location is a main-frame
navigation and is seeded.
Errors
Section titled “Errors”Shop.SessionStorage.seed: an origin is required; pass { origin } or construct the helper with oneShop.SessionStorage.seed: the page committed a document on https://pay.example while seed() was registering, so the app may have started without the entries; await that navigation before seeding, or seed before starting itShop.SessionStorage.seed: seed() needs the Navigation API (navigation.activation) to write only on the first arrival at https://pay.example, and this browser has none (Mozilla/5.0 …); use set() on the origin and reload insteadShop.SessionStorage.seed: could not write sessionStorage for https://pay.example (keys: orderId): QuotaExceededError: The quota has been exceeded.; the app loaded without the entriesShop.SessionStorage.seed: the seed for https://pay.example (keys: orderId) was never applied: the page closed without navigating there; remove the seed call, or navigate to the origin before the test endsThe first three are thrown by the seed call; the last two fail the test later, when the app’s document loads or when
the page closes.
Reference
Section titled “Reference”SessionStorage.seed()- The seed registry, how it works inside
- Playwright:
page.sessionStorage,page.addInitScript()