Skip to content

Commit 7c739b3

Browse files
xorsalclaude
andcommitted
feat(e2e): add mint-to-public tests with balance assertion
Add two E2E test scenarios using @wonderland/walletless: - Walletless MetaMask: simulates MetaMask wallet connection and minting - Embedded wallet: uses "Create New Account" button for minting Both tests assert that public balance increases after minting. Also add simulation before sending transactions to catch revert reasons early in useWriteContract hook. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 162cf8e commit 7c739b3

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

src/hooks/contracts/useWriteContract.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,30 @@ export const useWriteContract = (options: UseWriteContractOptions = {}) => {
175175
}
176176

177177
const tx = method(...(args as unknown[]));
178+
179+
// Simulate first to catch revert reasons before sending
180+
console.log(
181+
`[useWriteContract] Simulating ${String(functionName)}...`
182+
);
183+
try {
184+
const simulateResult = await (
185+
tx as { simulate: (opts: unknown) => Promise<unknown> }
186+
).simulate({ from: account.getAddress() });
187+
console.log(
188+
`[useWriteContract] Simulation successful:`,
189+
simulateResult
190+
);
191+
} catch (simErr) {
192+
const simErrorMsg =
193+
simErr instanceof Error ? simErr.message : 'Simulation failed';
194+
console.error(
195+
`[useWriteContract] Simulation failed for ${String(functionName)}:`,
196+
simErr
197+
);
198+
setError(simErrorMsg);
199+
return { success: false, error: `Simulation failed: ${simErrorMsg}` };
200+
}
201+
178202
const sentTx = (
179203
tx as {
180204
send: (opts: unknown) => {

src/hooks/mutations/useDripper.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ export const useDripper = (options: UseDripperOptions = {}) => {
7272
throw new Error('Account not available');
7373
}
7474

75+
// Simulate first to catch revert reasons before sending
76+
console.log('[useDripper] Simulating drip_to_public...', {
77+
dripperAddress,
78+
tokenAddress,
79+
amount: amount.toString(),
80+
account: account.getAddress().toString(),
81+
});
82+
7583
const result = await writeContract({
7684
contract: DripperContract,
7785
address: dripperAddress,
@@ -80,9 +88,11 @@ export const useDripper = (options: UseDripperOptions = {}) => {
8088
});
8189

8290
if (!result.success) {
91+
console.error('[useDripper] drip_to_public failed:', result.error);
8392
throw new Error(result.error ?? 'drip_to_public failed');
8493
}
8594

95+
console.log('[useDripper] drip_to_public success:', result);
8696
invalidateBalance();
8797
},
8898
onSuccess: () => options.onDripToPublicSuccess?.(),

tests/e2e/mint-to-public.spec.ts

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/**
2+
* E2E Test: Mint to Public Balance
3+
*
4+
* Tests the minting functionality for both:
5+
* 1. Walletless (MetaMask simulation via @wonderland/walletless)
6+
* 2. Embedded wallet (Create New Account)
7+
*
8+
* Completion criteria: Public balance increases after minting
9+
*/
10+
11+
import { test, expect, ANVIL_ACCOUNTS } from './fixtures/walletless';
12+
import { test as baseTest } from '@playwright/test';
13+
14+
const MINT_AMOUNT = '1';
15+
const SANDBOX_CONNECTION_TIMEOUT = 120000;
16+
const WALLET_OPERATION_TIMEOUT = 120000;
17+
18+
/**
19+
* Helper to clear browser storage before each test
20+
*/
21+
async function clearBrowserStorage(page: import('@playwright/test').Page) {
22+
await page.goto('/');
23+
await page.evaluate(async () => {
24+
const dbs = await indexedDB.databases();
25+
for (const db of dbs) {
26+
if (db.name) indexedDB.deleteDatabase(db.name);
27+
}
28+
localStorage.clear();
29+
sessionStorage.clear();
30+
});
31+
}
32+
33+
/**
34+
* Helper to connect to sandbox network
35+
*/
36+
async function connectToSandbox(page: import('@playwright/test').Page) {
37+
// Click "Connect Wallet" to open modal
38+
const connectBtn = page.locator('.wallet-connect-button');
39+
await expect(connectBtn).toBeVisible({ timeout: 30000 });
40+
await connectBtn.click();
41+
42+
// Wait for modal
43+
const modal = page.locator('.modal-content');
44+
await expect(modal).toBeVisible({ timeout: 5000 });
45+
46+
// Select sandbox network
47+
const networkSelect = page.locator('#modal-network-selector');
48+
await networkSelect.selectOption('sandbox');
49+
50+
// Wait for network connection
51+
const networkStatus = page.locator('.network-status');
52+
await expect(networkStatus).toContainText('connected', {
53+
timeout: SANDBOX_CONNECTION_TIMEOUT,
54+
});
55+
56+
return modal;
57+
}
58+
59+
/**
60+
* Helper to get current public balance from the UI
61+
*/
62+
async function getPublicBalance(
63+
page: import('@playwright/test').Page
64+
): Promise<bigint> {
65+
// Wait for balance card to be visible
66+
const balanceCard = page.locator('.token-balance-card');
67+
await expect(balanceCard).toBeVisible({ timeout: 30000 });
68+
69+
// Wait for loading to complete
70+
const loadingSpinner = page.locator('.balance-loading');
71+
if (await loadingSpinner.isVisible()) {
72+
await expect(loadingSpinner).not.toBeVisible({ timeout: 60000 });
73+
}
74+
75+
// Get the public balance value
76+
const publicBalanceItem = page.locator('.balance-item').filter({
77+
has: page.locator('.balance-label:has-text("Public")'),
78+
});
79+
80+
const balanceValue = publicBalanceItem.locator('.balance-value');
81+
await expect(balanceValue).toBeVisible({ timeout: 10000 });
82+
83+
const balanceText = await balanceValue.textContent();
84+
return BigInt(balanceText?.trim() || '0');
85+
}
86+
87+
/**
88+
* Helper to wait for balance to sync after minting
89+
*/
90+
async function waitForBalanceSync(
91+
page: import('@playwright/test').Page,
92+
expectedMinimum: bigint,
93+
timeout = 60000
94+
): Promise<bigint> {
95+
const startTime = Date.now();
96+
97+
while (Date.now() - startTime < timeout) {
98+
const balance = await getPublicBalance(page);
99+
if (balance >= expectedMinimum) {
100+
return balance;
101+
}
102+
// Wait a bit before checking again
103+
await page.waitForTimeout(2000);
104+
105+
// Check if there's a refetch happening
106+
const refetchBadge = page.locator('.balance-refetch-badge');
107+
if (await refetchBadge.isVisible()) {
108+
await expect(refetchBadge).not.toBeVisible({ timeout: 30000 });
109+
}
110+
}
111+
112+
throw new Error(
113+
`Balance did not reach expected minimum ${expectedMinimum} within ${timeout}ms`
114+
);
115+
}
116+
117+
/**
118+
* Helper to mint tokens to public balance
119+
*/
120+
async function mintToPublic(
121+
page: import('@playwright/test').Page,
122+
amount: string
123+
) {
124+
// Wait for dripper form to be visible
125+
const dripperContent = page.locator('.dripper-content');
126+
await expect(dripperContent).toBeVisible({ timeout: 60000 });
127+
128+
// Wait for contracts to load (loading spinner should disappear)
129+
const loadingSpinner = dripperContent.locator('.animate-spin');
130+
if (await loadingSpinner.isVisible()) {
131+
await expect(loadingSpinner).not.toBeVisible({ timeout: 120000 });
132+
}
133+
134+
// Enter amount
135+
const amountInput = page.locator('#amount');
136+
await expect(amountInput).toBeVisible({ timeout: 10000 });
137+
await expect(amountInput).toBeEnabled({ timeout: 10000 });
138+
await amountInput.fill(amount);
139+
140+
// Select public drip type
141+
const dripTypeSelect = page.locator('#drip-type');
142+
await expect(dripTypeSelect).toBeEnabled({ timeout: 10000 });
143+
await dripTypeSelect.selectOption('public');
144+
145+
// Find the drip button - look for button with btn-primary class containing "Drip"
146+
const dripButton = page.locator('button.btn-primary').filter({
147+
hasText: /Drip to public/i,
148+
});
149+
await expect(dripButton).toBeVisible({ timeout: 10000 });
150+
await expect(dripButton).toBeEnabled({ timeout: 30000 });
151+
152+
// Log button state before clicking
153+
const buttonText = await dripButton.textContent();
154+
console.log('Drip button text before click:', buttonText);
155+
156+
// Click the drip button
157+
await dripButton.click();
158+
console.log('Drip button clicked');
159+
160+
// Wait for transaction to process - button text changes to "Processing..."
161+
// Use a polling approach since the state change might be very quick
162+
const startTime = Date.now();
163+
let sawProcessing = false;
164+
165+
while (Date.now() - startTime < WALLET_OPERATION_TIMEOUT) {
166+
const currentText = await dripButton.textContent();
167+
168+
if (currentText?.includes('Processing')) {
169+
sawProcessing = true;
170+
console.log('Transaction processing...');
171+
}
172+
173+
// Check if processing is done (back to "Drip to")
174+
if (sawProcessing && currentText?.includes('Drip to')) {
175+
console.log('Transaction completed');
176+
break;
177+
}
178+
179+
// Also check for success notification - indicates transaction completed
180+
const successNotification = page.locator('.error-item.info');
181+
if (await successNotification.isVisible()) {
182+
const notifText = await successNotification.textContent();
183+
if (notifText?.includes('Successfully minted')) {
184+
console.log('Success notification appeared:', notifText);
185+
break;
186+
}
187+
}
188+
189+
await page.waitForTimeout(500);
190+
}
191+
}
192+
193+
test.describe('Mint to Public - Walletless (MetaMask)', () => {
194+
test.beforeEach(async ({ page }) => {
195+
await clearBrowserStorage(page);
196+
});
197+
198+
test('should mint tokens to public balance via walletless MetaMask', async ({
199+
page,
200+
walletless,
201+
}) => {
202+
console.log('\n=== E2E: Mint to Public via Walletless ===\n');
203+
console.log('Test account:', walletless.account.address);
204+
205+
// Navigate to app
206+
await page.goto('/');
207+
await page.waitForLoadState('networkidle');
208+
209+
// Connect to sandbox and open modal
210+
const modal = await connectToSandbox(page);
211+
console.log('Sandbox connected');
212+
213+
// Click MetaMask connect button
214+
const metamaskBtn = modal.locator('button:has-text("MetaMask")');
215+
await expect(metamaskBtn).toBeEnabled({ timeout: 10000 });
216+
await metamaskBtn.click();
217+
console.log('MetaMask button clicked, waiting for wallet connection...');
218+
219+
// Wait for modal to close (connection complete)
220+
await expect(modal).not.toBeVisible({ timeout: WALLET_OPERATION_TIMEOUT });
221+
console.log('Wallet connected');
222+
223+
// Wait for account section to be visible
224+
const accountSection = page.locator('.connected-account-section');
225+
await expect(accountSection).toBeVisible({
226+
timeout: WALLET_OPERATION_TIMEOUT,
227+
});
228+
229+
// Get initial public balance
230+
const initialBalance = await getPublicBalance(page);
231+
console.log('Initial public balance:', initialBalance.toString());
232+
233+
// Mint tokens to public balance
234+
console.log(`Minting ${MINT_AMOUNT} tokens to public balance...`);
235+
await mintToPublic(page, MINT_AMOUNT);
236+
console.log('Mint transaction submitted');
237+
238+
// Wait for balance to increase
239+
const expectedMinBalance = initialBalance + BigInt(MINT_AMOUNT);
240+
const finalBalance = await waitForBalanceSync(page, expectedMinBalance);
241+
console.log('Final public balance:', finalBalance.toString());
242+
243+
// Assert balance increased
244+
expect(finalBalance).toBeGreaterThanOrEqual(expectedMinBalance);
245+
console.log(
246+
`Balance increased by ${(finalBalance - initialBalance).toString()} tokens`
247+
);
248+
249+
console.log('\n=== TEST PASSED ===\n');
250+
});
251+
});
252+
253+
// Use base test (without walletless fixture) for embedded wallet test
254+
baseTest.describe('Mint to Public - Embedded Wallet (Create New Account)', () => {
255+
baseTest.beforeEach(async ({ page }) => {
256+
await clearBrowserStorage(page);
257+
});
258+
259+
baseTest(
260+
'should mint tokens to public balance via embedded wallet',
261+
async ({ page }) => {
262+
console.log('\n=== E2E: Mint to Public via Embedded Wallet ===\n');
263+
264+
// Navigate to app
265+
await page.goto('/');
266+
await page.waitForLoadState('networkidle');
267+
268+
// Connect to sandbox and open modal
269+
const modal = await connectToSandbox(page);
270+
console.log('Sandbox connected');
271+
272+
// Click "Create New Account" button
273+
const createAccountBtn = modal.locator(
274+
'button:has-text("Create New Account")'
275+
);
276+
await expect(createAccountBtn).toBeEnabled({ timeout: 10000 });
277+
await createAccountBtn.click();
278+
console.log('Create New Account clicked, waiting for account creation...');
279+
280+
// Wait for modal to close (account creation complete)
281+
await expect(modal).not.toBeVisible({ timeout: WALLET_OPERATION_TIMEOUT });
282+
console.log('Account created and connected');
283+
284+
// Wait for account section to be visible
285+
const accountSection = page.locator('.connected-account-section');
286+
await expect(accountSection).toBeVisible({
287+
timeout: WALLET_OPERATION_TIMEOUT,
288+
});
289+
290+
// Get initial public balance (should be 0 for new account)
291+
const initialBalance = await getPublicBalance(page);
292+
console.log('Initial public balance:', initialBalance.toString());
293+
294+
// Mint tokens to public balance
295+
console.log(`Minting ${MINT_AMOUNT} tokens to public balance...`);
296+
await mintToPublic(page, MINT_AMOUNT);
297+
console.log('Mint transaction submitted');
298+
299+
// Wait for balance to increase
300+
const expectedMinBalance = initialBalance + BigInt(MINT_AMOUNT);
301+
const finalBalance = await waitForBalanceSync(page, expectedMinBalance);
302+
console.log('Final public balance:', finalBalance.toString());
303+
304+
// Assert balance increased
305+
expect(finalBalance).toBeGreaterThanOrEqual(expectedMinBalance);
306+
console.log(
307+
`Balance increased by ${(finalBalance - initialBalance).toString()} tokens`
308+
);
309+
310+
console.log('\n=== TEST PASSED ===\n');
311+
}
312+
);
313+
});

0 commit comments

Comments
 (0)