Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: Add phantom support #1167

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions wallets/phantom/environment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
CI: boolean
HEADLESS: boolean
}
}
}

declare global {
interface Window {
ethereum: import('ethers').Eip1193Provider
}

// biome-ignore lint/suspicious/noExplicitAny: Web3Mock is a mock object
const Web3Mock: any
}

export {}
54 changes: 54 additions & 0 deletions wallets/phantom/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@synthetixio/synpress-phantom",
"version": "0.0.1-alpha.0",
"type": "module",
"exports": {
"types": "./types/index.d.ts",
"default": "./dist/index.js"
},
"main": "./dist/index.js",
"types": "./types/index.d.ts",
"files": [
"dist",
"src",
"types"
],
"scripts": {
"build": "pnpm run clean && pnpm run build:dist && pnpm run build:types",
"build:cache": "synpress-cache test/wallet-setup --phantom",
"build:dist": "tsup --tsconfig tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly --project tsconfig.build.json",
"clean": "rimraf dist types",
"test:coverage": "vitest run --coverage",
"test:e2e:headful": "playwright test",
"test:e2e:headless": "HEADLESS=true playwright test",
"test:e2e:headless:ui": "HEADLESS=true playwright test --ui",
"test:watch": "vitest watch",
"types:check": "tsc --noEmit"
},
"dependencies": {
"@synthetixio/synpress-cache": "workspace:*",
"@synthetixio/synpress-core": "workspace:*",
"@viem/anvil": "0.0.7",
"clipboardy": "4.0.0",
"fs-extra": "11.2.0",
"node-fetch": "3.3.2",
"underscore": "1.13.6",
"zod": "3.22.4"
},
"devDependencies": {
"@synthetixio/synpress-tsconfig": "workspace:*",
"@types/fs-extra": "11.0.4",
"@types/node": "20.11.17",
"@types/underscore": "1.11.15",
"@vitest/coverage-v8": "1.2.2",
"cypress": "13.9.0",
"rimraf": "5.0.5",
"tsup": "8.0.2",
"typescript": "5.3.3",
"vitest": "1.2.2"
},
"peerDependencies": {
"@playwright/test": "1.44.0"
}
}
50 changes: 50 additions & 0 deletions wallets/phantom/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { defineConfig, devices } from '@playwright/test'

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

// We're increasing the timeout to 60 seconds to allow all traces to be recorded.
// Sometimes it threw an error saying that traces were not recorded in the 30 seconds timeout limit.
timeout: 60_000,

// Run all tests in parallel.
fullyParallel: true,

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

// Fail all remaining tests on CI after the first failure. We want to reduce the feedback loop on CI to minimum.
maxFailures: process.env.CI ? 1 : 0,

// Opt out of parallel tests on CI since it supports only 1 worker.
workers: process.env.CI ? 1 : undefined,

// Concise 'dot' for CI, default 'html' when running locally.
// See https://playwright.dev/docs/test-reporters.
reporter: process.env.CI
? [['html', { open: 'never', outputFolder: `playwright-report-${process.env.HEADLESS ? 'headless' : 'headful'}` }]]
: 'html',

// Shared settings for all the projects below.
// See https://playwright.dev/docs/api/class-testoptions.
use: {
// We are using locally deployed MetaMask Test Dapp.
baseURL: '/',

// Collect all traces on CI, and only traces for failed tests when running locally.
// See https://playwright.dev/docs/trace-viewer.
trace: process.env.CI ? 'on' : 'retain-on-failure'
},

// Configure projects for major browsers.
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
})
61 changes: 61 additions & 0 deletions wallets/phantom/src/PhantomWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { type BrowserContext, type Page } from '@playwright/test'
import { HomePage } from './pages/HomePage/page'
import { LockPage } from './pages/LockPage/page'
import { NotificationPage } from './pages/NotificationPage/page'
import { SettingsPage } from './pages/SettingsPage/page'
import { ConfirmationPage } from './pages/ConfirmationPage/page'

export class PhantomWallet {
readonly lockPage: LockPage
readonly homePage: HomePage
readonly notificationPage: NotificationPage
readonly settingsPage: SettingsPage
readonly confirmationPage: ConfirmationPage

constructor(
readonly page: Page,
readonly context?: BrowserContext,
readonly password?: string,
readonly extensionId?: string | undefined
) {
this.lockPage = new LockPage(page)
this.homePage = new HomePage(page)
this.notificationPage = new NotificationPage(page)
Fixed Show fixed Hide fixed
this.settingsPage = new SettingsPage(page)
this.confirmationPage = new ConfirmationPage(page)
}
/**
* Does initial setup for the wallet.
*
* @param playwrightInstance. The playwright instance to use.
* @param secretWords. The secret words or private key to import.
* @param password. The password to set.
*/
async importWallet({ secretWords, password }: { secretWords: string; password: string }) {
this.lockPage.importWallet(secretWords, password)
}

async createWallet(password: string) {
console.log(password)
}

async getWalletAddress(wallet: string) {
console.log(wallet)
}

async addNewTokensFound() {
console.log('')
}

async disconnectWalletFromDapps() {
console.log('')
}

async acceptAccess() {
console.log('')
}

async rejectAccess() {
console.log('')
}
}
105 changes: 105 additions & 0 deletions wallets/phantom/src/fixtures/phantomFixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 { prepareExtension } from '@synthetixio/synpress-cache'
import fs from 'fs-extra'
import { PhantomWallet } from '../PhantomWallet'
import { getExtensionId } from '../fixtureActions'
import { persistLocalStorage } from '../fixtureActions/persistLocalStorage'
// import unlockForFixtures from '../fixtureActions/unlockForFixtures'
import { PASSWORD } from '../utils'

type PhantomFixtures = {
_contextPath: string
phantom: PhantomWallet
phantomPage: Page
extensionId: string
}

let _phantomPage: Page

export const phantomFixtures = (walletSetup: ReturnType<typeof defineWalletSetup>, slowMo = 0) => {
return base.extend<PhantomFixtures>({
_contextPath: async ({ browserName }, use, testInfo) => {
const contextDir = await createTempContextDir(browserName, testInfo.testId)

await use(contextDir)

const error = await removeTempContextDir(contextDir)
if (error) {
console.log('contextDir', error)
}
},
context: async ({ context: currentContext, _contextPath }, use) => {
// @todo: This is some weird behaviour, if there is only 1 setup file the hash function will always produce a different hash than cache.
const cacheDirPath = path.join(process.cwd(), CACHE_DIR_NAME, walletSetup.hash)
if (!(await fs.exists(cacheDirPath))) {
throw new Error(`Cache for ${cacheDirPath} does not exist. Create it first!`)
}

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

const phantomPath = await prepareExtension('Phantom')
// We don't need the `--load-extension` arg since the extension is already loaded in the cache.
const browserArgs = [`--disable-extensions-except=${phantomPath}`, '--enable-features=SharedClipboardUI']
if (process.env.HEADLESS) {
browserArgs.push('--headless=new')

if (slowMo) {
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
})

const { cookies, origins } = await currentContext.storageState()

if (cookies) {
await context.addCookies(cookies)
}
if (origins && origins.length > 0) {
await persistLocalStorage(origins, context)
}

const extensionId = await getExtensionId(context, 'Phantom')

_phantomPage = context.pages()[0] as Page

await _phantomPage.goto(`chrome-extension://${extensionId}/popup.html`)

// await unlockForFixtures(_phantomPage, PASSWORD)

await use(context)
},
phantomPage: async ({ context: _ }, use) => {
await use(_phantomPage)
},
extensionId: async ({ context }, use) => {
const extensionId = await getExtensionId(context, 'Keplr')

await use(extensionId)
},
phantom: async ({ context, extensionId }, use) => {
const phantomWallet = new PhantomWallet(_phantomPage, context, PASSWORD, extensionId)

await use(phantomWallet)
},

page: async ({ page }, use) => {
await page.goto('')
await use(page)
}
})
}
3 changes: 3 additions & 0 deletions wallets/phantom/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './PhantomWallet'
export * from './utils'
export * from './fixtures/phantomFixtures'
File renamed without changes.
13 changes: 13 additions & 0 deletions wallets/phantom/src/pages/ConfirmationPage/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Page } from '@playwright/test'
import { confirmationPageElements } from './selectors'

export class ConfirmationPage {
static readonly selectors = confirmationPageElements
readonly selectors = confirmationPageElements

readonly page: Page

constructor(page: Page) {
this.page = page
}
}
12 changes: 12 additions & 0 deletions wallets/phantom/src/pages/ConfirmationPage/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const confirmationPage = '.confirmation-page';
const confirmationPageFooter = `${confirmationPage} .confirmation-footer`;
const footer = {
footer: confirmationPageFooter,
cancelButton: `${confirmationPageFooter} .btn-secondary`,
approveButton: `${confirmationPageFooter} .btn-primary`,
};

export const confirmationPageElements = {
confirmationPage,
footer,
};
Empty file.
13 changes: 13 additions & 0 deletions wallets/phantom/src/pages/HomePage/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Page } from '@playwright/test'
import { homePageElements } from './selectors'

export class HomePage {
static readonly selectors = homePageElements
readonly selectors = homePageElements

readonly page: Page

constructor(page: Page) {
this.page = page
}
}
Loading
Loading