@avalix/chroma 0.0.15 → 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.
@@ -13,11 +13,11 @@ vi.mock('node:fs', async () => {
13
13
  ...actual,
14
14
  default: {
15
15
  ...actual,
16
- existsSync: vi.fn(),
17
- readdirSync: vi.fn(),
16
+ promises: {
17
+ ...actual.promises,
18
+ readdir: vi.fn(),
19
+ },
18
20
  },
19
- existsSync: vi.fn(),
20
- readdirSync: vi.fn(),
21
21
  }
22
22
  })
23
23
 
@@ -43,21 +43,20 @@ describe('metamask wallet', () => {
43
43
  })
44
44
 
45
45
  describe('getMetaMaskExtensionPath', () => {
46
+ const mockReaddir = () => vi.mocked(fs.promises.readdir) as unknown as ReturnType<typeof vi.fn>
47
+
46
48
  it('should return extension path when extension exists', async () => {
47
- const mockedFs = vi.mocked(fs)
48
- mockedFs.existsSync.mockReturnValue(true)
49
- mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
49
+ mockReaddir().mockResolvedValueOnce(['manifest.json'])
50
50
 
51
51
  const result = await getMetaMaskExtensionPath()
52
52
 
53
53
  expect(result).toContain('.chroma')
54
54
  expect(result).toContain(METAMASK_CONFIG.extensionName)
55
- expect(mockedFs.existsSync).toHaveBeenCalled()
55
+ expect(fs.promises.readdir).toHaveBeenCalled()
56
56
  })
57
57
 
58
58
  it('should throw error when extension directory does not exist', async () => {
59
- const mockedFs = vi.mocked(fs)
60
- mockedFs.existsSync.mockReturnValue(false)
59
+ mockReaddir().mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
61
60
 
62
61
  await expect(getMetaMaskExtensionPath()).rejects.toThrow(
63
62
  'MetaMask extension not found',
@@ -65,9 +64,7 @@ describe('metamask wallet', () => {
65
64
  })
66
65
 
67
66
  it('should throw error when extension directory is empty', async () => {
68
- const mockedFs = vi.mocked(fs)
69
- mockedFs.existsSync.mockReturnValue(true)
70
- mockedFs.readdirSync.mockReturnValue([])
67
+ mockReaddir().mockResolvedValueOnce([])
71
68
 
72
69
  await expect(getMetaMaskExtensionPath()).rejects.toThrow(
73
70
  'MetaMask extension not found',
@@ -75,8 +72,7 @@ describe('metamask wallet', () => {
75
72
  })
76
73
 
77
74
  it('should include download instructions in error message', async () => {
78
- const mockedFs = vi.mocked(fs)
79
- mockedFs.existsSync.mockReturnValue(false)
75
+ mockReaddir().mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
80
76
 
81
77
  await expect(getMetaMaskExtensionPath()).rejects.toThrow(
82
78
  'npx @avalix/chroma download-extensions',
@@ -84,9 +80,7 @@ describe('metamask wallet', () => {
84
80
  })
85
81
 
86
82
  it('should use correct path structure', async () => {
87
- const mockedFs = vi.mocked(fs)
88
- mockedFs.existsSync.mockReturnValue(true)
89
- mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
83
+ mockReaddir().mockResolvedValueOnce(['manifest.json'])
90
84
 
91
85
  const result = await getMetaMaskExtensionPath()
92
86
 
@@ -2,12 +2,13 @@ import type { BrowserContext, Page } from '@playwright/test'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import process from 'node:process'
5
+ import { DEFAULT_TEST_PASSWORD } from '../utils/test-defaults.js'
5
6
 
6
7
  // MetaMask specific configuration
7
8
  // https://github.com/MetaMask/metamask-extension/releases
8
- const VERSION = '13.17.0'
9
+ const VERSION = '13.28.0'
9
10
  export const METAMASK_CONFIG = {
10
- downloadUrl: `https://github.com/MetaMask/metamask-extension/releases/download/v${VERSION}/metamask-flask-chrome-${VERSION}-flask.0.zip`,
11
+ downloadUrl: `https://github.com/MetaMask/metamask-extension/releases/download/v${VERSION}/metamask-chrome-${VERSION}.zip`,
11
12
  extensionName: `metamask-extension-${VERSION}`,
12
13
  } as const
13
14
 
@@ -16,8 +17,9 @@ export async function getMetaMaskExtensionPath(): Promise<string> {
16
17
  const extensionsDir = path.resolve(process.cwd(), '.chroma')
17
18
  const extensionDir = path.join(extensionsDir, METAMASK_CONFIG.extensionName)
18
19
 
19
- // Check if extension exists
20
- if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) {
20
+ // Check if extension exists (readdir rejects if missing → treat as empty)
21
+ const entries = await fs.promises.readdir(extensionDir).catch(() => [] as string[])
22
+ if (entries.length === 0) {
21
23
  throw new Error(
22
24
  `MetaMask extension not found at: ${extensionDir}\n\n`
23
25
  + `Please download the extension first by running:\n`
@@ -48,7 +50,6 @@ async function findOnboardingPage(
48
50
  for (const p of pages) {
49
51
  if (p.url().includes(`chrome-extension://${extensionId}/`)) {
50
52
  await p.waitForLoadState('domcontentloaded')
51
- await p.getByText('I accept the risks').click()
52
53
  return p
53
54
  }
54
55
  }
@@ -100,8 +101,6 @@ async function findExtensionPopup(
100
101
  throw new Error(`MetaMask side panel not found for ID: ${extensionId}`)
101
102
  }
102
103
 
103
- const METAMASK_PASSWORD = 'h3llop0lkadot!'
104
-
105
104
  // Helper function to complete MetaMask onboarding flow
106
105
  async function completeOnboarding(
107
106
  extensionPage: Page,
@@ -125,8 +124,8 @@ async function completeOnboarding(
125
124
  await extensionPage.getByTestId('import-srp-confirm').click()
126
125
 
127
126
  // Set password
128
- await extensionPage.getByTestId('create-password-new-input').fill(METAMASK_PASSWORD)
129
- await extensionPage.getByTestId('create-password-confirm-input').fill(METAMASK_PASSWORD)
127
+ await extensionPage.getByTestId('create-password-new-input').fill(DEFAULT_TEST_PASSWORD)
128
+ await extensionPage.getByTestId('create-password-confirm-input').fill(DEFAULT_TEST_PASSWORD)
130
129
  await extensionPage.getByTestId('create-password-terms').click()
131
130
  await extensionPage.getByTestId('create-password-submit').click()
132
131
 
@@ -136,12 +135,6 @@ async function completeOnboarding(
136
135
 
137
136
  // Complete onboarding
138
137
  await extensionPage.getByTestId('manage-default-settings').click()
139
- await extensionPage.getByTestId('category-item-General').click()
140
- await extensionPage.getByTestId('basic-functionality-toggle').locator('.toggle-button').click()
141
- await extensionPage.getByText('I understand and want to continue').click()
142
- await extensionPage.getByTestId('basic-configuration-modal-toggle-button').click()
143
- await extensionPage.getByTestId('category-back-button').click()
144
-
145
138
  await extensionPage.getByTestId('category-item-Assets').click()
146
139
  await extensionPage.getByTestId('privacy-settings-settings').locator('.toggle-button').nth(0).click()
147
140
  await extensionPage.getByTestId('privacy-settings-settings').locator('.toggle-button').nth(1).click()
@@ -155,44 +148,30 @@ async function completeOnboarding(
155
148
  await extensionPage.close()
156
149
  }
157
150
 
158
- // MetaMask specific authorization implementation
159
- // Handles the "connect" popup when a dapp requests wallet connection
160
- export async function authorizeMetaMask(
161
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
162
- ): Promise<void> {
163
- const context = page.__extensionContext
164
- const extensionId = page.__extensionId
165
-
166
- const extensionPopup = await findExtensionPopup(context, extensionId)
167
-
168
- // Click "Connect" to authorize the dapp
169
- await extensionPopup.getByTestId('confirm-btn').click()
170
- await extensionPopup.close()
171
- }
172
-
173
- // MetaMask specific confirm implementation
174
- // Handles the confirm popup (e.g. sign message, send transaction)
175
- export async function confirmMetaMask(
176
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
151
+ // Approve a MetaMask popup covers both the dapp connect popup ("Connect"
152
+ // button = `confirm-btn`) and the sign / transaction popup ("Confirm" button
153
+ // = `confirm-footer-button`). The two flows share the same find-popup,
154
+ // click, close shape, so a single function accepts whichever button the
155
+ // popup presents.
156
+ export async function approveMetaMask(
157
+ context: BrowserContext,
158
+ extensionId: string,
177
159
  ): Promise<void> {
178
- const context = page.__extensionContext
179
- const extensionId = page.__extensionId
180
-
181
160
  const extensionPopup = await findExtensionPopup(context, extensionId)
182
161
 
183
- // Click "Confirm"
184
- await extensionPopup.getByTestId('confirm-footer-button').click()
162
+ const approveButton = extensionPopup.getByTestId('confirm-btn')
163
+ .or(extensionPopup.getByTestId('confirm-footer-button'))
164
+ .or(extensionPopup.getByTestId('confirm-sign-message-confirm-snap-footer-button'))
165
+ await approveButton.first().click()
185
166
  await extensionPopup.close()
186
167
  }
187
168
 
188
169
  // MetaMask specific reject implementation
189
170
  // Handles the reject/cancel popup (e.g. reject transaction, switch chain)
190
171
  export async function rejectMetaMask(
191
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
172
+ context: BrowserContext,
173
+ extensionId: string,
192
174
  ): Promise<void> {
193
- const context = page.__extensionContext
194
- const extensionId = page.__extensionId
195
-
196
175
  const extensionPopup = await findExtensionPopup(context, extensionId)
197
176
 
198
177
  // Click "Reject" or "Cancel" - MetaMask uses confirm-footer-cancel for tx/sign reject
@@ -203,34 +182,86 @@ export async function rejectMetaMask(
203
182
  await extensionPopup.close()
204
183
  }
205
184
 
206
- // Unlock MetaMask by navigating to unlock page and filling password
185
+ // Unlock MetaMask and leave its side panel open for the rest of the session.
186
+ //
187
+ // Idempotent: callers can invoke this from a fixture on every test without
188
+ // tracking state. The function:
189
+ // 1. Reuses the unlock tab MetaMask auto-opens on a locked profile (e.g. a
190
+ // cloned userDataDir) — opening a second one leaves the auto-opened tab
191
+ // around and queues dapp requests behind it.
192
+ // 2. After unlock succeeds, navigates that same tab to `sidepanel.html` so
193
+ // the wallet UI stays visible throughout the test session.
194
+ // 3. If MetaMask is already unlocked, just ensures a side panel tab exists.
207
195
  export async function unlockMetaMask(
208
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
196
+ context: BrowserContext,
197
+ extensionId: string,
209
198
  ): Promise<void> {
210
- const context = page.__extensionContext
211
- const extensionId = page.__extensionId
199
+ const sidePanelUrl = `chrome-extension://${extensionId}/sidepanel.html`
200
+ const extensionUrlPrefix = `chrome-extension://${extensionId}/`
201
+
202
+ // Poll for one of two signals:
203
+ // - an unlock tab → MetaMask auto-opened it on a locked profile
204
+ // - any non-unlock extension page → MetaMask is already unlocked
205
+ // (e.g. the sidepanel left over from a prior test in this worker)
206
+ // 10s deadline accommodates slow CI cold starts; the already-unlocked
207
+ // branch short-circuits within one tick on worker-scoped reuse, so the
208
+ // longer ceiling only applies on the first unlock per worker.
209
+ let unlockPage: Page | undefined
210
+ const deadline = Date.now() + 10_000
211
+ while (Date.now() < deadline) {
212
+ const extensionPages = context.pages().filter(p =>
213
+ p.url().startsWith(extensionUrlPrefix),
214
+ )
215
+
216
+ unlockPage = extensionPages.find(p => p.url().includes('unlock'))
217
+ if (unlockPage)
218
+ break
219
+
220
+ // A non-unlock extension page means MetaMask has booted unlocked; fall
221
+ // through to the sidepanel branch.
222
+ if (extensionPages.length > 0)
223
+ break
224
+
225
+ await new Promise(resolve => setTimeout(resolve, 100))
226
+ }
227
+
228
+ if (unlockPage) {
229
+ await unlockPage.bringToFront()
230
+ await unlockPage.waitForLoadState('domcontentloaded')
212
231
 
213
- const unlockUrl = `chrome-extension://${extensionId}/home.html#/onboarding/unlock`
214
- const unlockPage = await context.newPage()
215
- await unlockPage.goto(unlockUrl)
216
- await unlockPage.waitForLoadState('domcontentloaded')
232
+ // Fill password and unlock
233
+ await unlockPage.getByTestId('unlock-password').fill(DEFAULT_TEST_PASSWORD)
234
+ await unlockPage.getByTestId('unlock-submit').click()
217
235
 
218
- // Fill password and unlock
219
- await unlockPage.getByTestId('unlock-password').fill(METAMASK_PASSWORD)
220
- await unlockPage.getByTestId('unlock-submit').click()
236
+ // Some MetaMask versions show a post-unlock completion screen. Click it
237
+ // if present, otherwise continue — the absence is not an error.
238
+ await unlockPage.getByTestId('onboarding-complete-done').click({ timeout: 3_000 }).catch(() => {})
239
+
240
+ // Navigate to the side panel and leave the tab open for the session.
241
+ await unlockPage.goto(sidePanelUrl)
242
+ await unlockPage.waitForLoadState('domcontentloaded')
243
+ return
244
+ }
221
245
 
222
- // await unlockPage.getByTestId('onboarding-complete-done').click()
223
- await unlockPage.close()
246
+ // No unlock tab seen — either MetaMask is already unlocked, or it never
247
+ // booted within the deadline. Ensure a sidepanel tab exists; downstream
248
+ // calls will surface a clear failure if the profile turns out to still be
249
+ // locked.
250
+ const existing = context.pages().find(p => p.url().startsWith(sidePanelUrl))
251
+ if (existing)
252
+ return
253
+
254
+ const sidePanel = await context.newPage()
255
+ await sidePanel.goto(sidePanelUrl)
256
+ await sidePanel.waitForLoadState('domcontentloaded')
224
257
  }
225
258
 
226
259
  // MetaMask specific seed phrase import implementation
227
260
  export async function importSeedPhrase(
228
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
261
+ context: BrowserContext,
262
+ extensionId: string,
229
263
  { seedPhrase }: { seedPhrase: string },
230
264
  ): Promise<void> {
231
- const context = page.__extensionContext
232
- const extensionId = page.__extensionId
233
-
234
265
  const extensionPage = await findOnboardingPage(context, extensionId)
235
266
 
236
267
  try {
@@ -13,11 +13,11 @@ vi.mock('node:fs', async () => {
13
13
  ...actual,
14
14
  default: {
15
15
  ...actual,
16
- existsSync: vi.fn(),
17
- readdirSync: vi.fn(),
16
+ promises: {
17
+ ...actual.promises,
18
+ readdir: vi.fn(),
19
+ },
18
20
  },
19
- existsSync: vi.fn(),
20
- readdirSync: vi.fn(),
21
21
  }
22
22
  })
23
23
 
@@ -43,21 +43,20 @@ describe('polkadot-js wallet', () => {
43
43
  })
44
44
 
45
45
  describe('getPolkadotJSExtensionPath', () => {
46
+ const mockReaddir = () => vi.mocked(fs.promises.readdir) as unknown as ReturnType<typeof vi.fn>
47
+
46
48
  it('should return extension path when extension exists', async () => {
47
- const mockedFs = vi.mocked(fs)
48
- mockedFs.existsSync.mockReturnValue(true)
49
- mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
49
+ mockReaddir().mockResolvedValueOnce(['manifest.json'])
50
50
 
51
51
  const result = await getPolkadotJSExtensionPath()
52
52
 
53
53
  expect(result).toContain('.chroma')
54
54
  expect(result).toContain(POLKADOT_JS_CONFIG.extensionName)
55
- expect(mockedFs.existsSync).toHaveBeenCalled()
55
+ expect(fs.promises.readdir).toHaveBeenCalled()
56
56
  })
57
57
 
58
58
  it('should throw error when extension directory does not exist', async () => {
59
- const mockedFs = vi.mocked(fs)
60
- mockedFs.existsSync.mockReturnValue(false)
59
+ mockReaddir().mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
61
60
 
62
61
  await expect(getPolkadotJSExtensionPath()).rejects.toThrow(
63
62
  'Polkadot-JS extension not found',
@@ -65,9 +64,7 @@ describe('polkadot-js wallet', () => {
65
64
  })
66
65
 
67
66
  it('should throw error when extension directory is empty', async () => {
68
- const mockedFs = vi.mocked(fs)
69
- mockedFs.existsSync.mockReturnValue(true)
70
- mockedFs.readdirSync.mockReturnValue([])
67
+ mockReaddir().mockResolvedValueOnce([])
71
68
 
72
69
  await expect(getPolkadotJSExtensionPath()).rejects.toThrow(
73
70
  'Polkadot-JS extension not found',
@@ -75,8 +72,7 @@ describe('polkadot-js wallet', () => {
75
72
  })
76
73
 
77
74
  it('should include download instructions in error message', async () => {
78
- const mockedFs = vi.mocked(fs)
79
- mockedFs.existsSync.mockReturnValue(false)
75
+ mockReaddir().mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
80
76
 
81
77
  await expect(getPolkadotJSExtensionPath()).rejects.toThrow(
82
78
  'npx @avalix/chroma download-extensions',
@@ -84,9 +80,7 @@ describe('polkadot-js wallet', () => {
84
80
  })
85
81
 
86
82
  it('should use correct path structure', async () => {
87
- const mockedFs = vi.mocked(fs)
88
- mockedFs.existsSync.mockReturnValue(true)
89
- mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
83
+ mockReaddir().mockResolvedValueOnce(['manifest.json'])
90
84
 
91
85
  const result = await getPolkadotJSExtensionPath()
92
86
 
@@ -1,8 +1,10 @@
1
- import type { BrowserContext, Page } from '@playwright/test'
1
+ import type { BrowserContext } from '@playwright/test'
2
2
  import type { WalletAccount } from '../context-playwright/types.js'
3
3
  import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import process from 'node:process'
6
+ import { findExtensionPopup } from '../utils/find-extension-popup.js'
7
+ import { DEFAULT_TEST_PASSWORD } from '../utils/test-defaults.js'
6
8
 
7
9
  // Polkadot-JS specific configuration
8
10
  // https://github.com/polkadot-js/extension/releases
@@ -12,42 +14,14 @@ export const POLKADOT_JS_CONFIG = {
12
14
  extensionName: `polkadot-extension-${VERSION}`,
13
15
  } as const
14
16
 
15
- /*
16
- * Helper function to find extension popup
17
- * Coverage excluded: requires real browser context with Chrome extension APIs.
18
- */
19
- /* c8 ignore start */
20
- async function findExtensionPopup(context: BrowserContext, extensionId: string): Promise<Page> {
21
- // Wait for extension popup to appear with retry logic
22
- const maxAttempts = 10
23
- const retryDelay = 500
24
-
25
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
26
- const pages = context.pages()
27
- for (const p of pages) {
28
- if (p.url().includes(`chrome-extension://${extensionId}/`)) {
29
- await p.waitForLoadState('domcontentloaded')
30
- return p
31
- }
32
- }
33
-
34
- // If not found, wait a bit before retrying
35
- if (attempt < maxAttempts - 1) {
36
- await new Promise(resolve => setTimeout(resolve, retryDelay))
37
- }
38
- }
39
-
40
- throw new Error(`Extension popup not found for ID: ${extensionId}`)
41
- }
42
- /* c8 ignore stop */
43
-
44
17
  // Get Polkadot-JS extension path
45
18
  export async function getPolkadotJSExtensionPath(): Promise<string> {
46
19
  const extensionsDir = path.resolve(process.cwd(), '.chroma')
47
20
  const extensionDir = path.join(extensionsDir, POLKADOT_JS_CONFIG.extensionName)
48
21
 
49
- // Check if extension exists
50
- if (!fs.existsSync(extensionDir) || fs.readdirSync(extensionDir).length === 0) {
22
+ // Check if extension exists (readdir rejects if missing → treat as empty)
23
+ const entries = await fs.promises.readdir(extensionDir).catch(() => [] as string[])
24
+ if (entries.length === 0) {
51
25
  throw new Error(
52
26
  `Polkadot-JS extension not found at: ${extensionDir}\n\n`
53
27
  + `Please download the extension first by running:\n`
@@ -67,12 +41,10 @@ export async function getPolkadotJSExtensionPath(): Promise<string> {
67
41
 
68
42
  // Polkadot-JS specific account import implementation
69
43
  export async function importPolkadotJSAccount(
70
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
71
- { seed, name = 'Test Account', password = 'h3llop0lkadot!' }: WalletAccount,
44
+ context: BrowserContext,
45
+ extensionId: string,
46
+ { seed, name = 'Test Account', password = DEFAULT_TEST_PASSWORD }: WalletAccount,
72
47
  ): Promise<void> {
73
- const context = page.__extensionContext
74
- const extensionId = page.__extensionId
75
-
76
48
  const extensionPopupUrl = `chrome-extension://${extensionId}/index.html`
77
49
  const extensionPage = await context.newPage()
78
50
 
@@ -83,12 +55,11 @@ export async function importPolkadotJSAccount(
83
55
  const understoodButton = extensionPage.getByRole('button', { name: 'Understood, let me continue' })
84
56
  if (await understoodButton.count() > 0) {
85
57
  await understoodButton.click()
86
- await extensionPage.waitForTimeout(100)
87
58
  }
88
59
 
89
- if (await extensionPage.getByRole('button', { name: 'I Understand' }).isVisible()) {
90
- await extensionPage.getByRole('button', { name: 'I Understand' }).click()
91
- }
60
+ // The "I Understand" disclaimer may follow; wait briefly for it to settle.
61
+ const iUnderstand = extensionPage.getByRole('button', { name: 'I Understand' })
62
+ await iUnderstand.click({ timeout: 1000 }).catch(() => {})
92
63
 
93
64
  // Navigate to import seed page
94
65
  await extensionPage.goto(`${extensionPopupUrl}#/account/import-seed`)
@@ -108,11 +79,9 @@ export async function importPolkadotJSAccount(
108
79
 
109
80
  // Polkadot-JS specific authorization implementation
110
81
  export async function authorizePolkadotJS(
111
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
82
+ context: BrowserContext,
83
+ extensionId: string,
112
84
  ): Promise<void> {
113
- const context = page.__extensionContext
114
- const extensionId = page.__extensionId
115
-
116
85
  const extensionPopup = await findExtensionPopup(context, extensionId)
117
86
 
118
87
  if (await extensionPopup.getByRole('button', { name: 'I Understand' }).isVisible()) {
@@ -132,13 +101,11 @@ export async function authorizePolkadotJS(
132
101
 
133
102
  // Polkadot-JS specific transaction approval implementation
134
103
  export async function approvePolkadotJSTx(
135
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
104
+ context: BrowserContext,
105
+ extensionId: string,
136
106
  options: { password?: string } = {},
137
107
  ): Promise<void> {
138
- const { password = 'h3llop0lkadot!' } = options
139
- const context = page.__extensionContext
140
- const extensionId = page.__extensionId
141
-
108
+ const { password = DEFAULT_TEST_PASSWORD } = options
142
109
  const extensionPopup = await findExtensionPopup(context, extensionId)
143
110
  await extensionPopup.getByRole('textbox').fill(password)
144
111
  await extensionPopup.getByRole('button', { name: 'Sign the transaction' }).click()
@@ -146,11 +113,9 @@ export async function approvePolkadotJSTx(
146
113
 
147
114
  // Polkadot-JS specific transaction rejection implementation
148
115
  export async function rejectPolkadotJSTx(
149
- page: Page & { __extensionContext: BrowserContext, __extensionId: string },
116
+ context: BrowserContext,
117
+ extensionId: string,
150
118
  ): Promise<void> {
151
- const context = page.__extensionContext
152
- const extensionId = page.__extensionId
153
-
154
119
  const extensionPopup = await findExtensionPopup(context, extensionId)
155
120
  await extensionPopup.getByRole('link', { name: 'Cancel' }).click()
156
121
  }
@@ -13,11 +13,11 @@ vi.mock('node:fs', async () => {
13
13
  ...actual,
14
14
  default: {
15
15
  ...actual,
16
- existsSync: vi.fn(),
17
- readdirSync: vi.fn(),
16
+ promises: {
17
+ ...actual.promises,
18
+ readdir: vi.fn(),
19
+ },
18
20
  },
19
- existsSync: vi.fn(),
20
- readdirSync: vi.fn(),
21
21
  }
22
22
  })
23
23
 
@@ -43,21 +43,20 @@ describe('talisman wallet', () => {
43
43
  })
44
44
 
45
45
  describe('getTalismanExtensionPath', () => {
46
+ const mockReaddir = () => vi.mocked(fs.promises.readdir) as unknown as ReturnType<typeof vi.fn>
47
+
46
48
  it('should return extension path when extension exists', async () => {
47
- const mockedFs = vi.mocked(fs)
48
- mockedFs.existsSync.mockReturnValue(true)
49
- mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
49
+ mockReaddir().mockResolvedValueOnce(['manifest.json'])
50
50
 
51
51
  const result = await getTalismanExtensionPath()
52
52
 
53
53
  expect(result).toContain('.chroma')
54
54
  expect(result).toContain(TALISMAN_CONFIG.extensionName)
55
- expect(mockedFs.existsSync).toHaveBeenCalled()
55
+ expect(fs.promises.readdir).toHaveBeenCalled()
56
56
  })
57
57
 
58
58
  it('should throw error when extension directory does not exist', async () => {
59
- const mockedFs = vi.mocked(fs)
60
- mockedFs.existsSync.mockReturnValue(false)
59
+ mockReaddir().mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
61
60
 
62
61
  await expect(getTalismanExtensionPath()).rejects.toThrow(
63
62
  'Talisman extension not found',
@@ -65,9 +64,7 @@ describe('talisman wallet', () => {
65
64
  })
66
65
 
67
66
  it('should throw error when extension directory is empty', async () => {
68
- const mockedFs = vi.mocked(fs)
69
- mockedFs.existsSync.mockReturnValue(true)
70
- mockedFs.readdirSync.mockReturnValue([])
67
+ mockReaddir().mockResolvedValueOnce([])
71
68
 
72
69
  await expect(getTalismanExtensionPath()).rejects.toThrow(
73
70
  'Talisman extension not found',
@@ -75,8 +72,7 @@ describe('talisman wallet', () => {
75
72
  })
76
73
 
77
74
  it('should include download instructions in error message', async () => {
78
- const mockedFs = vi.mocked(fs)
79
- mockedFs.existsSync.mockReturnValue(false)
75
+ mockReaddir().mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
80
76
 
81
77
  await expect(getTalismanExtensionPath()).rejects.toThrow(
82
78
  'npx @avalix/chroma download-extensions',
@@ -84,9 +80,7 @@ describe('talisman wallet', () => {
84
80
  })
85
81
 
86
82
  it('should use correct path structure', async () => {
87
- const mockedFs = vi.mocked(fs)
88
- mockedFs.existsSync.mockReturnValue(true)
89
- mockedFs.readdirSync.mockReturnValue(['manifest.json'] as any)
83
+ mockReaddir().mockResolvedValueOnce(['manifest.json'])
90
84
 
91
85
  const result = await getTalismanExtensionPath()
92
86