@avalix/chroma 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.8",
5
5
  "description": "End-to-end testing library for Polkadot wallet interactions",
6
6
  "author": "Avalix Labs",
7
7
  "license": "MIT",
@@ -30,14 +30,20 @@
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",
39
+ "src"
35
40
  ],
36
41
  "scripts": {
37
42
  "build": "tsdown",
38
43
  "dev": "tsdown --watch",
39
44
  "lint": "eslint --fix .",
40
- "prepublishOnly": "npm run build"
45
+ "prepublishOnly": "npm run build",
46
+ "download-extensions": "rm -rf .chroma && tsx scripts/download-extensions.ts"
41
47
  },
42
48
  "peerDependencies": {
43
49
  "@playwright/test": "^1.55.0"
@@ -51,7 +57,8 @@
51
57
  "@types/node": "^24.3.3",
52
58
  "@types/unzipper": "^0.10.10",
53
59
  "eslint": "^9.35.0",
54
- "tsdown": "latest"
60
+ "tsdown": "latest",
61
+ "tsx": "^4.20.6"
55
62
  },
56
63
  "publishConfig": {
57
64
  "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()
@@ -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'