@avalix/chroma 0.0.7 → 0.0.9

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/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ interface BaseWalletInstance {
21
21
  approveTx: (options?: {
22
22
  password?: string;
23
23
  }) => Promise<void>;
24
+ rejectTx: () => Promise<void>;
24
25
  }
25
26
  interface PolkadotJsWalletInstance extends BaseWalletInstance {
26
27
  type: 'polkadot-js';
package/dist/index.js CHANGED
@@ -9,14 +9,22 @@ const POLKADOT_JS_CONFIG = {
9
9
  extensionName: "polkadot-extension-0.61.7"
10
10
  };
11
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;
12
+ const maxAttempts = 10;
13
+ const retryDelay = 500;
14
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
15
+ const pages = context.pages();
16
+ for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
17
+ await p.waitForLoadState("domcontentloaded");
18
+ return p;
19
+ }
20
+ if (attempt < maxAttempts - 1) await new Promise((resolve) => setTimeout(resolve, retryDelay));
21
+ }
14
22
  throw new Error(`Extension popup not found for ID: ${extensionId}`);
15
23
  }
16
24
  async function getPolkadotJSExtensionPath() {
17
25
  const extensionsDir = path.resolve(process.cwd(), ".chroma");
18
26
  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`);
27
+ 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`);
20
28
  console.log(`✅ Found Polkadot-JS extension at: ${extensionDir}`);
21
29
  return extensionDir;
22
30
  }
@@ -46,7 +54,6 @@ async function importPolkadotJSAccount(page, { seed, name = "Test Account", pass
46
54
  async function authorizePolkadotJS(page) {
47
55
  const context = page.__extensionContext;
48
56
  const extensionId = page.__extensionId;
49
- await new Promise((resolve) => setTimeout(resolve, 1e3));
50
57
  const extensionPopup = await findExtensionPopup$1(context, extensionId);
51
58
  await extensionPopup.getByText("Select all").click();
52
59
  await extensionPopup.getByRole("button", { name: /Connect \d+ account\(s\)/ }).click();
@@ -56,12 +63,17 @@ async function approvePolkadotJSTx(page, options = {}) {
56
63
  const { password = "h3llop0lkadot!" } = options;
57
64
  const context = page.__extensionContext;
58
65
  const extensionId = page.__extensionId;
59
- await new Promise((resolve) => setTimeout(resolve, 1e3));
60
66
  const extensionPopup = await findExtensionPopup$1(context, extensionId);
61
67
  await extensionPopup.getByRole("textbox").fill(password);
62
68
  await extensionPopup.getByRole("button", { name: "Sign the transaction" }).click();
63
69
  console.log("✅ Polkadot-JS transaction signed successfully");
64
70
  }
71
+ async function rejectPolkadotJSTx(page) {
72
+ const context = page.__extensionContext;
73
+ const extensionId = page.__extensionId;
74
+ await (await findExtensionPopup$1(context, extensionId)).getByRole("link", { name: "Cancel" }).click();
75
+ console.log("✅ Polkadot-JS transaction rejected successfully");
76
+ }
65
77
 
66
78
  //#endregion
67
79
  //#region src/wallets/talisman.ts
@@ -70,45 +82,55 @@ const TALISMAN_CONFIG = {
70
82
  extensionName: "talisman-extension-3.0.5"
71
83
  };
72
84
  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;
85
+ const maxAttempts = 10;
86
+ const retryDelay = 500;
87
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
88
+ const pages = context.pages();
89
+ for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
90
+ await p.setViewportSize({
91
+ width: 400,
92
+ height: 600
93
+ });
94
+ await p.waitForLoadState("domcontentloaded");
95
+ return p;
96
+ }
97
+ if (attempt < maxAttempts - 1) await new Promise((resolve) => setTimeout(resolve, retryDelay));
82
98
  }
83
99
  throw new Error(`Extension popup not found for ID: ${extensionId}`);
84
100
  }
85
101
  async function getTalismanExtensionPath() {
86
102
  const extensionsDir = path.resolve(process.cwd(), ".chroma");
87
103
  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`);
104
+ 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`);
89
105
  console.log(`✅ Found Talisman extension at: ${extensionDir}`);
90
106
  return extensionDir;
91
107
  }
92
108
  async function importEthPrivateKey(page, { seed, name = "Test Account", password = "h3llop0lkadot!" }) {
93
109
  const context = page.__extensionContext;
94
110
  const extensionId = page.__extensionId;
95
- await new Promise((resolve) => setTimeout(resolve, 2e3));
111
+ const maxAttempts = 20;
112
+ const retryDelay = 500;
96
113
  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;
114
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
115
+ const pages = context.pages();
116
+ for (const p of pages) {
117
+ const url = p.url();
118
+ console.log(`📄 Found page: ${url}`);
119
+ if (url.includes("onboarding.html") || url.includes(`chrome-extension://${extensionId}/`)) {
120
+ extensionPage = p;
121
+ console.log(`✅ Found Talisman onboarding page: ${url}`);
122
+ break;
123
+ }
124
+ }
125
+ if (extensionPage) break;
126
+ if (attempt < maxAttempts - 1) {
127
+ console.log(`⏳ Attempt ${attempt + 1}/${maxAttempts}: Waiting for onboarding page...`);
128
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
105
129
  }
106
130
  }
107
- if (!extensionPage) throw new Error(`Talisman onboarding page not found`);
131
+ if (!extensionPage) throw new Error(`Talisman onboarding page not found after ${maxAttempts} attempts`);
108
132
  try {
109
133
  await extensionPage.bringToFront();
110
- console.log("🔄 Reloading onboarding page for fresh state...");
111
- await extensionPage.reload();
112
134
  await extensionPage.waitForLoadState("domcontentloaded");
113
135
  await extensionPage.getByTestId("onboarding-get-started-button").click();
114
136
  await extensionPage.getByRole("textbox", { name: "Enter password" }).fill(password);
@@ -135,7 +157,6 @@ async function authorizeTalisman(page, options = {}) {
135
157
  const { accountName = "Test Account" } = options;
136
158
  const context = page.__extensionContext;
137
159
  const extensionId = page.__extensionId;
138
- await new Promise((resolve) => setTimeout(resolve, 1e3));
139
160
  const extensionPopup = await findExtensionPopup(context, extensionId);
140
161
  await extensionPopup.getByRole("button", { name: accountName }).click();
141
162
  await extensionPopup.getByTestId("connection-connect-button").click();
@@ -148,9 +169,13 @@ async function authorizeTalisman(page, options = {}) {
148
169
  async function approveTalismanTx(page) {
149
170
  const context = page.__extensionContext;
150
171
  const extensionId = page.__extensionId;
151
- await new Promise((resolve) => setTimeout(resolve, 1e3));
152
172
  await (await findExtensionPopup(context, extensionId)).getByRole("button", { name: "Approve" }).click();
153
173
  }
174
+ async function rejectTalismanTx(page) {
175
+ const context = page.__extensionContext;
176
+ const extensionId = page.__extensionId;
177
+ await (await findExtensionPopup(context, extensionId)).getByTestId("connection-reject-button").click();
178
+ }
154
179
 
155
180
  //#endregion
156
181
  //#region src/context-playwright/types.ts
@@ -206,6 +231,19 @@ function createWalletInstance(walletType, extensionId, context) {
206
231
  break;
207
232
  default: throw new Error(`Unsupported wallet type: ${walletType}`);
208
233
  }
234
+ },
235
+ rejectTx: async () => {
236
+ const page = context.pages()[0] || await context.newPage();
237
+ const extPage = createExtendedPage(page, context, extensionId);
238
+ switch (walletType) {
239
+ case "polkadot-js":
240
+ await rejectPolkadotJSTx(extPage);
241
+ break;
242
+ case "talisman":
243
+ await rejectTalismanTx(extPage);
244
+ break;
245
+ default: throw new Error(`Unsupported wallet type: ${walletType}`);
246
+ }
209
247
  }
210
248
  };
211
249
  switch (walletType) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@avalix/chroma",
3
3
  "type": "module",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
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,84 @@
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
+ rejectTx: () => Promise<void>
29
+ }
30
+
31
+ // Polkadot-JS specific wallet instance
32
+ export interface PolkadotJsWalletInstance extends BaseWalletInstance {
33
+ type: 'polkadot-js'
34
+ }
35
+
36
+ // Talisman specific wallet instance (with additional methods)
37
+ export interface TalismanWalletInstance extends BaseWalletInstance {
38
+ type: 'talisman'
39
+ importEthPrivateKey: (options: { privateKey: string, name?: string, password?: string }) => Promise<void>
40
+ }
41
+
42
+ // Union type of all wallet instances
43
+ export type WalletInstance = PolkadotJsWalletInstance | TalismanWalletInstance
44
+
45
+ // Map wallet type to its instance
46
+ export interface WalletTypeMap {
47
+ 'polkadot-js': PolkadotJsWalletInstance
48
+ 'talisman': TalismanWalletInstance
49
+ }
50
+
51
+ // Wallets collection - all wallet types
52
+ export type Wallets = WalletTypeMap
53
+
54
+ // Helper type to build a wallets object based on configured wallet types
55
+ export type ConfiguredWallets<T extends readonly WalletConfig[]> = {
56
+ [K in T[number]['type']]: WalletTypeMap[K]
57
+ }
58
+
59
+ // Extended page with wallet context
60
+ export type ExtendedPage = Page & {
61
+ __extensionContext: BrowserContext
62
+ __walletExtensionIds: Map<string, string>
63
+ }
64
+
65
+ // Complete test configuration - supports single and multi-wallet
66
+ export interface ChromaTestOptions<T extends readonly WalletConfig[] = WalletConfig[]> {
67
+ // Wallet configuration (single or multiple)
68
+ wallets?: T
69
+ // Common options
70
+ headless?: boolean
71
+ slowMo?: number
72
+ }
73
+
74
+ // Test fixtures (test-scoped: recreated per test)
75
+ export interface WalletFixtures<W = Wallets> {
76
+ page: ExtendedPage
77
+ wallets: W
78
+ }
79
+
80
+ // Worker fixtures (worker-scoped: persisted across tests)
81
+ export interface WalletWorkerFixtures {
82
+ walletContext: BrowserContext
83
+ walletExtensionIds: Map<string, string>
84
+ }
@@ -0,0 +1,140 @@
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
+ rejectPolkadotJSTx,
14
+ } from '../wallets/polkadot-js.js'
15
+ import {
16
+ approveTalismanTx,
17
+ authorizeTalisman,
18
+ importEthPrivateKey,
19
+ rejectTalismanTx,
20
+ } from '../wallets/talisman.js'
21
+
22
+ // Helper to create extended page with wallet context
23
+ function createExtendedPage(page: Page, context: BrowserContext, extensionId: string) {
24
+ const extPage = page as Page & { __extensionContext: BrowserContext, __extensionId: string }
25
+ extPage.__extensionContext = context
26
+ extPage.__extensionId = extensionId
27
+ return extPage
28
+ }
29
+
30
+ // Create wallet instance helper with proper typing
31
+ export function createWalletInstance(
32
+ walletType: string,
33
+ extensionId: string,
34
+ context: BrowserContext,
35
+ ): WalletInstance {
36
+ // Store the imported account name for later use
37
+ let importedAccountName: string | undefined
38
+
39
+ // Common methods for all wallets
40
+ const baseInstance: BaseWalletInstance = {
41
+ extensionId,
42
+ importMnemonic: async (options: WalletAccount) => {
43
+ const page = context.pages()[0] || await context.newPage()
44
+ const extPage = createExtendedPage(page, context, extensionId)
45
+
46
+ // Store the account name for future authorize calls
47
+ importedAccountName = options.name || 'Test Account'
48
+
49
+ switch (walletType) {
50
+ case 'polkadot-js':
51
+ await importPolkadotJSAccount(extPage, options)
52
+ break
53
+ case 'talisman':
54
+ throw new Error('Talisman importMnemonic is not yet implemented.')
55
+ default:
56
+ throw new Error(`Unsupported wallet type: ${walletType}`)
57
+ }
58
+ },
59
+ authorize: async (options: { accountName?: string } = {}) => {
60
+ const page = context.pages()[0] || await context.newPage()
61
+ const extPage = createExtendedPage(page, context, extensionId)
62
+
63
+ // Use provided account name or fall back to the imported one
64
+ const accountName = options.accountName || importedAccountName
65
+
66
+ switch (walletType) {
67
+ case 'polkadot-js':
68
+ await authorizePolkadotJS(extPage)
69
+ break
70
+ case 'talisman':
71
+ await authorizeTalisman(extPage, { accountName })
72
+ break
73
+ default:
74
+ throw new Error(`Unsupported wallet type: ${walletType}`)
75
+ }
76
+ },
77
+ approveTx: async (options: { password?: string } = {}) => {
78
+ const page = context.pages()[0] || await context.newPage()
79
+ const extPage = createExtendedPage(page, context, extensionId)
80
+
81
+ switch (walletType) {
82
+ case 'polkadot-js':
83
+ await approvePolkadotJSTx(extPage, options)
84
+ break
85
+ case 'talisman':
86
+ await approveTalismanTx(extPage)
87
+ break
88
+ default:
89
+ throw new Error(`Unsupported wallet type: ${walletType}`)
90
+ }
91
+ },
92
+ rejectTx: async () => {
93
+ const page = context.pages()[0] || await context.newPage()
94
+ const extPage = createExtendedPage(page, context, extensionId)
95
+
96
+ switch (walletType) {
97
+ case 'polkadot-js':
98
+ await rejectPolkadotJSTx(extPage)
99
+ break
100
+ case 'talisman':
101
+ await rejectTalismanTx(extPage)
102
+ break
103
+ default:
104
+ throw new Error(`Unsupported wallet type: ${walletType}`)
105
+ }
106
+ },
107
+ }
108
+
109
+ // Return wallet-specific instance with type discriminator
110
+ switch (walletType) {
111
+ case 'polkadot-js':
112
+ return {
113
+ ...baseInstance,
114
+ type: 'polkadot-js',
115
+ } as PolkadotJsWalletInstance
116
+
117
+ case 'talisman':
118
+ return {
119
+ ...baseInstance,
120
+ type: 'talisman',
121
+ importEthPrivateKey: async (options: { privateKey: string, name?: string, password?: string }) => {
122
+ const page = context.pages()[0] || await context.newPage()
123
+ const extPage = createExtendedPage(page, context, extensionId)
124
+
125
+ // Store the account name for future authorize calls
126
+ importedAccountName = options.name || 'Test Account'
127
+
128
+ // Use the seed property to pass the private key
129
+ await importEthPrivateKey(extPage, {
130
+ seed: options.privateKey,
131
+ name: options.name,
132
+ password: options.password,
133
+ })
134
+ },
135
+ } as TalismanWalletInstance
136
+
137
+ default:
138
+ throw new Error(`Unsupported wallet type: ${walletType}`)
139
+ }
140
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Main entry point for chroma package
2
+ export { createWalletTest, expect, test } from './context-playwright'
@@ -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,135 @@
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
+ // Wait for extension popup to appear with retry logic
16
+ const maxAttempts = 10
17
+ const retryDelay = 500
18
+
19
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
20
+ const pages = context.pages()
21
+ for (const p of pages) {
22
+ if (p.url().includes(`chrome-extension://${extensionId}/`)) {
23
+ await p.waitForLoadState('domcontentloaded')
24
+ return p
25
+ }
26
+ }
27
+
28
+ // If not found, wait a bit before retrying
29
+ if (attempt < maxAttempts - 1) {
30
+ await new Promise(resolve => setTimeout(resolve, retryDelay))
31
+ }
32
+ }
33
+
34
+ throw new Error(`Extension popup not found for ID: ${extensionId}`)
35
+ }
36
+
37
+ // Get Polkadot-JS extension path
38
+ export async function getPolkadotJSExtensionPath(): Promise<string> {
39
+ const extensionsDir = path.resolve(process.cwd(), '.chroma')
40
+ const extensionDir = path.join(extensionsDir, POLKADOT_JS_CONFIG.extensionName)
41
+
42
+ // Check if extension exists
43
+ if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) {
44
+ throw new Error(
45
+ `Polkadot-JS extension not found at: ${extensionDir}\n\n`
46
+ + `Please download the extension first by running:\n`
47
+ + ` npx @avalix/chroma download-extensions\n`,
48
+ )
49
+ }
50
+
51
+ console.log(`✅ Found Polkadot-JS extension at: ${extensionDir}`)
52
+ return extensionDir
53
+ }
54
+
55
+ // Polkadot-JS specific account import implementation
56
+ export async function importPolkadotJSAccount(
57
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
58
+ { seed, name = 'Test Account', password = 'h3llop0lkadot!' }: WalletAccount,
59
+ ): Promise<void> {
60
+ const context = page.__extensionContext
61
+ const extensionId = page.__extensionId
62
+
63
+ const extensionPopupUrl = `chrome-extension://${extensionId}/index.html`
64
+ const extensionPage = await context.newPage()
65
+
66
+ try {
67
+ await extensionPage.goto(extensionPopupUrl)
68
+
69
+ // Handle "Understood, let me continue" button if it exists
70
+ const understoodButton = extensionPage.getByRole('button', { name: 'Understood, let me continue' })
71
+ if (await understoodButton.count() > 0) {
72
+ await understoodButton.click()
73
+ await extensionPage.waitForTimeout(100)
74
+ }
75
+
76
+ // Navigate to import seed page
77
+ await extensionPage.goto(`${extensionPopupUrl}#/account/import-seed`)
78
+
79
+ // Fill seed phrase and account details
80
+ await extensionPage.locator('textarea').fill(seed)
81
+ await extensionPage.locator('button:has-text("Next")').click()
82
+ await extensionPage.locator('input[type="text"]').fill(name)
83
+ await extensionPage.locator('input[type="password"]').fill(password)
84
+ await extensionPage.locator('div').filter({ hasText: /^Repeat password for verification$/ }).getByRole('textbox').fill(password)
85
+ await extensionPage.getByRole('button', { name: 'Add the account with the supplied seed' }).click()
86
+
87
+ console.log(`✅ Created Polkadot-JS wallet account: ${name}`)
88
+ }
89
+ finally {
90
+ await extensionPage.close()
91
+ }
92
+ }
93
+
94
+ // Polkadot-JS specific authorization implementation
95
+ export async function authorizePolkadotJS(
96
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
97
+ ): Promise<void> {
98
+ const context = page.__extensionContext
99
+ const extensionId = page.__extensionId
100
+
101
+ const extensionPopup = await findExtensionPopup(context, extensionId)
102
+ await extensionPopup.getByText('Select all').click()
103
+ await extensionPopup.getByRole('button', { name: /Connect \d+ account\(s\)/ }).click()
104
+
105
+ console.log('✅ Polkadot-JS wallet connected successfully')
106
+ }
107
+
108
+ // Polkadot-JS specific transaction approval implementation
109
+ export async function approvePolkadotJSTx(
110
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
111
+ options: { password?: string } = {},
112
+ ): Promise<void> {
113
+ const { password = 'h3llop0lkadot!' } = options
114
+ const context = page.__extensionContext
115
+ const extensionId = page.__extensionId
116
+
117
+ const extensionPopup = await findExtensionPopup(context, extensionId)
118
+ await extensionPopup.getByRole('textbox').fill(password)
119
+ await extensionPopup.getByRole('button', { name: 'Sign the transaction' }).click()
120
+
121
+ console.log('✅ Polkadot-JS transaction signed successfully')
122
+ }
123
+
124
+ // Polkadot-JS specific transaction rejection implementation
125
+ export async function rejectPolkadotJSTx(
126
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
127
+ ): Promise<void> {
128
+ const context = page.__extensionContext
129
+ const extensionId = page.__extensionId
130
+
131
+ const extensionPopup = await findExtensionPopup(context, extensionId)
132
+ await extensionPopup.getByRole('link', { name: 'Cancel' }).click()
133
+
134
+ console.log('✅ Polkadot-JS transaction rejected successfully')
135
+ }
@@ -0,0 +1,180 @@
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
+ // Wait for extension popup to appear with retry logic
16
+ const maxAttempts = 10
17
+ const retryDelay = 500
18
+
19
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
20
+ const pages = context.pages()
21
+ for (const p of pages) {
22
+ if (p.url().includes(`chrome-extension://${extensionId}/`)) {
23
+ await p.setViewportSize({ width: 400, height: 600 })
24
+ await p.waitForLoadState('domcontentloaded')
25
+ return p
26
+ }
27
+ }
28
+
29
+ // If not found, wait a bit before retrying
30
+ if (attempt < maxAttempts - 1) {
31
+ await new Promise(resolve => setTimeout(resolve, retryDelay))
32
+ }
33
+ }
34
+
35
+ throw new Error(`Extension popup not found for ID: ${extensionId}`)
36
+ }
37
+
38
+ // Get Talisman extension path
39
+ export async function getTalismanExtensionPath(): Promise<string> {
40
+ const extensionsDir = path.resolve(process.cwd(), '.chroma')
41
+ const extensionDir = path.join(extensionsDir, TALISMAN_CONFIG.extensionName)
42
+
43
+ // Check if extension exists
44
+ if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) {
45
+ throw new Error(
46
+ `Talisman extension not found at: ${extensionDir}\n\n`
47
+ + `Please download the extension first by running:\n`
48
+ + ` npx @avalix/chroma download-extensions\n`,
49
+ )
50
+ }
51
+
52
+ console.log(`✅ Found Talisman extension at: ${extensionDir}`)
53
+ return extensionDir
54
+ }
55
+
56
+ // Talisman specific Ethereum private key import implementation
57
+ export async function importEthPrivateKey(
58
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
59
+ { seed, name = 'Test Account', password = 'h3llop0lkadot!' }: WalletAccount,
60
+ ): Promise<void> {
61
+ const context = page.__extensionContext
62
+ const extensionId = page.__extensionId
63
+
64
+ // Wait for Talisman to open its onboarding tab with retry logic
65
+ const maxAttempts = 20
66
+ const retryDelay = 500
67
+ let extensionPage: Page | null = null
68
+
69
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
70
+ const pages = context.pages()
71
+
72
+ for (const p of pages) {
73
+ const url = p.url()
74
+ console.log(`📄 Found page: ${url}`)
75
+ if (url.includes('onboarding.html') || url.includes(`chrome-extension://${extensionId}/`)) {
76
+ extensionPage = p
77
+ console.log(`✅ Found Talisman onboarding page: ${url}`)
78
+ break
79
+ }
80
+ }
81
+
82
+ if (extensionPage) {
83
+ break
84
+ }
85
+
86
+ // If not found, wait before retrying
87
+ if (attempt < maxAttempts - 1) {
88
+ console.log(`⏳ Attempt ${attempt + 1}/${maxAttempts}: Waiting for onboarding page...`)
89
+ await new Promise(resolve => setTimeout(resolve, retryDelay))
90
+ }
91
+ }
92
+
93
+ if (!extensionPage) {
94
+ throw new Error(`Talisman onboarding page not found after ${maxAttempts} attempts`)
95
+ }
96
+
97
+ try {
98
+ // Bring the onboarding page to front
99
+ await extensionPage.bringToFront()
100
+
101
+ // Wait for the page to load and become interactive
102
+ await extensionPage.waitForLoadState('domcontentloaded')
103
+
104
+ // Click the get started button
105
+ await extensionPage.getByTestId('onboarding-get-started-button').click()
106
+
107
+ // Fill the password
108
+ await extensionPage.getByRole('textbox', { name: 'Enter password' }).fill(password!)
109
+ await extensionPage.getByRole('textbox', { name: 'Confirm password' }).fill(password!)
110
+ await extensionPage.getByTestId('onboarding-password-confirm-button').click()
111
+
112
+ // Click the no thanks button
113
+ await extensionPage.getByRole('button', { name: 'No thanks' }).click()
114
+ await extensionPage.getByTestId('onboarding-enter-talisman-button').click()
115
+
116
+ // Import Ethereum account
117
+ await extensionPage.getByRole('button', { name: 'Add account Create or import' }).click()
118
+ await extensionPage.getByRole('button', { name: 'Import Import an existing' }).click()
119
+ await extensionPage.getByRole('button', { name: 'Import via Private Key' }).click()
120
+ await extensionPage.getByRole('button', { name: 'Select account platform' }).click()
121
+ await extensionPage.getByRole('option', { name: 'Ethereum' }).locator('div').click()
122
+ await extensionPage.getByRole('textbox', { name: 'Choose a name' }).fill(name!)
123
+ await extensionPage.getByRole('textbox', { name: 'Enter your private key' }).fill(seed!)
124
+ await extensionPage.getByRole('button', { name: 'Save' }).click()
125
+
126
+ await extensionPage.close()
127
+
128
+ console.log('✅ Talisman account import completed')
129
+ }
130
+ catch (error) {
131
+ console.error('❌ Error during Talisman account import:', error)
132
+ throw error
133
+ }
134
+ }
135
+
136
+ // Talisman specific authorization implementation
137
+ export async function authorizeTalisman(
138
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
139
+ options: { accountName?: string } = {},
140
+ ): Promise<void> {
141
+ const { accountName = 'Test Account' } = options
142
+ const context = page.__extensionContext
143
+ const extensionId = page.__extensionId
144
+
145
+ const extensionPopup = await findExtensionPopup(context, extensionId)
146
+
147
+ // Authorize Talisman account
148
+ await extensionPopup.getByRole('button', { name: accountName }).click()
149
+ await extensionPopup.getByTestId('connection-connect-button').click()
150
+
151
+ try {
152
+ const anotherPopup = await findExtensionPopup(context, extensionId)
153
+ await anotherPopup.getByRole('button', { name: 'Approve' }).click()
154
+ }
155
+ catch {
156
+ console.log('No another popup found, skipping')
157
+ }
158
+ }
159
+
160
+ // Talisman specific transaction approval implementation
161
+ export async function approveTalismanTx(
162
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
163
+ ): Promise<void> {
164
+ const context = page.__extensionContext
165
+ const extensionId = page.__extensionId
166
+
167
+ const extensionPopup = await findExtensionPopup(context, extensionId)
168
+ await extensionPopup.getByRole('button', { name: 'Approve' }).click()
169
+ }
170
+
171
+ // Talisman specific transaction rejection implementation
172
+ export async function rejectTalismanTx(
173
+ page: Page & { __extensionContext: BrowserContext, __extensionId: string },
174
+ ): Promise<void> {
175
+ const context = page.__extensionContext
176
+ const extensionId = page.__extensionId
177
+
178
+ const extensionPopup = await findExtensionPopup(context, extensionId)
179
+ await extensionPopup.getByTestId('connection-reject-button').click()
180
+ }