diff --git a/.gitignore b/.gitignore index 94d16f61e..0465d16a7 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ playwright/.cache **/ethereum-wallet-mock/cypress **/metamask/cypress + +### Cacheless + +**/metamask/downloads diff --git a/biome.json b/biome.json index 2f9cc4057..6397ca6fe 100644 --- a/biome.json +++ b/biome.json @@ -12,7 +12,8 @@ "**/test-results", "**/playwright-report", "**/.cache-synpress", - "**/.vitepress/cache" + "**/.vitepress/cache", + "**/downloads" ] }, "formatter": { diff --git a/docs/docs/platform-compatibility.md b/docs/docs/platform-compatibility.md index 2050ded0c..834af6f5e 100644 --- a/docs/docs/platform-compatibility.md +++ b/docs/docs/platform-compatibility.md @@ -4,7 +4,7 @@ While in **alpha**, the compatibility of Synpress is limited, but we're working hard to support more frameworks and platforms as soon as possible. ::: -Synpress, in its current state, is only compatible with Playwright (>=1.39.0) and requires Node 18+. Synpress is compatible with MacOS and Linux. +Synpress, in its current state, is only compatible with Playwright (>=1.39.0) and requires Node 18+. Synpress is compatible with MacOS, Windows, and Linux. ## Supported CI Providers @@ -17,10 +17,3 @@ See the [CI](./guides/ci) section for more information about how to set up Synpr ::: - [GitHub Actions](https://github.com/features/actions) - -## Windows - -Currently, Synpress is not compatible with Windows. In the meantime, we recommend using WSL. -::: tip NOTE -If you're knowledgeable about working with Windows and the Node.js environment, and you'd like to help us make Synpress compatible with Windows, please reach out to us on our Discord. -::: diff --git a/packages/cache/package.json b/packages/cache/package.json index c06037c4c..beaee8274 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -29,6 +29,7 @@ "types:check": "tsc --noEmit" }, "dependencies": { + "app-root-path": "3.1.0", "axios": "1.6.7", "chalk": "5.3.0", "commander": "12.0.0", diff --git a/packages/cache/src/cli/cliEntrypoint.ts b/packages/cache/src/cli/cliEntrypoint.ts index 9851573f4..934234764 100644 --- a/packages/cache/src/cli/cliEntrypoint.ts +++ b/packages/cache/src/cli/cliEntrypoint.ts @@ -1,4 +1,3 @@ -import os from 'node:os' import path from 'node:path' import chalk from 'chalk' import { Command } from 'commander' @@ -50,18 +49,6 @@ export const cliEntrypoint = async () => { console.log({ cacheDir: walletSetupDir, ...flags, headless: Boolean(process.env.HEADLESS) ?? false }, '\n') } - if (os.platform() === 'win32') { - console.log( - [ - chalk.redBright('🚨 Sorry, Windows is currently not supported. Please use WSL instead! 🚨'), - chalk.gray( - 'If you want to give it a crack over a hot cup of coffee and add Windows support yourself, please get in touch with the team on Discord so we can offer some guidance! 😇' - ) - ].join('\n') - ) - process.exit(1) - } - const compiledWalletSetupDirPath = await compileWalletSetupFunctions(walletSetupDir, flags.debug) // TODO: We should be using `prepareExtension` function from the wallet itself! diff --git a/packages/cache/src/cli/compileWalletSetupFunctions.ts b/packages/cache/src/cli/compileWalletSetupFunctions.ts index 45b00ab79..4c366edb2 100644 --- a/packages/cache/src/cli/compileWalletSetupFunctions.ts +++ b/packages/cache/src/cli/compileWalletSetupFunctions.ts @@ -1,4 +1,4 @@ -import path from 'node:path' +import path, { posix, win32 } from 'node:path' import { glob } from 'glob' import { build } from 'tsup' import { ensureCacheDirExists } from '../ensureCacheDirExists' @@ -6,13 +6,14 @@ import { FIXES_BANNER } from './compilationFixes' const OUT_DIR_NAME = 'wallet-setup-dist' -const createGlobPattern = (walletSetupDir: string) => path.join(walletSetupDir, '**', '*.setup.{ts,js,mjs}') - export async function compileWalletSetupFunctions(walletSetupDir: string, debug: boolean) { const outDir = path.join(ensureCacheDirExists(), OUT_DIR_NAME) - const globPattern = createGlobPattern(walletSetupDir) - const fileList = await glob(globPattern) + // Use a normalized glob pattern + const globPattern = path.join(walletSetupDir, '**', '*.setup.{ts,js,mjs}') + + // Use glob to find files, ensuring proper path handling + const fileList: string[] = await glob(globPattern, { absolute: false, windowsPathsNoEscape: true }) if (debug) { console.log('[DEBUG] Found the following wallet setup files:') @@ -29,27 +30,34 @@ export async function compileWalletSetupFunctions(walletSetupDir: string, debug: ) } - await build({ - name: 'cli-build', - silent: true, - entry: fileList, - clean: true, - outDir, - format: 'esm', - splitting: true, - sourcemap: false, - config: false, - // TODO: Make this list configurable. - external: ['@synthetixio/synpress', '@playwright/test', 'playwright-core', 'esbuild', 'tsup'], - banner: { - js: FIXES_BANNER - }, - esbuildOptions(options) { - // TODO: In this step, if the debug file is present, we should modify `console.log` so it prints from which file the log is coming from. - // We're dropping `console.log` and `debugger` statements because they do not play nicely with the Playwright Test Runner. - options.drop = debug ? [] : ['console', 'debugger'] - } + const normalized = fileList.map((file) => { + return file.split(win32.sep).join(posix.sep) }) + try { + await build({ + name: 'cli-build', + silent: true, + entry: normalized, + clean: true, + outDir, + format: 'esm', + splitting: true, + sourcemap: false, + config: false, + // TODO: Make this list configurable. + external: ['@synthetixio/synpress', '@playwright/test', 'playwright-core', 'esbuild', 'tsup'], + banner: { + js: FIXES_BANNER + }, + esbuildOptions(options) { + // TODO: In this step, if the debug file is present, we should modify `console.log` so it prints from which file the log is coming from. + // We're dropping `console.log` and `debugger` statements because they do not play nicely with the Playwright Test Runner. + options.drop = debug ? [] : ['console', 'debugger'] + } + }) + } catch (e) { + console.log('error within compile', e) + } return outDir } diff --git a/packages/cache/src/createCache.ts b/packages/cache/src/createCache.ts index 90ee2b550..d0c16effc 100644 --- a/packages/cache/src/createCache.ts +++ b/packages/cache/src/createCache.ts @@ -3,14 +3,11 @@ import { triggerCacheCreation } from './utils/triggerCacheCreation' export async function createCache(walletSetupDirPath: string, downloadExtension: () => Promise, force = false) { const setupFunctions = await getUniqueWalletSetupFunctions(walletSetupDirPath) - const cacheCreationPromises = await triggerCacheCreation(setupFunctions, downloadExtension, force) - if (cacheCreationPromises.length === 0) { console.log('No new setup functions to cache. Exiting...') return } - // TODO: This line has no unit test. Not sure how to do it. Look into it later. await Promise.all(cacheCreationPromises) diff --git a/packages/cache/src/defineWalletSetup.ts b/packages/cache/src/defineWalletSetup.ts index e61f717f2..0a425d92a 100644 --- a/packages/cache/src/defineWalletSetup.ts +++ b/packages/cache/src/defineWalletSetup.ts @@ -4,7 +4,7 @@ import { getWalletSetupFuncHash } from './utils/getWalletSetupFuncHash' // TODO: Should we export this type in the `release` package? export type WalletSetupFunction = (context: BrowserContext, walletPage: Page) => Promise -// TODO: This runs at least twice. Should we cache it somehow? +// This runs once for each setup file on building cache and setting up fixtures, then it runs once for each worker on e2e:test. Should we cache it somehow? /** * This function is used to define how a wallet should be set up. * Based on the contents of this function, a browser with the wallet extension is set up and cached so that it can be used by the tests later. diff --git a/packages/cache/src/utils/getWalletSetupFiles.ts b/packages/cache/src/utils/getWalletSetupFiles.ts index 41bd2f050..ccd6c8029 100644 --- a/packages/cache/src/utils/getWalletSetupFiles.ts +++ b/packages/cache/src/utils/getWalletSetupFiles.ts @@ -26,6 +26,5 @@ export async function getWalletSetupFiles(walletSetupDirPath: string) { ].join('\n') ) } - return fileList } diff --git a/packages/cache/src/utils/importWalletSetupFile.ts b/packages/cache/src/utils/importWalletSetupFile.ts index 498e86e65..ebae4ab90 100644 --- a/packages/cache/src/utils/importWalletSetupFile.ts +++ b/packages/cache/src/utils/importWalletSetupFile.ts @@ -10,8 +10,8 @@ const WalletSetupModule = z.object({ }) export async function importWalletSetupFile(walletSetupFilePath: string) { - const walletSetupModule = await import(walletSetupFilePath) - + const fileToImport = process.platform === 'win32' ? `file:\\\\\\${walletSetupFilePath}` : walletSetupFilePath + const walletSetupModule = await import(fileToImport) const result = WalletSetupModule.safeParse(walletSetupModule) if (!result.success) { throw new Error( diff --git a/packages/core/package.json b/packages/core/package.json index 332b9c4d0..0572ccc26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,7 +25,7 @@ "types:check": "tsc --noEmit" }, "devDependencies": { - "@synthetixio/synpress-tsconfig": "0.0.1-alpha.7", + "@synthetixio/synpress-tsconfig": "workspace:*", "@types/node": "20.11.17", "rimraf": "5.0.5", "tsup": "8.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d809f51..4480a02dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: packages/cache: dependencies: + app-root-path: + specifier: 3.1.0 + version: 3.1.0 axios: specifier: 1.6.7 version: 1.6.7 @@ -177,7 +180,7 @@ importers: version: 1.44.0 devDependencies: '@synthetixio/synpress-tsconfig': - specifier: 0.0.1-alpha.7 + specifier: workspace:* version: link:../tsconfig '@types/node': specifier: 20.11.17 @@ -203,10 +206,10 @@ importers: specifier: 0.0.1-alpha.7 version: link:../wallets/ethereum-wallet-mock '@synthetixio/synpress-cache': - specifier: 0.0.1-alpha.7 + specifier: workspace:* version: link:../packages/cache '@synthetixio/synpress-core': - specifier: 0.0.1-alpha.7 + specifier: workspace:* version: link:../packages/core '@synthetixio/synpress-metamask': specifier: 0.0.1-alpha.7 @@ -277,17 +280,32 @@ importers: specifier: 1.44.0 version: 1.44.0 '@synthetixio/synpress-cache': - specifier: 0.0.1-alpha.7 + specifier: workspace:* version: link:../../packages/cache '@synthetixio/synpress-core': - specifier: 0.0.1-alpha.7 + specifier: workspace:* version: link:../../packages/core '@viem/anvil': specifier: 0.0.7 version: 0.0.7 + app-root-path: + specifier: 3.1.0 + version: 3.1.0 + axios: + specifier: 1.6.7 + version: 1.6.7 + dotenv: + specifier: 16.4.2 + version: 16.4.2 + find-config: + specifier: 1.0.0 + version: 1.0.0 fs-extra: specifier: 11.2.0 version: 11.2.0 + unzipper: + specifier: 0.10.14 + version: 0.10.14 zod: specifier: 3.22.4 version: 3.22.4 @@ -295,12 +313,18 @@ importers: '@synthetixio/synpress-tsconfig': specifier: 0.0.1-alpha.7 version: link:../../packages/tsconfig + '@types/find-config': + specifier: 1.0.4 + version: 1.0.4 '@types/fs-extra': specifier: 11.0.4 version: 11.0.4 '@types/node': specifier: 20.11.17 version: 20.11.17 + '@types/unzipper': + specifier: 0.10.9 + version: 0.10.9 '@vitest/coverage-v8': specifier: 1.2.2 version: 1.2.2(vitest@1.2.2) @@ -2378,6 +2402,10 @@ packages: /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + /@types/find-config@1.0.4: + resolution: {integrity: sha512-BCXaKgzHK7KnfCQBRQBWGTA+QajOE9uFolXPt+9EktiiMS56D8oXF2ZCh9eCxuEyfqDmX/mYIcmWg9j9f659eg==} + dev: true + /@types/fs-extra@11.0.4: resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} dependencies: @@ -2929,6 +2957,11 @@ packages: normalize-path: 3.0.0 picomatch: 2.3.1 + /app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + dev: false + /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: true @@ -4506,6 +4539,13 @@ packages: dependencies: to-regex-range: 5.0.1 + /find-config@1.0.0: + resolution: {integrity: sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==} + engines: {node: '>= 0.12'} + dependencies: + user-home: 2.0.0 + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4662,6 +4702,7 @@ packages: /fstream@1.0.12: resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} engines: {node: '>=0.6'} + deprecated: This package is no longer supported. dependencies: graceful-fs: 4.2.11 inherits: 2.0.4 @@ -6471,6 +6512,11 @@ packages: dependencies: mimic-fn: 4.0.0 + /os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + dev: false + /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -8327,6 +8373,13 @@ packages: requires-port: 1.0.0 dev: true + /user-home@2.0.0: + resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} + engines: {node: '>=0.10.0'} + dependencies: + os-homedir: 1.0.2 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} diff --git a/release/package.json b/release/package.json index 6bb538a29..efd4295e2 100644 --- a/release/package.json +++ b/release/package.json @@ -39,8 +39,8 @@ }, "dependencies": { "@synthetixio/ethereum-wallet-mock": "0.0.1-alpha.7", - "@synthetixio/synpress-cache": "0.0.1-alpha.7", - "@synthetixio/synpress-core": "0.0.1-alpha.7", + "@synthetixio/synpress-cache": "workspace:*", + "@synthetixio/synpress-core": "workspace:*", "@synthetixio/synpress-metamask": "0.0.1-alpha.7" }, "devDependencies": { diff --git a/wallets/metamask/environment.d.ts b/wallets/metamask/environment.d.ts index e214b5b09..234a73346 100644 --- a/wallets/metamask/environment.d.ts +++ b/wallets/metamask/environment.d.ts @@ -3,6 +3,7 @@ declare global { interface ProcessEnv { CI: boolean HEADLESS: boolean + USE_CACHE: string } } } diff --git a/wallets/metamask/package.json b/wallets/metamask/package.json index d9b2eed90..c7d1b4168 100644 --- a/wallets/metamask/package.json +++ b/wallets/metamask/package.json @@ -30,16 +30,23 @@ "types:check": "tsc --noEmit" }, "dependencies": { - "@synthetixio/synpress-cache": "0.0.1-alpha.7", - "@synthetixio/synpress-core": "0.0.1-alpha.7", + "@synthetixio/synpress-cache": "workspace:*", + "@synthetixio/synpress-core": "workspace:*", "@viem/anvil": "0.0.7", + "app-root-path": "3.1.0", + "axios": "1.6.7", + "dotenv": "16.4.2", + "find-config": "1.0.0", "fs-extra": "11.2.0", + "unzipper": "0.10.14", "zod": "3.22.4" }, "devDependencies": { "@synthetixio/synpress-tsconfig": "0.0.1-alpha.7", + "@types/find-config": "1.0.4", "@types/fs-extra": "11.0.4", "@types/node": "20.11.17", + "@types/unzipper": "0.10.9", "@vitest/coverage-v8": "1.2.2", "rimraf": "5.0.5", "tsup": "8.0.2", diff --git a/wallets/metamask/src/config.ts b/wallets/metamask/src/config.ts new file mode 100644 index 000000000..6df34ef63 --- /dev/null +++ b/wallets/metamask/src/config.ts @@ -0,0 +1,14 @@ +import dotenv from 'dotenv' +import findConfig from 'find-config' + +export const loadEnv = () => { + const envFiles = ['.env', '.env.e2e', '.env.local', '.env.dev'] + envFiles.find((envFile) => { + const config = findConfig(envFile) + if (config) { + dotenv.config({ path: config }) + return true + } + return false + }) +} diff --git a/wallets/metamask/src/fixture-actions/importAndConnectForFixtures.ts b/wallets/metamask/src/fixture-actions/importAndConnectForFixtures.ts new file mode 100644 index 000000000..d672390ac --- /dev/null +++ b/wallets/metamask/src/fixture-actions/importAndConnectForFixtures.ts @@ -0,0 +1,39 @@ +import type { Page } from '@playwright/test' +import { MetaMask } from '..' +import { retryIfMetaMaskCrashAfterUnlock } from '..' +import { closePopover } from '../pages/HomePage/actions' + +export async function importAndConnectForFixtures( + page: Page, + seedPhrase: string, + password: string, + extensionId: string +) { + const metamask = new MetaMask(page.context(), page, password, extensionId) + + await metamask.importWallet(seedPhrase) + + await metamask.openSettings() + + const SidebarMenus = metamask.homePage.selectors.settings.SettingsSidebarMenus + + await metamask.openSidebarMenu(SidebarMenus.Advanced) + + await metamask.toggleDismissSecretRecoveryPhraseReminder() + + await page.goto(`chrome-extension://${extensionId}/home.html`) + + await retryIfMetaMaskCrashAfterUnlock(page) + + await closePopover(page) + + const newPage = await page.context().newPage() + + await newPage.goto('http://localhost:9999') + + await newPage.locator('#connectButton').click() + + await metamask.connectToDapp() + + await newPage.close() +} diff --git a/wallets/metamask/src/fixture-actions/noCacheMetaMaskSetup.ts b/wallets/metamask/src/fixture-actions/noCacheMetaMaskSetup.ts new file mode 100644 index 000000000..ca445fa30 --- /dev/null +++ b/wallets/metamask/src/fixture-actions/noCacheMetaMaskSetup.ts @@ -0,0 +1,89 @@ +import path from 'node:path' +import { type BrowserContext, chromium } from '@playwright/test' +import appRoot from 'app-root-path' +import axios from 'axios' +import fs from 'fs-extra' +import unzipper from 'unzipper' +import { DEFAULT_METAMASK_VERSION, EXTENSION_DOWNLOAD_URL } from '../utils/constants' + +async function prepareDownloadDirectory(version: string = DEFAULT_METAMASK_VERSION): Promise { + const downloadsDirectory = + process.platform === 'win32' ? appRoot.resolve('/node_modules') : path.join(process.cwd(), 'downloads') + await fs.ensureDir(downloadsDirectory) + + const metamaskDirectory = path.join(downloadsDirectory, `metamask-chrome-${version}.zip`) + const archiveFileExtension = path.extname(metamaskDirectory) + const outputPath = metamaskDirectory.replace(archiveFileExtension, '') + const metamaskManifestPath = path.join(outputPath, 'manifest.json') + + if (!fs.existsSync(metamaskManifestPath)) { + await downloadAndExtract(EXTENSION_DOWNLOAD_URL, metamaskDirectory) + } + + return outputPath +} + +async function downloadAndExtract(url: string, destination: string): Promise { + const response = await axios.get(url, { responseType: 'stream' }) + const writer = fs.createWriteStream(destination) + response.data.pipe(writer) + await new Promise((resolve) => writer.on('finish', resolve)) + + await unzipArchive(destination) +} + +async function unzipArchive(archivePath: string): Promise { + const archiveFileExtension = path.extname(archivePath) + const outputPath = archivePath.replace(archiveFileExtension, '') + + await fs.ensureDir(outputPath) + + try { + await new Promise((resolve, reject) => { + const stream = fs.createReadStream(archivePath).pipe(unzipper.Parse()) + + stream.on( + 'entry', + async (entry: { path: string; type: string; pipe: (arg: unknown) => void; autodrain: () => void }) => { + const fileName = entry.path + const type = entry.type as 'Directory' | 'File' + + if (type === 'Directory') { + await fs.mkdir(path.join(outputPath, fileName), { recursive: true }) + entry.autodrain() + return + } + + if (type === 'File') { + const writeStream = fs.createWriteStream(path.join(outputPath, fileName)) + entry.pipe(writeStream) + + await new Promise((res, rej) => { + writeStream.on('finish', res) + writeStream.on('error', rej) + }) + } + } + ) + stream.on('finish', resolve) + stream.on('error', reject) + }) + } catch (error: unknown) { + console.error(`[unzipArchive] Error unzipping archive: ${(error as { message: string }).message}`) + throw error + } +} + +export async function cachelessSetupMetaMask(metamaskVersion?: string): Promise { + const metamaskPath = await prepareDownloadDirectory(metamaskVersion || DEFAULT_METAMASK_VERSION) + const browserArgs = [`--load-extension=${metamaskPath}`, `--disable-extensions-except=${metamaskPath}`] + + if (process.env.HEADLESS) { + browserArgs.push('--headless=new') + } + const context = await chromium.launchPersistentContext('', { + headless: false, + args: browserArgs + }) + return context +} diff --git a/wallets/metamask/src/fixture-actions/prepareExtension.ts b/wallets/metamask/src/fixture-actions/prepareExtension.ts index 649e346d0..39eba14c5 100644 --- a/wallets/metamask/src/fixture-actions/prepareExtension.ts +++ b/wallets/metamask/src/fixture-actions/prepareExtension.ts @@ -1,7 +1,5 @@ import { downloadFile, ensureCacheDirExists, unzipArchive } from '@synthetixio/synpress-cache' - -export const DEFAULT_METAMASK_VERSION = '11.9.1' -export const EXTENSION_DOWNLOAD_URL = `https://github.com/MetaMask/metamask-extension/releases/download/v${DEFAULT_METAMASK_VERSION}/metamask-chrome-${DEFAULT_METAMASK_VERSION}.zip` +import { DEFAULT_METAMASK_VERSION, EXTENSION_DOWNLOAD_URL } from '../utils/constants' export async function prepareExtension() { const cacheDirPath = ensureCacheDirExists() diff --git a/wallets/metamask/src/fixture-actions/unlockForFixture.ts b/wallets/metamask/src/fixture-actions/unlockForFixture.ts index 36bfa5916..16cf6d252 100644 --- a/wallets/metamask/src/fixture-actions/unlockForFixture.ts +++ b/wallets/metamask/src/fixture-actions/unlockForFixture.ts @@ -40,7 +40,7 @@ async function unlockWalletButReloadIfSpinnerDoesNotVanish(metamask: MetaMask) { } } -async function retryIfMetaMaskCrashAfterUnlock(page: Page) { +export async function retryIfMetaMaskCrashAfterUnlock(page: Page) { const homePageLogoLocator = page.locator(HomePage.selectors.logo) const isHomePageLogoVisible = await homePageLogoLocator.isVisible() diff --git a/wallets/metamask/src/fixtures/metaMaskFixtures.ts b/wallets/metamask/src/fixtures/metaMaskFixtures.ts index 88b3cd55a..9d5373f07 100644 --- a/wallets/metamask/src/fixtures/metaMaskFixtures.ts +++ b/wallets/metamask/src/fixtures/metaMaskFixtures.ts @@ -11,14 +11,23 @@ import { } from '@synthetixio/synpress-cache' import { type Anvil, type CreateAnvilOptions, createPool } from '@viem/anvil' import fs from 'fs-extra' +import { loadEnv } from '../config' +import { importAndConnectForFixtures } from '../fixture-actions/importAndConnectForFixtures' +import { cachelessSetupMetaMask } from '../fixture-actions/noCacheMetaMaskSetup' import { persistLocalStorage } from '../fixture-actions/persistLocalStorage' +import { SEED_PHRASE } from '../utils/constants' import { waitForMetaMaskWindowToBeStable } from '../utils/waitFor' +loadEnv() + +const USECACHE: boolean = process.env.USE_CACHE === undefined || process.env.USE_CACHE === 'true' + type MetaMaskFixtures = { _contextPath: string metamask: MetaMask extensionId: string metamaskPage: Page + useCache: boolean createAnvilNode: (options?: CreateAnvilOptions) => Promise<{ anvil: Anvil; rpcUrl: string; chainId: number }> connectToAnvil: () => Promise deployToken: () => Promise @@ -31,52 +40,62 @@ let _metamaskPage: Page export const metaMaskFixtures = (walletSetup: ReturnType, slowMo = 0) => { return base.extend({ _contextPath: async ({ browserName }, use, testInfo) => { - const contextPath = await createTempContextDir(browserName, testInfo.testId) + if (USECACHE) { + const contextPath = await createTempContextDir(browserName, testInfo.testId) - await use(contextPath) + await use(contextPath) - const error = await removeTempContextDir(contextPath) - if (error) { - console.error(error) + const error = await removeTempContextDir(contextPath) + if (error) { + console.error(error) + } + } else { + await use('') } }, context: async ({ context: currentContext, _contextPath }, use) => { - const cacheDirPath = path.join(process.cwd(), CACHE_DIR_NAME, walletSetup.hash) - if (!(await fs.exists(cacheDirPath))) { - throw new Error(`Cache for ${walletSetup.hash} does not exist. Create it first!`) - } + let context + if (USECACHE) { + const cacheDirPath = path.join(process.cwd(), CACHE_DIR_NAME, walletSetup.hash) + if (!(await fs.exists(cacheDirPath))) { + throw new Error(`Cache for ${walletSetup.hash} does not exist. Create it first!`) + } - // Copying the cache to the temporary context directory. - await fs.copy(cacheDirPath, _contextPath) + // Copying the cache to the temporary context directory. + await fs.copy(cacheDirPath, _contextPath) - const metamaskPath = await prepareExtension() + const metamaskPath = await prepareExtension() - // We don't need the `--load-extension` arg since the extension is already loaded in the cache. - const browserArgs = [`--disable-extensions-except=${metamaskPath}`] + // We don't need the `--load-extension` arg since the extension is already loaded in the cache. + const browserArgs = [`--disable-extensions-except=${metamaskPath}`] - if (process.env.HEADLESS) { - browserArgs.push('--headless=new') + if (process.env.HEADLESS) { + browserArgs.push('--headless=new') - if (slowMo > 0) { - console.warn('[WARNING] Slow motion makes no sense in headless mode. It will be ignored!') + if (slowMo > 0) { + console.warn('[WARNING] Slow motion makes no sense in headless mode. It will be ignored!') + } } - } - const context = await chromium.launchPersistentContext(_contextPath, { - headless: false, - args: browserArgs, - slowMo: process.env.HEADLESS ? 0 : slowMo - }) + context = await chromium.launchPersistentContext(_contextPath, { + headless: false, + args: browserArgs, + slowMo: process.env.HEADLESS ? 0 : slowMo + }) - const { cookies, origins } = await currentContext.storageState() + const { cookies, origins } = await currentContext.storageState() - if (cookies) { - await context.addCookies(cookies) + if (cookies) { + await context.addCookies(cookies) + } + if (origins && origins.length > 0) { + await persistLocalStorage(origins, context) + } } - if (origins && origins.length > 0) { - await persistLocalStorage(origins, context) + if (!USECACHE) { + context = await cachelessSetupMetaMask() } - + if (!context) return // TODO: This should be stored in a store to speed up the tests. const extensionId = await getExtensionId(context, 'MetaMask') @@ -85,8 +104,13 @@ export const metaMaskFixtures = (walletSetup: ReturnType { const classes = await toggleLocator.getAttribute('class') @@ -20,8 +18,8 @@ export async function toggle(toggleLocator: Locator) { throw new Error('[ToggleShowTestNetworks] Toggle class returned null inside waitFor') } - if (isOn) { - return classes.includes('toggle-button--off') + if (!isOn) { + await toggleLocator.click() } return classes.includes('toggle-button--on') diff --git a/wallets/metamask/src/utils/waitFor.ts b/wallets/metamask/src/utils/waitFor.ts index c5680567e..58646e547 100644 --- a/wallets/metamask/src/utils/waitFor.ts +++ b/wallets/metamask/src/utils/waitFor.ts @@ -29,13 +29,9 @@ export const waitForMetaMaskLoad = async (page: Page) => { LoadingSelectors.loadingIndicators.map(async (selector) => { await waitForSelector(selector, page, DEFAULT_TIMEOUT) }) - ) - .then(() => { - console.log('All loading indicators are hidden') - }) - .catch((error) => { - console.error('Error: ', error) - }) + ).catch((error) => { + console.error('Error: ', error) + }) return page } diff --git a/wallets/metamask/test/e2e/PPOM.spec.ts b/wallets/metamask/test/e2e/PPOM.spec.ts index fdfa7ffbb..307995ead 100644 --- a/wallets/metamask/test/e2e/PPOM.spec.ts +++ b/wallets/metamask/test/e2e/PPOM.spec.ts @@ -10,7 +10,8 @@ const PPOM_ERROR = 'This is a deceptive request' const PPOM_WARNING = 'Request may not be safe' describe('using PPOM security mechanism', () => { - test('should prevent malicious ETH transfer', async ({ context, page, metamask }) => { + test('should prevent malicious ETH transfer', async ({ context, page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#maliciousRawEthButton').click() const notificationPage = await getNotificationPageAndWaitForLoad(context, metamask.extensionId || '') @@ -30,7 +31,8 @@ describe('using PPOM security mechanism', () => { ) }) - test('should prevent malicious ERC20 approval', async ({ context, page, metamask }) => { + test('should prevent malicious ERC20 approval', async ({ context, page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#maliciousApprovalButton').click() const notificationPage = await getNotificationPageAndWaitForLoad(context, metamask.extensionId || '') @@ -40,7 +42,8 @@ describe('using PPOM security mechanism', () => { ) }) - test('should prevent malicious approval for all', async ({ context, page, metamask }) => { + test('should prevent malicious approval for all', async ({ context, page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#maliciousSetApprovalForAll').click() const notificationPage = await getNotificationPageAndWaitForLoad(context, metamask.extensionId || '') diff --git a/wallets/metamask/test/e2e/approveNewNetwork.spec.ts b/wallets/metamask/test/e2e/approveNewNetwork.spec.ts index 6392f468a..18515ac5f 100644 --- a/wallets/metamask/test/e2e/approveNewNetwork.spec.ts +++ b/wallets/metamask/test/e2e/approveNewNetwork.spec.ts @@ -4,7 +4,9 @@ const test = synpress const { expect } = test -test('should add a new network', async ({ page, metamask, createAnvilNode }) => { +test('should add a new network', async ({ page, metamask, createAnvilNode, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') + await createAnvilNode({ chainId: 1338, port: 8546 diff --git a/wallets/metamask/test/e2e/confirmSignature.spec.ts b/wallets/metamask/test/e2e/confirmSignature.spec.ts index e67381f6f..d756e4ef6 100644 --- a/wallets/metamask/test/e2e/confirmSignature.spec.ts +++ b/wallets/metamask/test/e2e/confirmSignature.spec.ts @@ -4,7 +4,8 @@ const test = synpress const { expect } = test -test('should confirm `personal_sign`', async ({ page, metamask }) => { +test('should confirm `personal_sign`', async ({ page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#personalSign').click() await metamask.confirmSignature() @@ -51,7 +52,8 @@ test('should confirm `eth_signTypedData_v3`', async ({ page, metamask }) => { await expect(page.locator('#signTypedDataV3VerifyResult')).toHaveText('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') }) -test('should confirm `eth_signTypedData_v4`', async ({ page, metamask }) => { +test('should confirm `eth_signTypedData_v4`', async ({ page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#signTypedDataV4').click() await metamask.confirmSignature() diff --git a/wallets/metamask/test/e2e/connectToDapp.spec.ts b/wallets/metamask/test/e2e/connectToDapp.spec.ts index 1fa603ac8..ce15ceb11 100644 --- a/wallets/metamask/test/e2e/connectToDapp.spec.ts +++ b/wallets/metamask/test/e2e/connectToDapp.spec.ts @@ -12,20 +12,25 @@ test('should connect wallet to dapp', async ({ context, page, extensionId }) => await page.goto('/') - await page.locator('#connectButton').click() + const disabled = await page.locator('#connectButton').isDisabled() - await metamask.connectToDapp() + if (!disabled) { + await page.locator('#connectButton').click() + await metamask.connectToDapp() + } await expect(page.locator('#accounts')).toHaveText('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') }) -test('should connect multiple wallets to dapp', async ({ context, page, metamaskPage, extensionId }) => { +test('should connect multiple wallets to dapp', async ({ context, page, metamaskPage, extensionId, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') const metamask = new MetaMask(context, metamaskPage, basicSetup.walletPassword, extensionId) await metamask.addNewAccount('Account x2') await metamask.addNewAccount('Account x3') await page.goto('/') + await page.locator('#connectButton').click() // "accounts" param is order agnostic diff --git a/wallets/metamask/test/e2e/encrypt.spec.ts b/wallets/metamask/test/e2e/encrypt.spec.ts index 89215cd18..0836797ab 100644 --- a/wallets/metamask/test/e2e/encrypt.spec.ts +++ b/wallets/metamask/test/e2e/encrypt.spec.ts @@ -4,7 +4,8 @@ const test = synpress const { expect } = test -test('should provide public encryption key', async ({ page, metamask }) => { +test('should provide public encryption key', async ({ page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#getEncryptionKeyButton').click() await metamask.providePublicEncryptionKey() diff --git a/wallets/metamask/test/e2e/rejectSignature.spec.ts b/wallets/metamask/test/e2e/rejectSignature.spec.ts index e1ff3d43f..7e4c62abc 100644 --- a/wallets/metamask/test/e2e/rejectSignature.spec.ts +++ b/wallets/metamask/test/e2e/rejectSignature.spec.ts @@ -15,7 +15,8 @@ test('should reject `personal_sign`', async ({ page, metamask }) => { await expect(page.locator('#personalSignResult')).toHaveText('') }) -test('should reject `eth_signTypedData`', async ({ page, metamask }) => { +test('should reject `eth_signTypedData`', async ({ page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#signTypedData').click() await metamask.rejectSignature() @@ -25,7 +26,8 @@ test('should reject `eth_signTypedData`', async ({ page, metamask }) => { ) }) -test('should reject `eth_signTypedData_v3`', async ({ page, metamask }) => { +test('should reject `eth_signTypedData_v3`', async ({ page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#signTypedDataV3').click() await metamask.rejectSignature() @@ -35,7 +37,8 @@ test('should reject `eth_signTypedData_v3`', async ({ page, metamask }) => { ) }) -test('should reject `eth_signTypedData_v4`', async ({ page, metamask }) => { +test('should reject `eth_signTypedData_v4`', async ({ page, metamask, useCache }) => { + test.skip(!useCache, 'This test requires useCache to be true') await page.locator('#signTypedDataV4').click() await metamask.rejectSignature() diff --git a/wallets/metamask/test/unit/prepareExtension.test.ts b/wallets/metamask/test/unit/prepareExtension.test.ts index 840b7ba5f..53527b426 100644 --- a/wallets/metamask/test/unit/prepareExtension.test.ts +++ b/wallets/metamask/test/unit/prepareExtension.test.ts @@ -1,6 +1,7 @@ import * as core from '@synthetixio/synpress-cache' import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' -import { DEFAULT_METAMASK_VERSION, EXTENSION_DOWNLOAD_URL, prepareExtension } from '../../src' +import { prepareExtension } from '../../src' +import { DEFAULT_METAMASK_VERSION, EXTENSION_DOWNLOAD_URL } from '../../src/utils/constants' const MOCK_CACHE_DIR_PATH = 'mockCacheDirPath' const MOCK_EXTENSION_ARCHIVE_PATH = 'mockExtensionArchivePath' diff --git a/wallets/metamask/test/wallet-setup/basic.setup.d.ts b/wallets/metamask/test/wallet-setup/basic.setup.d.ts deleted file mode 100644 index 1bc70ea7c..000000000 --- a/wallets/metamask/test/wallet-setup/basic.setup.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export declare const SEED_PHRASE = 'test test test test test test test test test test test junk' -export declare const PASSWORD = 'Tester@1234' -declare const _default: { - hash: string - fn: import('@synthetixio/synpress-cache').WalletSetupFunction - walletPassword: string -} -export default _default -//# sourceMappingURL=basic.setup.d.ts.map diff --git a/wallets/metamask/test/wallet-setup/basic.setup.d.ts.map b/wallets/metamask/test/wallet-setup/basic.setup.d.ts.map deleted file mode 100644 index d4aad874b..000000000 --- a/wallets/metamask/test/wallet-setup/basic.setup.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"basic.setup.d.ts","sourceRoot":"","sources":["basic.setup.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW,gEAAgE,CAAA;AAExF,eAAO,MAAM,QAAQ,gBAAgB,CAAA;;;;;;AAErC,wBAIE"} \ No newline at end of file