diff --git a/packages/cache/src/cli/cliEntrypoint.ts b/packages/cache/src/cli/cliEntrypoint.ts index a1769b3b2..3f1b92a14 100644 --- a/packages/cache/src/cli/cliEntrypoint.ts +++ b/packages/cache/src/cli/cliEntrypoint.ts @@ -34,9 +34,10 @@ export const cliEntrypoint = async () => { .addHelpText('afterAll', `\n${footer}\n`) .parse(process.argv) - let walletSetupDir = program.args[0] - if (!walletSetupDir) { - walletSetupDir = path.join(process.cwd(), 'test', WALLET_SETUP_DIR_NAME) + let walletSetupDir = path.join(process.cwd(), 'test', WALLET_SETUP_DIR_NAME) + + if (program.args[0]) { + walletSetupDir = path.join(process.cwd(), program.args[0]) } const flags: CliFlags = program.opts() @@ -47,7 +48,14 @@ export const cliEntrypoint = async () => { if (flags.debug) { console.log('[DEBUG] Running with the following options:') - console.log({ cacheDir: walletSetupDir, ...flags, headless: Boolean(process.env.HEADLESS) ?? false }, '\n') + console.log( + { + cacheDir: walletSetupDir, + ...flags, + headless: Boolean(process.env.HEADLESS) ?? false + }, + '\n' + ) } if (os.platform() === 'win32') { @@ -62,10 +70,15 @@ export const cliEntrypoint = async () => { process.exit(1) } - const { outDir: compiledWalletSetupDirPath, functionStrings } = await compileWalletSetupFunctions(walletSetupDir, flags.debug) + console.log(chalk.greenBright('πŸš€ Building the cache for wallet setup functions... πŸš€\n')) + + const { outDir: compiledWalletSetupDirPath, setupFunctionHashes } = await compileWalletSetupFunctions( + walletSetupDir, + flags.debug + ) - // TODO: We should be using `prepareExtension` function from the wallet itself! - await createCache(compiledWalletSetupDirPath, functionStrings, prepareExtension, flags.force) + // TODO: We should be using `prepareExtension` function from the wallet itself! + await createCache(compiledWalletSetupDirPath, setupFunctionHashes, prepareExtension, flags.force) if (!flags.debug) { await rimraf(compiledWalletSetupDirPath) diff --git a/packages/cache/src/cli/compileWalletSetupFunctions.ts b/packages/cache/src/cli/compileWalletSetupFunctions.ts index d7070a80d..1fa1e8183 100644 --- a/packages/cache/src/cli/compileWalletSetupFunctions.ts +++ b/packages/cache/src/cli/compileWalletSetupFunctions.ts @@ -1,27 +1,29 @@ -import path from "node:path"; -import { glob } from "glob"; -import { build } from "tsup"; -import { ensureCacheDirExists } from "../ensureCacheDirExists"; -import { FIXES_BANNER } from "./compilationFixes"; -import buildWalletSetupFunction from "../utils/buildWalletSetupFunction"; +import path from 'node:path' +import fs from 'fs-extra' +import { glob } from 'glob' +import { build } from 'tsup' -const OUT_DIR_NAME = "wallet-setup-dist"; +import { ensureCacheDirExists } from '../ensureCacheDirExists' +import buildWalletSetupFunction from '../utils/buildWalletSetupFunction' +import { extractWalletSetupFunction } from '../utils/extractWalletSetupFunction' +import { getWalletSetupFuncHash } from '../utils/getWalletSetupFuncHash' +import { FIXES_BANNER } from './compilationFixes' -const createGlobPattern = (walletSetupDir: string) => - path.join(walletSetupDir, "**", "*.setup.{ts,js,mjs}"); +const OUT_DIR_NAME = '.wallet-setup-dist' -export async function compileWalletSetupFunctions( - walletSetupDir: string, - debug: boolean -) { - const outDir = path.join(ensureCacheDirExists(), OUT_DIR_NAME); +const createGlobPattern = (walletSetupDir: string) => path.join(walletSetupDir, '**', '*.setup.{ts,js,mjs}') - const globPattern = createGlobPattern(walletSetupDir); - const fileList = await glob(globPattern); +export async function compileWalletSetupFunctions(walletSetupDir: string, debug: boolean) { + const outDir = path.join(ensureCacheDirExists(), OUT_DIR_NAME) + + fs.ensureDirSync(outDir) + + const globPattern = createGlobPattern(walletSetupDir) + const fileList = await glob(globPattern) if (debug) { - console.log("[DEBUG] Found the following wallet setup files:"); - console.log(fileList, "\n"); + console.log('[DEBUG] Found the following wallet setup files:') + console.log(fileList, '\n') } // TODO: This error message is copied over from another function. Refactor this. @@ -29,48 +31,43 @@ export async function compileWalletSetupFunctions( throw new Error( [ `No wallet setup files found at ${walletSetupDir}`, - "Remember that all wallet setup files must end with `.setup.{ts,js,mjs}` extension!", - ].join("\n") - ); + 'Remember that all wallet setup files must end with `.setup.{ts,js,mjs}` extension!' + ].join('\n') + ) } await build({ - name: "cli-build", + name: 'cli-build', silent: true, entry: fileList, clean: true, outDir, - format: "esm", + format: 'esm', splitting: true, sourcemap: false, config: false, // TODO: Make this list configurable. - external: [ - "@synthetixio/synpress", - "@playwright/test", - "playwright-core", - "esbuild", - "tsup", - ], + external: ['@synthetixio/synpress', '@playwright/test', 'playwright-core', 'esbuild', 'tsup'], banner: { - js: FIXES_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"]; - }, - }); + options.drop = debug ? [] : ['console', 'debugger'] + } + }) - const functionStrings = await Promise.all( - fileList.map(async (fileName) => { - const walletSetupFunction = await import(fileName); + const setupFunctionHashes = await Promise.all( + fileList.map(async (filePath) => { + const sourceCode = fs.readFileSync(filePath, 'utf8') + const functionString = extractWalletSetupFunction(sourceCode) - return buildWalletSetupFunction(walletSetupFunction.toString()); - }) - ); + const rawFunctionBuild = buildWalletSetupFunction(functionString) - console.log({functionStrings}) + return getWalletSetupFuncHash(rawFunctionBuild) + }) + ) - return { outDir, functionStrings: functionStrings }; + return { outDir, setupFunctionHashes } } diff --git a/packages/cache/src/createCache.ts b/packages/cache/src/createCache.ts index 8a0d75f49..5540a08b9 100644 --- a/packages/cache/src/createCache.ts +++ b/packages/cache/src/createCache.ts @@ -1,30 +1,23 @@ -import { getUniqueWalletSetupFunctions } from "./utils/getUniqueWalletSetupFunctions"; -import { triggerCacheCreation } from "./utils/triggerCacheCreation"; +import { getUniqueWalletSetupFunctions } from './utils/getUniqueWalletSetupFunctions' +import { triggerCacheCreation } from './utils/triggerCacheCreation' export async function createCache( walletSetupDirPath: string, - functionStrings: string[], + hashes: string[], downloadExtension: () => Promise, force = false ) { - const setupFunctions = await getUniqueWalletSetupFunctions( - walletSetupDirPath - ); + const setupFunctions = await getUniqueWalletSetupFunctions(walletSetupDirPath) - const cacheCreationPromises = await triggerCacheCreation( - setupFunctions, - functionStrings, - downloadExtension, - force - ); + const cacheCreationPromises = await triggerCacheCreation(setupFunctions, hashes, downloadExtension, force) if (cacheCreationPromises.length === 0) { - console.log("No new setup functions to cache. Exiting..."); - return; + 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); + await Promise.all(cacheCreationPromises) - console.log("All wallet setup functions are now cached!"); + console.log('All wallet setup functions are now cached!') } diff --git a/packages/cache/src/defineWalletSetup.ts b/packages/cache/src/defineWalletSetup.ts index 961bd6d1f..ef65a62c9 100644 --- a/packages/cache/src/defineWalletSetup.ts +++ b/packages/cache/src/defineWalletSetup.ts @@ -1,6 +1,6 @@ import type { BrowserContext, Page } from 'playwright-core' +import buildWalletSetupFunction from './utils/buildWalletSetupFunction' import { getWalletSetupFuncHash } from './utils/getWalletSetupFuncHash' -import buildWalletSetupFunction from './utils/buildWalletSetupFunction'; // TODO: Should we export this type in the `release` package? export type WalletSetupFunction = (context: BrowserContext, walletPage: Page) => Promise diff --git a/packages/cache/src/utils/buildWalletSetupFunction.ts b/packages/cache/src/utils/buildWalletSetupFunction.ts index 23c837b16..a577ee2f8 100644 --- a/packages/cache/src/utils/buildWalletSetupFunction.ts +++ b/packages/cache/src/utils/buildWalletSetupFunction.ts @@ -1,19 +1,15 @@ -import { transformSync } from "esbuild"; -import { FIXES_BANNER } from "../cli/compilationFixes"; +import { transformSync } from 'esbuild' -export default function buildWalletSetupFunction( - walletSetupFunctionString: string -) { +export default function buildWalletSetupFunction(walletSetupFunctionString: string) { const { code } = transformSync(walletSetupFunctionString, { - format: "esm", + format: 'esm', minifyWhitespace: true, - target: "es2022", - drop: ["console", "debugger"], - loader: "ts", - logLevel: "silent", - platform: "node", - banner: FIXES_BANNER, - }); + target: 'es2022', + drop: ['console', 'debugger'], + loader: 'ts', + logLevel: 'silent', + platform: 'node' + }) - return code; + return code } diff --git a/packages/cache/src/utils/extractWalletSetupFunction.ts b/packages/cache/src/utils/extractWalletSetupFunction.ts new file mode 100644 index 000000000..a1670e013 --- /dev/null +++ b/packages/cache/src/utils/extractWalletSetupFunction.ts @@ -0,0 +1,11 @@ +export function extractWalletSetupFunction(sourceCode: string): string { + const match = sourceCode.match(/defineWalletSetup\s*\([^,]*,\s*(async\s*\([^)]*\)\s*=>\s*{[\s\S]*?})\s*\)/) + + if (!match || !match[1]) { + console.log('Failed to extract defineWalletSetup callback from:', sourceCode) + throw new Error('Could not find defineWalletSetup callback') + } + + // Return just the callback function (second parameter) + return match[1] +} diff --git a/packages/cache/src/utils/getWalletSetupFuncHash.ts b/packages/cache/src/utils/getWalletSetupFuncHash.ts index ee295f83e..27038c2b8 100644 --- a/packages/cache/src/utils/getWalletSetupFuncHash.ts +++ b/packages/cache/src/utils/getWalletSetupFuncHash.ts @@ -1,13 +1,12 @@ -import { createHash } from "node:crypto"; +import { createHash } from 'node:crypto' // Same length as the file part (first part before the `-`) of a Playwright Test ID. -export const WALLET_SETUP_FUNC_HASH_LENGTH = 10; +export const WALLET_SETUP_FUNC_HASH_LENGTH = 10 export function getWalletSetupFuncHash(walletSetupString: string) { - const hash = createHash("shake256", { - outputLength: WALLET_SETUP_FUNC_HASH_LENGTH, - }); + const hash = createHash('shake256', { + outputLength: WALLET_SETUP_FUNC_HASH_LENGTH + }) - return hash.update(walletSetupString).digest("hex"); + return hash.update(walletSetupString).digest('hex') } - diff --git a/packages/cache/src/utils/triggerCacheCreation.ts b/packages/cache/src/utils/triggerCacheCreation.ts index 30922086c..b2cbe5579 100644 --- a/packages/cache/src/utils/triggerCacheCreation.ts +++ b/packages/cache/src/utils/triggerCacheCreation.ts @@ -1,63 +1,48 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { ensureCacheDirExists } from "../ensureCacheDirExists"; -import { createCacheForWalletSetupFunction } from "./createCacheForWalletSetupFunction"; -import { isDirEmpty } from "./isDirEmpty"; -import { getWalletSetupFuncHash } from "./getWalletSetupFuncHash"; -import type { WalletSetupFunction } from "../defineWalletSetup"; +import path from 'node:path' +import fs from 'fs-extra' +import type { WalletSetupFunction } from '../defineWalletSetup' +import { ensureCacheDirExists } from '../ensureCacheDirExists' +import { createCacheForWalletSetupFunction } from './createCacheForWalletSetupFunction' +import { isDirEmpty } from './isDirEmpty' export async function triggerCacheCreation( - setupFunctions: Map< - string, - { fileName: string; setupFunction: WalletSetupFunction } - >, - functionStrings: string[], + setupFunctions: Map, + hashes: string[], downloadExtension: () => Promise, force: boolean ) { - const cacheDirPath = ensureCacheDirExists(); - const extensionPath = await downloadExtension(); + const cacheDirPath = ensureCacheDirExists() + const extensionPath = await downloadExtension() - return Array.from(setupFunctions).map( - async ([_, { fileName, setupFunction }], index) => { - // @ts-ignore - const funcHash = getWalletSetupFuncHash(functionStrings[index]); + return Array.from(setupFunctions).map(async ([_, { fileName, setupFunction }], index) => { + if (!hashes[index]) { + throw new Error(`No hash found for ${fileName}`) + } - const cachePath = path.join(cacheDirPath, funcHash); - const doesCacheDirExist = await fs.exists(cachePath); - const isCacheDirEmpty = await isDirEmpty(cachePath); + const funcHash = hashes[index] - if (doesCacheDirExist) { - if (isCacheDirEmpty) { - // In case of incorrect Playwright setup, the cache dir will be empty. For now, we're just deleting it. - await fs.remove(cachePath); - } else { - if (!force) { - console.log(`Cache already exists for ${funcHash}. Skipping...`); - return; - } + const cachePath = path.join(cacheDirPath, funcHash || 'unknown') + const doesCacheDirExist = await fs.exists(cachePath) + const isCacheDirEmpty = await isDirEmpty(cachePath) - console.log( - `Cache already exists for ${funcHash} but force flag is set. Deleting cache...` - ); - await fs.remove(cachePath); + if (doesCacheDirExist) { + if (isCacheDirEmpty) { + // In case of incorrect Playwright setup, the cache dir will be empty. For now, we're just deleting it. + await fs.remove(cachePath) + } else { + if (!force) { + console.log(`Cache already exists for ${funcHash}. Skipping...`) + return } - } - const fileNameWithCorrectExtension = fileName.replace( - /\.(ts|js|mjs)$/, - ".{ts,js,mjs}" - ); - console.log( - `Triggering cache creation for: ${funcHash} (${fileNameWithCorrectExtension})` - ); - // We're not inferring the return type here to make sure we don't accidentally await the function. - return createCacheForWalletSetupFunction( - extensionPath, - cachePath, - setupFunction, - fileNameWithCorrectExtension - ); + console.log(`Cache already exists for ${funcHash} but force flag is set. Deleting cache...`) + await fs.remove(cachePath) + } } - ); + + const fileNameWithCorrectExtension = fileName.replace(/\.(ts|js|mjs)$/, '.{ts,js,mjs}') + console.log(`Triggering cache creation for: ${funcHash} (${fileNameWithCorrectExtension})`) + // We're not inferring the return type here to make sure we don't accidentally await the function. + return createCacheForWalletSetupFunction(extensionPath, cachePath, setupFunction, fileNameWithCorrectExtension) + }) } diff --git a/packages/cache/test/createCache.test.ts b/packages/cache/test/createCache.test.ts index e777cacff..43d4b5015 100644 --- a/packages/cache/test/createCache.test.ts +++ b/packages/cache/test/createCache.test.ts @@ -1,114 +1,89 @@ -import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; - -import path from "node:path"; -import { createCache } from "../src/createCache"; -import type { WalletSetupFunction } from "../src/defineWalletSetup"; -import * as GetUniqueWalletSetupFunctions from "../src/utils/getUniqueWalletSetupFunctions"; -import * as TriggerCacheCreation from "../src/utils/triggerCacheCreation"; - -const ROOT_DIR = "/tmp"; - -const setupFunctions = new Map< - string, - { fileName: string; setupFunction: WalletSetupFunction } ->(); - -setupFunctions.set("hash1", { - fileName: path.join(ROOT_DIR, "hash1"), - setupFunction: vi.fn(), -}); -setupFunctions.set("hash2", { - fileName: path.join(ROOT_DIR, "hash2"), - setupFunction: vi.fn(), -}); -setupFunctions.set("hash3", { - fileName: path.join(ROOT_DIR, "hash3"), - setupFunction: vi.fn(), -}); - -const functionStrings = ["function1", "function2", "function3"]; - -vi.mock("../src/utils/getUniqueWalletSetupFunctions", async () => { +import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' + +import path from 'node:path' +import { createCache } from '../src/createCache' +import type { WalletSetupFunction } from '../src/defineWalletSetup' +import * as GetUniqueWalletSetupFunctions from '../src/utils/getUniqueWalletSetupFunctions' +import * as TriggerCacheCreation from '../src/utils/triggerCacheCreation' + +const ROOT_DIR = '/tmp' + +const setupFunctions = new Map() + +setupFunctions.set('hash1', { + fileName: path.join(ROOT_DIR, 'hash1'), + setupFunction: vi.fn() +}) +setupFunctions.set('hash2', { + fileName: path.join(ROOT_DIR, 'hash2'), + setupFunction: vi.fn() +}) +setupFunctions.set('hash3', { + fileName: path.join(ROOT_DIR, 'hash3'), + setupFunction: vi.fn() +}) + +const functionStrings = ['function1', 'function2', 'function3'] + +vi.mock('../src/utils/getUniqueWalletSetupFunctions', async () => { return { getUniqueWalletSetupFunctions: vi.fn().mockImplementation(async () => { - return setupFunctions; - }), - }; -}); + return setupFunctions + }) + } +}) -vi.mock("../src/utils/triggerCacheCreation", async () => { +vi.mock('../src/utils/triggerCacheCreation', async () => { return { triggerCacheCreation: vi.fn().mockImplementation(async () => { - return ["hash1", "hash2", "hash3"]; - }), - }; -}); + return ['hash1', 'hash2', 'hash3'] + }) + } +}) -describe("createCache", () => { - const consoleLogSpy = vi - .spyOn(console, "log") - .mockImplementation(() => undefined); +describe('createCache', () => { + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined) afterAll(() => { - vi.resetAllMocks(); - }); + vi.resetAllMocks() + }) afterEach(() => { - vi.clearAllMocks(); - }); - - it("calls getUniqueWalletSetupFunctions with correct arguments", async () => { - const getUniqueWalletSetupFunctionsSpy = vi.spyOn( - GetUniqueWalletSetupFunctions, - "getUniqueWalletSetupFunctions" - ); - - await createCache(ROOT_DIR, functionStrings, vi.fn(), false); - - expect(getUniqueWalletSetupFunctionsSpy).toHaveBeenCalledOnce(); - expect(getUniqueWalletSetupFunctionsSpy).toHaveBeenCalledWith(ROOT_DIR); - }); - - it("calls triggerCacheCreation with correct arguments", async () => { - const triggerCacheCreationSpy = vi.spyOn( - TriggerCacheCreation, - "triggerCacheCreation" - ); - - const downloadExtension = vi.fn(async () => - path.join(ROOT_DIR, "extension") - ); - await createCache(ROOT_DIR, functionStrings, downloadExtension, false); - - expect(triggerCacheCreationSpy).toHaveBeenCalledOnce(); - expect(triggerCacheCreationSpy).toHaveBeenCalledWith( - setupFunctions, - functionStrings, - downloadExtension, - false - ); - }); - - it("does nothing if no setup functions need caching", async () => { - vi.spyOn( - TriggerCacheCreation, - "triggerCacheCreation" - ).mockResolvedValueOnce([]); - - await createCache(ROOT_DIR, functionStrings, vi.fn(), false); - - expect(consoleLogSpy).toHaveBeenCalledOnce(); - expect(consoleLogSpy).toHaveBeenCalledWith( - "No new setup functions to cache. Exiting..." - ); - }); - - it("console.logs at the end", async () => { - await createCache(ROOT_DIR, functionStrings, vi.fn(), false); - - expect(consoleLogSpy).toHaveBeenCalledOnce(); - expect(consoleLogSpy).toHaveBeenCalledWith( - "All wallet setup functions are now cached!" - ); - }); -}); + vi.clearAllMocks() + }) + + it('calls getUniqueWalletSetupFunctions with correct arguments', async () => { + const getUniqueWalletSetupFunctionsSpy = vi.spyOn(GetUniqueWalletSetupFunctions, 'getUniqueWalletSetupFunctions') + + await createCache(ROOT_DIR, functionStrings, vi.fn(), false) + + expect(getUniqueWalletSetupFunctionsSpy).toHaveBeenCalledOnce() + expect(getUniqueWalletSetupFunctionsSpy).toHaveBeenCalledWith(ROOT_DIR) + }) + + it('calls triggerCacheCreation with correct arguments', async () => { + const triggerCacheCreationSpy = vi.spyOn(TriggerCacheCreation, 'triggerCacheCreation') + + const downloadExtension = vi.fn(async () => path.join(ROOT_DIR, 'extension')) + await createCache(ROOT_DIR, functionStrings, downloadExtension, false) + + expect(triggerCacheCreationSpy).toHaveBeenCalledOnce() + expect(triggerCacheCreationSpy).toHaveBeenCalledWith(setupFunctions, functionStrings, downloadExtension, false) + }) + + it('does nothing if no setup functions need caching', async () => { + vi.spyOn(TriggerCacheCreation, 'triggerCacheCreation').mockResolvedValueOnce([]) + + await createCache(ROOT_DIR, functionStrings, vi.fn(), false) + + expect(consoleLogSpy).toHaveBeenCalledOnce() + expect(consoleLogSpy).toHaveBeenCalledWith('No new setup functions to cache. Exiting...') + }) + + it('console.logs at the end', async () => { + await createCache(ROOT_DIR, functionStrings, vi.fn(), false) + + expect(consoleLogSpy).toHaveBeenCalledOnce() + expect(consoleLogSpy).toHaveBeenCalledWith('All wallet setup functions are now cached!') + }) +}) diff --git a/packages/cache/test/utils/getWalletSetupFuncHash.test.ts b/packages/cache/test/utils/getWalletSetupFuncHash.test.ts index 4e1ea8ed5..df051ef69 100644 --- a/packages/cache/test/utils/getWalletSetupFuncHash.test.ts +++ b/packages/cache/test/utils/getWalletSetupFuncHash.test.ts @@ -10,7 +10,9 @@ describe('getWalletSetupFuncHash', () => { // biome-ignore lint/suspicious/noExplicitAny: any type here is intentional } as any - expect(() => getWalletSetupFuncHash(incorrectFunctionObject)).toThrowError('The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object') + expect(() => getWalletSetupFuncHash(incorrectFunctionObject)).toThrowError( + 'The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object' + ) }) it('returns hash', async () => { diff --git a/packages/cache/test/utils/triggerCacheCreation.test.ts b/packages/cache/test/utils/triggerCacheCreation.test.ts index acbef2bde..02b544950 100644 --- a/packages/cache/test/utils/triggerCacheCreation.test.ts +++ b/packages/cache/test/utils/triggerCacheCreation.test.ts @@ -1,84 +1,73 @@ -import { fs, vol } from "memfs"; -import { - afterAll, - afterEach, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; - -import path from "node:path"; -import fsExtra from "fs-extra"; -import type { WalletSetupFunction } from "../../src"; -import * as EnsureCacheDirExists from "../../src/ensureCacheDirExists"; -import * as CreateCacheForWalletSetupFunction from "../../src/utils/createCacheForWalletSetupFunction"; -import { triggerCacheCreation } from "../../src/utils/triggerCacheCreation"; - -const ROOT_DIR = "/tmp"; -const EXTENSION_PATH = path.join(ROOT_DIR, "extension"); - -vi.mock("fs-extra", async () => { +import { fs, vol } from 'memfs' +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import path from 'node:path' +import fsExtra from 'fs-extra' +import type { WalletSetupFunction } from '../../src' +import * as EnsureCacheDirExists from '../../src/ensureCacheDirExists' +import * as CreateCacheForWalletSetupFunction from '../../src/utils/createCacheForWalletSetupFunction' +import { triggerCacheCreation } from '../../src/utils/triggerCacheCreation' + +const ROOT_DIR = '/tmp' +const EXTENSION_PATH = path.join(ROOT_DIR, 'extension') + +vi.mock('fs-extra', async () => { return { default: { ...fs.promises, exists: async (path: string) => { - return vol.existsSync(path); + return vol.existsSync(path) }, remove: async (path: string) => { - vol.rmdirSync(path); - }, - }, - }; -}); + vol.rmdirSync(path) + } + } + } +}) -vi.mock("../../src/ensureCacheDirExists", async () => { +vi.mock('../../src/ensureCacheDirExists', async () => { return { - ensureCacheDirExists: vi.fn(() => "/tmp"), - }; -}); + ensureCacheDirExists: vi.fn(() => '/tmp') + } +}) -vi.mock("../../src/utils/createCacheForWalletSetupFunction", async () => { +vi.mock('../../src/utils/createCacheForWalletSetupFunction', async () => { return { createCacheForWalletSetupFunction: vi.fn(async () => { - return "Resolved Quack! πŸ¦†"; - }), - }; -}); + return 'Resolved Quack! πŸ¦†' + }) + } +}) // We're not adding a test for code that uses `isDirEmpty` because soon it will be removed. -vi.mock("../../src/utils/isDirEmpty", async () => { +vi.mock('../../src/utils/isDirEmpty', async () => { return { isDirEmpty: vi.fn(async () => { - return false; - }), - }; -}); + return false + }) + } +}) -describe("triggerCacheCreation", () => { +describe('triggerCacheCreation', () => { const createCacheForWalletSetupFunctionSpy = vi.spyOn( CreateCacheForWalletSetupFunction, - "createCacheForWalletSetupFunction" - ); + 'createCacheForWalletSetupFunction' + ) - const downloadExtension = vi.fn(async () => EXTENSION_PATH); - const testSetupFunction = vi.fn(); + const downloadExtension = vi.fn(async () => EXTENSION_PATH) + const testSetupFunction = vi.fn() function prepareSetupFunctions(hashes: string[]) { - const setupFunctions = new Map< - string, - { fileName: string; setupFunction: WalletSetupFunction } - >(); + const setupFunctions = new Map() for (const hash of hashes) { setupFunctions.set(hash, { fileName: path.join(ROOT_DIR, `${hash}.ts`), - setupFunction: testSetupFunction, - }); + setupFunction: testSetupFunction + }) } - return setupFunctions; + return setupFunctions } function expectCreateCacheForWalletSetupFunction( @@ -86,9 +75,7 @@ describe("triggerCacheCreation", () => { setupFunctions: ReturnType, hash: string ) { - const fileNameWithCorrectExtension = setupFunctions - .get(hash) - ?.fileName?.replace(/\.(ts|js|mjs)$/, ".{ts,js,mjs}"); + const fileNameWithCorrectExtension = setupFunctions.get(hash)?.fileName?.replace(/\.(ts|js|mjs)$/, '.{ts,js,mjs}') expect(createCacheForWalletSetupFunctionSpy).toHaveBeenNthCalledWith( n, @@ -96,147 +83,119 @@ describe("triggerCacheCreation", () => { path.join(ROOT_DIR, hash), testSetupFunction, fileNameWithCorrectExtension - ); + ) } afterAll(() => { - vi.resetAllMocks(); - }); + vi.resetAllMocks() + }) beforeEach(() => { - vol.mkdirSync(ROOT_DIR); - }); + vol.mkdirSync(ROOT_DIR) + }) afterEach(() => { - vi.clearAllMocks(); - vol.reset(); // Clear the in-memory file system after each test - }); - - const setupFunctions = prepareSetupFunctions(["hash1", "hash2"]); - const functionStrings = ["function1", "function2"]; - - it("calls ensureCacheDirExists", async () => { - const ensureCacheDirExistsSpy = vi.spyOn( - EnsureCacheDirExists, - "ensureCacheDirExists" - ); - - await triggerCacheCreation( - setupFunctions, - functionStrings, - downloadExtension, - false - ); - - expect(ensureCacheDirExistsSpy).toHaveBeenCalledOnce(); - }); - - it("calls passed downloadExtension function", async () => { - const setupFunctions = prepareSetupFunctions(["hash1", "hash2"]); - await triggerCacheCreation(setupFunctions, [], downloadExtension, false); - - expect(downloadExtension).toHaveBeenCalledOnce(); - }); - - it.skip("calls createCacheForWalletSetupFunction with correct arguments", async () => { - await triggerCacheCreation( - setupFunctions, - functionStrings, - downloadExtension, - false - ); - - expect(createCacheForWalletSetupFunctionSpy).toHaveBeenCalledTimes(2); - expectCreateCacheForWalletSetupFunction(1, setupFunctions, "hash1"); - expectCreateCacheForWalletSetupFunction(2, setupFunctions, "hash2"); - }); - - it.skip("checks if cache already exists for each entry", async () => { - const existsSpy = vi.spyOn(fsExtra, "exists"); - await triggerCacheCreation( - setupFunctions, - functionStrings, - downloadExtension, - false - ); - - expect(existsSpy).toHaveBeenCalledTimes(2); - expect(existsSpy).toHaveBeenNthCalledWith(1, path.join(ROOT_DIR, "hash1")); - expect(existsSpy).toHaveBeenNthCalledWith(2, path.join(ROOT_DIR, "hash2")); - }); - - it("returns an array of createCacheForWalletSetupFunction promises", async () => { - const promises = await triggerCacheCreation( - setupFunctions, - functionStrings, - downloadExtension, - false - ); + vi.clearAllMocks() + vol.reset() // Clear the in-memory file system after each test + }) + + const setupFunctions = prepareSetupFunctions(['hash1', 'hash2']) + const functionStrings = ['function1', 'function2'] + + it('calls ensureCacheDirExists', async () => { + const ensureCacheDirExistsSpy = vi.spyOn(EnsureCacheDirExists, 'ensureCacheDirExists') + + await triggerCacheCreation(setupFunctions, functionStrings, downloadExtension, false) + + expect(ensureCacheDirExistsSpy).toHaveBeenCalledOnce() + }) + + it('calls passed downloadExtension function', async () => { + const setupFunctions = prepareSetupFunctions(['hash1', 'hash2']) + await triggerCacheCreation(setupFunctions, [], downloadExtension, false) + + expect(downloadExtension).toHaveBeenCalledOnce() + }) + + it.skip('calls createCacheForWalletSetupFunction with correct arguments', async () => { + await triggerCacheCreation(setupFunctions, functionStrings, downloadExtension, false) + + expect(createCacheForWalletSetupFunctionSpy).toHaveBeenCalledTimes(2) + expectCreateCacheForWalletSetupFunction(1, setupFunctions, 'hash1') + expectCreateCacheForWalletSetupFunction(2, setupFunctions, 'hash2') + }) + + it.skip('checks if cache already exists for each entry', async () => { + const existsSpy = vi.spyOn(fsExtra, 'exists') + await triggerCacheCreation(setupFunctions, functionStrings, downloadExtension, false) + + expect(existsSpy).toHaveBeenCalledTimes(2) + expect(existsSpy).toHaveBeenNthCalledWith(1, path.join(ROOT_DIR, 'hash1')) + expect(existsSpy).toHaveBeenNthCalledWith(2, path.join(ROOT_DIR, 'hash2')) + }) + + it('returns an array of createCacheForWalletSetupFunction promises', async () => { + const promises = await triggerCacheCreation(setupFunctions, functionStrings, downloadExtension, false) console.log(promises) - expect(promises).toHaveLength(2); - expect(promises[0]).toBeInstanceOf(Promise); - expect(promises[1]).toBeInstanceOf(Promise); - }); + expect(promises).toHaveLength(2) + expect(promises[0]).toBeInstanceOf(Promise) + expect(promises[1]).toBeInstanceOf(Promise) + }) - describe("when force flag is false", () => { - it.skip("ignores setup function for which cache already exists", async () => { - const setupFunctions = prepareSetupFunctions(["hash1", "hash2", "hash3"]); + describe('when force flag is false', () => { + it.skip('ignores setup function for which cache already exists', async () => { + const setupFunctions = prepareSetupFunctions(['hash1', 'hash2', 'hash3']) // Creating cache for 2nd setup function. - fs.mkdirSync(path.join(ROOT_DIR, "hash2")); + fs.mkdirSync(path.join(ROOT_DIR, 'hash2')) const promises = await triggerCacheCreation( setupFunctions, - [...functionStrings, "function3"], + [...functionStrings, 'function3'], downloadExtension, false - ); + ) - expect(promises).toHaveLength(2); - expect(createCacheForWalletSetupFunctionSpy).toHaveBeenCalledTimes(2); - expectCreateCacheForWalletSetupFunction(1, setupFunctions, "hash1"); - expectCreateCacheForWalletSetupFunction(2, setupFunctions, "hash3"); - }); - }); + expect(promises).toHaveLength(2) + expect(createCacheForWalletSetupFunctionSpy).toHaveBeenCalledTimes(2) + expectCreateCacheForWalletSetupFunction(1, setupFunctions, 'hash1') + expectCreateCacheForWalletSetupFunction(2, setupFunctions, 'hash3') + }) + }) - describe("when force flag is true", () => { - it.skip("removes cache if it already exists for given setup function", async () => { - const setupFunctions = prepareSetupFunctions(["hash1", "hash2", "hash3"]); + describe('when force flag is true', () => { + it.skip('removes cache if it already exists for given setup function', async () => { + const setupFunctions = prepareSetupFunctions(['hash1', 'hash2', 'hash3']) // Creating cache for 2nd setup function. - const pathToExistingCache = path.join(ROOT_DIR, "hash2"); - fs.mkdirSync(pathToExistingCache); + const pathToExistingCache = path.join(ROOT_DIR, 'hash2') + fs.mkdirSync(pathToExistingCache) - await triggerCacheCreation( - setupFunctions, - [...functionStrings, "function3"], - downloadExtension, - true - ); + await triggerCacheCreation(setupFunctions, [...functionStrings, 'function3'], downloadExtension, true) - expect(fs.existsSync(pathToExistingCache)).toBe(false); - }); + expect(fs.existsSync(pathToExistingCache)).toBe(false) + }) - it.skip("calls createCacheForWalletSetupFunction for setup functions that were previously cached", async () => { - const setupFunctions = prepareSetupFunctions(["hash1", "hash2", "hash3"]); + it.skip('calls createCacheForWalletSetupFunction for setup functions that were previously cached', async () => { + const setupFunctions = prepareSetupFunctions(['hash1', 'hash2', 'hash3']) // Creating cache for 2nd setup function. - fs.mkdirSync(path.join(ROOT_DIR, "hash2")); + fs.mkdirSync(path.join(ROOT_DIR, 'hash2')) const promises = await triggerCacheCreation( setupFunctions, - [...functionStrings, "function3"], + [...functionStrings, 'function3'], downloadExtension, true - ); - - expect(promises).toHaveLength(3); - expect(createCacheForWalletSetupFunctionSpy).toHaveBeenCalledTimes(3); - expectCreateCacheForWalletSetupFunction(1, setupFunctions, "hash1"); - expectCreateCacheForWalletSetupFunction(2, setupFunctions, "hash2"); - expectCreateCacheForWalletSetupFunction(3, setupFunctions, "hash3"); - }); - }); -}); + ) + + expect(promises).toHaveLength(3) + expect(createCacheForWalletSetupFunctionSpy).toHaveBeenCalledTimes(3) + expectCreateCacheForWalletSetupFunction(1, setupFunctions, 'hash1') + expectCreateCacheForWalletSetupFunction(2, setupFunctions, 'hash2') + expectCreateCacheForWalletSetupFunction(3, setupFunctions, 'hash3') + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 515670e2d..158e0a0c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,11 +237,11 @@ importers: specifier: workspace:* version: link:../wallets/ethereum-wallet-mock '@synthetixio/synpress-cache': - specifier: 0.0.4 - version: 0.0.4(playwright-core@1.48.2)(postcss@8.4.41)(typescript@5.3.3) + specifier: workspace:* + version: link:../packages/cache '@synthetixio/synpress-core': - specifier: 0.0.4 - version: 0.0.4(@playwright/test@1.48.2) + specifier: workspace:* + version: link:../packages/core '@synthetixio/synpress-metamask': specifier: workspace:* version: link:../wallets/metamask @@ -1528,12 +1528,6 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} - '@synthetixio/synpress-cache@0.0.4': - resolution: {integrity: sha512-G1qF0XgMyRDjQjTYFniuFvPaY6+AuSvcEi2bkdcMmKCD0NK+Zw/PLEk0wRy1r1LN1F4HLdXrjittiKU6mh5PLA==} - hasBin: true - peerDependencies: - playwright-core: 1.48.2 - '@synthetixio/synpress-core@0.0.4': resolution: {integrity: sha512-3zO8PGigi9sjdG359pZ0EB3BiBPweZVOyiobCopNDykKI/EoWNYY05bOttOG+/Wg0HynMzGTPTRwpJ0+a+k2Sg==} peerDependencies: @@ -6609,29 +6603,6 @@ snapshots: '@sindresorhus/is@5.6.0': {} - '@synthetixio/synpress-cache@0.0.4(playwright-core@1.48.2)(postcss@8.4.41)(typescript@5.3.3)': - dependencies: - axios: 1.6.7 - chalk: 5.3.0 - commander: 12.0.0 - esbuild: 0.20.0 - fs-extra: 11.2.0 - glob: 10.3.10 - gradient-string: 2.0.2 - playwright-core: 1.48.2 - progress: 2.0.3 - tsup: 8.0.2(postcss@8.4.41)(typescript@5.3.3) - unzipper: 0.10.14 - zod: 3.22.4 - transitivePeerDependencies: - - '@microsoft/api-extractor' - - '@swc/core' - - debug - - postcss - - supports-color - - ts-node - - typescript - '@synthetixio/synpress-core@0.0.4(@playwright/test@1.48.2)': dependencies: '@playwright/test': 1.48.2 diff --git a/release/package.json b/release/package.json index 5ea548b9f..0518185e8 100644 --- a/release/package.json +++ b/release/package.json @@ -40,8 +40,8 @@ }, "dependencies": { "@synthetixio/ethereum-wallet-mock": "workspace:*", - "@synthetixio/synpress-cache": "0.0.4", - "@synthetixio/synpress-core": "0.0.4", + "@synthetixio/synpress-cache": "workspace:*", + "@synthetixio/synpress-core": "workspace:*", "@synthetixio/synpress-metamask": "workspace:*" }, "devDependencies": { diff --git a/wallets/metamask/src/playwright/fixtures/metaMaskFixtures.ts b/wallets/metamask/src/playwright/fixtures/metaMaskFixtures.ts index 0d3a9cad1..03fdaf6c9 100644 --- a/wallets/metamask/src/playwright/fixtures/metaMaskFixtures.ts +++ b/wallets/metamask/src/playwright/fixtures/metaMaskFixtures.ts @@ -1,186 +1,171 @@ -import path from "node:path"; -import { type Page, chromium } from "@playwright/test"; -import { test as base } from "@playwright/test"; +import path from 'node:path' +import { type Page, chromium } from '@playwright/test' +import { test as base } from '@playwright/test' import { CACHE_DIR_NAME, createTempContextDir, defineWalletSetup, - removeTempContextDir, -} from "@synthetixio/synpress-cache"; -import { type Anvil, type CreateAnvilOptions, createPool } from "@viem/anvil"; -import fs from "fs-extra"; -import { prepareExtension } from "../../prepareExtension"; -import { MetaMask } from "../MetaMask"; -import { getExtensionId, unlockForFixture } from "../fixture-actions"; -import { persistLocalStorage } from "../fixture-actions/persistLocalStorage"; -import { waitForMetaMaskWindowToBeStable } from "../utils/waitFor"; + removeTempContextDir +} from '@synthetixio/synpress-cache' +import { type Anvil, type CreateAnvilOptions, createPool } from '@viem/anvil' +import fs from 'fs-extra' +import { prepareExtension } from '../../prepareExtension' +import { MetaMask } from '../MetaMask' +import { getExtensionId, unlockForFixture } from '../fixture-actions' +import { persistLocalStorage } from '../fixture-actions/persistLocalStorage' +import { waitForMetaMaskWindowToBeStable } from '../utils/waitFor' type MetaMaskFixtures = { - _contextPath: string; - metamask: MetaMask; - extensionId: string; - metamaskPage: Page; - createAnvilNode: ( - options?: CreateAnvilOptions - ) => Promise<{ anvil: Anvil; rpcUrl: string; chainId: number }>; - connectToAnvil: () => Promise; - deployToken: () => Promise; - deployAndMintERC1155: () => Promise; -}; + _contextPath: string + metamask: MetaMask + extensionId: string + metamaskPage: Page + createAnvilNode: (options?: CreateAnvilOptions) => Promise<{ anvil: Anvil; rpcUrl: string; chainId: number }> + connectToAnvil: () => Promise + deployToken: () => Promise + deployAndMintERC1155: () => Promise +} // If setup metamaskPage in a fixture, browser does not handle it properly (even if ethereum.isConnected() is true, it's not reflected on the page). -let _metamaskPage: Page; +let _metamaskPage: Page -export const metaMaskFixtures = ( - walletSetup: ReturnType, - slowMo = 0 -) => { +export const metaMaskFixtures = (walletSetup: ReturnType, slowMo = 0) => { return base.extend({ _contextPath: async ({ browserName }, use, testInfo) => { - const contextPath = await createTempContextDir( - browserName, - testInfo.testId - ); + const contextPath = await createTempContextDir(browserName, testInfo.testId) - await use(contextPath); + await use(contextPath) - const error = await removeTempContextDir(contextPath); + const error = await removeTempContextDir(contextPath) if (error) { - console.error(error); + console.error(error) } }, context: async ({ context: currentContext, _contextPath }, use) => { - const { walletPassword, hash } = await walletSetup; + const { walletPassword, hash } = await walletSetup - const cacheDirPath = path.join(process.cwd(), CACHE_DIR_NAME, hash); + const cacheDirPath = path.join(process.cwd(), CACHE_DIR_NAME, hash) if (!(await fs.exists(cacheDirPath))) { - throw new Error(`Cache for ${hash} does not exist. Create it first!`); + throw new Error(`Cache for ${hash} does not exist. Create it first!`) } // Copying the cache to the temporary context directory.ΕΎ - await fs.copy(cacheDirPath, _contextPath); + 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}`]; + const browserArgs = [`--disable-extensions-except=${metamaskPath}`] if (process.env.HEADLESS) { - browserArgs.push("--headless=new"); + browserArgs.push('--headless=new') if (slowMo > 0) { - console.warn( - "[WARNING] Slow motion makes no sense in headless mode. It will be ignored!" - ); + 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, - }); + slowMo: process.env.HEADLESS ? 0 : slowMo + }) - const { cookies, origins } = await currentContext.storageState(); + const { cookies, origins } = await currentContext.storageState() if (cookies) { - await context.addCookies(cookies); + await context.addCookies(cookies) } if (origins && origins.length > 0) { - await persistLocalStorage(origins, context); + await persistLocalStorage(origins, context) } // TODO: This should be stored in a store to speed up the tests. - const extensionId = await getExtensionId(context, "MetaMask"); + const extensionId = await getExtensionId(context, 'MetaMask') // TODO: Not sure if this is the best approach. Time will tell. // We're utilizing the blank page here. - _metamaskPage = context.pages()[0] as Page; + _metamaskPage = context.pages()[0] as Page - await _metamaskPage.goto(`chrome-extension://${extensionId}/home.html`); - await waitForMetaMaskWindowToBeStable(_metamaskPage); - await unlockForFixture(_metamaskPage, walletPassword); + await _metamaskPage.goto(`chrome-extension://${extensionId}/home.html`) + await waitForMetaMaskWindowToBeStable(_metamaskPage) + await unlockForFixture(_metamaskPage, walletPassword) - await use(context); + await use(context) - await context.close(); + await context.close() }, metamaskPage: async ({ context: _ }, use) => { - await use(_metamaskPage); + await use(_metamaskPage) }, extensionId: async ({ context }, use) => { - const extensionId = await getExtensionId(context, "MetaMask"); + const extensionId = await getExtensionId(context, 'MetaMask') - await use(extensionId); + await use(extensionId) }, metamask: async ({ context, extensionId }, use) => { - const { walletPassword } = await walletSetup; + const { walletPassword } = await walletSetup - const metamask = new MetaMask( - context, - _metamaskPage, - walletPassword, - extensionId - ); + const metamask = new MetaMask(context, _metamaskPage, walletPassword, extensionId) - await use(metamask); + await use(metamask) }, page: async ({ page }, use) => { - await page.goto("/"); + await page.goto('/') - await use(page); + await use(page) }, createAnvilNode: async ({ context: _ }, use) => { - const pool = createPool(); + const pool = createPool() await use(async (options?: CreateAnvilOptions) => { - const nodeId = Array.from(pool.instances()).length; - const anvil = await pool.start(nodeId, options); + const nodeId = Array.from(pool.instances()).length + const anvil = await pool.start(nodeId, options) - const rpcUrl = `http://${anvil.host}:${anvil.port}`; + const rpcUrl = `http://${anvil.host}:${anvil.port}` - const DEFAULT_ANVIL_CHAIN_ID = 31337; - const chainId = options?.chainId ?? DEFAULT_ANVIL_CHAIN_ID; + const DEFAULT_ANVIL_CHAIN_ID = 31337 + const chainId = options?.chainId ?? DEFAULT_ANVIL_CHAIN_ID - return { anvil, rpcUrl, chainId }; - }); + return { anvil, rpcUrl, chainId } + }) - await pool.empty(); + await pool.empty() }, connectToAnvil: async ({ metamask, createAnvilNode }, use) => { await use(async () => { const { rpcUrl, chainId } = await createAnvilNode({ - chainId: 1338, - }); + chainId: 1338 + }) await metamask.addNetwork({ - name: "Anvil", + name: 'Anvil', rpcUrl, chainId, - symbol: "ETH", - blockExplorerUrl: "https://etherscan.io/", - }); - }); + symbol: 'ETH', + blockExplorerUrl: 'https://etherscan.io/' + }) + }) }, deployToken: async ({ page, metamask, connectToAnvil }, use) => { await use(async () => { - await connectToAnvil(); + await connectToAnvil() - await page.locator("#createToken").click(); + await page.locator('#createToken').click() - await metamask.confirmTransaction(); - }); + await metamask.confirmTransaction() + }) }, deployAndMintERC1155: async ({ page, metamask, connectToAnvil }, use) => { await use(async () => { - await connectToAnvil(); + await connectToAnvil() - await page.locator("#deployERC1155Button").click(); - await metamask.confirmTransaction(); + await page.locator('#deployERC1155Button').click() + await metamask.confirmTransaction() - await page.locator("#batchMintButton").click(); - await metamask.confirmTransactionAndWaitForMining(); - }); - }, - }); -}; + await page.locator('#batchMintButton').click() + await metamask.confirmTransactionAndWaitForMining() + }) + } + }) +}