Skip to content

Commit e96d035

Browse files
satoshai-devclaude
andauthored
feat: support Playwright config-level Stacks settings (#21)
Add PlaystacksOptions interface for setting Stacks config in playwright.config.ts `use` block. Export a pre-configured `test` that reads options from config and a `testWithStacks()` that accepts file-level overrides. Closes #16 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aac68c0 commit e96d035

File tree

3 files changed

+201
-38
lines changed

3 files changed

+201
-38
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@satoshai/playstacks': minor
3+
---
4+
5+
Support Playwright config-level Stacks settings
6+
7+
Adds `PlaystacksOptions` that can be set in `playwright.config.ts`'s `use` block (`stacksPrivateKey`, `stacksNetwork`, `stacksFeeMultiplier`, etc.), eliminating the need to repeat config in every test file. File-level `testWithStacks()` overrides still take precedence.

packages/playstacks/src/fixtures.ts

Lines changed: 187 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import type { ClarityValue } from '@stacks/transactions';
33

44
import {
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+
4183
export 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
}

packages/playstacks/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
// Public API
2-
export { testWithStacks, type StacksFixture, type StacksFixtures } from './fixtures.js';
2+
export {
3+
test,
4+
testWithStacks,
5+
type StacksFixture,
6+
type StacksFixtures,
7+
type PlaystacksOptions,
8+
} from './fixtures.js';
39
export {
410
PlaystacksError,
511
NetworkError,

0 commit comments

Comments
 (0)