@avalix/chroma 0.0.6 → 0.0.7

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  End-to-end testing library for Polkadot wallet interactions using Playwright.
4
4
 
5
- > **Current Status**: This library currently supports **Polkadot JS Extension** only. Support for other wallets like Talisman is planned for future releases.
5
+ > **⚠️ Active Development**: This library is currently under active development. The API may change and breaking changes can occur between versions. Please pin your version and review changelogs carefully when updating.
6
6
 
7
7
  ## Installation
8
8
 
@@ -12,6 +12,18 @@ npm install @avalix/chroma @playwright/test
12
12
 
13
13
  **Note**: `@playwright/test` is a peer dependency and must be installed separately to avoid conflicts.
14
14
 
15
+ ### Download Extensions
16
+
17
+ Before running your tests, you need to download the wallet extensions:
18
+
19
+ ```bash
20
+ npx @avalix/chroma download-extensions
21
+ ```
22
+
23
+ This will download the wallet extensions (Polkadot JS and Talisman) to `./.chroma` directory in your project root.
24
+
25
+ **Important**: You must run this command before running Playwright tests. If the extension is not found, tests will fail with a helpful error message.
26
+
15
27
  ## Quick Start
16
28
 
17
29
  ### Basic Usage
@@ -19,9 +31,11 @@ npm install @avalix/chroma @playwright/test
19
31
  ```typescript
20
32
  import { expect, test } from '@avalix/chroma'
21
33
 
22
- test('should connect wallet and sign transaction', async ({ page, importAccount, authorize, approveTx }) => {
34
+ test('should connect wallet and sign transaction', async ({ page, wallets }) => {
35
+ const polkadotJs = wallets['polkadot-js']
36
+
23
37
  // Import a test account
24
- await importAccount({
38
+ await polkadotJs.importMnemonic({
25
39
  seed: 'bottom drive obey lake curtain smoke basket hold race lonely fit walk',
26
40
  name: 'Test Account',
27
41
  password: 'securePassword123'
@@ -32,11 +46,11 @@ test('should connect wallet and sign transaction', async ({ page, importAccount,
32
46
 
33
47
  // Connect wallet
34
48
  await page.click('button:has-text("Connect Wallet")')
35
- await authorize()
49
+ await polkadotJs.authorize()
36
50
 
37
51
  // Perform transaction
38
52
  await page.click('button:has-text("Send Transaction")')
39
- await approveTx({ password: 'securePassword123' })
53
+ await polkadotJs.approveTx({ password: 'securePassword123' })
40
54
 
41
55
  // Verify transaction success
42
56
  await expect(page.locator('.transaction-success')).toBeVisible()
@@ -48,33 +62,71 @@ test('should connect wallet and sign transaction', async ({ page, importAccount,
48
62
  ```typescript
49
63
  import { createWalletTest, expect } from '@avalix/chroma'
50
64
 
51
- // Create test with custom configuration
52
65
  const customTest = createWalletTest({
53
- walletType: 'polkadot-js',
54
- walletConfig: {
55
- customPath: './my-custom-extension'
56
- },
66
+ wallets: [{ type: 'polkadot-js' }],
57
67
  headless: false,
58
68
  slowMo: 100
59
69
  })
60
70
 
61
- customTest('test with custom config', async ({ page, importAccount, authorize }) => {
62
- // Your test code here
63
- await importAccount({
71
+ customTest('test with custom config', async ({ page, wallets }) => {
72
+ const polkadotJs = wallets['polkadot-js']
73
+
74
+ await polkadotJs.importMnemonic({
64
75
  seed: 'your seed phrase here...',
65
76
  name: 'My Test Account'
66
77
  })
67
- // ... rest of your test
78
+ await page.goto('http://localhost:3000')
79
+ await polkadotJs.authorize()
80
+ })
81
+ ```
82
+
83
+ ### Multiple Wallets
84
+
85
+ ```typescript
86
+ import { createWalletTest, expect } from '@avalix/chroma'
87
+
88
+ // Test with multiple wallet extensions
89
+ const multiWalletTest = createWalletTest({
90
+ wallets: [
91
+ { type: 'polkadot-js' },
92
+ { type: 'talisman' }
93
+ ],
94
+ headless: false,
95
+ slowMo: 150
96
+ })
97
+
98
+ multiWalletTest('test with multiple wallets', async ({ page, wallets }) => {
99
+ const polkadotJs = wallets['polkadot-js']
100
+ const talisman = wallets.talisman
101
+
102
+ // Import to Polkadot JS
103
+ await polkadotJs.importMnemonic({
104
+ seed: 'bottom drive obey lake curtain smoke basket hold race lonely fit walk',
105
+ name: 'Alice'
106
+ })
107
+
108
+ // Import to Talisman using Ethereum private key
109
+ await talisman.importEthPrivateKey({
110
+ privateKey: '0x...',
111
+ name: 'Bob'
112
+ })
113
+
114
+ await page.goto('http://localhost:3000')
115
+
116
+ // Use specific wallet
117
+ await polkadotJs.authorize()
118
+ await polkadotJs.approveTx()
68
119
  })
69
120
  ```
70
121
 
71
122
  ## Features
72
123
 
73
- - 🔐 **Automatic Extension Setup**: Downloads and configures Polkadot JS extension automatically
124
+ - 🔐 **Easy Extension Setup**: Simple command to download wallet extensions
74
125
  - 🧪 **Test Fixtures**: Ready-to-use Playwright fixtures for wallet operations
75
126
  - 📝 **Account Management**: Import accounts with seed phrases and custom names
76
127
  - ✅ **Transaction Approval**: Approve transactions with password authentication
77
128
  - 🔗 **dApp Authorization**: Connect wallet to decentralized applications
129
+ - 🔀 **Multi-Wallet Support**: Test with multiple wallet extensions simultaneously
78
130
  - ⚙️ **Configurable**: Custom extension paths, headless mode, and slow motion settings
79
131
 
80
132
  ## API Reference
@@ -87,110 +139,152 @@ Pre-configured test function with Polkadot JS extension.
87
139
  ```typescript
88
140
  import { test } from '@avalix/chroma'
89
141
 
90
- test('my wallet test', async ({ page, importAccount, authorize, approveTx }) => {
91
- // Test implementation
142
+ test('my wallet test', async ({ page, wallets }) => {
143
+ const polkadotJs = wallets['polkadot-js']
144
+
145
+ await polkadotJs.importMnemonic({ seed: '...' })
146
+ await polkadotJs.authorize()
92
147
  })
93
148
  ```
94
149
 
95
150
  #### `createWalletTest(options?: ChromaTestOptions)`
96
- Create a custom test function with specific configuration.
151
+ Create a custom test function with specific configuration. Supports single and multi-wallet modes.
97
152
 
98
153
  ```typescript
99
154
  import { createWalletTest } from '@avalix/chroma'
100
155
 
156
+ // Single wallet (default)
157
+ const test = createWalletTest()
158
+
159
+ // Single wallet with custom config
101
160
  const customTest = createWalletTest({
102
- walletType: 'polkadot-js', // Currently only 'polkadot-js' is supported
103
- walletConfig: {
104
- customPath: './custom-extension', // Optional: path to custom extension
105
- downloadUrl: 'https://...' // Optional: custom download URL
106
- },
107
- headless: true, // Optional: run in headless mode
108
- slowMo: 150 // Optional: slow motion delay in ms (default: 150)
161
+ wallets: [{ type: 'polkadot-js' }],
162
+ headless: false,
163
+ slowMo: 150
164
+ })
165
+
166
+ // Multiple wallets
167
+ const multiTest = createWalletTest({
168
+ wallets: [
169
+ { type: 'polkadot-js' },
170
+ { type: 'talisman' }
171
+ ]
172
+ })
173
+
174
+ // Usage
175
+ test('example', async ({ page, wallets }) => {
176
+ const polkadotJs = wallets['polkadot-js']
177
+
178
+ await polkadotJs.importMnemonic({ seed: '...' })
179
+ await polkadotJs.authorize()
180
+ await polkadotJs.approveTx()
109
181
  })
110
182
  ```
111
183
 
112
184
  ### Test Fixtures
113
185
 
114
- #### `importAccount(options: WalletAccount)`
115
- Import a wallet account using seed phrase.
186
+ #### `page`
187
+ Playwright page instance with wallet extension(s) loaded.
188
+
189
+ #### `wallets`
190
+ Typed object containing wallet instances for each configured wallet. Provides full TypeScript autocomplete.
116
191
 
117
192
  ```typescript
193
+ // Base wallet instance (common methods)
194
+ interface BaseWalletInstance {
195
+ extensionId: string
196
+ importMnemonic: (options: WalletAccount) => Promise<void>
197
+ authorize: (options?: { accountName?: string }) => Promise<void>
198
+ approveTx: (options?: { password?: string }) => Promise<void>
199
+ }
200
+
201
+ // Polkadot-JS wallet instance
202
+ interface PolkadotJsWalletInstance extends BaseWalletInstance {
203
+ type: 'polkadot-js'
204
+ }
205
+
206
+ // Talisman wallet instance (with additional methods)
207
+ interface TalismanWalletInstance extends BaseWalletInstance {
208
+ type: 'talisman'
209
+ importEthPrivateKey: (options: { privateKey: string, name?: string, password?: string }) => Promise<void>
210
+ }
211
+
212
+ // Note: Talisman currently does not support importMnemonic - use importEthPrivateKey instead
213
+
214
+ // Wallets collection - each wallet has its specific type
215
+ interface Wallets {
216
+ 'polkadot-js': PolkadotJsWalletInstance
217
+ 'talisman': TalismanWalletInstance
218
+ }
219
+
118
220
  interface WalletAccount {
119
221
  seed: string
120
222
  name?: string // Default: 'Test Account'
121
223
  password?: string // Default: 'h3llop0lkadot!'
122
224
  }
123
-
124
- await importAccount({
125
- seed: 'your twelve word seed phrase here...',
126
- name: 'My Test Account',
127
- password: 'securePassword123'
128
- })
129
225
  ```
130
226
 
131
- #### `authorize()`
132
- Authorize the dApp to connect with the wallet. Call this after triggering wallet connection from your dApp.
227
+ **Usage:**
133
228
 
134
229
  ```typescript
135
- await authorize()
136
- ```
230
+ test('example', async ({ page, wallets }) => {
231
+ const polkadotJs = wallets['polkadot-js'] // Type: PolkadotJsWalletInstance
137
232
 
138
- #### `approveTx(options?)`
139
- Approve a transaction with the wallet password.
233
+ // Import mnemonic (available on all wallets)
234
+ await polkadotJs.importMnemonic({
235
+ seed: 'bottom drive obey lake curtain smoke basket hold race lonely fit walk',
236
+ name: 'Test Account',
237
+ password: 'securePassword123'
238
+ })
140
239
 
141
- ```typescript
142
- await approveTx({ password: 'myPassword' })
240
+ await page.goto('http://localhost:3000')
241
+ await polkadotJs.authorize()
242
+ await polkadotJs.approveTx({ password: 'securePassword123' })
243
+ })
143
244
 
144
- // Or use default password
145
- await approveTx()
146
- ```
245
+ // Talisman-specific features
246
+ test('talisman example', async ({ page, wallets }) => {
247
+ const talisman = wallets.talisman // Type: TalismanWalletInstance
147
248
 
148
- ### Utility Functions
249
+ // Talisman-specific method: import Ethereum private key
250
+ await talisman.importEthPrivateKey({
251
+ privateKey: '0x...',
252
+ name: 'My Account',
253
+ password: 'mypassword'
254
+ })
149
255
 
150
- #### `downloadAndExtractPolkadotExtension(targetDir?)`
151
- Download and extract Polkadot JS extension to specified directory.
256
+ // Common methods also available
257
+ await talisman.authorize({ accountName: 'My Account' })
258
+ await talisman.approveTx()
259
+ })
260
+ ```
152
261
 
153
- ```typescript
154
- import { downloadAndExtractPolkadotExtension } from '@avalix/chroma'
262
+ ## Configuration
155
263
 
156
- // Download to custom directory
157
- const extensionPath = await downloadAndExtractPolkadotExtension('./my-extensions')
264
+ ### Extension Download
265
+ Run the download command to get the required wallet extensions:
158
266
 
159
- // Download to default directory (./.chroma)
160
- const extensionPath = await downloadAndExtractPolkadotExtension()
267
+ ```bash
268
+ npx @avalix/chroma download-extensions
161
269
  ```
162
270
 
163
- ### TypeScript Types
271
+ Extensions will be downloaded to `./.chroma` directory in your project root. Add this directory to your `.gitignore`:
164
272
 
165
- ```typescript
166
- import type {
167
- ChromaTestOptions,
168
- WalletAccount,
169
- WalletConfig,
170
- WalletFixtures,
171
- WalletType
172
- } from '@avalix/chroma'
273
+ ```gitignore
274
+ .chroma/
173
275
  ```
174
276
 
175
- ## Configuration
176
-
177
- ### Default Directory
178
- The Polkadot JS extension will be automatically downloaded to `./.chroma` directory in your project root. You can customize this by:
179
-
180
- 1. Using `downloadAndExtractPolkadotExtension('./custom-path')`
181
- 2. Using `createWalletTest()` with `walletConfig.customPath`
182
-
183
277
  ### Browser Settings
184
278
  - **Headless Mode**: Disabled by default for better debugging
185
279
  - **Slow Motion**: 150ms delay between actions (configurable)
186
- - **Extension Loading**: Automatically loads only the Polkadot JS extension
280
+ - **Extension Loading**: Automatically loads configured wallet extensions
187
281
 
188
282
  ## Supported Wallets
189
283
 
190
284
  | Wallet | Status | Version |
191
285
  |--------|--------|---------|
192
286
  | Polkadot JS Extension | ✅ Supported | v0.61.7 |
193
- | Talisman | Planned | - |
287
+ | Talisman | Supported | v3.0.5 |
194
288
  | SubWallet | ⏳ Planned | - |
195
289
 
196
290
  ## Requirements
@@ -201,8 +295,9 @@ The Polkadot JS extension will be automatically downloaded to `./.chroma` direct
201
295
  ## Contributing
202
296
 
203
297
  This project is in active development. Currently focusing on:
204
- - Polkadot JS Extension support
298
+ - Polkadot JS Extension and Talisman support
205
299
  - Core testing fixtures
300
+ - Additional wallet integrations
206
301
  - Documentation improvements
207
302
 
208
303
  ## License
package/dist/index.d.ts CHANGED
@@ -1,40 +1,64 @@
1
- import { BrowserContext, BrowserContext as BrowserContext$1, Page, Page as Page$1, expect, test as test$1 } from "@playwright/test";
1
+ import { BrowserContext, Page, expect } from "@playwright/test";
2
+ import * as playwright_test0 from "playwright/test";
2
3
 
3
- //#region src/context-playwright/index.d.ts
4
- type WalletType = "polkadot-js" | "talisman";
5
- interface WalletConfig {
6
- downloadUrl?: string;
7
- customPath?: string;
8
- }
4
+ //#region src/context-playwright/types.d.ts
5
+ type WalletType = 'polkadot-js' | 'talisman';
9
6
  interface WalletAccount {
10
7
  seed: string;
11
8
  name?: string;
12
9
  password?: string;
13
10
  }
14
- interface ChromaTestOptions {
15
- walletType?: WalletType;
16
- walletConfig?: WalletConfig;
17
- headless?: boolean;
18
- slowMo?: number;
19
- }
20
- interface ExtendedPage extends Page$1 {
21
- __extensionContext: BrowserContext$1;
22
- __extensionId: string;
11
+ interface WalletConfig {
12
+ type: WalletType;
13
+ downloadUrl?: string;
23
14
  }
24
- interface WalletFixtures {
25
- page: ExtendedPage;
26
- walletType: WalletType;
27
- walletConfig: WalletConfig;
28
- importAccount: (options: WalletAccount) => Promise<void>;
29
- authorize: () => Promise<void>;
15
+ interface BaseWalletInstance {
16
+ extensionId: string;
17
+ importMnemonic: (options: WalletAccount) => Promise<void>;
18
+ authorize: (options?: {
19
+ accountName?: string;
20
+ }) => Promise<void>;
30
21
  approveTx: (options?: {
31
22
  password?: string;
32
23
  }) => Promise<void>;
33
24
  }
34
- declare function createWalletTest(options?: ChromaTestOptions): ReturnType<typeof test$1.extend<WalletFixtures>>;
35
- declare const test: ReturnType<typeof createWalletTest>;
25
+ interface PolkadotJsWalletInstance extends BaseWalletInstance {
26
+ type: 'polkadot-js';
27
+ }
28
+ interface TalismanWalletInstance extends BaseWalletInstance {
29
+ type: 'talisman';
30
+ importEthPrivateKey: (options: {
31
+ privateKey: string;
32
+ name?: string;
33
+ password?: string;
34
+ }) => Promise<void>;
35
+ }
36
+ interface WalletTypeMap {
37
+ 'polkadot-js': PolkadotJsWalletInstance;
38
+ 'talisman': TalismanWalletInstance;
39
+ }
40
+ type Wallets = WalletTypeMap;
41
+ type ConfiguredWallets<T extends readonly WalletConfig[]> = { [K in T[number]['type']]: WalletTypeMap[K] };
42
+ type ExtendedPage = Page & {
43
+ __extensionContext: BrowserContext;
44
+ __walletExtensionIds: Map<string, string>;
45
+ };
46
+ interface ChromaTestOptions<T extends readonly WalletConfig[] = WalletConfig[]> {
47
+ wallets?: T;
48
+ headless?: boolean;
49
+ slowMo?: number;
50
+ }
51
+ interface WalletFixtures<W = Wallets> {
52
+ page: ExtendedPage;
53
+ wallets: W;
54
+ }
55
+ interface WalletWorkerFixtures {
56
+ walletContext: BrowserContext;
57
+ walletExtensionIds: Map<string, string>;
58
+ }
36
59
  //#endregion
37
- //#region src/context-playwright/download-polkadot-js.d.ts
38
- declare function downloadAndExtractPolkadotExtension(targetDir?: string): Promise<string>;
60
+ //#region src/context-playwright/index.d.ts
61
+ declare function createWalletTest<const T extends readonly WalletConfig[]>(options?: ChromaTestOptions<T>): playwright_test0.TestType<playwright_test0.PlaywrightTestArgs & playwright_test0.PlaywrightTestOptions & WalletFixtures<T extends readonly WalletConfig[] ? ConfiguredWallets<T> : WalletTypeMap>, playwright_test0.PlaywrightWorkerArgs & playwright_test0.PlaywrightWorkerOptions & WalletWorkerFixtures>;
62
+ declare const test: ReturnType<typeof createWalletTest>;
39
63
  //#endregion
40
- export { type BrowserContext, type ChromaTestOptions, type Page, type WalletAccount, type WalletConfig, type WalletFixtures, type WalletType, createWalletTest, downloadAndExtractPolkadotExtension, expect, test };
64
+ export { createWalletTest, expect, test };
package/dist/index.js CHANGED
@@ -1,140 +1,289 @@
1
1
  import { chromium, expect, test as test$1 } from "@playwright/test";
2
- import fs, { createWriteStream } from "node:fs";
2
+ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
- import { pipeline } from "node:stream/promises";
6
- import { Extract } from "unzipper";
7
5
 
8
- //#region src/context-playwright/download-polkadot-js.ts
9
- const POLKADOT_JS_EXTENSION = "https://github.com/polkadot-js/extension/releases/download/v0.61.7/master-chrome-build.zip";
10
- async function downloadAndExtractPolkadotExtension(targetDir) {
11
- const extensionsDir = targetDir || path.resolve(process.cwd(), ".chroma");
12
- const extensionDir = path.join(extensionsDir, "polkadot-extension-chrome");
13
- const zipPath = path.join(extensionsDir, "polkadot-extension.zip");
14
- await fs.promises.mkdir(extensionsDir, { recursive: true });
15
- if (fs.existsSync(extensionDir) && fs.readdirSync(extensionDir).length > 0) {
16
- console.log("✅ Polkadot extension already exists at:", extensionDir);
17
- return extensionDir;
6
+ //#region src/wallets/polkadot-js.ts
7
+ const POLKADOT_JS_CONFIG = {
8
+ downloadUrl: "https://github.com/polkadot-js/extension/releases/download/v0.61.7/master-chrome-build.zip",
9
+ extensionName: "polkadot-extension-0.61.7"
10
+ };
11
+ async function findExtensionPopup$1(context, extensionId) {
12
+ const pages = context.pages();
13
+ for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) return p;
14
+ throw new Error(`Extension popup not found for ID: ${extensionId}`);
15
+ }
16
+ async function getPolkadotJSExtensionPath() {
17
+ const extensionsDir = path.resolve(process.cwd(), ".chroma");
18
+ const extensionDir = path.join(extensionsDir, POLKADOT_JS_CONFIG.extensionName);
19
+ if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) throw new Error(`Polkadot-JS extension not found at: ${extensionDir}\n\nPlease download the extension first by running:\n npx @avalix/chroma download-extensions\n\nOr if you're using this as a dependency:\n npm run chroma:download\n`);
20
+ console.log(`✅ Found Polkadot-JS extension at: ${extensionDir}`);
21
+ return extensionDir;
22
+ }
23
+ async function importPolkadotJSAccount(page, { seed, name = "Test Account", password = "h3llop0lkadot!" }) {
24
+ const context = page.__extensionContext;
25
+ const extensionPopupUrl = `chrome-extension://${page.__extensionId}/index.html`;
26
+ const extensionPage = await context.newPage();
27
+ try {
28
+ await extensionPage.goto(extensionPopupUrl);
29
+ const understoodButton = extensionPage.getByRole("button", { name: "Understood, let me continue" });
30
+ if (await understoodButton.count() > 0) {
31
+ await understoodButton.click();
32
+ await extensionPage.waitForTimeout(100);
33
+ }
34
+ await extensionPage.goto(`${extensionPopupUrl}#/account/import-seed`);
35
+ await extensionPage.locator("textarea").fill(seed);
36
+ await extensionPage.locator("button:has-text(\"Next\")").click();
37
+ await extensionPage.locator("input[type=\"text\"]").fill(name);
38
+ await extensionPage.locator("input[type=\"password\"]").fill(password);
39
+ await extensionPage.locator("div").filter({ hasText: /^Repeat password for verification$/ }).getByRole("textbox").fill(password);
40
+ await extensionPage.getByRole("button", { name: "Add the account with the supplied seed" }).click();
41
+ console.log(`✅ Created Polkadot-JS wallet account: ${name}`);
42
+ } finally {
43
+ await extensionPage.close();
44
+ }
45
+ }
46
+ async function authorizePolkadotJS(page) {
47
+ const context = page.__extensionContext;
48
+ const extensionId = page.__extensionId;
49
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
50
+ const extensionPopup = await findExtensionPopup$1(context, extensionId);
51
+ await extensionPopup.getByText("Select all").click();
52
+ await extensionPopup.getByRole("button", { name: /Connect \d+ account\(s\)/ }).click();
53
+ console.log("✅ Polkadot-JS wallet connected successfully");
54
+ }
55
+ async function approvePolkadotJSTx(page, options = {}) {
56
+ const { password = "h3llop0lkadot!" } = options;
57
+ const context = page.__extensionContext;
58
+ const extensionId = page.__extensionId;
59
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
60
+ const extensionPopup = await findExtensionPopup$1(context, extensionId);
61
+ await extensionPopup.getByRole("textbox").fill(password);
62
+ await extensionPopup.getByRole("button", { name: "Sign the transaction" }).click();
63
+ console.log("✅ Polkadot-JS transaction signed successfully");
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/wallets/talisman.ts
68
+ const TALISMAN_CONFIG = {
69
+ downloadUrl: "https://github.com/avalix-labs/polkadot-wallets/raw/refs/heads/main/talisman/talisman-3.0.5.zip",
70
+ extensionName: "talisman-extension-3.0.5"
71
+ };
72
+ async function findExtensionPopup(context, extensionId) {
73
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
74
+ const pages = context.pages();
75
+ for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
76
+ p.setViewportSize({
77
+ width: 400,
78
+ height: 600
79
+ });
80
+ p.waitForLoadState("domcontentloaded");
81
+ return p;
18
82
  }
83
+ throw new Error(`Extension popup not found for ID: ${extensionId}`);
84
+ }
85
+ async function getTalismanExtensionPath() {
86
+ const extensionsDir = path.resolve(process.cwd(), ".chroma");
87
+ const extensionDir = path.join(extensionsDir, TALISMAN_CONFIG.extensionName);
88
+ if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) throw new Error(`Talisman extension not found at: ${extensionDir}\n\nPlease download the extension first by running:\n npx @avalix/chroma download-extensions\n\nOr if you're using this as a dependency:\n npm run chroma:download\n`);
89
+ console.log(`✅ Found Talisman extension at: ${extensionDir}`);
90
+ return extensionDir;
91
+ }
92
+ async function importEthPrivateKey(page, { seed, name = "Test Account", password = "h3llop0lkadot!" }) {
93
+ const context = page.__extensionContext;
94
+ const extensionId = page.__extensionId;
95
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
96
+ let extensionPage = null;
97
+ const pages = context.pages();
98
+ for (const page$1 of pages) {
99
+ const url = page$1.url();
100
+ console.log(`📄 Found page: ${url}`);
101
+ if (url.includes("onboarding.html") || url.includes(`chrome-extension://${extensionId}/`)) {
102
+ extensionPage = page$1;
103
+ console.log(`✅ Found Talisman onboarding page: ${url}`);
104
+ break;
105
+ }
106
+ }
107
+ if (!extensionPage) throw new Error(`Talisman onboarding page not found`);
19
108
  try {
20
- console.log("📥 Downloading Polkadot JS extension...");
21
- const response = await fetch(POLKADOT_JS_EXTENSION);
22
- if (!response.ok) throw new Error(`Failed to download extension: ${response.status} ${response.statusText}`);
23
- const writeStream = createWriteStream(zipPath);
24
- await pipeline(response.body, writeStream);
25
- console.log("📦 Extracting extension...");
26
- await pipeline(fs.createReadStream(zipPath), Extract({ path: extensionDir }));
27
- await fs.promises.unlink(zipPath);
28
- console.log(" Polkadot extension downloaded and extracted to:", extensionDir);
29
- return extensionDir;
109
+ await extensionPage.bringToFront();
110
+ console.log("🔄 Reloading onboarding page for fresh state...");
111
+ await extensionPage.reload();
112
+ await extensionPage.waitForLoadState("domcontentloaded");
113
+ await extensionPage.getByTestId("onboarding-get-started-button").click();
114
+ await extensionPage.getByRole("textbox", { name: "Enter password" }).fill(password);
115
+ await extensionPage.getByRole("textbox", { name: "Confirm password" }).fill(password);
116
+ await extensionPage.getByTestId("onboarding-password-confirm-button").click();
117
+ await extensionPage.getByRole("button", { name: "No thanks" }).click();
118
+ await extensionPage.getByTestId("onboarding-enter-talisman-button").click();
119
+ await extensionPage.getByRole("button", { name: "Add account Create or import" }).click();
120
+ await extensionPage.getByRole("button", { name: "Import Import an existing" }).click();
121
+ await extensionPage.getByRole("button", { name: "Import via Private Key" }).click();
122
+ await extensionPage.getByRole("button", { name: "Select account platform" }).click();
123
+ await extensionPage.getByRole("option", { name: "Ethereum" }).locator("div").click();
124
+ await extensionPage.getByRole("textbox", { name: "Choose a name" }).fill(name);
125
+ await extensionPage.getByRole("textbox", { name: "Enter your private key" }).fill(seed);
126
+ await extensionPage.getByRole("button", { name: "Save" }).click();
127
+ await extensionPage.close();
128
+ console.log("✅ Talisman account import completed");
30
129
  } catch (error) {
31
- if (fs.existsSync(zipPath)) await fs.promises.unlink(zipPath).catch(() => {});
32
- if (fs.existsSync(extensionDir)) await fs.promises.rmdir(extensionDir, { recursive: true }).catch(() => {});
33
- throw new Error(`Failed to download/extract Polkadot extension: ${error instanceof Error ? error.message : String(error)}`);
130
+ console.error("❌ Error during Talisman account import:", error);
131
+ throw error;
132
+ }
133
+ }
134
+ async function authorizeTalisman(page, options = {}) {
135
+ const { accountName = "Test Account" } = options;
136
+ const context = page.__extensionContext;
137
+ const extensionId = page.__extensionId;
138
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
139
+ const extensionPopup = await findExtensionPopup(context, extensionId);
140
+ await extensionPopup.getByRole("button", { name: accountName }).click();
141
+ await extensionPopup.getByTestId("connection-connect-button").click();
142
+ try {
143
+ await (await findExtensionPopup(context, extensionId)).getByRole("button", { name: "Approve" }).click();
144
+ } catch {
145
+ console.log("No another popup found, skipping");
34
146
  }
35
147
  }
148
+ async function approveTalismanTx(page) {
149
+ const context = page.__extensionContext;
150
+ const extensionId = page.__extensionId;
151
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
152
+ await (await findExtensionPopup(context, extensionId)).getByRole("button", { name: "Approve" }).click();
153
+ }
36
154
 
37
155
  //#endregion
38
- //#region src/context-playwright/index.ts
39
- const DEFAULT_WALLET_CONFIGS = {
40
- "polkadot-js": { downloadUrl: "https://github.com/polkadot-js/extension/releases/download/v0.61.7/master-chrome-build.zip" },
41
- "talisman": {}
42
- };
43
- async function getExtensionPath(walletType, walletConfig) {
44
- const { customPath } = walletConfig;
45
- if (customPath) return customPath;
156
+ //#region src/context-playwright/types.ts
157
+ const WALLET_TYPES = ["polkadot-js", "talisman"];
158
+
159
+ //#endregion
160
+ //#region src/context-playwright/wallet-factory.ts
161
+ function createExtendedPage(page, context, extensionId) {
162
+ const extPage = page;
163
+ extPage.__extensionContext = context;
164
+ extPage.__extensionId = extensionId;
165
+ return extPage;
166
+ }
167
+ function createWalletInstance(walletType, extensionId, context) {
168
+ let importedAccountName;
169
+ const baseInstance = {
170
+ extensionId,
171
+ importMnemonic: async (options) => {
172
+ const page = context.pages()[0] || await context.newPage();
173
+ const extPage = createExtendedPage(page, context, extensionId);
174
+ importedAccountName = options.name || "Test Account";
175
+ switch (walletType) {
176
+ case "polkadot-js":
177
+ await importPolkadotJSAccount(extPage, options);
178
+ break;
179
+ case "talisman": throw new Error("Talisman importMnemonic is not yet implemented.");
180
+ default: throw new Error(`Unsupported wallet type: ${walletType}`);
181
+ }
182
+ },
183
+ authorize: async (options = {}) => {
184
+ const page = context.pages()[0] || await context.newPage();
185
+ const extPage = createExtendedPage(page, context, extensionId);
186
+ const accountName = options.accountName || importedAccountName;
187
+ switch (walletType) {
188
+ case "polkadot-js":
189
+ await authorizePolkadotJS(extPage);
190
+ break;
191
+ case "talisman":
192
+ await authorizeTalisman(extPage, { accountName });
193
+ break;
194
+ default: throw new Error(`Unsupported wallet type: ${walletType}`);
195
+ }
196
+ },
197
+ approveTx: async (options = {}) => {
198
+ const page = context.pages()[0] || await context.newPage();
199
+ const extPage = createExtendedPage(page, context, extensionId);
200
+ switch (walletType) {
201
+ case "polkadot-js":
202
+ await approvePolkadotJSTx(extPage, options);
203
+ break;
204
+ case "talisman":
205
+ await approveTalismanTx(extPage);
206
+ break;
207
+ default: throw new Error(`Unsupported wallet type: ${walletType}`);
208
+ }
209
+ }
210
+ };
46
211
  switch (walletType) {
47
- case "polkadot-js": return await downloadAndExtractPolkadotExtension();
48
- case "talisman": throw new Error("Talisman wallet download not implemented yet");
212
+ case "polkadot-js": return {
213
+ ...baseInstance,
214
+ type: "polkadot-js"
215
+ };
216
+ case "talisman": return {
217
+ ...baseInstance,
218
+ type: "talisman",
219
+ importEthPrivateKey: async (options) => {
220
+ const page = context.pages()[0] || await context.newPage();
221
+ const extPage = createExtendedPage(page, context, extensionId);
222
+ importedAccountName = options.name || "Test Account";
223
+ await importEthPrivateKey(extPage, {
224
+ seed: options.privateKey,
225
+ name: options.name,
226
+ password: options.password
227
+ });
228
+ }
229
+ };
49
230
  default: throw new Error(`Unsupported wallet type: ${walletType}`);
50
231
  }
51
232
  }
52
- async function findExtensionPopup(context, extensionId) {
53
- const pages = context.pages();
54
- for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) return p;
55
- throw new Error(`Extension popup not found for ID: ${extensionId}`);
233
+
234
+ //#endregion
235
+ //#region src/context-playwright/index.ts
236
+ async function getExtensionPathForWallet(config) {
237
+ const { type } = config;
238
+ switch (type) {
239
+ case "polkadot-js": return await getPolkadotJSExtensionPath();
240
+ case "talisman": return await getTalismanExtensionPath();
241
+ default: throw new Error(`Unsupported wallet type: ${type}`);
242
+ }
56
243
  }
57
244
  function createWalletTest(options = {}) {
58
- const { walletType = "polkadot-js", walletConfig, headless = false, slowMo = 150 } = options;
59
- const finalWalletConfig = walletConfig || DEFAULT_WALLET_CONFIGS[walletType];
245
+ const { headless = false, slowMo = 150 } = options;
246
+ const walletConfigs = options.wallets && options.wallets.length > 0 ? options.wallets : [{ type: "polkadot-js" }];
247
+ const isMultiWallet = walletConfigs.length > 1;
60
248
  return test$1.extend({
61
- walletType: async ({}, use) => {
62
- await use(walletType);
63
- },
64
- walletConfig: async ({}, use) => {
65
- await use(finalWalletConfig);
66
- },
67
- page: async ({}, use) => {
68
- const extensionPath = await getExtensionPath(walletType, finalWalletConfig);
249
+ walletContext: [async ({}, use) => {
250
+ const extensionPathsString = (await Promise.all(walletConfigs.map((config) => getExtensionPathForWallet(config)))).join(",");
69
251
  const context = await chromium.launchPersistentContext("", {
70
252
  headless,
71
253
  channel: "chromium",
72
- args: [`--load-extension=${extensionPath}`, `--disable-extensions-except=${extensionPath}`],
254
+ args: [`--load-extension=${extensionPathsString}`, `--disable-extensions-except=${extensionPathsString}`],
73
255
  slowMo
74
256
  });
75
- const page = context.pages()[0] || await context.newPage();
76
- const extendedPage = page;
77
- extendedPage.__extensionContext = context;
78
- let [background] = context.serviceWorkers();
79
- if (!background) background = await context.waitForEvent("serviceworker");
80
- extendedPage.__extensionId = background.url().split("/")[2];
81
- await use(page);
257
+ await use(context);
82
258
  await context.close();
259
+ }, { scope: "worker" }],
260
+ walletExtensionIds: [async ({ walletContext }, use) => {
261
+ const extensionIds = /* @__PURE__ */ new Map();
262
+ if (walletContext.serviceWorkers().length === 0) await walletContext.waitForEvent("serviceworker");
263
+ if (isMultiWallet) await new Promise((resolve) => setTimeout(resolve, 1e3));
264
+ const allServiceWorkers = walletContext.serviceWorkers();
265
+ for (let i = 0; i < walletConfigs.length && i < allServiceWorkers.length; i++) {
266
+ const extensionId = allServiceWorkers[i].url().split("/")[2];
267
+ const walletType = walletConfigs[i].type;
268
+ extensionIds.set(walletType, extensionId);
269
+ console.log(`✅ Loaded ${walletType} extension with ID: ${extensionId}`);
270
+ }
271
+ await use(extensionIds);
272
+ }, { scope: "worker" }],
273
+ page: async ({ walletContext, walletExtensionIds }, use) => {
274
+ const extendedPage = walletContext.pages()[0] || await walletContext.newPage();
275
+ extendedPage.__extensionContext = walletContext;
276
+ extendedPage.__walletExtensionIds = walletExtensionIds;
277
+ await use(extendedPage);
83
278
  },
84
- importAccount: async ({ page }, use) => {
85
- const importAccount = async ({ seed, name = "Test Account", password = "h3llop0lkadot!" }) => {
86
- const context = page.__extensionContext;
87
- const extensionPopupUrl = `chrome-extension://${page.__extensionId}/index.html`;
88
- const extensionPage = await context.newPage();
89
- try {
90
- await extensionPage.goto(extensionPopupUrl);
91
- const understoodButton = extensionPage.getByRole("button", { name: "Understood, let me continue" });
92
- if (await understoodButton.count() > 0) {
93
- await understoodButton.click();
94
- await extensionPage.waitForTimeout(100);
95
- }
96
- await extensionPage.goto(`${extensionPopupUrl}#/account/import-seed`);
97
- await extensionPage.locator("textarea").fill(seed);
98
- await extensionPage.locator("button:has-text(\"Next\")").click();
99
- await extensionPage.locator("input[type=\"text\"]").fill(name);
100
- await extensionPage.locator("input[type=\"password\"]").fill(password);
101
- await extensionPage.locator("div").filter({ hasText: /^Repeat password for verification$/ }).getByRole("textbox").fill(password);
102
- await extensionPage.getByRole("button", { name: "Add the account with the supplied seed" }).click();
103
- console.log(`✅ Created wallet account: ${name}`);
104
- } finally {
105
- await extensionPage.close();
106
- }
107
- };
108
- await use(importAccount);
109
- },
110
- authorize: async ({ page }, use) => {
111
- const authorize = async () => {
112
- const context = page.__extensionContext;
113
- const extensionId = page.__extensionId;
114
- await new Promise((resolve) => setTimeout(resolve, 1e3));
115
- const extensionPopup = await findExtensionPopup(context, extensionId);
116
- await extensionPopup.getByText("Select all").click();
117
- await extensionPopup.getByRole("button", { name: /Connect \d+ account\(s\)/ }).click();
118
- console.log("✅ Wallet connected successfully");
119
- };
120
- await use(authorize);
121
- },
122
- approveTx: async ({ page }, use) => {
123
- const approveTx = async (options$1 = {}) => {
124
- const { password = "h3llop0lkadot!" } = options$1;
125
- const context = page.__extensionContext;
126
- const extensionId = page.__extensionId;
127
- await new Promise((resolve) => setTimeout(resolve, 1e3));
128
- const extensionPopup = await findExtensionPopup(context, extensionId);
129
- await extensionPopup.getByRole("textbox").fill(password);
130
- await extensionPopup.getByRole("button", { name: "Sign the transaction" }).click();
131
- console.log("✅ Transaction signed successfully");
132
- };
133
- await use(approveTx);
279
+ wallets: async ({ walletContext, walletExtensionIds }, use) => {
280
+ const walletMap = {};
281
+ for (const [walletType, extensionId] of walletExtensionIds) if (WALLET_TYPES.includes(walletType)) walletMap[walletType] = createWalletInstance(walletType, extensionId, walletContext);
282
+ await use(walletMap);
134
283
  }
135
284
  });
136
285
  }
137
286
  const test = createWalletTest();
138
287
 
139
288
  //#endregion
140
- export { createWalletTest, downloadAndExtractPolkadotExtension, expect, test };
289
+ export { createWalletTest, expect, test };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@avalix/chroma",
3
3
  "type": "module",
4
- "version": "0.0.6",
4
+ "version": "0.0.7",
5
5
  "description": "End-to-end testing library for Polkadot wallet interactions",
6
6
  "author": "Avalix Labs",
7
7
  "license": "MIT",
@@ -30,14 +30,19 @@
30
30
  "main": "./dist/index.js",
31
31
  "module": "./dist/index.js",
32
32
  "types": "./dist/index.d.ts",
33
+ "bin": {
34
+ "chroma": "./scripts/cli.js"
35
+ },
33
36
  "files": [
34
- "dist"
37
+ "dist",
38
+ "scripts"
35
39
  ],
36
40
  "scripts": {
37
41
  "build": "tsdown",
38
42
  "dev": "tsdown --watch",
39
43
  "lint": "eslint --fix .",
40
- "prepublishOnly": "npm run build"
44
+ "prepublishOnly": "npm run build",
45
+ "download-extensions": "rm -rf .chroma && tsx scripts/download-extensions.ts"
41
46
  },
42
47
  "peerDependencies": {
43
48
  "@playwright/test": "^1.55.0"
@@ -51,7 +56,8 @@
51
56
  "@types/node": "^24.3.3",
52
57
  "@types/unzipper": "^0.10.10",
53
58
  "eslint": "^9.35.0",
54
- "tsdown": "latest"
59
+ "tsdown": "latest",
60
+ "tsx": "^4.20.6"
55
61
  },
56
62
  "publishConfig": {
57
63
  "access": "public"
package/scripts/cli.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ const command = process.argv[2]
11
+
12
+ if (command === 'download-extensions') {
13
+ const scriptPath = path.join(__dirname, 'download-extensions.ts')
14
+
15
+ // Use tsx to run TypeScript file
16
+ const child = spawn('npx', ['tsx', scriptPath], {
17
+ stdio: 'inherit',
18
+ shell: true,
19
+ })
20
+
21
+ child.on('exit', (code) => {
22
+ process.exit(code || 0)
23
+ })
24
+ }
25
+ else {
26
+ console.log('Unknown command:', command)
27
+ console.log('\nAvailable commands:')
28
+ console.log(' download-extensions - Download wallet extensions for testing')
29
+ process.exit(1)
30
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process'
3
+ import { downloadAndExtractExtension } from '../src/utils/download-extension.js'
4
+ import { POLKADOT_JS_CONFIG } from '../src/wallets/polkadot-js.js'
5
+ import { TALISMAN_CONFIG } from '../src/wallets/talisman.js'
6
+
7
+ async function main() {
8
+ console.log('🚀 Downloading Chroma wallet extensions...\n')
9
+
10
+ try {
11
+ // Download Polkadot-JS extension
12
+ await downloadAndExtractExtension({
13
+ downloadUrl: POLKADOT_JS_CONFIG.downloadUrl,
14
+ extensionName: POLKADOT_JS_CONFIG.extensionName,
15
+ })
16
+
17
+ // Download Talisman extension
18
+ await downloadAndExtractExtension({
19
+ downloadUrl: TALISMAN_CONFIG.downloadUrl,
20
+ extensionName: TALISMAN_CONFIG.extensionName,
21
+ })
22
+
23
+ console.log('\n✅ All extensions downloaded successfully!')
24
+ console.log('You can now run your Playwright tests.')
25
+ }
26
+ catch (error) {
27
+ console.error('\n❌ Failed to download extensions:', error instanceof Error ? error.message : String(error))
28
+ process.exit(1)
29
+ }
30
+ }
31
+
32
+ main()