Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Set storage state when using persistent context #14949

Open
Mosorathia opened this issue Jun 17, 2022 · 23 comments
Open

[Feature] Set storage state when using persistent context #14949

Mosorathia opened this issue Jun 17, 2022 · 23 comments

Comments

@Mosorathia
Copy link

Mosorathia commented Jun 17, 2022

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

@dgozman
Copy link
Contributor

dgozman commented Jun 17, 2022

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

@Mosorathia
Copy link
Author

Mosorathia commented Jun 21, 2022

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

@mxschmitt
Copy link
Member

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

@mxschmitt mxschmitt changed the title How to set Local Storage in non-incognito mode [Fetaure] Set storage state when using persistent context Jun 22, 2022
@mxschmitt mxschmitt changed the title [Fetaure] Set storage state when using persistent context [Feature] Set storage state when using persistent context Jun 22, 2022
@AndrewEgorov
Copy link

AndrewEgorov commented Jan 4, 2023

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.

@Attic0n
Copy link

Attic0n commented Jun 1, 2023

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

@dr3adx

This comment was marked as spam.

@kumarragMSFT
Copy link

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?

@dr3adx

This comment was marked as off-topic.

@dr3adx

This comment was marked as off-topic.

@dgozman
Copy link
Contributor

dgozman commented Aug 23, 2023

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

@dr3adx
Copy link

dr3adx commented Aug 23, 2023

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

This comment was marked as off-topic.

@ahmedelkolfat

This comment was marked as off-topic.

@aminaodzak
Copy link

aminaodzak commented Sep 6, 2023

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

@sanzenwin
Copy link

Any news?

@airbender-1
Copy link

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:https://${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:https://${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 !!!

@medabalimi-jagadeesh
Copy link

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.

@tg44
Copy link

tg44 commented Feb 19, 2024

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

@MaksimMedvedev
Copy link

Another vote, we need this as well

@ENEMBI
Copy link

ENEMBI commented Mar 27, 2024

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

@ellhans1
Copy link

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

@airbender-1
Copy link

airbender-1 commented Apr 30, 2024

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

@fungairino
Copy link

fungairino commented Apr 30, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests