diff --git a/docs/docs/guides/playwright.md b/docs/docs/guides/playwright.md index fd10839d..aa065968 100644 --- a/docs/docs/guides/playwright.md +++ b/docs/docs/guides/playwright.md @@ -7,7 +7,7 @@ Let's digest a simple test step by step: ::: code-group ```typescript [example.spec.ts] import { MetaMask, testWithSynpress, unlockForFixture } from '@synthetixio/synpress' -import BasicSetup from '../playwright/wallet-setup/basic.setup' +import BasicSetup from '../wallet-setup/basic.setup' const test = testWithSynpress(BasicSetup, unlockForFixture) @@ -34,7 +34,7 @@ First, you need to import the `testWithSynpress` function from `@synthetixio/syn ```typescript import { MetaMask, testWithSynpress, unlockForFixture } from '@synthetixio/synpress' -import BasicSetup from '../playwright/wallet-setup/basic.setup' +import BasicSetup from '../wallet-setup/basic.setup' const test = testWithSynpress(BasicSetup, unlockForFixture) ``` diff --git a/examples/new-dawn/test/e2e/01_basic.spec.ts b/examples/new-dawn/test/e2e/01_basic.spec.ts index 9bc5594f..64a0cd6f 100644 --- a/examples/new-dawn/test/e2e/01_basic.spec.ts +++ b/examples/new-dawn/test/e2e/01_basic.spec.ts @@ -1,5 +1,5 @@ import { MetaMask, testWithSynpress, unlockForFixture } from '@synthetixio/synpress' -import BasicSetup from '../playwright/wallet-setup/basic.setup' +import BasicSetup from '../wallet-setup/basic.setup'; const test = testWithSynpress(BasicSetup, unlockForFixture) diff --git a/wallets/metamask/cypress.config.ts b/wallets/metamask/cypress.config.ts index b16945de..795d6764 100644 --- a/wallets/metamask/cypress.config.ts +++ b/wallets/metamask/cypress.config.ts @@ -1,30 +1,16 @@ -import { defineConfig } from 'cypress' -import { prepareExtension } from './src/prepareExtension' +import { defineConfig } from "cypress"; +import { installSynpress } from "./src/cypress"; export default defineConfig({ + chromeWebSecurity: false, e2e: { - baseUrl: 'http://localhost:9999', - supportFile: 'src/support/e2e.{js,jsx,ts,tsx}', - specPattern: 'test/**/*.cy.{js,jsx,ts,tsx}', - fixturesFolder: 'src/fixture-actions', + baseUrl: "http://localhost:9999", + supportFile: "src/cypress/support/e2e.{js,jsx,ts,tsx}", + specPattern: "test/**/*.cy.{js,jsx,ts,tsx}", + fixturesFolder: "src/cypress/fixtures", testIsolation: false, - setupNodeEvents(on, config) { - const browsers = config.browsers.filter((b) => b.name === 'chrome') - if (browsers.length === 0) { - throw new Error('No Chrome browser found in the configuration') - } - - on('before:browser:launch', async (browser, launchOptions) => { - const metamasExtensionPath = await prepareExtension() - launchOptions.extensions.push(metamasExtensionPath) - - return launchOptions - }) - - return { - ...config, - browsers - } - } - } -}) + async setupNodeEvents(on, config) { + return installSynpress(on, config); + }, + }, +}); diff --git a/wallets/metamask/package.json b/wallets/metamask/package.json index 75c13b2f..9ef00b4a 100644 --- a/wallets/metamask/package.json +++ b/wallets/metamask/package.json @@ -26,7 +26,7 @@ "test:e2e:headful": "playwright test", "test:e2e:headful:cypress": "cypress run --browser chrome --headed", "test:e2e:headless": "HEADLESS=true playwright test", - "test:e2e:headless:cypress": "cypress run --browser chrome", + "test:e2e:headless:cypress": "cypress run --headless --browser chrome", "test:e2e:headless:ui": "HEADLESS=true playwright test --ui", "test:watch": "vitest watch", "types:check": "tsc --noEmit" diff --git a/wallets/metamask/src/cypress/errors.ts b/wallets/metamask/src/cypress/errors.ts new file mode 100644 index 00000000..105c9eef --- /dev/null +++ b/wallets/metamask/src/cypress/errors.ts @@ -0,0 +1,4 @@ +export const NO_CONTEXT = + "No browser context found. Connect Playwright first - connectPlaywright()"; +export const NO_METAMASK_PAGE = "No MetaMask page found. Use getMetaMaskPage()"; +export const MISSING_INIT = "MetaMask not initialized. Use initMetaMask()"; diff --git a/wallets/metamask/src/cypress/index.ts b/wallets/metamask/src/cypress/index.ts new file mode 100644 index 00000000..9847db27 --- /dev/null +++ b/wallets/metamask/src/cypress/index.ts @@ -0,0 +1,3 @@ +export * from "./initMetaMask"; +export { default as setupTasks } from "./setupTasks"; +export { default as installSynpress } from "./installSynpress"; diff --git a/wallets/metamask/src/cypress/initMetaMask.ts b/wallets/metamask/src/cypress/initMetaMask.ts new file mode 100644 index 00000000..2aeace0b --- /dev/null +++ b/wallets/metamask/src/cypress/initMetaMask.ts @@ -0,0 +1,83 @@ +import { type BrowserContext, type Page, chromium } from "@playwright/test"; +import { getExtensionId } from "@synthetixio/synpress-fixtures"; +import { PASSWORD, SEED_PHRASE } from "../constants"; +import { MetaMask } from "../metamask"; +import { MISSING_INIT, NO_CONTEXT, NO_METAMASK_PAGE } from "./errors"; + +let context: BrowserContext | undefined; +let extensionId: string | undefined; +let metamaskPage: Page | undefined; +let metamask: MetaMask | undefined; + +export async function getMetaMaskExtensionId() { + if (extensionId) return extensionId; + + if (!context) { + console.error(NO_CONTEXT); + return; + } + + extensionId = await getExtensionId(context, "MetaMask"); + return extensionId; +} + +const isMetaMaskPage = (page: Page) => + page.url().includes(`chrome-extension://${extensionId}`); + +const getMetaMaskPage = async () => { + await getMetaMaskExtensionId(); + + if (!context) { + console.error(NO_CONTEXT); + return; + } + + metamaskPage = context.pages().find(isMetaMaskPage); + return metamaskPage; +}; + +export async function connectPlaywrightToChrome(port: number) { + const debuggerDetails = await fetch(`http://127.0.0.1:${port}/json/version`); + + const debuggerDetailsConfig = (await debuggerDetails.json()) as { + webSocketDebuggerUrl: string; + }; + + const browser = await chromium.connectOverCDP( + debuggerDetailsConfig.webSocketDebuggerUrl + ); + + context = browser.contexts()[0]; + + return browser.isConnected(); +} + +export async function initMetaMask(port: number) { + await connectPlaywrightToChrome(port); + + if (!context) { + console.error(NO_CONTEXT); + return; + } + + await getMetaMaskPage(); + + if (!metamaskPage) { + console.error(NO_METAMASK_PAGE); + return; + } + + metamask = new MetaMask(context, metamaskPage, PASSWORD); + await metamask.importWallet(SEED_PHRASE); +} + +export function getMetaMask() { + if (!context || !metamaskPage || !metamask) { + console.error(MISSING_INIT); + return; + } + + if (metamask) return metamask; + + return new MetaMask(context, metamaskPage, PASSWORD); +} diff --git a/wallets/metamask/src/cypress/installSynpress.ts b/wallets/metamask/src/cypress/installSynpress.ts new file mode 100644 index 00000000..4494160f --- /dev/null +++ b/wallets/metamask/src/cypress/installSynpress.ts @@ -0,0 +1,58 @@ +import { initMetaMask, setupTasks } from "."; +import { prepareExtension } from "../prepareExtension"; + +let port = 0; + +function ensureRdpPort(args: string[]) { + const existing = args.find( + (arg) => arg.slice(0, 23) === "--remote-debugging-port" + ); + + if (existing) { + return Number(existing.split("=")[1]); + } + + const port = 40000 + Math.round(Math.random() * 25000); + + args.push(`--remote-debugging-port=${port}`); + + return port; +} + +export default function installSynpress( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +) { + const browsers = config.browsers.filter((b) => b.name === "chrome"); + if (browsers.length === 0) { + throw new Error("No Chrome browser found in the configuration"); + } + + on("before:browser:launch", async (_, launchOptions) => { + // Enable debug mode to establish playwright connection + const args = Array.isArray(launchOptions) + ? launchOptions + : launchOptions.args; + port = ensureRdpPort(args); + + // Preserved cache is not supported for Cypress - https://docs.cypress.io/guides/guides/launching-browsers#Cypress-Profile + // launchOptions.args.push('--user-data-dir=X') + + // Add MetaMask extension + const metamaskExtensionPath = await prepareExtension(); + launchOptions.extensions.push(metamaskExtensionPath); + + return launchOptions; + }); + + on("before:spec", async () => { + await initMetaMask(port); + }); + + setupTasks(on); + + return { + ...config, + browsers, + }; +} diff --git a/wallets/metamask/src/cypress/setupTasks.ts b/wallets/metamask/src/cypress/setupTasks.ts new file mode 100644 index 00000000..6b761014 --- /dev/null +++ b/wallets/metamask/src/cypress/setupTasks.ts @@ -0,0 +1,34 @@ +import { getMetaMask } from "."; + +export default function setupTasks(on: Cypress.PluginEvents) { + on("task", { + importWallet: async function (seedPhrase: string) { + const metamask = getMetaMask(); + if (metamask) { + await metamask.importWallet(seedPhrase); + } + return true; + }, + addNewAccount: async function (accountName: string) { + const metamask = getMetaMask(); + if (metamask) { + await metamask.addNewAccount(accountName); + } + return true; + }, + importWalletFromPrivateKey: async function (privateKey: string) { + const metamask = getMetaMask(); + if (metamask) { + await metamask.importWalletFromPrivateKey(privateKey); + } + return true; + }, + openSettings: async function () { + const metamask = getMetaMask(); + if (metamask) { + await metamask.openSettings(); + } + return true; + }, + }); +} diff --git a/wallets/metamask/src/support/commands.ts b/wallets/metamask/src/cypress/support/commands.ts similarity index 53% rename from wallets/metamask/src/support/commands.ts rename to wallets/metamask/src/cypress/support/commands.ts index 95857aea..95e26a6c 100644 --- a/wallets/metamask/src/support/commands.ts +++ b/wallets/metamask/src/cypress/support/commands.ts @@ -25,13 +25,26 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // -// declare global { -// namespace Cypress { -// interface Chainable { -// login(email: string, password: string): Chainable -// drag(subject: string, options?: Partial): Chainable -// dismiss(subject: string, options?: Partial): Chainable -// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable -// } -// } -// } +// import { MetaMask } from "../metamask"; +// import { PASSWORD } from "../constants"; + +declare global { + namespace Cypress { + interface Chainable { + importWallet(seedPhrase: string): Chainable; + addNewAccount(accountName: string): Chainable; + importWalletFromPrivateKey(privateKey: string): Chainable; + openSettings(): Chainable; + } + } +} +Cypress.Commands.add("importWallet", (seedPhrase) => + cy.task("importWallet", seedPhrase) +); +Cypress.Commands.add("addNewAccount", (accountName) => + cy.task("addNewAccount", accountName) +); +Cypress.Commands.add("importWalletFromPrivateKey", (privateKey) => + cy.task("importWalletFromPrivateKey", privateKey) +); +Cypress.Commands.add("openSettings", () => cy.task("openSettings")); diff --git a/wallets/metamask/src/support/e2e.ts b/wallets/metamask/src/cypress/support/e2e.ts similarity index 93% rename from wallets/metamask/src/support/e2e.ts rename to wallets/metamask/src/cypress/support/e2e.ts index 0eef2ce9..c90b6b6d 100644 --- a/wallets/metamask/src/support/e2e.ts +++ b/wallets/metamask/src/cypress/support/e2e.ts @@ -15,5 +15,3 @@ // Import commands.js using ES2015 syntax: import './commands' - -// TODO: Add MetaMask initial setup here. diff --git a/wallets/metamask/src/index.ts b/wallets/metamask/src/index.ts index 72f433c8..8ff7d092 100644 --- a/wallets/metamask/src/index.ts +++ b/wallets/metamask/src/index.ts @@ -1,3 +1,4 @@ -export * from './metamask' -export * from './fixture-actions/unlockForFixture' -export { default as homePageSelectors } from './pages/HomePage/selectors' +export * from "./metamask"; +export * from "./fixture-actions/unlockForFixture"; +export * from "./cypress"; +export { default as homePageSelectors } from "./pages/HomePage/selectors"; diff --git a/wallets/metamask/test/e2e/cypress/addNetwork.cy.ts b/wallets/metamask/test/e2e/cypress/addNetwork.cy.ts deleted file mode 100644 index adb8e603..00000000 --- a/wallets/metamask/test/e2e/cypress/addNetwork.cy.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('My First Test', () => { - it('Does not do much!', () => { - expect(true).to.equal(true) - }) -}) diff --git a/wallets/metamask/test/e2e/cypress/metamask/setupMetaMask.cy.ts b/wallets/metamask/test/e2e/cypress/metamask/setupMetaMask.cy.ts new file mode 100644 index 00000000..89252d8b --- /dev/null +++ b/wallets/metamask/test/e2e/cypress/metamask/setupMetaMask.cy.ts @@ -0,0 +1,11 @@ +it("should add new MetaMask account", () => { + cy.addNewAccount("Synpress with Cypress test"); + + cy.wait(10000); +}); + +it("should open MetaMask settings", () => { + cy.openSettings(); + + cy.wait(10000); +});