microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
66.85k stars 3.66k forks source link

[Question] Access chrome extension context from @playwright/test #11108

Closed Meemaw closed 2 years ago

Meemaw commented 2 years ago

Hi.

We're exploring Playwright as our E2E test runner. One of our main requirements is ability to communicate with chrome extensions (e.g. Metamask). I'm working on a proof of concept with @playwright/test where I successfully setup MetaMask using chromium.launchPersistentContext, but facing some issues.

After the MetaMask setup, we would want that window.ethereum to get populated/injected by the extension in the default test page/context (one passed in by the test fixture). I assume this doesn't work because @playwright/test creates an isolated context for each test, and that context is completely unrelated to the one created via chromium.launchPersistentContext.

If my understanding is correct, we would either need to somehow link those 2 contexts (probably not possible/doesn't make sense), or have an option to launch extensions in the text context. To make this performant, we would probably only want to do this once for all tests.

Any help/pointers would be much appreciated.

aslushnikov commented 2 years ago

@Meemaw if I understand correctly, substituting default context fixture with the one from the persistent context will do the trick for you. Since page fixture depends on context fixture, the default page will be created in the persistent context.

Here's an example:

import { test as base, expect, chromium, firefox, webkit } from '@playwright/test';
import path from 'path';

export const test = base.extend({
  context: async ({ browserName }, use) => {
    const browserTypes = {chromium, firefox, webkit};
    const context = await browserTypes[browserName].launchPersistentContext(path.join(__dirname, 'ppp'), {});
    await use(context);
    await context.close();
  },
});

test.only('basic test', async ({ page }) => {
  await page.goto('https://playwright.dev/');
});

Does it help?

Meemaw commented 2 years ago

@aslushnikov found this shortly after I posted the question. It works, but I have a follow up question. What is the scope of context? Is this being executed for each test, or can it be changed to per worker scope?

aslushnikov commented 2 years ago

@Meemaw context fixture is a "test" fixture, meaning that it'll be recreated for every test. If you want to re-use the context you have a few options:

  1. Use the approach suggested here - https://playwright.dev/docs/test-retries#reuse-single-page-between-tests
  2. Create a globalContext worker fixture and override page fixture to depend on it.

The second approach looks like this:

import { test as base, expect, chromium, firefox, webkit } from '@playwright/test';
import path from 'path';

export const test = base.extend({
  globalContext: [async ({ browserName }, use) => {
    const browserTypes = {chromium, firefox, webkit};
    const context = await browserTypes[browserName].launchPersistentContext(path.join(__dirname, 'ppp'), {});
    await use(context);
    await context.close();
  }, { scope: 'worker'}],

  page: async ({ globalContext }, use) => {
    const page = await globalContext.newPage();
    await use(page);
    await page.close();
  },
});

test.only('basic test', async ({ page }) => {
  await page.goto('https://playwright.dev/');
});
aslushnikov commented 2 years ago

Do note though that profile directory has to be unique per worker, so don't hardcode it's name and rather generate one randomly.

Meemaw commented 2 years ago

@aslushnikov thanks for the feedback. I'm having some issues with the "globalContext":

  1. There seems to be some Typescript issues with scope: "worker":

    Screenshot 2021-12-27 at 20 37 46
  2. Is "globalContext" somehow special global fixture? I tried something similar with my own fixture and was expecting it to have a same value across tests on a single worker:

export const test = base.extend<{ workerId: string }>({
  workerId: [
    async ({}, use) => {
      const workerId = Math.random().toString(36).substring(2, 7)
      await use(workerId)
    },
    { scope: "worker" },
  ],
  context: async ({ browserName, workerId }, use, { title }) => {
    console.log(workerId) // this is different between tests even on same worker

    ...someCode
    await use(context)
  },
})

I would like something like parallelIndex that is stable across tests, but is random every time we invoke tests.

aslushnikov commented 2 years ago
  1. There seems to be some Typescript issues with scope: "worker":

@Meemaw for the first, the following works for me with typescript:

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

type WorkerContextFixture = {
  globalContext: BrowserContext,
}

export const test = base.extend<{}, WorkerContextFixture>({
  globalContext: [async ({ browserName, }, use, info) => {
    const browserTypes = {chromium, firefox, webkit};
    const userDataDir = path.join(__dirname, 'profile-' + info.workerIndex);
    const context = await browserTypes[browserName].launchPersistentContext(userDataDir, {});
    await use(context);
    await context.close();
  }, { scope: 'worker'}],

  page: async ({ globalContext }, use) => {
    const page = await globalContext.newPage();
    await use(page);
    await page.close();
  },
});

test('basic test', async ({ page }) => {
  await page.goto('https://playwright.dev/');
});

test('basic test 2 ', async ({ page }) => {
  await page.goto('https://playwright.dev/');
});
  1. Is "globalContext" somehow special global fixture? I tried something similar with my own fixture and was expecting it to have a same value across tests on a single worker:

This should work. Two things though:

If it still doesn't work, would you mind sharing some code?

Meemaw commented 2 years ago

@aslushnikov that works. Thanks for the help, things make sense now 👍

I have 1 final question that is somehow related to Chrome Extensions:

Screenshot 2021-12-28 at 21 31 44

I saw some issues related to this (e.g https://github.com/microsoft/playwright/issues/5586), so wondering how hard would it be to add support for something like that?

aslushnikov commented 2 years ago

Is there a way/workaround to click on the extension icon in the tabbar? I have 1 extension (Kaikas) that doesn't play well if not triggered like this. They even have a screenshoot

@Meemaw this is tracked separately at https://github.com/microsoft/playwright/issues/5593. Please upvote!

Given how popular the issue is, it's likely to appear sometime in future!

paambaati commented 2 years ago

@aslushnikov I'm doing this exact same thing, with the addition of needing to set storage state on that persistent context. How would you recommend I do it?

I tried this but this isn't working though –

export const test = base.extend({
  context: async ({ browserName, }, use) => {
    const browserTypes = { chromium, firefox, webkit }
    const extensionPath = '/some/path/'
    const context = await browserTypes[browserName].launchPersistentContext('/some/dir', {
        headless: false,
        args: [
            `--disable-extensions-except=${extensionPath}`,
            `--load-extension=${extensionPath}`,
        ]
    })
    await use({
      ...context,
      storageState: () => Promise.resolve({/* storage state */}),
    })
    await context.close()
  },
})
paambaati commented 2 years ago

@aslushnikov This doesn't work either (after having read https://playwright.dev/docs/test-fixtures#overriding-fixtures) –

export const test = base.extend({
  storageState: async ({}, use) => {
    console.log('[INFO] Extending storage state')
    const customStorageState = { /* storage state */ }
    await use(customStorageState)
  },
  context: async ({ browserName, }, use) => {
    console.log('[INFO] Extending browser context')
    const browserTypes = { chromium, firefox, webkit }
    const extensionPath = '/some/path/'
    const context = await browserTypes[browserName].launchPersistentContext('/some/dir', {
        headless: false,
        args: [
            `--disable-extensions-except=${extensionPath}`,
            `--load-extension=${extensionPath}`,
        ]
    })
    await use(context)
    await context.close()
  },
})

Curiously, when I use this custom test method, I do see the [INFO] Extending browser context line being printed, but not [INFO] Extending storage state.