@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/README.md +168 -73
- package/dist/index.d.ts +51 -27
- package/dist/index.js +258 -109
- package/package.json +11 -4
- package/scripts/cli.js +30 -0
- package/scripts/download-extensions.ts +32 -0
- package/src/context-playwright/index.ts +139 -0
- package/src/context-playwright/types.ts +83 -0
- package/src/context-playwright/wallet-factory.ts +123 -0
- package/src/index.ts +2 -0
- package/src/utils/download-extension.ts +68 -0
- package/src/wallets/polkadot-js.ts +114 -0
- package/src/wallets/talisman.ts +159 -0
package/dist/index.js
CHANGED
|
@@ -1,140 +1,289 @@
|
|
|
1
1
|
import { chromium, expect, test as test$1 } from "@playwright/test";
|
|
2
|
-
import 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/
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await
|
|
25
|
-
|
|
26
|
-
await
|
|
27
|
-
await
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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/
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
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
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
59
|
-
const
|
|
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
|
-
|
|
62
|
-
await
|
|
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=${
|
|
254
|
+
args: [`--load-extension=${extensionPathsString}`, `--disable-extensions-except=${extensionPathsString}`],
|
|
73
255
|
slowMo
|
|
74
256
|
});
|
|
75
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
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.
|
|
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'
|