fix: evaluator: add stakeKrk and unstakeKrk browser helpers (#460)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0b5752ca52
commit
b7bbbb9b89
1 changed files with 182 additions and 0 deletions
182
scripts/harb-evaluator/helpers/stake.ts
Normal file
182
scripts/harb-evaluator/helpers/stake.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Shared staking helpers for holdout scenarios.
|
||||
*
|
||||
* stakeKrk — drives the stake page UI (navigate → fill → submit → wait for Ponder).
|
||||
* unstakeKrk — expands the active position collapse and clicks Unstake.
|
||||
*
|
||||
* Both helpers handle the password gate on first visit automatically.
|
||||
*/
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import { navigateSPA } from '../../../tests/setup/navigate';
|
||||
import { getStackConfig } from '../../../tests/setup/stack';
|
||||
|
||||
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Navigate to /app/stake, handling the auth guard's password gate if triggered.
|
||||
* The guard redirects to /login when localStorage 'authentificated' is absent.
|
||||
* After a successful login, router.push('/') causes '/' → '/stake' redirect so
|
||||
* we end up on the stake page without a second navigateSPA call.
|
||||
*/
|
||||
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()) {
|
||||
console.log('[stake] Password prompt detected, entering lobsterDao...');
|
||||
await passwordInput.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 });
|
||||
console.log('[stake] Authenticated, stake page loading');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the Ponder GraphQL API until at least one active position exists for owner.
|
||||
* Throws if no active position appears within timeoutMs.
|
||||
*/
|
||||
async function waitForActivePosition(graphqlUrl: string, owner: string, timeoutMs = 30_000): Promise<void> {
|
||||
const query = `
|
||||
query PositionsByOwner($owner: String!) {
|
||||
positionss(where: { owner: $owner }, limit: 5) {
|
||||
items {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const resp = await fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ query, variables: { owner } }),
|
||||
});
|
||||
const payload = (await resp.json()) as {
|
||||
data?: { positionss?: { items?: Array<{ id: string; status: string }> } };
|
||||
};
|
||||
const items = payload?.data?.positionss?.items ?? [];
|
||||
const active = items.filter(p => p.status === 'Active');
|
||||
if (active.length > 0) {
|
||||
console.log(`[stake] Ponder indexed ${active.length} active position(s)`);
|
||||
return;
|
||||
}
|
||||
console.log('[stake] Waiting for Ponder to index staking position...');
|
||||
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: Ponder GraphQL is HTTP-only (no push). See AGENTS.md #Engineering Principles.
|
||||
await new Promise(r => setTimeout(r, 2_000));
|
||||
}
|
||||
throw new Error(`No active staking position found in Ponder GraphQL within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
// ── Exported helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Navigate to the stake page, fill the amount, select tax rate, and submit.
|
||||
* Wallet must already be connected. App must be password-unlocked.
|
||||
*
|
||||
* @param page - Playwright page with injected wallet
|
||||
* @param amount - KRK amount to stake (as string, e.g. '1000')
|
||||
* @param taxRateIndex - Tax rate option index (0-based). Use the highest available for max tax.
|
||||
*/
|
||||
export async function stakeKrk(page: Page, amount: string, taxRateIndex: number): Promise<void> {
|
||||
console.log(`[stake] Staking ${amount} KRK at tax rate index ${taxRateIndex}...`);
|
||||
|
||||
await navigateToStakePage(page);
|
||||
|
||||
// The Token Amount slider is the readiness signal used in the e2e test.
|
||||
const tokenAmountSlider = page.getByRole('slider', { name: 'Token Amount' });
|
||||
await expect(tokenAmountSlider).toBeVisible({ timeout: 15_000 });
|
||||
console.log('[stake] Stake form loaded');
|
||||
|
||||
// Fill staking amount.
|
||||
const stakeAmountInput = page.getByLabel('Staking Amount');
|
||||
await expect(stakeAmountInput).toBeVisible({ timeout: 10_000 });
|
||||
await stakeAmountInput.fill(amount);
|
||||
console.log(`[stake] Filled staking amount: ${amount}`);
|
||||
|
||||
// Select tax rate.
|
||||
const taxSelect = page.getByRole('combobox', { name: 'Tax' });
|
||||
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 });
|
||||
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".
|
||||
try {
|
||||
await page
|
||||
.getByRole('button', { name: /Sign Transaction|Waiting/i })
|
||||
.waitFor({ state: 'visible', timeout: 5_000 });
|
||||
console.log('[stake] Transaction initiated, waiting for completion...');
|
||||
await page
|
||||
.getByRole('main')
|
||||
.getByRole('button', { name: /Stake|Snatch and Stake/i })
|
||||
.waitFor({ state: 'visible', timeout: 60_000 });
|
||||
console.log('[stake] Stake transaction completed');
|
||||
} catch {
|
||||
console.log('[stake] Transaction state not observed (may have completed instantly)');
|
||||
}
|
||||
|
||||
// Resolve the connected account address to scope the Ponder query to this 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' });
|
||||
return accounts[0].toLowerCase();
|
||||
});
|
||||
|
||||
// Poll Ponder GraphQL until the new position is indexed (indexing latency up to 30s).
|
||||
const { graphqlUrl } = getStackConfig();
|
||||
await waitForActivePosition(graphqlUrl, accountAddress);
|
||||
console.log('[stake] ✅ Staking complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to an active staking position and click Unstake.
|
||||
* Assumes the wallet has exactly one active position (simplest case).
|
||||
*
|
||||
* @param page - Playwright page with injected wallet
|
||||
*/
|
||||
export async function unstakeKrk(page: Page): Promise<void> {
|
||||
console.log('[stake] Unstaking active position...');
|
||||
|
||||
await navigateToStakePage(page);
|
||||
|
||||
// Wait for the active position collapse to appear.
|
||||
const activeCollapse = page.locator('.f-collapse-active').first();
|
||||
await expect(activeCollapse).toBeVisible({ timeout: 15_000 });
|
||||
console.log('[stake] Active position found');
|
||||
|
||||
// FCollapse starts collapsed (isShow = false). Click the toggle icon to expand.
|
||||
// The icon carries class .toggle-collapse; clicking it fires openClose() in FCollapse.
|
||||
const toggleIcon = activeCollapse.locator('.toggle-collapse').first();
|
||||
await toggleIcon.click();
|
||||
console.log('[stake] Expanded position collapse');
|
||||
|
||||
// The Unstake button appears in the collapsed body when unstake.state === 'Unstakeable'.
|
||||
const unstakeButton = activeCollapse.getByRole('button', { name: /^unstake$/i });
|
||||
await expect(unstakeButton).toBeVisible({ timeout: 10_000 });
|
||||
console.log('[stake] Clicking Unstake...');
|
||||
await unstakeButton.click();
|
||||
|
||||
// Wait for transaction: SignTransaction → Waiting → position detaches from DOM.
|
||||
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');
|
||||
} catch {
|
||||
console.log('[stake] Transaction state not observed (may have completed instantly)');
|
||||
}
|
||||
|
||||
console.log('[stake] ✅ Unstaking complete');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue