@avalix/chroma 0.0.16 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,18 +1,29 @@
1
+ import { cp, rm } from "node:fs/promises";
2
+ import path, { resolve } from "node:path";
1
3
  import { chromium, expect, test as test$1 } from "@playwright/test";
2
4
  import fs from "node:fs";
3
- import path from "node:path";
4
5
  import process from "node:process";
5
6
 
7
+ //#region src/utils/test-defaults.ts
8
+ /**
9
+ * Default password used by chroma to bootstrap wallets in tests.
10
+ *
11
+ * Wallet methods accept an explicit `password` option that overrides this
12
+ * value, so callers that already supply their own password are unaffected.
13
+ */
14
+ const DEFAULT_TEST_PASSWORD = "h3llop0lkadot!";
15
+
16
+ //#endregion
6
17
  //#region src/wallets/metamask.ts
7
- const VERSION$2 = "13.17.0";
18
+ const VERSION$2 = "13.28.0";
8
19
  const METAMASK_CONFIG = {
9
- downloadUrl: `https://github.com/MetaMask/metamask-extension/releases/download/v${VERSION$2}/metamask-flask-chrome-${VERSION$2}-flask.0.zip`,
20
+ downloadUrl: `https://github.com/MetaMask/metamask-extension/releases/download/v${VERSION$2}/metamask-chrome-${VERSION$2}.zip`,
10
21
  extensionName: `metamask-extension-${VERSION$2}`
11
22
  };
12
23
  async function getMetaMaskExtensionPath() {
13
24
  const extensionsDir = path.resolve(process.cwd(), ".chroma");
14
25
  const extensionDir = path.join(extensionsDir, METAMASK_CONFIG.extensionName);
15
- if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) throw new Error(`MetaMask extension not found at: ${extensionDir}\n\nPlease download the extension first by running:\n npx @avalix/chroma download-extensions\n`);
26
+ if ((await fs.promises.readdir(extensionDir).catch(() => [])).length === 0) throw new Error(`MetaMask extension not found at: ${extensionDir}\n\nPlease download the extension first by running:\n npx @avalix/chroma download-extensions\n`);
16
27
  return extensionDir;
17
28
  }
18
29
  /* c8 ignore start */
@@ -23,7 +34,6 @@ async function findOnboardingPage$1(context, extensionId) {
23
34
  const pages = context.pages();
24
35
  for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
25
36
  await p.waitForLoadState("domcontentloaded");
26
- await p.getByText("I accept the risks").click();
27
37
  return p;
28
38
  }
29
39
  if (attempt < maxAttempts - 1) await new Promise((resolve) => setTimeout(resolve, retryDelay));
@@ -50,7 +60,6 @@ async function findExtensionPopup$2(context, extensionId) {
50
60
  }
51
61
  throw new Error(`MetaMask side panel not found for ID: ${extensionId}`);
52
62
  }
53
- const METAMASK_PASSWORD = "h3llop0lkadot!";
54
63
  async function completeOnboarding$1(extensionPage, seedPhrase) {
55
64
  await extensionPage.bringToFront();
56
65
  await extensionPage.waitForLoadState("domcontentloaded");
@@ -58,18 +67,13 @@ async function completeOnboarding$1(extensionPage, seedPhrase) {
58
67
  await extensionPage.getByTestId("onboarding-import-with-srp-button").click();
59
68
  await extensionPage.getByTestId("srp-input-import__srp-note").pressSequentially(seedPhrase, { delay: 50 });
60
69
  await extensionPage.getByTestId("import-srp-confirm").click();
61
- await extensionPage.getByTestId("create-password-new-input").fill(METAMASK_PASSWORD);
62
- await extensionPage.getByTestId("create-password-confirm-input").fill(METAMASK_PASSWORD);
70
+ await extensionPage.getByTestId("create-password-new-input").fill(DEFAULT_TEST_PASSWORD);
71
+ await extensionPage.getByTestId("create-password-confirm-input").fill(DEFAULT_TEST_PASSWORD);
63
72
  await extensionPage.getByTestId("create-password-terms").click();
64
73
  await extensionPage.getByTestId("create-password-submit").click();
65
74
  await extensionPage.getByTestId("metametrics-checkbox").click();
66
75
  await extensionPage.getByTestId("metametrics-i-agree").click();
67
76
  await extensionPage.getByTestId("manage-default-settings").click();
68
- await extensionPage.getByTestId("category-item-General").click();
69
- await extensionPage.getByTestId("basic-functionality-toggle").locator(".toggle-button").click();
70
- await extensionPage.getByText("I understand and want to continue").click();
71
- await extensionPage.getByTestId("basic-configuration-modal-toggle-button").click();
72
- await extensionPage.getByTestId("category-back-button").click();
73
77
  await extensionPage.getByTestId("category-item-Assets").click();
74
78
  await extensionPage.getByTestId("privacy-settings-settings").locator(".toggle-button").nth(0).click();
75
79
  await extensionPage.getByTestId("privacy-settings-settings").locator(".toggle-button").nth(1).click();
@@ -81,40 +85,44 @@ async function completeOnboarding$1(extensionPage, seedPhrase) {
81
85
  await extensionPage.getByTestId("onboarding-complete-done").click();
82
86
  await extensionPage.close();
83
87
  }
84
- async function authorizeMetaMask(page) {
85
- const context = page.__extensionContext;
86
- const extensionId = page.__extensionId;
88
+ async function approveMetaMask(context, extensionId) {
87
89
  const extensionPopup = await findExtensionPopup$2(context, extensionId);
88
- await extensionPopup.getByTestId("confirm-btn").click();
90
+ await extensionPopup.getByTestId("confirm-btn").or(extensionPopup.getByTestId("confirm-footer-button")).or(extensionPopup.getByTestId("confirm-sign-message-confirm-snap-footer-button")).first().click();
89
91
  await extensionPopup.close();
90
92
  }
91
- async function confirmMetaMask(page) {
92
- const context = page.__extensionContext;
93
- const extensionId = page.__extensionId;
94
- const extensionPopup = await findExtensionPopup$2(context, extensionId);
95
- await extensionPopup.getByTestId("confirm-footer-button").click();
96
- await extensionPopup.close();
97
- }
98
- async function rejectMetaMask(page) {
99
- const context = page.__extensionContext;
100
- const extensionId = page.__extensionId;
93
+ async function rejectMetaMask(context, extensionId) {
101
94
  const extensionPopup = await findExtensionPopup$2(context, extensionId);
102
95
  await extensionPopup.getByTestId("confirm-footer-cancel").or(extensionPopup.getByTestId("page-container-footer-cancel")).or(extensionPopup.getByRole("button", { name: /Reject|Cancel/i })).first().click();
103
96
  await extensionPopup.close();
104
97
  }
105
- async function unlockMetaMask(page) {
106
- const context = page.__extensionContext;
107
- const unlockUrl = `chrome-extension://${page.__extensionId}/home.html#/onboarding/unlock`;
108
- const unlockPage = await context.newPage();
109
- await unlockPage.goto(unlockUrl);
110
- await unlockPage.waitForLoadState("domcontentloaded");
111
- await unlockPage.getByTestId("unlock-password").fill(METAMASK_PASSWORD);
112
- await unlockPage.getByTestId("unlock-submit").click();
113
- await unlockPage.close();
98
+ async function unlockMetaMask(context, extensionId) {
99
+ const sidePanelUrl = `chrome-extension://${extensionId}/sidepanel.html`;
100
+ const extensionUrlPrefix = `chrome-extension://${extensionId}/`;
101
+ let unlockPage;
102
+ const deadline = Date.now() + 1e4;
103
+ while (Date.now() < deadline) {
104
+ const extensionPages = context.pages().filter((p) => p.url().startsWith(extensionUrlPrefix));
105
+ unlockPage = extensionPages.find((p) => p.url().includes("unlock"));
106
+ if (unlockPage) break;
107
+ if (extensionPages.length > 0) break;
108
+ await new Promise((resolve) => setTimeout(resolve, 100));
109
+ }
110
+ if (unlockPage) {
111
+ await unlockPage.bringToFront();
112
+ await unlockPage.waitForLoadState("domcontentloaded");
113
+ await unlockPage.getByTestId("unlock-password").fill(DEFAULT_TEST_PASSWORD);
114
+ await unlockPage.getByTestId("unlock-submit").click();
115
+ await unlockPage.getByTestId("onboarding-complete-done").click({ timeout: 3e3 }).catch(() => {});
116
+ await unlockPage.goto(sidePanelUrl);
117
+ await unlockPage.waitForLoadState("domcontentloaded");
118
+ return;
119
+ }
120
+ if (context.pages().find((p) => p.url().startsWith(sidePanelUrl))) return;
121
+ const sidePanel = await context.newPage();
122
+ await sidePanel.goto(sidePanelUrl);
123
+ await sidePanel.waitForLoadState("domcontentloaded");
114
124
  }
115
- async function importSeedPhrase(page, { seedPhrase }) {
116
- const context = page.__extensionContext;
117
- const extensionId = page.__extensionId;
125
+ async function importSeedPhrase(context, extensionId, { seedPhrase }) {
118
126
  const extensionPage = await findOnboardingPage$1(context, extensionId);
119
127
  try {
120
128
  await completeOnboarding$1(extensionPage, seedPhrase);
@@ -126,19 +134,17 @@ async function importSeedPhrase(page, { seedPhrase }) {
126
134
  /* c8 ignore stop */
127
135
 
128
136
  //#endregion
129
- //#region src/wallets/polkadot-js.ts
130
- const VERSION$1 = "0.62.6";
131
- const POLKADOT_JS_CONFIG = {
132
- downloadUrl: `https://github.com/polkadot-js/extension/releases/download/v${VERSION$1}/master-chrome-build.zip`,
133
- extensionName: `polkadot-extension-${VERSION$1}`
134
- };
137
+ //#region src/utils/find-extension-popup.ts
138
+ /**
139
+ * Poll the BrowserContext for a page whose URL points at the given Chrome
140
+ * extension. Returns once the page is reachable and DOM-loaded.
141
+ */
135
142
  /* c8 ignore start */
136
- async function findExtensionPopup$1(context, extensionId) {
137
- const maxAttempts = 10;
138
- const retryDelay = 500;
143
+ async function findExtensionPopup$1(context, extensionId, options = {}) {
144
+ const { maxAttempts = 10, retryDelay = 500, viewport } = options;
139
145
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
140
- const pages = context.pages();
141
- for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
146
+ for (const p of context.pages()) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
147
+ if (viewport) await p.setViewportSize(viewport);
142
148
  await p.waitForLoadState("domcontentloaded");
143
149
  return p;
144
150
  }
@@ -147,25 +153,29 @@ async function findExtensionPopup$1(context, extensionId) {
147
153
  throw new Error(`Extension popup not found for ID: ${extensionId}`);
148
154
  }
149
155
  /* c8 ignore stop */
156
+
157
+ //#endregion
158
+ //#region src/wallets/polkadot-js.ts
159
+ const VERSION$1 = "0.62.6";
160
+ const POLKADOT_JS_CONFIG = {
161
+ downloadUrl: `https://github.com/polkadot-js/extension/releases/download/v${VERSION$1}/master-chrome-build.zip`,
162
+ extensionName: `polkadot-extension-${VERSION$1}`
163
+ };
150
164
  async function getPolkadotJSExtensionPath() {
151
165
  const extensionsDir = path.resolve(process.cwd(), ".chroma");
152
166
  const extensionDir = path.join(extensionsDir, POLKADOT_JS_CONFIG.extensionName);
153
- 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`);
167
+ if ((await fs.promises.readdir(extensionDir).catch(() => [])).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`);
154
168
  return extensionDir;
155
169
  }
156
170
  /* c8 ignore start */
157
- async function importPolkadotJSAccount(page, { seed, name = "Test Account", password = "h3llop0lkadot!" }) {
158
- const context = page.__extensionContext;
159
- const extensionPopupUrl = `chrome-extension://${page.__extensionId}/index.html`;
171
+ async function importPolkadotJSAccount(context, extensionId, { seed, name = "Test Account", password = DEFAULT_TEST_PASSWORD }) {
172
+ const extensionPopupUrl = `chrome-extension://${extensionId}/index.html`;
160
173
  const extensionPage = await context.newPage();
161
174
  try {
162
175
  await extensionPage.goto(extensionPopupUrl);
163
176
  const understoodButton = extensionPage.getByRole("button", { name: "Understood, let me continue" });
164
- if (await understoodButton.count() > 0) {
165
- await understoodButton.click();
166
- await extensionPage.waitForTimeout(100);
167
- }
168
- if (await extensionPage.getByRole("button", { name: "I Understand" }).isVisible()) await extensionPage.getByRole("button", { name: "I Understand" }).click();
177
+ if (await understoodButton.count() > 0) await understoodButton.click();
178
+ await extensionPage.getByRole("button", { name: "I Understand" }).click({ timeout: 1e3 }).catch(() => {});
169
179
  await extensionPage.goto(`${extensionPopupUrl}#/account/import-seed`);
170
180
  await extensionPage.locator("textarea").fill(seed);
171
181
  await extensionPage.locator("button:has-text(\"Next\")").click();
@@ -177,25 +187,19 @@ async function importPolkadotJSAccount(page, { seed, name = "Test Account", pass
177
187
  await extensionPage.close();
178
188
  }
179
189
  }
180
- async function authorizePolkadotJS(page) {
181
- const context = page.__extensionContext;
182
- const extensionId = page.__extensionId;
190
+ async function authorizePolkadotJS(context, extensionId) {
183
191
  const extensionPopup = await findExtensionPopup$1(context, extensionId);
184
192
  if (await extensionPopup.getByRole("button", { name: "I Understand" }).isVisible()) await extensionPopup.getByRole("button", { name: "I Understand" }).click();
185
193
  if (!await extensionPopup.getByText("Select all").locator("..").locator("input[type=\"checkbox\"]").isChecked().catch(() => false)) await extensionPopup.getByText("Select all").click();
186
194
  await extensionPopup.getByRole("button", { name: /Connect \d+ account\(s\)/ }).click();
187
195
  }
188
- async function approvePolkadotJSTx(page, options = {}) {
189
- const { password = "h3llop0lkadot!" } = options;
190
- const context = page.__extensionContext;
191
- const extensionId = page.__extensionId;
196
+ async function approvePolkadotJSTx(context, extensionId, options = {}) {
197
+ const { password = DEFAULT_TEST_PASSWORD } = options;
192
198
  const extensionPopup = await findExtensionPopup$1(context, extensionId);
193
199
  await extensionPopup.getByRole("textbox").fill(password);
194
200
  await extensionPopup.getByRole("button", { name: "Sign the transaction" }).click();
195
201
  }
196
- async function rejectPolkadotJSTx(page) {
197
- const context = page.__extensionContext;
198
- const extensionId = page.__extensionId;
202
+ async function rejectPolkadotJSTx(context, extensionId) {
199
203
  await (await findExtensionPopup$1(context, extensionId)).getByRole("link", { name: "Cancel" }).click();
200
204
  }
201
205
  /* c8 ignore stop */
@@ -208,28 +212,17 @@ const TALISMAN_CONFIG = {
208
212
  extensionName: `talisman-extension-${VERSION}`
209
213
  };
210
214
  /* c8 ignore start */
211
- async function findExtensionPopup(context, extensionId) {
212
- const maxAttempts = 10;
213
- const retryDelay = 500;
214
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
215
- const pages = context.pages();
216
- for (const p of pages) if (p.url().includes(`chrome-extension://${extensionId}/`)) {
217
- await p.setViewportSize({
218
- width: 400,
219
- height: 600
220
- });
221
- await p.waitForLoadState("domcontentloaded");
222
- return p;
223
- }
224
- if (attempt < maxAttempts - 1) await new Promise((resolve) => setTimeout(resolve, retryDelay));
225
- }
226
- throw new Error(`Extension popup not found for ID: ${extensionId}`);
215
+ function findExtensionPopup(context, extensionId) {
216
+ return findExtensionPopup$1(context, extensionId, { viewport: {
217
+ width: 400,
218
+ height: 600
219
+ } });
227
220
  }
228
221
  /* c8 ignore stop */
229
222
  async function getTalismanExtensionPath() {
230
223
  const extensionsDir = path.resolve(process.cwd(), ".chroma");
231
224
  const extensionDir = path.join(extensionsDir, TALISMAN_CONFIG.extensionName);
232
- 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`);
225
+ if ((await fs.promises.readdir(extensionDir).catch(() => [])).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`);
233
226
  return extensionDir;
234
227
  }
235
228
  /* c8 ignore start */
@@ -260,9 +253,7 @@ async function completeOnboarding(extensionPage, password) {
260
253
  await extensionPage.getByRole("link", { name: "Security & Privacy" }).click();
261
254
  await extensionPage.getByTestId("component-toggle-button").first().click();
262
255
  }
263
- async function importPolkadotMnemonic(page, { seed, name = "Test Account", password = "h3llop0lkadot!" }) {
264
- const context = page.__extensionContext;
265
- const extensionId = page.__extensionId;
256
+ async function importPolkadotMnemonic(context, extensionId, { seed, name = "Test Account", password = DEFAULT_TEST_PASSWORD }) {
266
257
  const extensionPage = await findOnboardingPage(context, extensionId);
267
258
  try {
268
259
  await completeOnboarding(extensionPage, password);
@@ -281,9 +272,7 @@ async function importPolkadotMnemonic(page, { seed, name = "Test Account", passw
281
272
  throw error;
282
273
  }
283
274
  }
284
- async function importEthPrivateKey(page, { seed, name = "Test Account", password = "h3llop0lkadot!" }) {
285
- const context = page.__extensionContext;
286
- const extensionId = page.__extensionId;
275
+ async function importEthPrivateKey(context, extensionId, { seed, name = "Test Account", password = DEFAULT_TEST_PASSWORD }) {
287
276
  const extensionPage = await findOnboardingPage(context, extensionId);
288
277
  try {
289
278
  await completeOnboarding(extensionPage, password);
@@ -303,9 +292,7 @@ async function importEthPrivateKey(page, { seed, name = "Test Account", password
303
292
  throw error;
304
293
  }
305
294
  }
306
- async function authorizeTalisman(page, options = {}) {
307
- const context = page.__extensionContext;
308
- const extensionId = page.__extensionId;
295
+ async function authorizeTalisman(context, extensionId, options = {}) {
309
296
  const { accountName = "Test Account" } = options;
310
297
  const extensionPopup = await findExtensionPopup(context, extensionId);
311
298
  await extensionPopup.waitForLoadState("domcontentloaded");
@@ -318,16 +305,12 @@ async function authorizeTalisman(page, options = {}) {
318
305
  await (await findExtensionPopup(context, extensionId)).getByRole("button", { name: "Approve" }).click();
319
306
  } catch {}
320
307
  }
321
- async function approveTalismanTx(page) {
322
- const context = page.__extensionContext;
323
- const extensionId = page.__extensionId;
308
+ async function approveTalismanTx(context, extensionId) {
324
309
  const extensionPopup = await findExtensionPopup(context, extensionId);
325
310
  if (await extensionPopup.getByRole("button", { name: "Yes" }).isVisible()) await extensionPopup.getByRole("button", { name: "Yes" }).click();
326
311
  await extensionPopup.getByRole("button", { name: "Approve" }).click();
327
312
  }
328
- async function rejectTalismanTx(page) {
329
- const context = page.__extensionContext;
330
- const extensionId = page.__extensionId;
313
+ async function rejectTalismanTx(context, extensionId) {
331
314
  const extensionPopup = await findExtensionPopup(context, extensionId);
332
315
  await extensionPopup.getByTestId("connection-reject-button").or(extensionPopup.getByRole("button", { name: "Cancel" })).or(extensionPopup.getByRole("button", { name: "Reject" })).click();
333
316
  }
@@ -336,30 +319,14 @@ async function rejectTalismanTx(page) {
336
319
  //#endregion
337
320
  //#region src/context-playwright/wallet-factory.ts
338
321
  /* c8 ignore start */
339
- function createExtendedPage(page, context, extensionId) {
340
- const extPage = page;
341
- extPage.__extensionContext = context;
342
- extPage.__extensionId = extensionId;
343
- return extPage;
344
- }
345
- /* c8 ignore stop */
346
- /* c8 ignore start */
347
322
  function createPolkadotJsWallet(extensionId, context) {
348
323
  return {
349
324
  extensionId,
350
325
  type: "polkadot-js",
351
- importMnemonic: async (options) => {
352
- await importPolkadotJSAccount(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId), options);
353
- },
354
- authorize: async () => {
355
- await authorizePolkadotJS(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
356
- },
357
- approveTx: async (options = {}) => {
358
- await approvePolkadotJSTx(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId), options);
359
- },
360
- rejectTx: async () => {
361
- await rejectPolkadotJSTx(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
362
- }
326
+ importMnemonic: (options) => importPolkadotJSAccount(context, extensionId, options),
327
+ authorize: () => authorizePolkadotJS(context, extensionId),
328
+ approveTx: (options = {}) => approvePolkadotJSTx(context, extensionId, options),
329
+ rejectTx: () => rejectPolkadotJSTx(context, extensionId)
363
330
  };
364
331
  }
365
332
  /* c8 ignore stop */
@@ -369,29 +336,23 @@ function createTalismanWallet(extensionId, context) {
369
336
  return {
370
337
  extensionId,
371
338
  type: "talisman",
372
- importPolkadotMnemonic: async (options) => {
373
- const extPage = createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId);
339
+ importPolkadotMnemonic: (options) => {
374
340
  importedAccountName = options.name || "Test Account";
375
- await importPolkadotMnemonic(extPage, options);
341
+ return importPolkadotMnemonic(context, extensionId, options);
376
342
  },
377
- importEthPrivateKey: async (options) => {
378
- const extPage = createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId);
343
+ importEthPrivateKey: (options) => {
379
344
  importedAccountName = options.name || "Test Account";
380
- await importEthPrivateKey(extPage, {
345
+ return importEthPrivateKey(context, extensionId, {
381
346
  seed: options.privateKey,
382
347
  name: options.name,
383
348
  password: options.password
384
349
  });
385
350
  },
386
- authorize: async (options = {}) => {
387
- await authorizeTalisman(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId), { accountName: options.accountName || importedAccountName });
388
- },
389
- approveTx: async () => {
390
- await approveTalismanTx(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
351
+ authorize: (options = {}) => {
352
+ return authorizeTalisman(context, extensionId, { accountName: options.accountName || importedAccountName });
391
353
  },
392
- rejectTx: async () => {
393
- await rejectTalismanTx(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
394
- }
354
+ approveTx: () => approveTalismanTx(context, extensionId),
355
+ rejectTx: () => rejectTalismanTx(context, extensionId)
395
356
  };
396
357
  }
397
358
  /* c8 ignore stop */
@@ -400,21 +361,10 @@ function createMetaMaskWallet(extensionId, context) {
400
361
  return {
401
362
  extensionId,
402
363
  type: "metamask",
403
- importSeedPhrase: async (options) => {
404
- await importSeedPhrase(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId), { seedPhrase: options.seedPhrase });
405
- },
406
- unlock: async () => {
407
- await unlockMetaMask(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
408
- },
409
- authorize: async () => {
410
- await authorizeMetaMask(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
411
- },
412
- reject: async () => {
413
- await rejectMetaMask(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
414
- },
415
- confirm: async () => {
416
- await confirmMetaMask(createExtendedPage(context.pages()[0] || await context.newPage(), context, extensionId));
417
- }
364
+ importSeedPhrase: (options) => importSeedPhrase(context, extensionId, options),
365
+ unlock: () => unlockMetaMask(context, extensionId),
366
+ approve: () => approveMetaMask(context, extensionId),
367
+ reject: () => rejectMetaMask(context, extensionId)
418
368
  };
419
369
  }
420
370
  /* c8 ignore stop */
@@ -438,12 +388,22 @@ async function getExtensionPathForWallet(config) {
438
388
  function createWalletTest(options = {}) {
439
389
  const { headless = false, slowMo = 150 } = options;
440
390
  const walletConfigs = options.wallets && options.wallets.length > 0 ? options.wallets : [{ type: "polkadot-js" }];
441
- const isMultiWallet = walletConfigs.length > 1;
442
391
  /* c8 ignore start */
443
392
  return test$1.extend({
444
- walletContext: [async ({}, use) => {
393
+ walletContext: [async ({}, use, workerInfo) => {
445
394
  const extensionPathsString = (await Promise.all(walletConfigs.map((config) => getExtensionPathForWallet(config)))).join(",");
446
- const context = await chromium.launchPersistentContext("", {
395
+ const userDataDirOption = options.userDataDir;
396
+ const userDataDir = typeof userDataDirOption === "function" ? await userDataDirOption({ workerIndex: workerInfo.workerIndex }) : userDataDirOption ?? "";
397
+ if (options.cloneUserDataDirFrom && userDataDir) {
398
+ const sourceAbs = resolve(options.cloneUserDataDirFrom);
399
+ if (sourceAbs === resolve(userDataDir)) throw new Error(`cloneUserDataDirFrom and userDataDir must be different paths; both resolved to "${sourceAbs}"`);
400
+ await rm(userDataDir, {
401
+ recursive: true,
402
+ force: true
403
+ });
404
+ await cp(options.cloneUserDataDirFrom, userDataDir, { recursive: true });
405
+ }
406
+ const context = await chromium.launchPersistentContext(userDataDir, {
447
407
  headless,
448
408
  channel: "chromium",
449
409
  args: [`--load-extension=${extensionPathsString}`, `--disable-extensions-except=${extensionPathsString}`],
@@ -454,21 +414,21 @@ function createWalletTest(options = {}) {
454
414
  }, { scope: "worker" }],
455
415
  walletExtensionIds: [async ({ walletContext }, use) => {
456
416
  const extensionIds = /* @__PURE__ */ new Map();
457
- if (walletContext.serviceWorkers().length === 0) await walletContext.waitForEvent("serviceworker");
458
- if (isMultiWallet) await new Promise((resolve) => setTimeout(resolve, 1e3));
417
+ const expected = walletConfigs.length;
418
+ const deadline = Date.now() + 1e4;
419
+ while (walletContext.serviceWorkers().length < expected) {
420
+ if (Date.now() > deadline) break;
421
+ await Promise.race([walletContext.waitForEvent("serviceworker", { timeout: 2e3 }).catch(() => {}), new Promise((resolve) => setTimeout(resolve, 200))]);
422
+ }
459
423
  const allServiceWorkers = walletContext.serviceWorkers();
460
424
  for (let i = 0; i < walletConfigs.length && i < allServiceWorkers.length; i++) {
461
425
  const extensionId = allServiceWorkers[i].url().split("/")[2];
462
- const walletType = walletConfigs[i].type;
463
- extensionIds.set(walletType, extensionId);
426
+ extensionIds.set(walletConfigs[i].type, extensionId);
464
427
  }
465
428
  await use(extensionIds);
466
429
  }, { scope: "worker" }],
467
- page: async ({ walletContext, walletExtensionIds }, use) => {
468
- const extendedPage = walletContext.pages()[0] || await walletContext.newPage();
469
- extendedPage.__extensionContext = walletContext;
470
- extendedPage.__walletExtensionIds = walletExtensionIds;
471
- await use(extendedPage);
430
+ page: async ({ walletContext }, use) => {
431
+ await use(walletContext.pages()[0] || await walletContext.newPage());
472
432
  },
473
433
  wallets: async ({ walletContext, walletExtensionIds }, use) => {
474
434
  const walletMap = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@avalix/chroma",
3
3
  "type": "module",
4
- "version": "0.0.16",
4
+ "version": "1.0.0",
5
5
  "description": "End-to-end testing library for Polkadot wallet interactions",
6
6
  "author": "Avalix Labs",
7
7
  "license": "MIT",
@@ -51,16 +51,15 @@
51
51
  "test:unit:coverage": "vitest run --coverage"
52
52
  },
53
53
  "peerDependencies": {
54
- "@playwright/test": "^1.57.0"
54
+ "@playwright/test": "^1.59.1"
55
55
  },
56
56
  "dependencies": {
57
57
  "adm-zip": "^0.5.16"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@antfu/eslint-config": "^6.5.1",
61
- "@playwright/test": "^1.57.0",
61
+ "@playwright/test": "^1.59.1",
62
62
  "@types/node": "^24.10.2",
63
- "@types/unzipper": "^0.10.11",
64
63
  "@vitest/coverage-v8": "4.0.18",
65
64
  "eslint": "^9.39.1",
66
65
  "tsdown": "^0.20.1",
@@ -18,11 +18,8 @@ async function getVersion(): Promise<string> {
18
18
 
19
19
  async function clearChromaDir(): Promise<void> {
20
20
  const chromaDir = path.resolve(process.cwd(), '.chroma')
21
-
22
- if (fs.existsSync(chromaDir)) {
23
- console.log('šŸ—‘ļø Clearing existing .chroma directory...')
24
- await fs.promises.rm(chromaDir, { recursive: true, force: true })
25
- }
21
+ console.log('šŸ—‘ļø Clearing existing .chroma directory...')
22
+ await fs.promises.rm(chromaDir, { recursive: true, force: true })
26
23
  }
27
24
 
28
25
  async function main() {
@@ -34,23 +31,21 @@ async function main() {
34
31
  // Clear existing .chroma directory
35
32
  await clearChromaDir()
36
33
 
37
- // Download Polkadot-JS extension
38
- await downloadAndExtractExtension({
39
- downloadUrl: POLKADOT_JS_CONFIG.downloadUrl,
40
- extensionName: POLKADOT_JS_CONFIG.extensionName,
41
- })
42
-
43
- // Download Talisman extension
44
- await downloadAndExtractExtension({
45
- downloadUrl: TALISMAN_CONFIG.downloadUrl,
46
- extensionName: TALISMAN_CONFIG.extensionName,
47
- })
48
-
49
- // Download MetaMask extension
50
- await downloadAndExtractExtension({
51
- downloadUrl: METAMASK_CONFIG.downloadUrl,
52
- extensionName: METAMASK_CONFIG.extensionName,
53
- })
34
+ // Download all extensions in parallel — they're independent network ops
35
+ await Promise.all([
36
+ downloadAndExtractExtension({
37
+ downloadUrl: POLKADOT_JS_CONFIG.downloadUrl,
38
+ extensionName: POLKADOT_JS_CONFIG.extensionName,
39
+ }),
40
+ downloadAndExtractExtension({
41
+ downloadUrl: TALISMAN_CONFIG.downloadUrl,
42
+ extensionName: TALISMAN_CONFIG.extensionName,
43
+ }),
44
+ downloadAndExtractExtension({
45
+ downloadUrl: METAMASK_CONFIG.downloadUrl,
46
+ extensionName: METAMASK_CONFIG.extensionName,
47
+ }),
48
+ ])
54
49
 
55
50
  console.log('\nāœ… All extensions downloaded successfully!')
56
51
  console.log('You can now run your Playwright tests.')
@@ -1,8 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { getMetaMaskExtensionPath } from '../wallets/metamask.js'
3
- import { getPolkadotJSExtensionPath } from '../wallets/polkadot-js.js'
4
- import { getTalismanExtensionPath } from '../wallets/talisman.js'
5
- import { createWalletTest, getExtensionPathForWallet } from './index.js'
2
+ import { createWalletTest } from './index.js'
6
3
 
7
4
  // Mock @playwright/test
8
5
  vi.mock('@playwright/test', () => {
@@ -118,44 +115,37 @@ describe('context-playwright/index', () => {
118
115
  expect(result).toBeDefined()
119
116
  })
120
117
 
121
- it('should handle wallets with downloadUrl option', () => {
118
+ it('should accept userDataDir as a string', () => {
122
119
  const result = createWalletTest({
123
- wallets: [{
124
- type: 'polkadot-js',
125
- downloadUrl: 'https://custom-url.com/extension.zip',
126
- }],
120
+ userDataDir: '.cache/wallet-setup',
127
121
  })
128
122
 
129
123
  expect(result).toBeDefined()
130
124
  })
131
- })
132
125
 
133
- describe('getExtensionPathForWallet', () => {
134
- it('should return polkadot-js extension path', async () => {
135
- const result = await getExtensionPathForWallet({ type: 'polkadot-js' })
126
+ it('should accept userDataDir as a per-worker function', () => {
127
+ const result = createWalletTest({
128
+ userDataDir: ({ workerIndex }) => `.cache/wallet-w${workerIndex}`,
129
+ })
136
130
 
137
- expect(result).toBe('/mock/path/polkadot-extension')
138
- expect(getPolkadotJSExtensionPath).toHaveBeenCalled()
131
+ expect(result).toBeDefined()
139
132
  })
140
133
 
141
- it('should return talisman extension path', async () => {
142
- const result = await getExtensionPathForWallet({ type: 'talisman' })
134
+ it('should accept userDataDir as an async function', () => {
135
+ const result = createWalletTest({
136
+ userDataDir: async ({ workerIndex }) => `.cache/wallet-w${workerIndex}`,
137
+ })
143
138
 
144
- expect(result).toBe('/mock/path/talisman-extension')
145
- expect(getTalismanExtensionPath).toHaveBeenCalled()
139
+ expect(result).toBeDefined()
146
140
  })
147
141
 
148
- it('should return metamask extension path', async () => {
149
- const result = await getExtensionPathForWallet({ type: 'metamask' })
150
-
151
- expect(result).toBe('/mock/path/metamask-extension')
152
- expect(getMetaMaskExtensionPath).toHaveBeenCalled()
153
- })
142
+ it('should accept cloneUserDataDirFrom alongside userDataDir', () => {
143
+ const result = createWalletTest({
144
+ userDataDir: ({ workerIndex }) => `.cache/wallet-w${workerIndex}`,
145
+ cloneUserDataDirFrom: '.cache/wallet-setup',
146
+ })
154
147
 
155
- it('should throw error for unsupported wallet type', async () => {
156
- await expect(
157
- getExtensionPathForWallet({ type: 'unsupported' as any }),
158
- ).rejects.toThrow('Unsupported wallet type: unsupported')
148
+ expect(result).toBeDefined()
159
149
  })
160
150
  })
161
151
  })