playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Feature] API for changing localStorage

Open djasnowski opened this issue 4 years ago • 41 comments

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.

djasnowski avatar Apr 21 '21 18:04 djasnowski

@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 avatar Apr 21 '21 18:04 dgozman

@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.

djasnowski avatar Apr 21 '21 19:04 djasnowski

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 avatar Apr 21 '21 19:04 dgozman

@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.

djasnowski avatar Apr 21 '21 19:04 djasnowski

@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.

dgozman avatar Apr 21 '21 19:04 dgozman

@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.

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.

djasnowski avatar Apr 21 '21 21:04 djasnowski

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

chambo-e avatar Apr 30 '21 20:04 chambo-e

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));

🤷‍♂️

oscargullberg avatar Jul 02 '21 16:07 oscargullberg

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

sergioariveros avatar Jul 22 '21 04:07 sergioariveros

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.)

mindplay-dk avatar Sep 13 '21 09:09 mindplay-dk

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); })(); `

o-lagatta avatar Nov 18 '21 09:11 o-lagatta

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

hacknlove avatar Feb 03 '22 19:02 hacknlove

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 avatar Feb 05 '22 20:02 dcerniglia

@dcerniglia Look into page.addInitScript():

beforeEach(async ({ page }) => {
  // ...
  await page.addInitScript(value => {
    window.localStorage.setItem('debug', value);
  }, testConfigJson);
});

dgozman avatar Feb 05 '22 21:02 dgozman

@dgozman That did it. Thank you!

dcerniglia avatar Feb 06 '22 00:02 dcerniglia

Use case: when page opens, the default value of an input is token from a localStorage. Waiting for an API ;)

tetianaman avatar Feb 10 '22 14:02 tetianaman

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?

mindplay-dk avatar Feb 22 '22 14:02 mindplay-dk

Ha ha, it works, I messed up somewhere else.

Well, there you have a work around. 😉

mindplay-dk avatar Feb 22 '22 14:02 mindplay-dk

Thanks for that workaround @mindplay-dk !

We hope that Playwright will have this method without "tricks" soon hehe

manuman94 avatar Mar 15 '22 15:03 manuman94

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 🙏🏽.

IEvangelist avatar May 03 '22 02:05 IEvangelist

for someone

await page.addInitScript(()=>{
	window.localStorage.setItem('key', 'value');
});

b5414 avatar May 12 '22 12:05 b5414

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.

Exoow avatar Jul 06 '22 12:07 Exoow

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.

np-xavier avatar Jul 27 '22 09:07 np-xavier

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

marindedic avatar Sep 13 '22 13:09 marindedic

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)

mykhailo-kobylianskyi avatar Sep 14 '22 21:09 mykhailo-kobylianskyi

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.

Realman78 avatar Sep 14 '22 21:09 Realman78

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;

One meme for the road

usersina avatar Oct 13 '22 17:10 usersina

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);

doublejosh avatar Nov 30 '22 23:11 doublejosh

@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);

pengwings avatar Jan 12 '23 07:01 pengwings

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
    })
  })

giovanedann avatar Mar 03 '23 16:03 giovanedann