@@ -3,6 +3,9 @@ import type { ClarityValue } from '@stacks/transactions';
33
44import {
55 type PlaystacksConfig ,
6+ type NetworkOption ,
7+ type FeeConfig ,
8+ type ConfirmationConfig ,
69 resolveConfig ,
710 isMnemonicConfig ,
811} from './config.js' ;
@@ -38,12 +41,194 @@ export interface StacksFixture {
3841 getNonce : ( address ?: string ) => Promise < bigint > ;
3942}
4043
44+ /**
45+ * Playwright config-level options for Stacks.
46+ * Set these in `playwright.config.ts` `use` block.
47+ *
48+ * @example
49+ * ```ts
50+ * // playwright.config.ts
51+ * export default defineConfig({
52+ * use: {
53+ * stacksPrivateKey: process.env.TEST_STX_KEY!,
54+ * stacksNetwork: 'testnet',
55+ * stacksFeeMultiplier: 2,
56+ * },
57+ * });
58+ * ```
59+ */
60+ export interface PlaystacksOptions {
61+ /** Hex-encoded private key */
62+ stacksPrivateKey : string | undefined ;
63+ /** BIP-39 mnemonic phrase */
64+ stacksMnemonic : string | undefined ;
65+ /** Account index for mnemonic derivation. Default: 0 */
66+ stacksAccountIndex : number ;
67+ /** Network to connect to */
68+ stacksNetwork : NetworkOption | undefined ;
69+ /** Fee multiplier. Default: 1.0 */
70+ stacksFeeMultiplier : number ;
71+ /** Max fee in microstacks. Default: 500_000 */
72+ stacksFeeMax : number ;
73+ /** Fixed fee in microstacks. Skips estimation when set. */
74+ stacksFeeFixed : number | undefined ;
75+ /** Tx confirmation timeout in ms. Default: 120_000 */
76+ stacksConfirmationTimeout : number ;
77+ /** Tx confirmation poll interval in ms. Default: 2_000 */
78+ stacksConfirmationPollInterval : number ;
79+ /** Request timeout in ms for API calls. Default: 30_000 */
80+ stacksRequestTimeout : number ;
81+ }
82+
4183export interface StacksFixtures {
4284 stacks : StacksFixture ;
4385}
4486
87+ /** Build a PlaystacksConfig from Playwright options, with file-level overrides merged on top */
88+ function buildConfigFromOptions (
89+ options : PlaystacksOptions ,
90+ overrides ?: Partial < PlaystacksConfig > ,
91+ ) : PlaystacksConfig {
92+ const network = overrides ?. network ?? options . stacksNetwork ;
93+ if ( ! network ) {
94+ throw new Error (
95+ 'Stacks network is required. Set stacksNetwork in playwright.config.ts use block or pass network to testWithStacks().'
96+ ) ;
97+ }
98+
99+ const fee : FeeConfig = {
100+ multiplier : options . stacksFeeMultiplier ,
101+ maxFee : options . stacksFeeMax ,
102+ fixed : options . stacksFeeFixed ,
103+ ...overrides ?. fee ,
104+ } ;
105+
106+ const confirmation : ConfirmationConfig = {
107+ timeout : options . stacksConfirmationTimeout ,
108+ pollInterval : options . stacksConfirmationPollInterval ,
109+ ...overrides ?. confirmation ,
110+ } ;
111+
112+ const requestTimeout = overrides ?. requestTimeout ?? options . stacksRequestTimeout ;
113+
114+ // Determine key source: file-level overrides take precedence over config-level
115+ const privateKey = ( overrides && 'privateKey' in overrides && overrides . privateKey )
116+ ? overrides . privateKey as string
117+ : options . stacksPrivateKey ;
118+ const mnemonic = ( overrides && 'mnemonic' in overrides && overrides . mnemonic )
119+ ? overrides . mnemonic as string
120+ : options . stacksMnemonic ;
121+ const accountIndex = ( overrides && 'accountIndex' in overrides && overrides . accountIndex !== undefined )
122+ ? overrides . accountIndex as number
123+ : options . stacksAccountIndex ;
124+
125+ if ( mnemonic ) {
126+ return { mnemonic, accountIndex, network, fee, confirmation, requestTimeout } ;
127+ }
128+ if ( privateKey ) {
129+ return { privateKey, network, fee, confirmation, requestTimeout } ;
130+ }
131+
132+ throw new Error (
133+ 'Stacks private key or mnemonic is required. Set stacksPrivateKey/stacksMnemonic in playwright.config.ts use block or pass privateKey/mnemonic to testWithStacks().'
134+ ) ;
135+ }
136+
137+ /** Resolve config (handling async mnemonic derivation) */
138+ async function resolvePlaystacksConfig ( config : PlaystacksConfig ) {
139+ if ( isMnemonicConfig ( config ) ) {
140+ const privateKey = await derivePrivateKeyFromMnemonic (
141+ config . mnemonic ,
142+ config . accountIndex ?? 0
143+ ) ;
144+ return resolveConfig ( config , privateKey ) ;
145+ }
146+ return resolveConfig ( config ) ;
147+ }
148+
149+ /** Create the stacks fixture from a resolved config */
150+ function createFixture (
151+ handler : MockProviderHandler ,
152+ resolved : ReturnType < typeof resolveConfig > ,
153+ ) : StacksFixture {
154+ const network = handler . network ;
155+ return {
156+ wallet : {
157+ address : handler . identity . address ,
158+ publicKey : handler . identity . publicKey ,
159+ rejectNext : ( ) => handler . rejectNext ( ) ,
160+ lastTxId : ( ) => handler . lastTxId ,
161+ } ,
162+ waitForTx : ( txid : string ) =>
163+ waitForConfirmation ( network , txid , resolved . confirmation , resolved . requestTimeout ) ,
164+ callReadOnly : ( options : ReadOnlyCallOptions ) =>
165+ callReadOnly ( network , options , handler . identity . address , resolved . requestTimeout ) ,
166+ getBalance : ( address ?: string ) =>
167+ fetchBalance ( network , address ?? handler . identity . address , resolved . requestTimeout ) ,
168+ getNonce : ( address ?: string ) =>
169+ fetchNonce ( network , address ?? handler . identity . address , resolved . requestTimeout ) ,
170+ } ;
171+ }
172+
173+ /**
174+ * Base test with Stacks config options and fixture.
175+ * Use with `playwright.config.ts` `use` block for shared config.
176+ *
177+ * @example
178+ * ```ts
179+ * // playwright.config.ts
180+ * export default defineConfig({
181+ * use: {
182+ * stacksPrivateKey: process.env.TEST_STX_KEY!,
183+ * stacksNetwork: 'testnet',
184+ * },
185+ * });
186+ *
187+ * // test file
188+ * import { test, expect } from '@satoshai/playstacks';
189+ * test('my test', async ({ stacks }) => { ... });
190+ * ```
191+ */
192+ // Cache resolved configs keyed by options to avoid re-deriving mnemonic keys,
193+ // while supporting multiple Playwright projects with different Stacks settings.
194+ const configCache = new Map < string , Promise < ReturnType < typeof resolveConfig > > > ( ) ;
195+
196+ export const test = base . extend < StacksFixtures & PlaystacksOptions > ( {
197+ // Options with defaults (set via playwright.config.ts `use` block)
198+ stacksPrivateKey : [ undefined , { option : true } ] ,
199+ stacksMnemonic : [ undefined , { option : true } ] ,
200+ stacksAccountIndex : [ 0 , { option : true } ] ,
201+ stacksNetwork : [ undefined , { option : true } ] ,
202+ stacksFeeMultiplier : [ 1.0 , { option : true } ] ,
203+ stacksFeeMax : [ 500_000 , { option : true } ] ,
204+ stacksFeeFixed : [ undefined , { option : true } ] ,
205+ stacksConfirmationTimeout : [ 120_000 , { option : true } ] ,
206+ stacksConfirmationPollInterval : [ 2_000 , { option : true } ] ,
207+ stacksRequestTimeout : [ 30_000 , { option : true } ] ,
208+
209+ stacks : async ( { page, stacksPrivateKey, stacksMnemonic, stacksAccountIndex, stacksNetwork, stacksFeeMultiplier, stacksFeeMax, stacksFeeFixed, stacksConfirmationTimeout, stacksConfirmationPollInterval, stacksRequestTimeout } , use ) => {
210+ const options : PlaystacksOptions = {
211+ stacksPrivateKey, stacksMnemonic, stacksAccountIndex, stacksNetwork,
212+ stacksFeeMultiplier, stacksFeeMax, stacksFeeFixed,
213+ stacksConfirmationTimeout, stacksConfirmationPollInterval,
214+ stacksRequestTimeout,
215+ } ;
216+ const cacheKey = JSON . stringify ( options ) ;
217+ if ( ! configCache . has ( cacheKey ) ) {
218+ const config = buildConfigFromOptions ( options ) ;
219+ configCache . set ( cacheKey , resolvePlaystacksConfig ( config ) ) ;
220+ }
221+ const resolved = await configCache . get ( cacheKey ) ! ;
222+ const handler = new MockProviderHandler ( resolved ) ;
223+ handler . resetNonce ( ) ;
224+ await handler . install ( page ) ;
225+ await use ( createFixture ( handler , resolved ) ) ;
226+ } ,
227+ } ) ;
228+
45229/**
46230 * Create a Playwright test function with Stacks wallet fixtures.
231+ * File-level config overrides any config-level settings from `playwright.config.ts`.
47232 *
48233 * @example
49234 * ```ts
@@ -66,14 +251,7 @@ export function testWithStacks(config: PlaystacksConfig) {
66251 let resolvedConfigPromise : Promise < ReturnType < typeof resolveConfig > > | null = null ;
67252
68253 async function getResolvedConfig ( ) {
69- if ( isMnemonicConfig ( config ) ) {
70- const privateKey = await derivePrivateKeyFromMnemonic (
71- config . mnemonic ,
72- config . accountIndex ?? 0
73- ) ;
74- return resolveConfig ( config , privateKey ) ;
75- }
76- return resolveConfig ( config ) ;
254+ return resolvePlaystacksConfig ( config ) ;
77255 }
78256
79257 return base . extend < StacksFixtures > ( {
@@ -83,37 +261,9 @@ export function testWithStacks(config: PlaystacksConfig) {
83261 }
84262 const resolved = await resolvedConfigPromise ;
85263 const handler = new MockProviderHandler ( resolved ) ;
86-
87- // Reset nonce tracking for each test to avoid stale nonces
88264 handler . resetNonce ( ) ;
89-
90- // Install the mock provider before any navigation
91265 await handler . install ( page ) ;
92-
93- const network = handler . network ;
94-
95- const fixture : StacksFixture = {
96- wallet : {
97- address : handler . identity . address ,
98- publicKey : handler . identity . publicKey ,
99- rejectNext : ( ) => handler . rejectNext ( ) ,
100- lastTxId : ( ) => handler . lastTxId ,
101- } ,
102-
103- waitForTx : ( txid : string ) =>
104- waitForConfirmation ( network , txid , resolved . confirmation , resolved . requestTimeout ) ,
105-
106- callReadOnly : ( options : ReadOnlyCallOptions ) =>
107- callReadOnly ( network , options , handler . identity . address , resolved . requestTimeout ) ,
108-
109- getBalance : ( address ?: string ) =>
110- fetchBalance ( network , address ?? handler . identity . address , resolved . requestTimeout ) ,
111-
112- getNonce : ( address ?: string ) =>
113- fetchNonce ( network , address ?? handler . identity . address , resolved . requestTimeout ) ,
114- } ;
115-
116- await use ( fixture ) ;
266+ await use ( createFixture ( handler , resolved ) ) ;
117267 } ,
118268 } ) ;
119269}
0 commit comments