playwright
playwright copied to clipboard
[Feature] API for changing localStorage
I'm trying to get the localStorage of an app via:
const storage = await context.storageState(); and it outputs: { cookies: [], origins: [] }
and then I try:
const localStorage = await page.evaluate(() =>
JSON.stringify(window.localStorage)
);
console.log(localStorage);
and I get: page.evaluate: Evaluation failed: DOMException: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
I know this page has localStorage because In between tests, I can see the localStorage in the Application tab.
@naknode Most likely your page is at some restricted origin, e.g. about:blank. Try logging page.url() or page.evaluate(() => window.location.href) and see what it says.
We can look into this in detail, but we need a repro script to run locally.
@dgozman I realized I wasn't doing a page.goTo() so I was getting that about:blank. Once I fixed that... I am able to get the localStorage
The repo is pretty basic. It's a blank repo since we're having a separate repo for our e2e test.
So now that i do const localStorage = await page.evaluate(() => window.localStorage);, I get the localStorage I need.
Modifying it now is another matter. Doing window.localStorage.setItem() doesn't seem to work.
Modifying it now is another matter. Doing
window.localStorage.setItem()doesn't seem to work.
I am not sure I can help with this, web apis are up to the browser. I'd suggest to open Developer Tools and play with local storage in the console. Take a look at our Debugging Tools doc.
@dgozman
Modifying it now is another matter. Doing
window.localStorage.setItem()doesn't seem to work.I am not sure I can help with this, web apis are up to the browser. I'd suggest to open Developer Tools and play with local storage in the console. Take a look at our Debugging Tools doc.
Well, I finally found a way...
await page.evaluate(
`window.localStorage.setItem('X', 'Y')`
);
It feels hacky. It works. Does playwright not support localStorage modifying with ease? I mean, I know there's storageState, but it just seems to read and put in arrays.
@naknode It seems like you want to modify localStorage from your script. Could you please elaborate on the usecase? Meanwhile, using evaluate is what I would do, unless we introduce some kind of API for that. For the new API, we need to understand the usecases first though.
Just to confirm, after you use evaluate to set values in the local storage, does context.storageState() return you the values? This methods is primarily used for authentication purposes.
@naknode It seems like you want to modify
localStoragefrom your script. Could you please elaborate on the usecase? Meanwhile, usingevaluateis what I would do, unless we introduce some kind of API for that. For the new API, we need to understand the usecases first though.Just to confirm, after you use
evaluateto set values in the local storage, doescontext.storageState()return you the values? This methods is primarily used for authentication purposes.
Sure. I have a localStorage that needs to change (permission-wise) and I'd like to modify it so I can test permissions on the page.
Hello, we have approximately the same use case
We'd like to inject a pre-existing StorageState to set localStorage values into an existing BrowserContext
I wonder if it would be possible to expose this function in the client side
We also had to inject some localStorage values into the browser context before tests.
I ended up doing this in globalSetup as an ugly workaround:
const storageState = await page.context().storageState();
const customStorageState = { ...storageState };
customStorageState.origins = [
{
origin: "https://staging.some-site.com",
localStorage: [{ name: "@cookieConsent", value: "1" }],
},
];
await fsPromises.writeFile("state.json", JSON.stringify(customStorageState));
🤷♂️
Same here, we also need to inject some key/values in the localStorage, our case is because we are enabling multiple mocks from the localStorage to test specific scenarios, it would be nice if we can have something like localStorage.setItem("key", "value"); to edit and enter new data
Well, cookies() has an addCookies() counterpart - it seems like storageState() ought to have an addStorageState() counterpart?
But it looks like the intended workflow is to use contexts - and inject cookies and storage at context creation time:
https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state
The guide also explains how to "reuse authentication state", meaning cookies and storage:
https://playwright.dev/docs/auth/#reuse-authentication-state
(I don't like the example, which relies on disk storage - but looks like it will also accept a data structure like the one returned by storageState(), so you probably don't actually need to use a file here.)
(And support for sessionStorage is missing on both ends, but that's a separate issue.)
Somehow Playwright during the execution is not getting the localStorage because is executing before the values are even set by the web application. So from my end after running the authentication, localStorage is empty, but if I check in Chromium the data is there. My understanding is that needs some delay before it reads the localStorage ?
`(async () => { const browser = await chromium.launch({ headless: false }); const page = await browser.newPage();
await page.goto('http://localhost:4200');
await page.fill('[name="loginfmt"]', '[email protected]'); await page.click('[type="submit"]');
await page.fill('input[type="password"]', 'password'); await page.click('[type="submit"]');
await page.click('text=No');
// (checking the application listing button - I know now I am logged In) await page.waitForSelector('.button.header__link');
console.log('reading the local storage'); // get the data from localstorage await page.evaluate(() => { const returner = {}; // Local storage is empty, seems the execution runs before the actual values are set in the page localStorage for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); console.log('key', key); console.log('getItem', localStorage.getItem(key)); returner[key] = JSON.stringify(localStorage.getItem(key)); } return returner; });
// tried again but still empty const localStorage = await page.evaluate(() => JSON.stringify(window.localStorage) );
console.log('localStorageVariable', localStorage); })(); `
For the new API, we need to understand the usecases first though.
Here an usecase:
https://plausible.io/docs/excluding-localstorage
If you do track with plausible, and you want to run tests on your production environment, you can set localStorage.plausible_ignore=true
page.evaluate seems good enough for this simple usecase
I'm running up agains the same issues. Our use case is that we want to want to set baseUrl from a Jenkinsfile environment variable before the tests run. We're using a runtime library that reads local storage before loading and want to be able to pass different urls (dev, staging, prod, etc.) before the page loads. Essentially, I want to do something like this:
beforeEach(() => {
const testUrl = process.env.TEST_URL);
if (testUrl) {
const testConfig = {"baseUrl": testUrl, "theme": "test" };
const testConfigJson = JSON.stringify(testConfig);
window.localStorage.setItem('debug', testConfigJson);
}
});
@dcerniglia Look into page.addInitScript():
beforeEach(async ({ page }) => {
// ...
await page.addInitScript(value => {
window.localStorage.setItem('debug', value);
}, testConfigJson);
});
@dgozman That did it. Thank you!
Use case: when page opens, the default value of an input is token from a localStorage. Waiting for an API ;)
Does anyone have a workaround to set localStorage and sessionStorage for any origin?
You can't simply do this with addInitScript or evaluate, because the security origin won't match.
I've been trying to reference this code for a userland implementation of something similar:
https://github.com/microsoft/playwright/blob/ab9d5a0dc4744fbfad6be2ed219b81977b6aa97d/packages/playwright-core/src/server/browserContext.ts#L369-L389
Here's what I came up with:
async function injectStorageItem(context: BrowserContext, origin: string, type: "localStorage" | "sessionStorage", key: string, value: string) {
const page = await context.newPage();
initConsoleOutput(page);
await page.route(
() => true,
route => { route.fulfill({ body: `<html></html>` }) }
);
await page.goto(origin, { waitUntil: "load" });
await page.evaluate(
({ type, key, value }) => { window[type].setItem(key, value) },
{ type, key, value }
);
await page.close();
}
To see if this works, I do a console.log(await context.storageState()); which shows origins: [], so it doesn't work.
I think my strategy is similar to that of browserContext.setStorageState: intercept all routes and just put an empty HTML document in there, navigate to the origin of the storage I'm trying to inject, then evaluate a function that calls e.g. localStorage.setItem to inject the state.
I've tried attaching on("console") and on("pageerror") handlers and put console.log statements all over the place - including the body of the function I pass to evaluate, where I was able to confirm that origin === window.origin, and so it seems like this should be working... it's on the right origin, and it doesn't error - but it just doesn't set anything in storage.
I suspect the magic missing piece is this internalCallMetadata thing, which is not supported by the public API?
Any idea what could be wrong?
Note that I've also tried injecting storage via CDP with DOMStorage.setDOMStorageItem and that didn't work either - it gives an error message saying "no frame matches the security origin" or something. I figured this was because that API doesn't actually allow injections of storage to a security origin that isn't currently open. There is barely any documentation for it, and I eventually gave up wiped the code in favor of the approach I described above. I wondering if I'm somehow running into the same security limitation with both approaches?
Any other ideas for a workaround?
Ha ha, it works, I messed up somewhere else.
Well, there you have a work around. 😉
Thanks for that workaround @mindplay-dk !
We hope that Playwright will have this method without "tricks" soon hehe
Ha ha, it works, I messed up somewhere else.
Well, there you have a work around. 😉
Hi @mindplay-dk,
Where is the workaround? I need something that works in the .NET SDK, I'm testing an end-to-end scenario where a test user logs into my app. It then uses existing localStorage values to set the rendered culture is so that its appropriate translated version is displayed. I'm then going to validate that the page renders the correctly localized versions. This would be really useful, thank you ahead of time 🙏🏽.
for someone
await page.addInitScript(()=>{
window.localStorage.setItem('key', 'value');
});
Use case: without local storage, our application forces to user to fill in certain data before work can be continued. Setting local storage directly would speed up the tests.
Adding my own use case: Our applications have one-time modal confirmation boxes that appear when a new session is launched. Whether the modal shows or not is set and read in Local Storage, so being able to set them all to 'confirmed' at the start of the test would be much better than trying to wait to see if the modal is present and then make it go away with clicks.
for someone
await page.addInitScript(()=>{ window.localStorage.setItem('key', 'value'); });
This should be put in the beforeAll/beforeEach block? Because I have done it that way and it is not consistent. Code: http://md-paster.herokuapp.com/63207fc3db3731001626e289
for someone
await page.addInitScript(()=>{ window.localStorage.setItem('key', 'value'); });This should be put in the beforeAll/beforeEach block? Because I have done it that way and it is not consistent. Code: http://md-paster.herokuapp.com/63207fc3db3731001626e289
I've noticed that it works well until as a parameter get string value directly, like 'random'. However nothing happen if I try to put here string variable. Will work:
window.localStorage.setItem('some-key', 'some-value')
Will NOT work
const value : string = "some-value"
window.localStorage.setItem('some-key', value)
It works fine if you're using the build to test and disabling the devTools. Dev tools probably have some kind of persistence with local storage which I couldn't get rid of. Make sure to DISABLE them, not just NOT OPEN.
By collecting the answers together and digging into the docs a bit, I've managed to implement my own setStorageState function which is the exact opposite of browserContext.storageState
Supposing you have the following folder structure:
playwright-setup/
├── globalSetup.ts
├── helpers.ts
├── storageState.json // content can be initially set to `{"cookies": [],"origins": []}`
helpers.ts
import { Page } from 'playwright';
/**
* Set the cookies as well as the localStorage items for each origin
* to the context of the current page.
*
* This function causes an extra `page.goto` at the end. The `pageOptions`
* param is for that navigation.
*
* @param page The page in question
* @param param1 An object containing `cookies` and `origins`
* @param pageOptions page options for the navigation
*/
export async function setStorageState(
page: Page,
{ cookies, origins }: { cookies: Cookie[]; origins: Origin[] },
pageOptions?: PageOptions,
) {
const initialUrl = page.url();
// 1. Set all origins which include the localStorage
for (const { origin, localStorage } of origins) {
// 1.1 Navigate to the origin
await page.goto(origin);
// 1.2 Loop through the items in localStorage and assign them
for (const { name, value } of localStorage) {
await page.evaluate(async (args: string) => {
const [name, value] = args.split(',');
window.localStorage.setItem(name, value);
// console.log(JSON.stringify(window.localStorage));
}, `${name},${value}`);
}
}
// 2. Set cookies
await page.context().addCookies(cookies.map((cookie) => ({ ...cookie, sameSite: <SameSite>cookie.sameSite })));
// 3. Navigate back to the initial url
await page.goto(initialUrl, pageOptions);
}
// =========================== Types =========================== //
interface PageOptions {
referer?: string | undefined;
timeout?: number | undefined;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit' | undefined;
}
interface Cookie {
name: string;
value: string;
domain: string;
path: string;
expires: number;
httpOnly: boolean;
secure: boolean;
sameSite?: string;
}
enum SameSite {
Lax = 'Lax',
None = 'None',
Strict = 'Strict',
}
interface Origin {
origin: string;
localStorage: LocalStorage[];
}
interface LocalStorage {
name: string;
value: string;
}
Finally, use it like this.
import * as storageState from './storageState.json';
async function globalSetup(config: FullConfig) {
...
await setStorageState(page, { cookies: storageState.cookies, origins: storageState.origins });
...
// Save signed-in state back to 'storageState.json' just in case
await page.context().storageState({ path: './playwright-setup/storageState.json' });
await browser.close();
}
export default globalSetup;
For the sake of completeness, here's the global setup file if anyone is interested
globalSetup.ts
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch({
headless: !!process.env.CI,
});
const page = await browser.newPage();
// Add login state from previous session if any
await setStorageState(page, { cookies: storageState.cookies, origins: storageState.origins });
// Go to the login page
await page.goto('https://www.my-website.com/login/');
// Check if we're already logged in by seeing if we got redirected to the dashboard
const loggedIn = page.url().startsWith('https://www.my-website.com/dashboard/');
// If not logged in, simply login manually
if (!loggedIn) {
await page.getByLabel('User Name').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByText('Submit').click();
}
// Check if we finally logged in, if not throw an error
if (!page.url().startsWith('https://www.my-website.com/dashboard/')) {
await page.screenshot({ path: 'out/authentication.jpg', fullPage: true });
throw new Error("Failed to login! See screenshot in 'out/authentication.jpg'");
}
// Save signed-in state to 'storageState.json' just in case something changed
await page.context().storageState({ path: './playwright-setup/storageState.json' });
await browser.close();
}
export default globalSetup;
I was hoping this thread would also reveal how to read localStorage values as part of an expect statement.
UPDATE: Turns out you can inspect localStorage directly...
const localStorage = await page.evaluate(() => window.localStorage);
@dcerniglia Look into
page.addInitScript():beforeEach(async ({ page }) => { // ... await page.addInitScript(value => { window.localStorage.setItem('debug', value); }, testConfigJson); });
Hi @dgozman, I tried this and page.evaluate() but for some reason when I use page.evaluate(), my target value in local storage is set as "undefined" and when I use page.addInitScript(), my target value isn't changed at all.
const authState = {
users: {
[authData.id]: authData.token,
},
currentUserId: authData.id,
currentUser: null,
currentWorkspaceId: null,
};
await page.evaluate(authState => {
window.localStorage.setItem("AuthState", JSON.stringify(authState));
});
I'm running this in a globalSetup function so I can share the login session across test cases. authData comes from a response to a POST login request. Is there something I'm missing? Thanks in advance.
EDIT: I solved my issue, I forgot to add "authState" at the end of the evaluate statement:
await page.evaluate(authState => {
window.localStorage.setItem("AuthState", JSON.stringify(authState));
}, authState);
Hey guys, if someone face issues testing localStorage, this worked:
test('should save the access token on localStorage after submit succeeds', async ({
page
}) => {
const accessToken = faker.datatype.uuid()
await page.route('http://localhost:5050/api/login', async (route) => {
const json = {
accessToken
}
await route.fulfill({ status: 200, json })
})
await page
.getByPlaceholder(/enter your e-mail/i)
.fill(faker.internet.email())
await page
.getByPlaceholder(/enter your password/i)
.fill(faker.internet.password(6))
await page.getByRole('button', { name: /sign in/i }).click()
expect(await page.evaluate(() => window.localStorage)).toStrictEqual({
accessToken
})
})