Skip to content

Commit

Permalink
📦️ feat(playwright-utils): Add package with the main fixture
Browse files Browse the repository at this point in the history
  • Loading branch information
duckception committed Oct 26, 2023
1 parent eefab9d commit cc7e6e1
Show file tree
Hide file tree
Showing 14 changed files with 703 additions and 6 deletions.
10 changes: 10 additions & 0 deletions packages/playwright-utils/environment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
CI: boolean
HEADLESS: boolean
}
}
}

export {}
55 changes: 55 additions & 0 deletions packages/playwright-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "playwright-utils",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/index.js"
}
}
},
"main": "./dist/index.js",
"files": [
"dist",
"src",
"types"
],
"scripts": {
"build": "pnpm run clean && pnpm run build:dist && pnpm run build:types",
"build:cache": "tsx src/buildCache.ts",
"build:cache:force": "tsx src/buildCache.ts --force",
"build:cache:force:headless": "HEADLESS=true tsx src/buildCache.ts --force",
"build:cache:headless": "HEADLESS=true tsx src/buildCache.ts",
"build:dist": "tsup --tsconfig tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly --project tsconfig.build.json",
"clean": "rimraf dist types",
"lint": "biome check . --apply",
"lint:check": "biome check . --verbose",
"lint:unsafe": "biome check . --apply-unsafe",
"local:test:e2e": "playwright test",
"local:test:e2e:headless": "HEADLESS=true playwright test",
"serve:test-dapp": "serve node_modules/@metamask/test-dapp/dist -p 9011",
"types:check": "tsc --noEmit"
},
"dependencies": {
"core": "workspace:*",
"fs-extra": "^11.1.1",
"metamask": "workspace:*"
},
"devDependencies": {
"@metamask/test-dapp": "^7.2.0",
"@types/fs-extra": "^11.0.2",
"@types/node": "^20.8.0",
"rimraf": "^5.0.1",
"serve": "^14.2.1",
"tsconfig": "workspace:*",
"tsup": "^7.2.0",
"tsx": "^3.12.10",
"typescript": "^5.2.2"
},
"peerDependencies": {
"@playwright/test": "1.39.0"
}
}
47 changes: 47 additions & 0 deletions packages/playwright-utils/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { defineConfig, devices } from '@playwright/test'

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
// Look for test files in the "test" directory, relative to this configuration file.
testDir: './test',

// Run all tests in parallel.
// TODO: Enable later once we have more tests.
fullyParallel: false,

// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!process.env.CI,

// Workers to run parallel tests with.
workers: 2,

// Concise 'dot' for CI, default 'html' when running locally.
// See https://playwright.dev/docs/test-reporters.
reporter: process.env.CI ? 'list' : 'html',

// Shared settings for all the projects below.
// See https://playwright.dev/docs/api/class-testoptions.
use: {
// Collect traces when running locally.
// See https://playwright.dev/docs/trace-viewer.
trace: process.env.CI ? 'off' : 'on',
baseURL: 'http://localhost:9011/'
},

// Configure projects for major browsers.
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],

// Web server configuration.
webServer: {
command: 'pnpm run serve:test-dapp',
url: 'http://127.0.0.1:9011',
reuseExistingServer: !process.env.CI
}
})
16 changes: 16 additions & 0 deletions packages/playwright-utils/src/buildCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import path from 'node:path'
import { createCache } from 'core'
import { prepareExtension } from 'metamask'
import { WALLET_SETUP_DIR_NAME } from './constants'

const isForce = process.argv.includes('--force')
const isHeadless = !!process.env.HEADLESS

const walletSetupDirPath = path.join(process.cwd(), 'test', WALLET_SETUP_DIR_NAME)
console.log({ walletSetupDirPath, isForce, isHeadless })

console.log('\t---- ⏳ Triggering the `createCache` function ----\n')

await createCache(walletSetupDirPath, prepareExtension, isForce)

console.log('\n\t---- ✅ `createCache` function finished ----\n')
1 change: 1 addition & 0 deletions packages/playwright-utils/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WALLET_SETUP_DIR_NAME = 'wallet-setup'
1 change: 1 addition & 0 deletions packages/playwright-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './testWithSynpress'
104 changes: 104 additions & 0 deletions packages/playwright-utils/src/testWithSynpress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import path from 'node:path'
import type {
Fixtures,
Page,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions
} from '@playwright/test'
import { chromium, test as base } from '@playwright/test'
import { defineWalletSetup, waitForExtensionOnLoadPage } from 'core'
import { createTempContextDir, removeTempContextDir } from 'core'
import { CACHE_DIR_NAME } from 'core'
import fs from 'fs-extra'
import { getExtensionId, prepareExtension } from 'metamask'
import { unlockMetaMask } from './utils/unlockMetaMask'

// Base types of the `test` fixture from Playwright.
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions

type PrivateSynpressFixtures = {
_contextPath: string
}

type PublicSynpressFixtures = {
extensionId: string
metamaskPage: Page
}

type SynpressFixtures = TestFixtures & PrivateSynpressFixtures & PublicSynpressFixtures

// TODO: Bad practice. Use a store!
let _extensionId: string
let _metamaskPage: Page

const synpressFixtures = (
walletSetup: ReturnType<typeof defineWalletSetup>,
slowMo = 0
): Fixtures<SynpressFixtures, WorkerFixtures> => ({
_contextPath: async ({ browserName }, use, testInfo) => {
const contextPath = await createTempContextDir(browserName, testInfo.testId)

await use(contextPath)

const error = await removeTempContextDir(contextPath)
if (error) {
console.error(error)
}
},
context: async ({ context: _, _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!`)
}

// Copying the cache to the temporary context directory.
await fs.copy(cacheDirPath, _contextPath)

const metamaskPath = await prepareExtension()

// We don't need the `--load-extension` args since it is already loaded in the cache.
const browserArgs = [`--disable-extensions-except=${metamaskPath}`]

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!')
}
}

const context = await chromium.launchPersistentContext(_contextPath, {
headless: false,
args: browserArgs,
slowMo: process.env.HEADLESS ? 0 : slowMo
})

_metamaskPage = await waitForExtensionOnLoadPage(context)

await unlockMetaMask(_metamaskPage, walletSetup.walletPassword)

await use(context)

await context.close()
},
extensionId: async ({ context }, use) => {
if (_extensionId) {
await use(_extensionId)
}

_extensionId = await getExtensionId(context, 'MetaMask')

await use(_extensionId)
},
metamaskPage: async ({ context: _ }, use) => {
await use(_metamaskPage)
}
})

export const testWithSynpress = (walletSetup: ReturnType<typeof defineWalletSetup>, slowMo?: number) => {
// biome-ignore lint/suspicious/noExplicitAny: satisfying TypeScript here - this type doesn't matter since we are overriding it
return base.extend<PublicSynpressFixtures>(synpressFixtures(walletSetup, slowMo) as any)
}
47 changes: 47 additions & 0 deletions packages/playwright-utils/src/utils/unlockMetaMask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Page } from '@playwright/test'
import { errors as playwrightErrors } from '@playwright/test'
import { CrashPageSelectors, HomePageSelectors, LoadingSelectors, unlock } from 'metamask'

export async function unlockMetaMask(page: Page, password: string) {
await unlock(page, password)

await page.locator(LoadingSelectors.spinner).waitFor({
state: 'hidden',
timeout: 3000 // TODO: Extract & Make this timeout configurable.
})

await retryIfMetaMaskCrashAfterUnlock(page)
}

async function retryIfMetaMaskCrashAfterUnlock(page: Page) {
const isHomePageVisible = await page.locator(HomePageSelectors.logo).isVisible()

if (!isHomePageVisible) {
if (await page.locator(CrashPageSelectors.header).isVisible()) {
const errors = await page.locator(CrashPageSelectors.errors).allTextContents()

console.warn(['[RetryIfMetaMaskCrashAfterUnlock] MetaMask crashed due to:', ...errors].join('\n'))

console.log('[RetryIfMetaMaskCrashAfterUnlock] Reloading page...')
await page.reload()

try {
await page.locator(HomePageSelectors.logo).waitFor({
state: 'visible',
timeout: 10_000 // TODO: Extract & Make this timeout configurable.
})
console.log('[RetryIfMetaMaskCrashAfterUnlock] Successfully restored MetaMask!')
} catch (e) {
if (e instanceof playwrightErrors.TimeoutError) {
throw new Error(
['[RetryIfMetaMaskCrashAfterUnlock] Reload did not help. Throwing with the crash cause:', ...errors].join(
'\n'
)
)
}

throw e
}
}
}
}
33 changes: 33 additions & 0 deletions packages/playwright-utils/test/testWithSynpress.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HomePageSelectors, connectToDapp, lock, unlock } from 'metamask'
import { testWithSynpress } from '../src'
import basicSetup from './wallet-setup/basic.setup'

const test = testWithSynpress(basicSetup, 1_000)

const { describe, expect } = test

describe.configure({
mode: 'parallel'
})

describe('testWithSynpress', () => {
test('should connect wallet to MetaMask E2E Test Dapp', async ({ context, page, extensionId }) => {
await page.goto('/')

await page.locator('#connectButton').click()

await connectToDapp(context, extensionId)

await expect(page.locator('#accounts')).toHaveText('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
})

test('should lock & unlock MetaMask', async ({ metamaskPage }) => {
await metamaskPage.bringToFront()

await lock(metamaskPage)

await unlock(metamaskPage, basicSetup.walletPassword)

await expect(metamaskPage.locator(HomePageSelectors.logo)).toBeVisible()
})
})
13 changes: 13 additions & 0 deletions packages/playwright-utils/test/wallet-setup/basic.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineWalletSetup } from 'core'
import { OnboardingPage } from 'metamask'

const SEED_PHRASE = 'test test test test test test test test test test test junk'
const PASSWORD = 'Tester@1234'

export default defineWalletSetup(PASSWORD, async (_, walletPage) => {
const onboardingPage = new OnboardingPage(walletPage)

await onboardingPage.importWallet(SEED_PHRASE, PASSWORD)

await walletPage.getByTestId('selected-account-click').click()
})
12 changes: 12 additions & 0 deletions packages/playwright-utils/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "tsconfig/base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "types",
"declaration": true,
"sourceMap": true,
"declarationMap": true
},
"include": ["src"],
"files": ["environment.d.ts"]
}
11 changes: 11 additions & 0 deletions packages/playwright-utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"rootDir": "."
},
"include": [
"src",
"test"
],
"files": ["environment.d.ts", "playwright.config.ts"]
}
10 changes: 10 additions & 0 deletions packages/playwright-utils/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'tsup'

export default defineConfig({
name: 'playwright-utils',
entry: ['src/index.ts'],
outDir: 'dist',
format: 'esm',
splitting: false,
sourcemap: true
})
Loading

0 comments on commit cc7e6e1

Please sign in to comment.