@avalix/chroma 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/context-playwright/index.ts +139 -0
- package/src/context-playwright/types.ts +83 -0
- package/src/context-playwright/wallet-factory.ts +123 -0
- package/src/index.ts +2 -0
- package/src/utils/download-extension.ts +68 -0
- package/src/wallets/polkadot-js.ts +114 -0
- package/src/wallets/talisman.ts +159 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avalix/chroma",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.8",
|
|
5
5
|
"description": "End-to-end testing library for Polkadot wallet interactions",
|
|
6
6
|
"author": "Avalix Labs",
|
|
7
7
|
"license": "MIT",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
|
-
"scripts"
|
|
38
|
+
"scripts",
|
|
39
|
+
"src"
|
|
39
40
|
],
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "tsdown",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChromaTestOptions,
|
|
3
|
+
ConfiguredWallets,
|
|
4
|
+
ExtendedPage,
|
|
5
|
+
WalletConfig,
|
|
6
|
+
WalletFixtures,
|
|
7
|
+
WalletInstance,
|
|
8
|
+
Wallets,
|
|
9
|
+
WalletType,
|
|
10
|
+
WalletWorkerFixtures,
|
|
11
|
+
} from './types.js'
|
|
12
|
+
import { test as base, chromium } from '@playwright/test'
|
|
13
|
+
import { getPolkadotJSExtensionPath } from '../wallets/polkadot-js.js'
|
|
14
|
+
import { getTalismanExtensionPath } from '../wallets/talisman.js'
|
|
15
|
+
import { WALLET_TYPES } from './types.js'
|
|
16
|
+
import { createWalletInstance } from './wallet-factory.js'
|
|
17
|
+
|
|
18
|
+
// Helper function to get extension path for a wallet config
|
|
19
|
+
async function getExtensionPathForWallet(config: WalletConfig): Promise<string> {
|
|
20
|
+
const { type } = config
|
|
21
|
+
|
|
22
|
+
switch (type) {
|
|
23
|
+
case 'polkadot-js':
|
|
24
|
+
return await getPolkadotJSExtensionPath()
|
|
25
|
+
case 'talisman':
|
|
26
|
+
return await getTalismanExtensionPath()
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported wallet type: ${type}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create a test function with wallet configuration
|
|
33
|
+
// Supports single and multi-wallet modes
|
|
34
|
+
export function createWalletTest<const T extends readonly WalletConfig[]>(
|
|
35
|
+
options: ChromaTestOptions<T> = {} as ChromaTestOptions<T>,
|
|
36
|
+
) {
|
|
37
|
+
const { headless = false, slowMo = 150 } = options
|
|
38
|
+
|
|
39
|
+
// Default to polkadot-js if no wallets specified
|
|
40
|
+
const walletConfigs: readonly WalletConfig[] = options.wallets && options.wallets.length > 0
|
|
41
|
+
? options.wallets
|
|
42
|
+
: [{ type: 'polkadot-js' }]
|
|
43
|
+
|
|
44
|
+
const isMultiWallet = walletConfigs.length > 1
|
|
45
|
+
|
|
46
|
+
// Compute the expected wallets type
|
|
47
|
+
type ExpectedWallets = T extends readonly WalletConfig[] ? ConfiguredWallets<T> : Wallets
|
|
48
|
+
|
|
49
|
+
return base.extend<WalletFixtures<ExpectedWallets>, WalletWorkerFixtures>({
|
|
50
|
+
// Worker-scoped: Browser context with extension(s) (persists across all tests in worker)
|
|
51
|
+
// eslint-disable-next-line no-empty-pattern
|
|
52
|
+
walletContext: [async ({}, use) => {
|
|
53
|
+
// Get all extension paths
|
|
54
|
+
const extensionPaths = await Promise.all(
|
|
55
|
+
walletConfigs.map(config => getExtensionPathForWallet(config)),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Join paths with comma for Chrome args
|
|
59
|
+
const extensionPathsString = extensionPaths.join(',')
|
|
60
|
+
|
|
61
|
+
const context = await chromium.launchPersistentContext('', {
|
|
62
|
+
headless,
|
|
63
|
+
channel: 'chromium',
|
|
64
|
+
args: [
|
|
65
|
+
`--load-extension=${extensionPathsString}`,
|
|
66
|
+
`--disable-extensions-except=${extensionPathsString}`,
|
|
67
|
+
],
|
|
68
|
+
slowMo,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await use(context)
|
|
72
|
+
await context.close()
|
|
73
|
+
}, { scope: 'worker' }],
|
|
74
|
+
|
|
75
|
+
// Worker-scoped: Map of wallet type to extension ID
|
|
76
|
+
walletExtensionIds: [async ({ walletContext }, use) => {
|
|
77
|
+
const extensionIds = new Map<string, string>()
|
|
78
|
+
|
|
79
|
+
// Wait for all service workers to load
|
|
80
|
+
const serviceWorkers = walletContext.serviceWorkers()
|
|
81
|
+
if (serviceWorkers.length === 0) {
|
|
82
|
+
// Wait for at least one service worker
|
|
83
|
+
await walletContext.waitForEvent('serviceworker')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Give some time for all extensions to load
|
|
87
|
+
if (isMultiWallet) {
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get all service workers (one per extension)
|
|
92
|
+
const allServiceWorkers = walletContext.serviceWorkers()
|
|
93
|
+
|
|
94
|
+
// Map service workers to wallet types
|
|
95
|
+
// Note: The order should match the walletConfigs order
|
|
96
|
+
for (let i = 0; i < walletConfigs.length && i < allServiceWorkers.length; i++) {
|
|
97
|
+
const extensionId = allServiceWorkers[i].url().split('/')[2]
|
|
98
|
+
const walletType = walletConfigs[i].type
|
|
99
|
+
extensionIds.set(walletType, extensionId)
|
|
100
|
+
console.log(`✅ Loaded ${walletType} extension with ID: ${extensionId}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await use(extensionIds)
|
|
104
|
+
}, { scope: 'worker' }],
|
|
105
|
+
|
|
106
|
+
// Main page with extension context (uses worker-scoped context)
|
|
107
|
+
page: async ({ walletContext, walletExtensionIds }, use) => {
|
|
108
|
+
const page = walletContext.pages()[0] || await walletContext.newPage()
|
|
109
|
+
|
|
110
|
+
// Store context and extension IDs on page
|
|
111
|
+
const extendedPage = page as ExtendedPage
|
|
112
|
+
extendedPage.__extensionContext = walletContext
|
|
113
|
+
extendedPage.__walletExtensionIds = walletExtensionIds
|
|
114
|
+
|
|
115
|
+
await use(extendedPage)
|
|
116
|
+
// Note: Don't close the page or context here since they're worker-scoped
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Wallet instances for each configured wallet
|
|
120
|
+
wallets: async ({ walletContext, walletExtensionIds }, use) => {
|
|
121
|
+
const walletMap: Partial<ExpectedWallets> = {}
|
|
122
|
+
|
|
123
|
+
// Create wallet instance for each configured wallet
|
|
124
|
+
for (const [walletType, extensionId] of walletExtensionIds) {
|
|
125
|
+
if (WALLET_TYPES.includes(walletType as WalletType)) {
|
|
126
|
+
const instance = createWalletInstance(walletType, extensionId, walletContext);
|
|
127
|
+
(walletMap as Record<string, WalletInstance>)[walletType] = instance
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await use(walletMap as ExpectedWallets)
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Default test with Polkadot JS wallet (with persistent wallet support via worker-scoped fixtures)
|
|
137
|
+
export const test: ReturnType<typeof createWalletTest> = createWalletTest()
|
|
138
|
+
|
|
139
|
+
export { expect } from '@playwright/test'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// Wallet types - single source of truth
|
|
4
|
+
export type WalletType = 'polkadot-js' | 'talisman'
|
|
5
|
+
|
|
6
|
+
// Available wallet types as constant array
|
|
7
|
+
export const WALLET_TYPES: readonly WalletType[] = ['polkadot-js', 'talisman'] as const
|
|
8
|
+
|
|
9
|
+
// Wallet account configuration
|
|
10
|
+
export interface WalletAccount {
|
|
11
|
+
seed: string
|
|
12
|
+
name?: string
|
|
13
|
+
password?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Configuration for a single wallet
|
|
17
|
+
export interface WalletConfig {
|
|
18
|
+
type: WalletType
|
|
19
|
+
downloadUrl?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Base wallet instance - common methods for all wallets
|
|
23
|
+
export interface BaseWalletInstance {
|
|
24
|
+
extensionId: string
|
|
25
|
+
importMnemonic: (options: WalletAccount) => Promise<void>
|
|
26
|
+
authorize: (options?: { accountName?: string }) => Promise<void>
|
|
27
|
+
approveTx: (options?: { password?: string }) => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Polkadot-JS specific wallet instance
|
|
31
|
+
export interface PolkadotJsWalletInstance extends BaseWalletInstance {
|
|
32
|
+
type: 'polkadot-js'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Talisman specific wallet instance (with additional methods)
|
|
36
|
+
export interface TalismanWalletInstance extends BaseWalletInstance {
|
|
37
|
+
type: 'talisman'
|
|
38
|
+
importEthPrivateKey: (options: { privateKey: string, name?: string, password?: string }) => Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Union type of all wallet instances
|
|
42
|
+
export type WalletInstance = PolkadotJsWalletInstance | TalismanWalletInstance
|
|
43
|
+
|
|
44
|
+
// Map wallet type to its instance
|
|
45
|
+
export interface WalletTypeMap {
|
|
46
|
+
'polkadot-js': PolkadotJsWalletInstance
|
|
47
|
+
'talisman': TalismanWalletInstance
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wallets collection - all wallet types
|
|
51
|
+
export type Wallets = WalletTypeMap
|
|
52
|
+
|
|
53
|
+
// Helper type to build a wallets object based on configured wallet types
|
|
54
|
+
export type ConfiguredWallets<T extends readonly WalletConfig[]> = {
|
|
55
|
+
[K in T[number]['type']]: WalletTypeMap[K]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extended page with wallet context
|
|
59
|
+
export type ExtendedPage = Page & {
|
|
60
|
+
__extensionContext: BrowserContext
|
|
61
|
+
__walletExtensionIds: Map<string, string>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Complete test configuration - supports single and multi-wallet
|
|
65
|
+
export interface ChromaTestOptions<T extends readonly WalletConfig[] = WalletConfig[]> {
|
|
66
|
+
// Wallet configuration (single or multiple)
|
|
67
|
+
wallets?: T
|
|
68
|
+
// Common options
|
|
69
|
+
headless?: boolean
|
|
70
|
+
slowMo?: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Test fixtures (test-scoped: recreated per test)
|
|
74
|
+
export interface WalletFixtures<W = Wallets> {
|
|
75
|
+
page: ExtendedPage
|
|
76
|
+
wallets: W
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Worker fixtures (worker-scoped: persisted across tests)
|
|
80
|
+
export interface WalletWorkerFixtures {
|
|
81
|
+
walletContext: BrowserContext
|
|
82
|
+
walletExtensionIds: Map<string, string>
|
|
83
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test'
|
|
2
|
+
import type {
|
|
3
|
+
BaseWalletInstance,
|
|
4
|
+
PolkadotJsWalletInstance,
|
|
5
|
+
TalismanWalletInstance,
|
|
6
|
+
WalletAccount,
|
|
7
|
+
WalletInstance,
|
|
8
|
+
} from './types.js'
|
|
9
|
+
import {
|
|
10
|
+
approvePolkadotJSTx,
|
|
11
|
+
authorizePolkadotJS,
|
|
12
|
+
importPolkadotJSAccount,
|
|
13
|
+
} from '../wallets/polkadot-js.js'
|
|
14
|
+
import {
|
|
15
|
+
approveTalismanTx,
|
|
16
|
+
authorizeTalisman,
|
|
17
|
+
importEthPrivateKey,
|
|
18
|
+
} from '../wallets/talisman.js'
|
|
19
|
+
|
|
20
|
+
// Helper to create extended page with wallet context
|
|
21
|
+
function createExtendedPage(page: Page, context: BrowserContext, extensionId: string) {
|
|
22
|
+
const extPage = page as Page & { __extensionContext: BrowserContext, __extensionId: string }
|
|
23
|
+
extPage.__extensionContext = context
|
|
24
|
+
extPage.__extensionId = extensionId
|
|
25
|
+
return extPage
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create wallet instance helper with proper typing
|
|
29
|
+
export function createWalletInstance(
|
|
30
|
+
walletType: string,
|
|
31
|
+
extensionId: string,
|
|
32
|
+
context: BrowserContext,
|
|
33
|
+
): WalletInstance {
|
|
34
|
+
// Store the imported account name for later use
|
|
35
|
+
let importedAccountName: string | undefined
|
|
36
|
+
|
|
37
|
+
// Common methods for all wallets
|
|
38
|
+
const baseInstance: BaseWalletInstance = {
|
|
39
|
+
extensionId,
|
|
40
|
+
importMnemonic: async (options: WalletAccount) => {
|
|
41
|
+
const page = context.pages()[0] || await context.newPage()
|
|
42
|
+
const extPage = createExtendedPage(page, context, extensionId)
|
|
43
|
+
|
|
44
|
+
// Store the account name for future authorize calls
|
|
45
|
+
importedAccountName = options.name || 'Test Account'
|
|
46
|
+
|
|
47
|
+
switch (walletType) {
|
|
48
|
+
case 'polkadot-js':
|
|
49
|
+
await importPolkadotJSAccount(extPage, options)
|
|
50
|
+
break
|
|
51
|
+
case 'talisman':
|
|
52
|
+
throw new Error('Talisman importMnemonic is not yet implemented.')
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unsupported wallet type: ${walletType}`)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
authorize: async (options: { accountName?: string } = {}) => {
|
|
58
|
+
const page = context.pages()[0] || await context.newPage()
|
|
59
|
+
const extPage = createExtendedPage(page, context, extensionId)
|
|
60
|
+
|
|
61
|
+
// Use provided account name or fall back to the imported one
|
|
62
|
+
const accountName = options.accountName || importedAccountName
|
|
63
|
+
|
|
64
|
+
switch (walletType) {
|
|
65
|
+
case 'polkadot-js':
|
|
66
|
+
await authorizePolkadotJS(extPage)
|
|
67
|
+
break
|
|
68
|
+
case 'talisman':
|
|
69
|
+
await authorizeTalisman(extPage, { accountName })
|
|
70
|
+
break
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Unsupported wallet type: ${walletType}`)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
approveTx: async (options: { password?: string } = {}) => {
|
|
76
|
+
const page = context.pages()[0] || await context.newPage()
|
|
77
|
+
const extPage = createExtendedPage(page, context, extensionId)
|
|
78
|
+
|
|
79
|
+
switch (walletType) {
|
|
80
|
+
case 'polkadot-js':
|
|
81
|
+
await approvePolkadotJSTx(extPage, options)
|
|
82
|
+
break
|
|
83
|
+
case 'talisman':
|
|
84
|
+
await approveTalismanTx(extPage)
|
|
85
|
+
break
|
|
86
|
+
default:
|
|
87
|
+
throw new Error(`Unsupported wallet type: ${walletType}`)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Return wallet-specific instance with type discriminator
|
|
93
|
+
switch (walletType) {
|
|
94
|
+
case 'polkadot-js':
|
|
95
|
+
return {
|
|
96
|
+
...baseInstance,
|
|
97
|
+
type: 'polkadot-js',
|
|
98
|
+
} as PolkadotJsWalletInstance
|
|
99
|
+
|
|
100
|
+
case 'talisman':
|
|
101
|
+
return {
|
|
102
|
+
...baseInstance,
|
|
103
|
+
type: 'talisman',
|
|
104
|
+
importEthPrivateKey: async (options: { privateKey: string, name?: string, password?: string }) => {
|
|
105
|
+
const page = context.pages()[0] || await context.newPage()
|
|
106
|
+
const extPage = createExtendedPage(page, context, extensionId)
|
|
107
|
+
|
|
108
|
+
// Store the account name for future authorize calls
|
|
109
|
+
importedAccountName = options.name || 'Test Account'
|
|
110
|
+
|
|
111
|
+
// Use the seed property to pass the private key
|
|
112
|
+
await importEthPrivateKey(extPage, {
|
|
113
|
+
seed: options.privateKey,
|
|
114
|
+
name: options.name,
|
|
115
|
+
password: options.password,
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
} as TalismanWalletInstance
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unsupported wallet type: ${walletType}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs, { createWriteStream } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { pipeline } from 'node:stream/promises'
|
|
5
|
+
import { Extract } from 'unzipper'
|
|
6
|
+
|
|
7
|
+
export interface DownloadExtensionOptions {
|
|
8
|
+
downloadUrl: string
|
|
9
|
+
extensionName: string
|
|
10
|
+
targetDir?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function downloadAndExtractExtension(options: DownloadExtensionOptions): Promise<string> {
|
|
14
|
+
const { downloadUrl, extensionName, targetDir } = options
|
|
15
|
+
|
|
16
|
+
// Default to a directory in the user's project, not relative to this package
|
|
17
|
+
const extensionsDir = targetDir || path.resolve(process.cwd(), '.chroma')
|
|
18
|
+
const extensionDir = path.join(extensionsDir, extensionName)
|
|
19
|
+
const zipPath = path.join(extensionsDir, `${extensionName}.zip`)
|
|
20
|
+
|
|
21
|
+
// Create extensions directory if it doesn't exist
|
|
22
|
+
await fs.promises.mkdir(extensionsDir, { recursive: true })
|
|
23
|
+
|
|
24
|
+
// Check if extension is already downloaded and extracted
|
|
25
|
+
if (fs.existsSync(extensionDir) && fs.readdirSync(extensionDir).length > 0) {
|
|
26
|
+
console.log(`✅ ${extensionName} already exists at:`, extensionDir)
|
|
27
|
+
return extensionDir
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
console.log(`📥 Downloading ${extensionName}...`)
|
|
32
|
+
|
|
33
|
+
// Download the ZIP file
|
|
34
|
+
const response = await fetch(downloadUrl)
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Failed to download extension: ${response.status} ${response.statusText}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Save ZIP file
|
|
40
|
+
const writeStream = createWriteStream(zipPath)
|
|
41
|
+
await pipeline(response.body!, writeStream)
|
|
42
|
+
|
|
43
|
+
console.log('📦 Extracting extension...')
|
|
44
|
+
|
|
45
|
+
// Standard zip extraction
|
|
46
|
+
await pipeline(
|
|
47
|
+
fs.createReadStream(zipPath),
|
|
48
|
+
Extract({ path: extensionDir }),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Clean up ZIP file
|
|
52
|
+
await fs.promises.unlink(zipPath)
|
|
53
|
+
|
|
54
|
+
console.log(`✅ ${extensionName} downloaded and extracted to:`, extensionDir)
|
|
55
|
+
return extensionDir
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// Clean up on error
|
|
59
|
+
if (fs.existsSync(zipPath)) {
|
|
60
|
+
await fs.promises.unlink(zipPath).catch(() => {})
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync(extensionDir)) {
|
|
63
|
+
await fs.promises.rm(extensionDir, { recursive: true, force: true }).catch(() => {})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error(`Failed to download/extract ${extensionName}: ${error instanceof Error ? error.message : String(error)}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test'
|
|
2
|
+
import type { WalletAccount } from '../context-playwright/types.js'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
// Polkadot-JS specific configuration
|
|
8
|
+
export const POLKADOT_JS_CONFIG = {
|
|
9
|
+
downloadUrl: 'https://github.com/polkadot-js/extension/releases/download/v0.61.7/master-chrome-build.zip',
|
|
10
|
+
extensionName: 'polkadot-extension-0.61.7',
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
// Helper function to find extension popup
|
|
14
|
+
async function findExtensionPopup(context: BrowserContext, extensionId: string): Promise<Page> {
|
|
15
|
+
const pages = context.pages()
|
|
16
|
+
for (const p of pages) {
|
|
17
|
+
if (p.url().includes(`chrome-extension://${extensionId}/`)) {
|
|
18
|
+
return p
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Extension popup not found for ID: ${extensionId}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get Polkadot-JS extension path
|
|
25
|
+
export async function getPolkadotJSExtensionPath(): Promise<string> {
|
|
26
|
+
const extensionsDir = path.resolve(process.cwd(), '.chroma')
|
|
27
|
+
const extensionDir = path.join(extensionsDir, POLKADOT_JS_CONFIG.extensionName)
|
|
28
|
+
|
|
29
|
+
// Check if extension exists
|
|
30
|
+
if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Polkadot-JS extension not found at: ${extensionDir}\n\n`
|
|
33
|
+
+ `Please download the extension first by running:\n`
|
|
34
|
+
+ ` npx @avalix/chroma download-extensions\n\n`
|
|
35
|
+
+ `Or if you're using this as a dependency:\n`
|
|
36
|
+
+ ` npm run chroma:download\n`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`✅ Found Polkadot-JS extension at: ${extensionDir}`)
|
|
41
|
+
return extensionDir
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Polkadot-JS specific account import implementation
|
|
45
|
+
export async function importPolkadotJSAccount(
|
|
46
|
+
page: Page & { __extensionContext: BrowserContext, __extensionId: string },
|
|
47
|
+
{ seed, name = 'Test Account', password = 'h3llop0lkadot!' }: WalletAccount,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const context = page.__extensionContext
|
|
50
|
+
const extensionId = page.__extensionId
|
|
51
|
+
|
|
52
|
+
const extensionPopupUrl = `chrome-extension://${extensionId}/index.html`
|
|
53
|
+
const extensionPage = await context.newPage()
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await extensionPage.goto(extensionPopupUrl)
|
|
57
|
+
|
|
58
|
+
// Handle "Understood, let me continue" button if it exists
|
|
59
|
+
const understoodButton = extensionPage.getByRole('button', { name: 'Understood, let me continue' })
|
|
60
|
+
if (await understoodButton.count() > 0) {
|
|
61
|
+
await understoodButton.click()
|
|
62
|
+
await extensionPage.waitForTimeout(100)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Navigate to import seed page
|
|
66
|
+
await extensionPage.goto(`${extensionPopupUrl}#/account/import-seed`)
|
|
67
|
+
|
|
68
|
+
// Fill seed phrase and account details
|
|
69
|
+
await extensionPage.locator('textarea').fill(seed)
|
|
70
|
+
await extensionPage.locator('button:has-text("Next")').click()
|
|
71
|
+
await extensionPage.locator('input[type="text"]').fill(name)
|
|
72
|
+
await extensionPage.locator('input[type="password"]').fill(password)
|
|
73
|
+
await extensionPage.locator('div').filter({ hasText: /^Repeat password for verification$/ }).getByRole('textbox').fill(password)
|
|
74
|
+
await extensionPage.getByRole('button', { name: 'Add the account with the supplied seed' }).click()
|
|
75
|
+
|
|
76
|
+
console.log(`✅ Created Polkadot-JS wallet account: ${name}`)
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await extensionPage.close()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Polkadot-JS specific authorization implementation
|
|
84
|
+
export async function authorizePolkadotJS(
|
|
85
|
+
page: Page & { __extensionContext: BrowserContext, __extensionId: string },
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const context = page.__extensionContext
|
|
88
|
+
const extensionId = page.__extensionId
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
90
|
+
|
|
91
|
+
const extensionPopup = await findExtensionPopup(context, extensionId)
|
|
92
|
+
await extensionPopup.getByText('Select all').click()
|
|
93
|
+
await extensionPopup.getByRole('button', { name: /Connect \d+ account\(s\)/ }).click()
|
|
94
|
+
|
|
95
|
+
console.log('✅ Polkadot-JS wallet connected successfully')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Polkadot-JS specific transaction approval implementation
|
|
99
|
+
export async function approvePolkadotJSTx(
|
|
100
|
+
page: Page & { __extensionContext: BrowserContext, __extensionId: string },
|
|
101
|
+
options: { password?: string } = {},
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const { password = 'h3llop0lkadot!' } = options
|
|
104
|
+
const context = page.__extensionContext
|
|
105
|
+
const extensionId = page.__extensionId
|
|
106
|
+
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
108
|
+
const extensionPopup = await findExtensionPopup(context, extensionId)
|
|
109
|
+
|
|
110
|
+
await extensionPopup.getByRole('textbox').fill(password)
|
|
111
|
+
await extensionPopup.getByRole('button', { name: 'Sign the transaction' }).click()
|
|
112
|
+
|
|
113
|
+
console.log('✅ Polkadot-JS transaction signed successfully')
|
|
114
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test'
|
|
2
|
+
import type { WalletAccount } from '../context-playwright/types.js'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
// Talisman specific configuration
|
|
8
|
+
export const TALISMAN_CONFIG = {
|
|
9
|
+
downloadUrl: 'https://github.com/avalix-labs/polkadot-wallets/raw/refs/heads/main/talisman/talisman-3.0.5.zip',
|
|
10
|
+
extensionName: 'talisman-extension-3.0.5',
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
// Helper function to find extension popup
|
|
14
|
+
async function findExtensionPopup(context: BrowserContext, extensionId: string): Promise<Page> {
|
|
15
|
+
// delay for 1 second
|
|
16
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
17
|
+
|
|
18
|
+
const pages = context.pages()
|
|
19
|
+
for (const p of pages) {
|
|
20
|
+
if (p.url().includes(`chrome-extension://${extensionId}/`)) {
|
|
21
|
+
p.setViewportSize({ width: 400, height: 600 })
|
|
22
|
+
p.waitForLoadState('domcontentloaded')
|
|
23
|
+
return p
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error(`Extension popup not found for ID: ${extensionId}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get Talisman extension path
|
|
31
|
+
export async function getTalismanExtensionPath(): Promise<string> {
|
|
32
|
+
const extensionsDir = path.resolve(process.cwd(), '.chroma')
|
|
33
|
+
const extensionDir = path.join(extensionsDir, TALISMAN_CONFIG.extensionName)
|
|
34
|
+
|
|
35
|
+
// Check if extension exists
|
|
36
|
+
if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Talisman extension not found at: ${extensionDir}\n\n`
|
|
39
|
+
+ `Please download the extension first by running:\n`
|
|
40
|
+
+ ` npx @avalix/chroma download-extensions\n\n`
|
|
41
|
+
+ `Or if you're using this as a dependency:\n`
|
|
42
|
+
+ ` npm run chroma:download\n`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`✅ Found Talisman extension at: ${extensionDir}`)
|
|
47
|
+
return extensionDir
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Talisman specific Ethereum private key import implementation
|
|
51
|
+
export async function importEthPrivateKey(
|
|
52
|
+
page: Page & { __extensionContext: BrowserContext, __extensionId: string },
|
|
53
|
+
{ seed, name = 'Test Account', password = 'h3llop0lkadot!' }: WalletAccount,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const context = page.__extensionContext
|
|
56
|
+
const extensionId = page.__extensionId
|
|
57
|
+
|
|
58
|
+
// Wait for Talisman to open its onboarding tab
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
60
|
+
|
|
61
|
+
// Find the onboarding tab
|
|
62
|
+
let extensionPage: Page | null = null
|
|
63
|
+
const pages = context.pages()
|
|
64
|
+
|
|
65
|
+
for (const page of pages) {
|
|
66
|
+
const url = page.url()
|
|
67
|
+
console.log(`📄 Found page: ${url}`)
|
|
68
|
+
if (url.includes('onboarding.html') || url.includes(`chrome-extension://${extensionId}/`)) {
|
|
69
|
+
extensionPage = page
|
|
70
|
+
console.log(`✅ Found Talisman onboarding page: ${url}`)
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!extensionPage) {
|
|
76
|
+
throw new Error(`Talisman onboarding page not found`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Bring the onboarding page to front
|
|
81
|
+
await extensionPage.bringToFront()
|
|
82
|
+
|
|
83
|
+
// Reload the onboarding page to ensure fresh state
|
|
84
|
+
console.log('🔄 Reloading onboarding page for fresh state...')
|
|
85
|
+
await extensionPage.reload()
|
|
86
|
+
|
|
87
|
+
// Wait for the page to load and become interactive
|
|
88
|
+
await extensionPage.waitForLoadState('domcontentloaded')
|
|
89
|
+
// await extensionPage.waitForTimeout(20000) // Give Talisman more time to initialize
|
|
90
|
+
|
|
91
|
+
// Click the get started button
|
|
92
|
+
await extensionPage.getByTestId('onboarding-get-started-button').click()
|
|
93
|
+
|
|
94
|
+
// Fill the password
|
|
95
|
+
await extensionPage.getByRole('textbox', { name: 'Enter password' }).fill(password!)
|
|
96
|
+
await extensionPage.getByRole('textbox', { name: 'Confirm password' }).fill(password!)
|
|
97
|
+
await extensionPage.getByTestId('onboarding-password-confirm-button').click()
|
|
98
|
+
|
|
99
|
+
// Click the no thanks button
|
|
100
|
+
await extensionPage.getByRole('button', { name: 'No thanks' }).click()
|
|
101
|
+
await extensionPage.getByTestId('onboarding-enter-talisman-button').click()
|
|
102
|
+
|
|
103
|
+
// Import Ethereum account
|
|
104
|
+
await extensionPage.getByRole('button', { name: 'Add account Create or import' }).click()
|
|
105
|
+
await extensionPage.getByRole('button', { name: 'Import Import an existing' }).click()
|
|
106
|
+
await extensionPage.getByRole('button', { name: 'Import via Private Key' }).click()
|
|
107
|
+
await extensionPage.getByRole('button', { name: 'Select account platform' }).click()
|
|
108
|
+
await extensionPage.getByRole('option', { name: 'Ethereum' }).locator('div').click()
|
|
109
|
+
await extensionPage.getByRole('textbox', { name: 'Choose a name' }).fill(name!)
|
|
110
|
+
await extensionPage.getByRole('textbox', { name: 'Enter your private key' }).fill(seed!)
|
|
111
|
+
await extensionPage.getByRole('button', { name: 'Save' }).click()
|
|
112
|
+
|
|
113
|
+
await extensionPage.close()
|
|
114
|
+
|
|
115
|
+
console.log('✅ Talisman account import completed')
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error('❌ Error during Talisman account import:', error)
|
|
119
|
+
throw error
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Talisman specific authorization implementation
|
|
124
|
+
export async function authorizeTalisman(
|
|
125
|
+
page: Page & { __extensionContext: BrowserContext, __extensionId: string },
|
|
126
|
+
options: { accountName?: string } = {},
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const { accountName = 'Test Account' } = options
|
|
129
|
+
const context = page.__extensionContext
|
|
130
|
+
const extensionId = page.__extensionId
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
132
|
+
|
|
133
|
+
const extensionPopup = await findExtensionPopup(context, extensionId)
|
|
134
|
+
|
|
135
|
+
// Authorize Talisman account
|
|
136
|
+
await extensionPopup.getByRole('button', { name: accountName }).click()
|
|
137
|
+
await extensionPopup.getByTestId('connection-connect-button').click()
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const anotherPopup = await findExtensionPopup(context, extensionId)
|
|
141
|
+
await anotherPopup.getByRole('button', { name: 'Approve' }).click()
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
console.log('No another popup found, skipping')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Talisman specific transaction approval implementation
|
|
149
|
+
export async function approveTalismanTx(
|
|
150
|
+
page: Page & { __extensionContext: BrowserContext, __extensionId: string },
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
const context = page.__extensionContext
|
|
153
|
+
const extensionId = page.__extensionId
|
|
154
|
+
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
156
|
+
const extensionPopup = await findExtensionPopup(context, extensionId)
|
|
157
|
+
|
|
158
|
+
await extensionPopup.getByRole('button', { name: 'Approve' }).click()
|
|
159
|
+
}
|