playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Feature] Set storage state when using persistent context

Open Mosorathia opened this issue 2 years ago • 23 comments

I want to set Local storage for a browser but open it in non-incognito mode..

playwright = Playwright.create();
chromium = playwright.chromium();
browser = chromium.launch(new BrowserType.LaunchOptions().setHeadless(false));
browserContext = browser.newContext(new Browser.NewContextOptions().setStorageStatePath(Paths.get("./Properties/logss/abc.txt")));
                
page = browserContext.newPage();

Here i am able to set localstorage but only in incognito mode.. But i want to open browser in Non-Incognito mode so i am launching it with persistent context..

browserContext=chromium.launchPersistentContext(Paths.get("C:\\Users\\mohib.s\\AppData\\Local\\Google\\Chrome\\User Data"), new BrowserType.LaunchPersistentContextOptions().setChannel("chrome").setHeadless(false));

How can i set the session storage for browser opened with persistent context.. Can you please help me with this

Mosorathia avatar Jun 17 '22 11:06 Mosorathia

@Mosorathia If you are launching the persistent context, you can set up the user data dir to your liking by using the browser with that user data dir first. localStorage and many other things will be persisted in the user data dir for you.

dgozman avatar Jun 17 '22 20:06 dgozman

@dgozman I want to set the local storage on launch of the browser at run time.. I don't want to set it prior to the execution.. Is there a way like Browser.NewContextOptions().setStorageStatePath() for setting local storage at launch time when opened in non-incognito mode using persistentContext..

Mosorathia avatar Jun 21 '22 12:06 Mosorathia

There is not yet, marking it as a feature request by that.

mxschmitt avatar Jun 22 '22 07:06 mxschmitt

I also want to vote for this. I have following scenario:

  • Extension which works in different apps
  • Some of this apps (sites) to login require 2MFA authorization. To do this without persistent context I use globalSetup -> login using MFA -> save storage -> reuse in other tests. But using persistent context it's impossible to do.

Would be really helpful to have an ability to use/set storage state in persistent context.

AndrewEgorov avatar Jan 04 '23 03:01 AndrewEgorov

Another vote, I desperately need a way to deal with both MFA and extensions.

Attic0n avatar Jun 01 '23 11:06 Attic0n

why hasnt been this added yet wtf? its been a year, we need to be able to set AND clear localstorage/cookies for persistent context!

dr3adx avatar Jun 02 '23 12:06 dr3adx

Recently I've observed that even on launching a browser context using .LaunchPersistentContextAsync the opened browser window is using profile information from anywhere on my machine instead of restricting itself to the userdatadir that is passed. For example if I launch a browser context by passing the path to an empty directory in LaunchPersistentContextAync function I'd expect it to stop at the login page and prompt for an email address, however it continues to detect my work profile in my machine and completes the login with that. Is there a way to prevent this detection of profiles that are not directly in userDataDir?

kumarragMSFT avatar Jun 27 '23 10:06 kumarragMSFT

NEED THIS FIXED, CMON GUYS

dr3adx avatar Jul 31 '23 17:07 dr3adx

bump

dr3adx avatar Aug 22 '23 19:08 dr3adx

A workaround to set cookies for the persistent context:

// Get cookies from some other context, for example the one you were authenticating in.
const cookies = await someOtherContext.cookies();

// Alternatively, retrieve cookies from a saved storage state.
const cookies = require('path/to/storageState.json').cookies;

....

// Now launch persistent context and add cookies.
const context = await chromium.launchPersistentContext('path/to/user/data/dir');
await context.addCookies(cookies);

// Persistent context has cookies and is ready to use.
const page = context.pages()[0];
await page.goto('https://playwright.dev');

dgozman avatar Aug 23 '23 18:08 dgozman

A workaround to set cookies for the persistent context:

// Get cookies from some other context, for example the one you were authenticating in.
const cookies = await someOtherContext.cookies();

// Alternatively, retrieve cookies from a saved storage state.
const cookies = require('path/to/storageState.json').cookies;

....

// Now launch persistent context and add cookies.
const context = await chromium.launchPersistentContext('path/to/user/data/dir');
await context.addCookies(cookies);

// Persistent context has cookies and is ready to use.
const page = context.pages()[0];
await page.goto('https://playwright.dev');

but bro loading cookies is only half of what loadStorage does, loadStorage or whatever is the function name, loads local storage as well under "Origins". How to do this programatically?

dr3adx avatar Aug 23 '23 19:08 dr3adx

bump cmon guys

dr3adx avatar Sep 02 '23 11:09 dr3adx

come on guys!!

ahmedelkolfat avatar Sep 06 '23 12:09 ahmedelkolfat

Are you making any improvement or work on this, please?

aminaodzak avatar Sep 06 '23 14:09 aminaodzak

Any news?

sanzenwin avatar Dec 01 '23 08:12 sanzenwin

Another vote. Need it for extensions + cookie authentication.

Extensions require persistent context. While automatic authentication for all tests in project "LoggedIn" cannot find the saved storageState.

// test/tests/e2e/fixtures.ts

import { test as base, chromium, type BrowserContext } from '@playwright/test';
import path from 'path';

export const test = base.extend<{
  context: BrowserContext;
  extensionId: string;
}>({
  context: async ({ }, use) => {
    const pathToExtension = path.join(__dirname, '../../../src');
    const context = await chromium.launchPersistentContext('', {
      headless: false,
      args: [
        `--headless=new`,
        `--disable-extensions-except=${pathToExtension}`,
        `--load-extension=${pathToExtension}`,
      ],
    });
    await use(context);
    await context.close();
  },
  extensionId: async ({ context }, use) => {
    // for manifest v3:
    let [background] = context.serviceWorkers();
    if (!background)
      background = await context.waitForEvent('serviceworker');

    const extensionId = background.url().split('/')[2];
    await use(extensionId);
  },
});

export const expect = test.expect;
// test/playwright.config.ts

import { defineConfig, devices } from '@playwright/test'
import dotenv from 'dotenv'
import path from 'path';

dotenv.config({ path: path.resolve(__dirname, 'environment', '.env.production') });

export default defineConfig({
 // Folder for all tests files
 testDir: 'tests/e2e',

 // Folder for test artifacts such as screenshots, videos, traces, etc.
 outputDir: 'test_results',

//   // path to the global setup files.
//   globalSetup: require.resolve('./global-setup'),

//   // path to the global teardown files.
//   globalTeardown: require.resolve('./global-teardown'),

 // Each test timeout [msec]
 timeout: 5000,

 // Fail the build on CI if you accidentally left test.only in the source code.
 forbidOnly: !!process.env.CI,

 // Opt out of parallel tests on CI.
 workers: process.env.CI ? 1 : undefined,

 use: {
   // Maximum time each action such as `click()` can take. Defaults to 0 (no limit).
   actionTimeout: 0,

   // Name of the browser that runs tests. For example `chromium`, `firefox`, `webkit`.
   browserName: 'chromium',

   // Toggles bypassing Content-Security-Policy.
   bypassCSP: false,

   // Channel to use, for example "chrome", "chrome-beta", "msedge", "msedge-beta".
   channel: 'chrome',

   // Run browser in headless mode.
   headless: false,
 },

 projects: [
   {
     name: 'LoginSetup',
     testMatch: 'login.setup.spec.ts',
     testDir: 'tests/e2e/login_setup',
     retries: 0
   },
   {
     name: 'LoggedIn',
     testDir: 'tests/e2e/logged_in',
     dependencies: ['LoginSetup'],
     use: {
       ...devices['Desktop Chrome'],
       storageState: 'playwright/.auth/user.json',
     }
   },
   {
     name: 'LoggedOut',
     testDir: 'tests/e2e/logged_out'
   }
 ]
},
);
// test/tests/e2e/login_setup/login.setup.spec.ts

import { type Page, type BrowserContext } from '@playwright/test';
import { test, expect } from '../fixtures';

const authFile = 'playwright/.auth/user.json';

const popupUrl = (extensionId: string) => `chrome-extension://${extensionId}/popup/popup.html`

const server_base_url = process.env.SERVER_URL

// first call to server may take much longer before it warms up,
// so we set a longer timeout for this specific test
test.setTimeout(10000)

test('popup login', async ({ page, context, extensionId }) => {
    const loginPage = await openLoginPage(page, context, extensionId)
    expect(await loginPage.url()).toBe(`${server_base_url}/login`)

    const dashboardPage = await login(loginPage)
    expect(await dashboardPage.url()).toBe(`${server_base_url}/`)

    await page.context().storageState({ path: authFile });
  });

  async function login(loginPage: Page): Promise<Page> {
    const username = process.env.LOGIN_USERNAME
    const password = process.env.LOGIN_PASS

    await loginPage.locator('input[type=email]').fill(username)
    await loginPage.locator('input[type=password]').fill(password)

    const loginButtonOnLoginPage = await loginPage.getByRole('button').filter({hasText: 'Login'})

    await loginButtonOnLoginPage.click()

    await loginPage.waitForURL(server_base_url);

    return loginPage
  }

  async function openLoginPage(page: Page, context: BrowserContext, extensionId: string): Promise<Page> {
    await page.goto(popupUrl(extensionId))

    const loginButton = page.getByRole('button').filter({hasText: 'Login'})

    // Start waiting for new page before clicking. Note no await.
    const pagePromise = context.waitForEvent('page');

    await loginButton.click()

    const loginPage = await pagePromise;
    await loginPage.waitForLoadState();

    return loginPage
  }
// test/tests/e2e/logged_in/popup.spec.ts

import { test, expect } from '../fixtures';

const popupUrl = (extensionId: string) => `chrome-extension://${extensionId}/popup/popup.html`

test('popup page', async ({ page, extensionId, context }) => {
    await page.goto(popupUrl(extensionId))

    const state = await context.storageState()

   // Fails here because there is no cookies in the context storage state:
    expect(state.cookies.length).toBeGreaterThan(0) // <---- Fails !!!

airbender-1 avatar Dec 08 '23 16:12 airbender-1

Another up-vote for this feature request. On my recommendation, my company invested a lot of time and effort on Playwright-java, now we're struck on this issue for a while.

medabalimi-jagadeesh avatar Feb 05 '24 08:02 medabalimi-jagadeesh

Another vote for extensions + cookie authentication. @airbender-1 is there any workaround to inject a storageState to a running context?

tg44 avatar Feb 19 '24 15:02 tg44

Another vote, we need this as well

MaksimMedvedev avatar Mar 26 '24 13:03 MaksimMedvedev

It is a very important feature. I need it to create an authorization setup for extension tests. Upvote!

ENEMBI avatar Mar 27 '24 11:03 ENEMBI

Another upvote. This is a particularly helpful feature for testing extensions

ellhans1 avatar Apr 24 '24 15:04 ellhans1

@tg44 referencing your comment

My workaround was injecting cookies from file (json)

import { type BrowserContext } from '@playwright/test';
import path from 'path';
import fs from 'fs';
import { storageStateRelativePath } from './test_constants'
import { test as base } from './fixtures-incognito';

// see: https://github.com/microsoft/playwright/issues/26693
process.env.PW_CHROMIUM_ATTACH_TO_OTHER = "1";

export const test = base.extend<{
  context: BrowserContext;
  extensionId: string;
}>({
  context: async ({ context }, use) => {
    // Patch [BUG](https://github.com/microsoft/playwright/issues/14949)
    // manually inject saved storageState if the state file is found
    // 1. cookies
    const cookies = loadCookies()
    if (cookies) await context.addCookies(cookies);
    // 2. localStorage - TODO

    const state = await context.storageState()
    expect(state.cookies.length).toBeGreaterThan(0)
    
    await use(context);

    await context.close();
  },
});

type Cookies = Parameters<BrowserContext['addCookies']>[0]

function loadCookies(): Cookies | null {
  const authFile = path.resolve(__dirname, '..', '..', storageStateRelativePath)
  if (!fs.existsSync(authFile)) return null

  const cookies: Cookies = require(authFile).cookies

  return cookies
}

export const expect = test.expect;

authFile is just a json with default hard-coded value for cookie of specific user.

{
  "cookies": [
    {
      "name": "mycookie-name",
      "value": "ab123...",
      "domain": ".app.mydomain",
      "path": "/",
      "expires": 1708554480.933069,
      "httpOnly": true,
      "secure": true,
      "sameSite": "Lax"
    }
  ],
  "origins": []
}

Here is the fixture-incognito that the above "test" fixture depends on:

import { test as base, chromium, type BrowserContext } from '@playwright/test';
import path from 'path';

export const test = base.extend<{
  context: BrowserContext;
  extensionId: string;
}>({
  context: async ({ }, use) => {
    const pathToExtension = path.join(__dirname, '../../../src');

    const context = await chromium.launchPersistentContext('', {
      headless: false,
      args: [
        `--headless=new`,
        `--disable-extensions-except=${pathToExtension}`,
        `--load-extension=${pathToExtension}`,
      ],
      // slowMo: 2000
    });

    await use(context);
    await context.close();
  },
  extensionId: async ({ context }, use) => {
    // for manifest v3:
    let [background] = context.serviceWorkers();
    if (!background)
      background = await context.waitForEvent('serviceworker');

    const extensionId = background.url().split('/')[2];
    await use(extensionId);
  },
});

export const expect = test.expect;

airbender-1 avatar Apr 30 '24 20:04 airbender-1

Rather than mess around with loading and injecting the cookies (which was insufficient for us since we also needed to set up local storage and browser extension storage), what we did was to reuse the profile from an initial project that logs the test user in. We duplicate it for every new test context and it comes loaded with the needed auth cookies and local storage for our tests to do their thing. See:

https://github.com/pixiebrix/pixiebrix-extension/blob/main/end-to-end-tests/fixtures/authSetup.ts#L78-L86 https://github.com/pixiebrix/pixiebrix-extension/blob/main/end-to-end-tests/fixtures/extensionBase.ts#L77-L89

fungairino avatar Apr 30 '24 20:04 fungairino

Another up-vote for this feature request

LironHaroni avatar Sep 18 '24 13:09 LironHaroni