fix: evaluator: add stakeKrk and unstakeKrk browser helpers (#460)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-05 15:09:54 +00:00
parent ad9a113a9c
commit 4e6182acc6

View file

@ -22,14 +22,22 @@ import { getStackConfig } from '../../../tests/setup/stack';
async function navigateToStakePage(page: Page): Promise<void> {
await navigateSPA(page, '/app/stake');
// If the auth guard redirected to /login, the password input will be visible.
const passwordInput = page.getByLabel('Password');
if (await passwordInput.isVisible()) {
// Race between the stake form and the login page to determine which route mounted.
// Point-in-time isVisible() is unreliable here: CSS transitions or async component
// setup can leave the element in the DOM but not yet "visible" right after networkidle.
const isLoginPage = await page
.getByLabel('Password')
.waitFor({ state: 'visible', timeout: 3_000 })
.then(() => true)
.catch(() => false);
if (isLoginPage) {
console.log('[stake] Password prompt detected, entering lobsterDao...');
await passwordInput.fill('lobsterDao');
await page.getByLabel('Password').fill('lobsterDao');
await page.getByRole('button', { name: 'Login' }).click();
// router.push('/') in LoginView → '/' redirects to '/stake' → stake page loads.
await page.waitForLoadState('networkidle', { timeout: 10_000 });
// Allow extra time for the two-hop client-side navigation to settle in slow CI.
await page.waitForLoadState('networkidle', { timeout: 20_000 });
console.log('[stake] Authenticated, stake page loading');
}
}
@ -37,6 +45,9 @@ async function navigateToStakePage(page: Page): Promise<void> {
/**
* Poll the Ponder GraphQL API until at least one active position exists for owner.
* Throws if no active position appears within timeoutMs.
*
* owner must be lowercase: the e2e tests and this helper both use address.toLowerCase()
* to match Ponder's storage format (verified against 01-acquire-and-stake.spec.ts).
*/
async function waitForActivePosition(graphqlUrl: string, owner: string, timeoutMs = 30_000): Promise<void> {
const query = `
@ -104,13 +115,14 @@ export async function stakeKrk(page: Page, amount: string, taxRateIndex: number)
await taxSelect.selectOption({ value: String(taxRateIndex) });
console.log(`[stake] Tax rate index ${taxRateIndex} selected`);
// Click the main form's stake button (not the small position-card buttons).
const stakeButton = page.getByRole('main').getByRole('button', { name: /Stake|Snatch and Stake/i });
// Anchored regex avoids matching the "Unstake" buttons on any expanded position cards.
const stakeButton = page.getByRole('main').getByRole('button', { name: /^(Snatch and )?Stake$/i });
await expect(stakeButton).toBeVisible({ timeout: 5_000 });
console.log('[stake] Clicking Stake button...');
await stakeButton.click();
// Wait for transaction: button cycles "Stake" → "Sign Transaction"/"Waiting" → "Stake".
// The intermediate states may be missed if the tx completes instantly (Anvil automine).
try {
await page
.getByRole('button', { name: /Sign Transaction|Waiting/i })
@ -118,7 +130,7 @@ export async function stakeKrk(page: Page, amount: string, taxRateIndex: number)
console.log('[stake] Transaction initiated, waiting for completion...');
await page
.getByRole('main')
.getByRole('button', { name: /Stake|Snatch and Stake/i })
.getByRole('button', { name: /^(Snatch and )?Stake$/i })
.waitFor({ state: 'visible', timeout: 60_000 });
console.log('[stake] Stake transaction completed');
} catch {
@ -126,9 +138,13 @@ export async function stakeKrk(page: Page, amount: string, taxRateIndex: number)
}
// Resolve the connected account address to scope the Ponder query to this wallet.
// Guard against an empty accounts array (locked or disconnected wallet).
const accountAddress = await page.evaluate(async () => {
const ethereum = (window as unknown as { ethereum: { request: (req: { method: string }) => Promise<string[]> } }).ethereum;
const accounts = await ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
throw new Error('[stake] eth_accounts returned empty array — wallet may be locked');
}
return accounts[0].toLowerCase();
});
@ -166,17 +182,23 @@ export async function unstakeKrk(page: Page): Promise<void> {
console.log('[stake] Clicking Unstake...');
await unstakeButton.click();
// Wait for transaction: SignTransaction → Waiting → position detaches from DOM.
// Observe the sign/waiting state if visible (may be missed on fast Anvil automine).
try {
await activeCollapse
.getByRole('button', { name: /Sign Transaction|Waiting/i })
.waitFor({ state: 'visible', timeout: 5_000 });
console.log('[stake] Unstake transaction initiated, waiting for completion...');
await activeCollapse.waitFor({ state: 'detached', timeout: 60_000 });
console.log('[stake] Position removed from UI');
console.log('[stake] Unstake transaction initiated');
} catch {
console.log('[stake] Transaction state not observed (may have completed instantly)');
console.log('[stake] Transaction sign/waiting state not observed (may have completed instantly)');
}
// Always wait for the collapse to detach — this verifies the Unstake click actually
// worked and the position was removed, regardless of whether the transient states
// were observed above.
// TODO: add a Ponder waitForPositionGone poll here for holdout scenarios that assert
// off-chain state (e.g. status === 'Closed') immediately after unstaking.
await activeCollapse.waitFor({ state: 'detached', timeout: 60_000 });
console.log('[stake] Position removed from UI');
console.log('[stake] ✅ Unstaking complete');
}