@avalix/chroma 0.0.16 → 1.0.0
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/README.md +103 -3
- package/dist/index.d.mts +115 -20
- package/dist/index.mjs +122 -162
- package/package.json +3 -4
- package/scripts/download-extensions.ts +17 -22
- package/src/context-playwright/index.test.ts +19 -29
- package/src/context-playwright/index.ts +50 -36
- package/src/context-playwright/types.ts +19 -17
- package/src/context-playwright/wallet-factory.test.ts +38 -0
- package/src/context-playwright/wallet-factory.ts +18 -81
- package/src/utils/download-extension.integration.test.ts +1 -1
- package/src/utils/download-extension.test.ts +16 -34
- package/src/utils/download-extension.ts +11 -7
- package/src/utils/find-extension-popup.ts +39 -0
- package/src/utils/test-defaults.ts +7 -0
- package/src/wallets/metamask.test.ts +12 -18
- package/src/wallets/metamask.ts +91 -60
- package/src/wallets/polkadot-js.test.ts +12 -18
- package/src/wallets/polkadot-js.ts +19 -54
- package/src/wallets/talisman.test.ts +12 -18
- package/src/wallets/talisman.ts +20 -49
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChromaTestOptions,
|
|
3
3
|
ConfiguredWallets,
|
|
4
|
-
ExtendedPage,
|
|
5
4
|
WalletConfig,
|
|
6
5
|
WalletFixtures,
|
|
7
|
-
|
|
6
|
+
WalletType,
|
|
7
|
+
WalletTypeMap,
|
|
8
8
|
WalletWorkerFixtures,
|
|
9
9
|
} from './types.js'
|
|
10
|
+
import { cp, rm } from 'node:fs/promises'
|
|
11
|
+
import { resolve as resolvePath } from 'node:path'
|
|
10
12
|
import { test as base, chromium } from '@playwright/test'
|
|
11
13
|
import { getMetaMaskExtensionPath } from '../wallets/metamask.js'
|
|
12
14
|
import { getPolkadotJSExtensionPath } from '../wallets/polkadot-js.js'
|
|
@@ -14,7 +16,7 @@ import { getTalismanExtensionPath } from '../wallets/talisman.js'
|
|
|
14
16
|
import { walletFactories } from './wallet-factory.js'
|
|
15
17
|
|
|
16
18
|
// Helper function to get extension path for a wallet config
|
|
17
|
-
|
|
19
|
+
async function getExtensionPathForWallet(config: WalletConfig): Promise<string> {
|
|
18
20
|
const { type } = config
|
|
19
21
|
|
|
20
22
|
switch (type) {
|
|
@@ -41,10 +43,8 @@ export function createWalletTest<const T extends readonly WalletConfig[]>(
|
|
|
41
43
|
? options.wallets
|
|
42
44
|
: [{ type: 'polkadot-js' }]
|
|
43
45
|
|
|
44
|
-
const isMultiWallet = walletConfigs.length > 1
|
|
45
|
-
|
|
46
46
|
// Compute the expected wallets type
|
|
47
|
-
type ExpectedWallets = T extends readonly WalletConfig[] ? ConfiguredWallets<T> :
|
|
47
|
+
type ExpectedWallets = T extends readonly WalletConfig[] ? ConfiguredWallets<T> : WalletTypeMap
|
|
48
48
|
|
|
49
49
|
/*
|
|
50
50
|
* Playwright Fixtures - Coverage Exclusion
|
|
@@ -58,7 +58,7 @@ export function createWalletTest<const T extends readonly WalletConfig[]>(
|
|
|
58
58
|
return base.extend<WalletFixtures<ExpectedWallets>, WalletWorkerFixtures>({
|
|
59
59
|
// Worker-scoped: Browser context with extension(s) (persists across all tests in worker)
|
|
60
60
|
// eslint-disable-next-line no-empty-pattern
|
|
61
|
-
walletContext: [async ({}, use) => {
|
|
61
|
+
walletContext: [async ({}, use, workerInfo) => {
|
|
62
62
|
// Get all extension paths
|
|
63
63
|
const extensionPaths = await Promise.all(
|
|
64
64
|
walletConfigs.map(config => getExtensionPathForWallet(config)),
|
|
@@ -67,7 +67,29 @@ export function createWalletTest<const T extends readonly WalletConfig[]>(
|
|
|
67
67
|
// Join paths with comma for Chrome args
|
|
68
68
|
const extensionPathsString = extensionPaths.join(',')
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
// Resolve userDataDir (string, function, or default empty)
|
|
71
|
+
const userDataDirOption = options.userDataDir
|
|
72
|
+
const userDataDir = typeof userDataDirOption === 'function'
|
|
73
|
+
? await userDataDirOption({ workerIndex: workerInfo.workerIndex })
|
|
74
|
+
: userDataDirOption ?? ''
|
|
75
|
+
|
|
76
|
+
// Optional clone: reset target then copy from source. Skipped when
|
|
77
|
+
// userDataDir is empty (clone into a temp dir would defeat its purpose).
|
|
78
|
+
if (options.cloneUserDataDirFrom && userDataDir) {
|
|
79
|
+
// Guard against rm wiping the source: resolve both to absolute paths
|
|
80
|
+
// and refuse if they point at the same location.
|
|
81
|
+
const sourceAbs = resolvePath(options.cloneUserDataDirFrom)
|
|
82
|
+
const targetAbs = resolvePath(userDataDir)
|
|
83
|
+
if (sourceAbs === targetAbs) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`cloneUserDataDirFrom and userDataDir must be different paths; both resolved to "${sourceAbs}"`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
await rm(userDataDir, { recursive: true, force: true })
|
|
89
|
+
await cp(options.cloneUserDataDirFrom, userDataDir, { recursive: true })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
71
93
|
headless,
|
|
72
94
|
channel: 'chromium',
|
|
73
95
|
args: [
|
|
@@ -83,44 +105,36 @@ export function createWalletTest<const T extends readonly WalletConfig[]>(
|
|
|
83
105
|
|
|
84
106
|
// Worker-scoped: Map of wallet type to extension ID
|
|
85
107
|
walletExtensionIds: [async ({ walletContext }, use) => {
|
|
86
|
-
const extensionIds = new Map<
|
|
87
|
-
|
|
88
|
-
// Wait
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
const extensionIds = new Map<WalletType, string>()
|
|
109
|
+
|
|
110
|
+
// Wait until one service worker per configured wallet has registered.
|
|
111
|
+
// Bounded by 10s so a stuck worker fails fast instead of hanging the suite.
|
|
112
|
+
const expected = walletConfigs.length
|
|
113
|
+
const deadline = Date.now() + 10_000
|
|
114
|
+
while (walletContext.serviceWorkers().length < expected) {
|
|
115
|
+
if (Date.now() > deadline) {
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
await Promise.race([
|
|
119
|
+
walletContext.waitForEvent('serviceworker', { timeout: 2_000 }).catch(() => {}),
|
|
120
|
+
new Promise(resolve => setTimeout(resolve, 200)),
|
|
121
|
+
])
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
//
|
|
124
|
+
// Map service workers to wallet types (order matches walletConfigs order)
|
|
101
125
|
const allServiceWorkers = walletContext.serviceWorkers()
|
|
102
|
-
|
|
103
|
-
// Map service workers to wallet types
|
|
104
|
-
// Note: The order should match the walletConfigs order
|
|
105
126
|
for (let i = 0; i < walletConfigs.length && i < allServiceWorkers.length; i++) {
|
|
106
127
|
const extensionId = allServiceWorkers[i].url().split('/')[2]
|
|
107
|
-
|
|
108
|
-
extensionIds.set(walletType, extensionId)
|
|
128
|
+
extensionIds.set(walletConfigs[i].type, extensionId)
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
await use(extensionIds)
|
|
112
132
|
}, { scope: 'worker' }],
|
|
113
133
|
|
|
114
|
-
// Main page
|
|
115
|
-
page: async ({ walletContext
|
|
134
|
+
// Main page (uses worker-scoped context)
|
|
135
|
+
page: async ({ walletContext }, use) => {
|
|
116
136
|
const page = walletContext.pages()[0] || await walletContext.newPage()
|
|
117
|
-
|
|
118
|
-
// Store context and extension IDs on page
|
|
119
|
-
const extendedPage = page as ExtendedPage
|
|
120
|
-
extendedPage.__extensionContext = walletContext
|
|
121
|
-
extendedPage.__walletExtensionIds = walletExtensionIds
|
|
122
|
-
|
|
123
|
-
await use(extendedPage)
|
|
137
|
+
await use(page)
|
|
124
138
|
// Note: Don't close the page or context here since they're worker-scoped
|
|
125
139
|
},
|
|
126
140
|
|
|
@@ -130,7 +144,7 @@ export function createWalletTest<const T extends readonly WalletConfig[]>(
|
|
|
130
144
|
|
|
131
145
|
// Create wallet instance for each configured wallet
|
|
132
146
|
for (const [walletType, extensionId] of walletExtensionIds) {
|
|
133
|
-
const factory = walletFactories[walletType
|
|
147
|
+
const factory = walletFactories[walletType]
|
|
134
148
|
if (factory) {
|
|
135
149
|
walletMap[walletType as keyof ExpectedWallets] = factory(extensionId, walletContext) as ExpectedWallets[keyof ExpectedWallets]
|
|
136
150
|
}
|
|
@@ -3,12 +3,8 @@ import type {
|
|
|
3
3
|
MetaMaskWalletInstance,
|
|
4
4
|
PolkadotJsWalletInstance,
|
|
5
5
|
TalismanWalletInstance,
|
|
6
|
-
WalletInstance,
|
|
7
6
|
} from './wallet-factory.js'
|
|
8
7
|
|
|
9
|
-
// Re-export wallet instance types
|
|
10
|
-
export type { MetaMaskWalletInstance, PolkadotJsWalletInstance, TalismanWalletInstance, WalletInstance }
|
|
11
|
-
|
|
12
8
|
// Wallet types - single source of truth
|
|
13
9
|
export type WalletType = 'polkadot-js' | 'talisman' | 'metamask'
|
|
14
10
|
|
|
@@ -22,7 +18,6 @@ export interface WalletAccount {
|
|
|
22
18
|
// Configuration for a single wallet
|
|
23
19
|
export interface WalletConfig {
|
|
24
20
|
type: WalletType
|
|
25
|
-
downloadUrl?: string
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
// Map wallet type to its instance
|
|
@@ -32,20 +27,11 @@ export interface WalletTypeMap {
|
|
|
32
27
|
'metamask': MetaMaskWalletInstance
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
// Wallets collection - all wallet types
|
|
36
|
-
export type Wallets = WalletTypeMap
|
|
37
|
-
|
|
38
30
|
// Helper type to build a wallets object based on configured wallet types
|
|
39
31
|
export type ConfiguredWallets<T extends readonly WalletConfig[]> = {
|
|
40
32
|
[K in T[number]['type']]: WalletTypeMap[K]
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
// Extended page with wallet context
|
|
44
|
-
export type ExtendedPage = Page & {
|
|
45
|
-
__extensionContext: BrowserContext
|
|
46
|
-
__walletExtensionIds: Map<string, string>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
35
|
// Complete test configuration - supports single and multi-wallet
|
|
50
36
|
export interface ChromaTestOptions<T extends readonly WalletConfig[] = WalletConfig[]> {
|
|
51
37
|
// Wallet configuration (single or multiple)
|
|
@@ -53,16 +39,32 @@ export interface ChromaTestOptions<T extends readonly WalletConfig[] = WalletCon
|
|
|
53
39
|
// Common options
|
|
54
40
|
headless?: boolean
|
|
55
41
|
slowMo?: number
|
|
42
|
+
/**
|
|
43
|
+
* Persistent profile dir for the browser context.
|
|
44
|
+
* - Empty/undefined (default): temp dir is used; state is lost each run.
|
|
45
|
+
* - String: shared profile path. Requires `workers: 1` if used by multiple workers.
|
|
46
|
+
* - Function: receives the worker index, returns the path. Use for parallel
|
|
47
|
+
* isolation (e.g. `({ workerIndex }) => `.cache/wallet-w${workerIndex}``).
|
|
48
|
+
*/
|
|
49
|
+
userDataDir?: string | ((info: { workerIndex: number }) => string | Promise<string>)
|
|
50
|
+
/**
|
|
51
|
+
* If set, the source dir is copied into `userDataDir` before launch (target is
|
|
52
|
+
* removed first). Use with the Playwright setup-project pattern: a setup
|
|
53
|
+
* project writes to the source dir, then test projects clone it per worker so
|
|
54
|
+
* each parallel worker boots from the same prepared state.
|
|
55
|
+
* No-op if `userDataDir` resolves to an empty string.
|
|
56
|
+
*/
|
|
57
|
+
cloneUserDataDirFrom?: string
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// Test fixtures (test-scoped: recreated per test)
|
|
59
|
-
export interface WalletFixtures<W =
|
|
60
|
-
page:
|
|
61
|
+
export interface WalletFixtures<W = WalletTypeMap> {
|
|
62
|
+
page: Page
|
|
61
63
|
wallets: W
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Worker fixtures (worker-scoped: persisted across tests)
|
|
65
67
|
export interface WalletWorkerFixtures {
|
|
66
68
|
walletContext: BrowserContext
|
|
67
|
-
walletExtensionIds: Map<
|
|
69
|
+
walletExtensionIds: Map<WalletType, string>
|
|
68
70
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BrowserContext, Page } from '@playwright/test'
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import {
|
|
4
|
+
createMetaMaskWallet,
|
|
4
5
|
createPolkadotJsWallet,
|
|
5
6
|
createTalismanWallet,
|
|
6
7
|
walletFactories,
|
|
@@ -22,6 +23,13 @@ vi.mock('../wallets/talisman.js', () => ({
|
|
|
22
23
|
rejectTalismanTx: vi.fn(),
|
|
23
24
|
}))
|
|
24
25
|
|
|
26
|
+
vi.mock('../wallets/metamask.js', () => ({
|
|
27
|
+
importSeedPhrase: vi.fn(),
|
|
28
|
+
unlockMetaMask: vi.fn(),
|
|
29
|
+
approveMetaMask: vi.fn(),
|
|
30
|
+
rejectMetaMask: vi.fn(),
|
|
31
|
+
}))
|
|
32
|
+
|
|
25
33
|
// Create mock browser context
|
|
26
34
|
function createMockContext(): BrowserContext {
|
|
27
35
|
const mockPage = {
|
|
@@ -51,6 +59,11 @@ describe('wallet-factory', () => {
|
|
|
51
59
|
expect(walletFactories.talisman).toBeDefined()
|
|
52
60
|
expect(typeof walletFactories.talisman).toBe('function')
|
|
53
61
|
})
|
|
62
|
+
|
|
63
|
+
it('should have metamask factory', () => {
|
|
64
|
+
expect(walletFactories.metamask).toBeDefined()
|
|
65
|
+
expect(typeof walletFactories.metamask).toBe('function')
|
|
66
|
+
})
|
|
54
67
|
})
|
|
55
68
|
|
|
56
69
|
describe('createPolkadotJsWallet', () => {
|
|
@@ -103,4 +116,29 @@ describe('wallet-factory', () => {
|
|
|
103
116
|
expect(typeof wallet.rejectTx).toBe('function')
|
|
104
117
|
})
|
|
105
118
|
})
|
|
119
|
+
|
|
120
|
+
describe('createMetaMaskWallet', () => {
|
|
121
|
+
const extensionId = 'test-extension-id'
|
|
122
|
+
let mockContext: BrowserContext
|
|
123
|
+
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
mockContext = createMockContext()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should create wallet with correct type and extensionId', () => {
|
|
129
|
+
const wallet = createMetaMaskWallet(extensionId, mockContext)
|
|
130
|
+
|
|
131
|
+
expect(wallet.type).toBe('metamask')
|
|
132
|
+
expect(wallet.extensionId).toBe(extensionId)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should have all required methods', () => {
|
|
136
|
+
const wallet = createMetaMaskWallet(extensionId, mockContext)
|
|
137
|
+
|
|
138
|
+
expect(typeof wallet.importSeedPhrase).toBe('function')
|
|
139
|
+
expect(typeof wallet.unlock).toBe('function')
|
|
140
|
+
expect(typeof wallet.approve).toBe('function')
|
|
141
|
+
expect(typeof wallet.reject).toBe('function')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
106
144
|
})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { BrowserContext
|
|
1
|
+
import type { BrowserContext } from '@playwright/test'
|
|
2
2
|
import type { WalletAccount } from './types.js'
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
confirmMetaMask,
|
|
4
|
+
approveMetaMask,
|
|
6
5
|
importSeedPhrase as importMetaMaskSeedPhrase,
|
|
7
6
|
rejectMetaMask,
|
|
8
7
|
unlockMetaMask,
|
|
@@ -21,16 +20,6 @@ import {
|
|
|
21
20
|
rejectTalismanTx,
|
|
22
21
|
} from '../wallets/talisman.js'
|
|
23
22
|
|
|
24
|
-
// Helper to create extended page with wallet context
|
|
25
|
-
/* c8 ignore start */
|
|
26
|
-
function createExtendedPage(page: Page, context: BrowserContext, extensionId: string) {
|
|
27
|
-
const extPage = page as Page & { __extensionContext: BrowserContext, __extensionId: string }
|
|
28
|
-
extPage.__extensionContext = context
|
|
29
|
-
extPage.__extensionId = extensionId
|
|
30
|
-
return extPage
|
|
31
|
-
}
|
|
32
|
-
/* c8 ignore stop */
|
|
33
|
-
|
|
34
23
|
/*
|
|
35
24
|
* Factory function for Polkadot-JS wallet
|
|
36
25
|
* Coverage excluded: methods interact with Chrome extension APIs via browser context.
|
|
@@ -40,26 +29,10 @@ export function createPolkadotJsWallet(extensionId: string, context: BrowserCont
|
|
|
40
29
|
return {
|
|
41
30
|
extensionId,
|
|
42
31
|
type: 'polkadot-js' as const,
|
|
43
|
-
importMnemonic:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
authorize: async () => {
|
|
49
|
-
const page = context.pages()[0] || await context.newPage()
|
|
50
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
51
|
-
await authorizePolkadotJS(extPage)
|
|
52
|
-
},
|
|
53
|
-
approveTx: async (options: { password?: string } = {}) => {
|
|
54
|
-
const page = context.pages()[0] || await context.newPage()
|
|
55
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
56
|
-
await approvePolkadotJSTx(extPage, options)
|
|
57
|
-
},
|
|
58
|
-
rejectTx: async () => {
|
|
59
|
-
const page = context.pages()[0] || await context.newPage()
|
|
60
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
61
|
-
await rejectPolkadotJSTx(extPage)
|
|
62
|
-
},
|
|
32
|
+
importMnemonic: (options: WalletAccount) => importPolkadotJSAccount(context, extensionId, options),
|
|
33
|
+
authorize: () => authorizePolkadotJS(context, extensionId),
|
|
34
|
+
approveTx: (options: { password?: string } = {}) => approvePolkadotJSTx(context, extensionId, options),
|
|
35
|
+
rejectTx: () => rejectPolkadotJSTx(context, extensionId),
|
|
63
36
|
}
|
|
64
37
|
}
|
|
65
38
|
/* c8 ignore stop */
|
|
@@ -75,38 +48,24 @@ export function createTalismanWallet(extensionId: string, context: BrowserContex
|
|
|
75
48
|
return {
|
|
76
49
|
extensionId,
|
|
77
50
|
type: 'talisman' as const,
|
|
78
|
-
importPolkadotMnemonic:
|
|
79
|
-
const page = context.pages()[0] || await context.newPage()
|
|
80
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
51
|
+
importPolkadotMnemonic: (options: WalletAccount) => {
|
|
81
52
|
importedAccountName = options.name || 'Test Account'
|
|
82
|
-
|
|
53
|
+
return importPolkadotMnemonic(context, extensionId, options)
|
|
83
54
|
},
|
|
84
|
-
importEthPrivateKey:
|
|
85
|
-
const page = context.pages()[0] || await context.newPage()
|
|
86
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
55
|
+
importEthPrivateKey: (options: { privateKey: string, name?: string, password?: string }) => {
|
|
87
56
|
importedAccountName = options.name || 'Test Account'
|
|
88
|
-
|
|
57
|
+
return importEthPrivateKey(context, extensionId, {
|
|
89
58
|
seed: options.privateKey,
|
|
90
59
|
name: options.name,
|
|
91
60
|
password: options.password,
|
|
92
61
|
})
|
|
93
62
|
},
|
|
94
|
-
authorize:
|
|
95
|
-
const page = context.pages()[0] || await context.newPage()
|
|
96
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
63
|
+
authorize: (options: { accountName?: string } = {}) => {
|
|
97
64
|
const accountName = options.accountName || importedAccountName
|
|
98
|
-
|
|
99
|
-
},
|
|
100
|
-
approveTx: async () => {
|
|
101
|
-
const page = context.pages()[0] || await context.newPage()
|
|
102
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
103
|
-
await approveTalismanTx(extPage)
|
|
104
|
-
},
|
|
105
|
-
rejectTx: async () => {
|
|
106
|
-
const page = context.pages()[0] || await context.newPage()
|
|
107
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
108
|
-
await rejectTalismanTx(extPage)
|
|
65
|
+
return authorizeTalisman(context, extensionId, { accountName })
|
|
109
66
|
},
|
|
67
|
+
approveTx: () => approveTalismanTx(context, extensionId),
|
|
68
|
+
rejectTx: () => rejectTalismanTx(context, extensionId),
|
|
110
69
|
}
|
|
111
70
|
}
|
|
112
71
|
/* c8 ignore stop */
|
|
@@ -120,31 +79,10 @@ export function createMetaMaskWallet(extensionId: string, context: BrowserContex
|
|
|
120
79
|
return {
|
|
121
80
|
extensionId,
|
|
122
81
|
type: 'metamask' as const,
|
|
123
|
-
importSeedPhrase:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
unlock: async () => {
|
|
129
|
-
const page = context.pages()[0] || await context.newPage()
|
|
130
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
131
|
-
await unlockMetaMask(extPage)
|
|
132
|
-
},
|
|
133
|
-
authorize: async () => {
|
|
134
|
-
const page = context.pages()[0] || await context.newPage()
|
|
135
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
136
|
-
await authorizeMetaMask(extPage)
|
|
137
|
-
},
|
|
138
|
-
reject: async () => {
|
|
139
|
-
const page = context.pages()[0] || await context.newPage()
|
|
140
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
141
|
-
await rejectMetaMask(extPage)
|
|
142
|
-
},
|
|
143
|
-
confirm: async () => {
|
|
144
|
-
const page = context.pages()[0] || await context.newPage()
|
|
145
|
-
const extPage = createExtendedPage(page, context, extensionId)
|
|
146
|
-
await confirmMetaMask(extPage)
|
|
147
|
-
},
|
|
82
|
+
importSeedPhrase: (options: { seedPhrase: string }) => importMetaMaskSeedPhrase(context, extensionId, options),
|
|
83
|
+
unlock: () => unlockMetaMask(context, extensionId),
|
|
84
|
+
approve: () => approveMetaMask(context, extensionId),
|
|
85
|
+
reject: () => rejectMetaMask(context, extensionId),
|
|
148
86
|
}
|
|
149
87
|
}
|
|
150
88
|
/* c8 ignore stop */
|
|
@@ -160,4 +98,3 @@ export const walletFactories = {
|
|
|
160
98
|
export type PolkadotJsWalletInstance = ReturnType<typeof createPolkadotJsWallet>
|
|
161
99
|
export type TalismanWalletInstance = ReturnType<typeof createTalismanWallet>
|
|
162
100
|
export type MetaMaskWalletInstance = ReturnType<typeof createMetaMaskWallet>
|
|
163
|
-
export type WalletInstance = PolkadotJsWalletInstance | TalismanWalletInstance | MetaMaskWalletInstance
|
|
@@ -29,7 +29,7 @@ vi.mock('node:stream/promises', () => ({
|
|
|
29
29
|
}))
|
|
30
30
|
|
|
31
31
|
// Mock fs module
|
|
32
|
-
// - `fs.
|
|
32
|
+
// - `fs.promises.*` is accessed via the default import
|
|
33
33
|
// - `createWriteStream` is accessed as a named import
|
|
34
34
|
vi.mock('node:fs', async () => {
|
|
35
35
|
const actual = await vi.importActual<typeof import('node:fs')>('node:fs')
|
|
@@ -37,8 +37,6 @@ vi.mock('node:fs', async () => {
|
|
|
37
37
|
...actual,
|
|
38
38
|
default: {
|
|
39
39
|
...actual,
|
|
40
|
-
existsSync: vi.fn(),
|
|
41
|
-
readdirSync: vi.fn(),
|
|
42
40
|
promises: {
|
|
43
41
|
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
44
42
|
rm: vi.fn().mockResolvedValue(undefined),
|
|
@@ -73,9 +71,8 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
73
71
|
|
|
74
72
|
describe('when extension already exists', () => {
|
|
75
73
|
it('should skip download and return existing path', async () => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
|
|
74
|
+
const mockedReaddir = vi.mocked(fs.promises.readdir) as unknown as Mock
|
|
75
|
+
mockedReaddir.mockResolvedValueOnce(['manifest.json'])
|
|
79
76
|
|
|
80
77
|
const result = await downloadAndExtractExtension(mockOptions)
|
|
81
78
|
|
|
@@ -90,9 +87,8 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
90
87
|
|
|
91
88
|
describe('when extension does not exist', () => {
|
|
92
89
|
beforeEach(() => {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
mockedFs.readdirSync.mockReturnValue([])
|
|
90
|
+
const mockedReaddir = vi.mocked(fs.promises.readdir) as unknown as Mock
|
|
91
|
+
mockedReaddir.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
|
|
96
92
|
})
|
|
97
93
|
|
|
98
94
|
it('should throw error when download fails with bad status', async () => {
|
|
@@ -132,14 +128,6 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
132
128
|
statusText: 'Internal Server Error',
|
|
133
129
|
})
|
|
134
130
|
|
|
135
|
-
mockedFs.existsSync.mockImplementation((p: fs.PathLike) => {
|
|
136
|
-
const pathStr = p.toString()
|
|
137
|
-
if (pathStr.endsWith('test-extension') && !pathStr.includes('.zip')) {
|
|
138
|
-
return false
|
|
139
|
-
}
|
|
140
|
-
return true
|
|
141
|
-
})
|
|
142
|
-
|
|
143
131
|
await expect(downloadAndExtractExtension(mockOptions)).rejects.toThrow()
|
|
144
132
|
|
|
145
133
|
expect(mockedFs.promises.rm).toHaveBeenCalled()
|
|
@@ -148,10 +136,6 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
148
136
|
|
|
149
137
|
describe('successful download and extraction', () => {
|
|
150
138
|
beforeEach(() => {
|
|
151
|
-
const mockedFs = vi.mocked(fs)
|
|
152
|
-
mockedFs.existsSync.mockReturnValue(false)
|
|
153
|
-
mockedFs.readdirSync.mockReturnValue([])
|
|
154
|
-
|
|
155
139
|
mockFetch.mockResolvedValue({
|
|
156
140
|
ok: true,
|
|
157
141
|
status: 200,
|
|
@@ -160,10 +144,9 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
160
144
|
})
|
|
161
145
|
|
|
162
146
|
it('should extract and return correct path', async () => {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
mockReaddir.mockResolvedValue(['manifest.json', 'popup.html'])
|
|
147
|
+
const mockReaddir = vi.mocked(fs.promises.readdir) as unknown as Mock
|
|
148
|
+
mockReaddir.mockResolvedValueOnce([]) // existence check
|
|
149
|
+
mockReaddir.mockResolvedValueOnce(['manifest.json', 'popup.html']) // post-extraction
|
|
167
150
|
|
|
168
151
|
const result = await downloadAndExtractExtension({
|
|
169
152
|
...mockOptions,
|
|
@@ -182,8 +165,8 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
182
165
|
const mockReaddir = mockedFs.promises.readdir as unknown as Mock
|
|
183
166
|
const mockStat = mockedFs.promises.stat as unknown as Mock
|
|
184
167
|
|
|
185
|
-
//
|
|
186
|
-
mockReaddir.
|
|
168
|
+
mockReaddir.mockResolvedValueOnce([]) // existence check
|
|
169
|
+
mockReaddir.mockResolvedValueOnce(['extension-folder']) // post-extraction
|
|
187
170
|
mockStat.mockResolvedValue({ isDirectory: () => true })
|
|
188
171
|
|
|
189
172
|
const result = await downloadAndExtractExtension({
|
|
@@ -209,7 +192,8 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
209
192
|
const mockReaddir = mockedFs.promises.readdir as unknown as Mock
|
|
210
193
|
const mockStat = mockedFs.promises.stat as unknown as Mock
|
|
211
194
|
|
|
212
|
-
mockReaddir.
|
|
195
|
+
mockReaddir.mockResolvedValueOnce([]) // existence check
|
|
196
|
+
mockReaddir.mockResolvedValueOnce(['only-file.dat']) // post-extraction
|
|
213
197
|
mockStat.mockResolvedValue({ isDirectory: () => false })
|
|
214
198
|
|
|
215
199
|
const result = await downloadAndExtractExtension({
|
|
@@ -229,9 +213,8 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
229
213
|
|
|
230
214
|
describe('targetDir option', () => {
|
|
231
215
|
it('should use custom targetDir when provided', async () => {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
|
|
216
|
+
const mockReaddir = vi.mocked(fs.promises.readdir) as unknown as Mock
|
|
217
|
+
mockReaddir.mockResolvedValueOnce(['manifest.json'])
|
|
235
218
|
|
|
236
219
|
const customOptions: DownloadExtensionOptions = {
|
|
237
220
|
...mockOptions,
|
|
@@ -244,9 +227,8 @@ describe('downloadAndExtractExtension (unit tests)', () => {
|
|
|
244
227
|
})
|
|
245
228
|
|
|
246
229
|
it('should use default .chroma directory when targetDir not provided', async () => {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
|
|
230
|
+
const mockReaddir = vi.mocked(fs.promises.readdir) as unknown as Mock
|
|
231
|
+
mockReaddir.mockResolvedValueOnce(['manifest.json'])
|
|
250
232
|
|
|
251
233
|
const result = await downloadAndExtractExtension(mockOptions)
|
|
252
234
|
|
|
@@ -4,6 +4,10 @@ import process from 'node:process'
|
|
|
4
4
|
import { pipeline } from 'node:stream/promises'
|
|
5
5
|
import AdmZip from 'adm-zip'
|
|
6
6
|
|
|
7
|
+
async function safeReaddir(dir: string): Promise<string[]> {
|
|
8
|
+
return fs.promises.readdir(dir).catch(() => [] as string[])
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export interface DownloadExtensionOptions {
|
|
8
12
|
downloadUrl: string
|
|
9
13
|
extensionName: string
|
|
@@ -52,7 +56,7 @@ export async function downloadAndExtractExtension(options: DownloadExtensionOpti
|
|
|
52
56
|
await fs.promises.mkdir(extensionsDir, { recursive: true })
|
|
53
57
|
|
|
54
58
|
// Check if extension is already downloaded and extracted
|
|
55
|
-
if (
|
|
59
|
+
if ((await safeReaddir(extensionDir)).length > 0) {
|
|
56
60
|
console.log(`✅ ${extensionName} already exists at:`, extensionDir)
|
|
57
61
|
return extensionDir
|
|
58
62
|
}
|
|
@@ -86,12 +90,12 @@ export async function downloadAndExtractExtension(options: DownloadExtensionOpti
|
|
|
86
90
|
return extensionDir
|
|
87
91
|
}
|
|
88
92
|
catch (error) {
|
|
89
|
-
// Clean up on error
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// Clean up on error (force: true silently ignores missing paths)
|
|
94
|
+
await Promise.all(
|
|
95
|
+
[zipPath, extensionDir, tempExtractDir].map(dir =>
|
|
96
|
+
fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {}),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
95
99
|
|
|
96
100
|
throw new Error(`Failed to download/extract ${extensionName}: ${error instanceof Error ? error.message : String(error)}`)
|
|
97
101
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export interface FindExtensionPopupOptions {
|
|
4
|
+
maxAttempts?: number
|
|
5
|
+
retryDelay?: number
|
|
6
|
+
viewport?: { width: number, height: number }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Poll the BrowserContext for a page whose URL points at the given Chrome
|
|
11
|
+
* extension. Returns once the page is reachable and DOM-loaded.
|
|
12
|
+
*/
|
|
13
|
+
/* c8 ignore start */
|
|
14
|
+
export async function findExtensionPopup(
|
|
15
|
+
context: BrowserContext,
|
|
16
|
+
extensionId: string,
|
|
17
|
+
options: FindExtensionPopupOptions = {},
|
|
18
|
+
): Promise<Page> {
|
|
19
|
+
const { maxAttempts = 10, retryDelay = 500, viewport } = options
|
|
20
|
+
|
|
21
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
22
|
+
for (const p of context.pages()) {
|
|
23
|
+
if (p.url().includes(`chrome-extension://${extensionId}/`)) {
|
|
24
|
+
if (viewport) {
|
|
25
|
+
await p.setViewportSize(viewport)
|
|
26
|
+
}
|
|
27
|
+
await p.waitForLoadState('domcontentloaded')
|
|
28
|
+
return p
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (attempt < maxAttempts - 1) {
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new Error(`Extension popup not found for ID: ${extensionId}`)
|
|
38
|
+
}
|
|
39
|
+
/* c8 ignore stop */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default password used by chroma to bootstrap wallets in tests.
|
|
3
|
+
*
|
|
4
|
+
* Wallet methods accept an explicit `password` option that overrides this
|
|
5
|
+
* value, so callers that already supply their own password are unaffected.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_TEST_PASSWORD = 'h3llop0lkadot!'
|