893 lines
44 KiB
TypeScript
893 lines
44 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import { Wallet } from 'ethers';
|
|
import { createWalletContext } from '../../setup/wallet-provider';
|
|
import { getStackConfig, validateStackHealthy } from '../../setup/stack';
|
|
import {
|
|
createPersonaFeedback,
|
|
addFeedbackStep,
|
|
writePersonaFeedback,
|
|
mintEth,
|
|
buyKrk,
|
|
resetChainState,
|
|
connectWallet,
|
|
type PersonaFeedback,
|
|
} from './helpers';
|
|
import { mkdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
const STACK_CONFIG = getStackConfig();
|
|
const STACK_RPC_URL = STACK_CONFIG.rpcUrl;
|
|
const STACK_WEBAPP_URL = STACK_CONFIG.webAppUrl;
|
|
const STAKE_PAGE_URL = 'http://localhost:5173/stake';
|
|
|
|
// Different accounts for different personas
|
|
const MARCUS_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; // Anvil #0
|
|
const SARAH_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'; // Anvil #1
|
|
const PRIYA_KEY = '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a'; // Anvil #2
|
|
|
|
test.describe('Test B: Staker Journey', () => {
|
|
test.beforeAll(async () => {
|
|
await resetChainState(STACK_RPC_URL);
|
|
await validateStackHealthy(STACK_CONFIG);
|
|
});
|
|
|
|
test.describe.serial('Marcus - Degen/MEV ("where\'s the edge?")', () => {
|
|
let feedback: PersonaFeedback;
|
|
const accountKey = MARCUS_KEY;
|
|
const accountAddr = new Wallet(accountKey).address;
|
|
|
|
test.beforeAll(() => {
|
|
feedback = createPersonaFeedback('marcus', 'B', 'staker');
|
|
});
|
|
|
|
test('Marcus pre-funds wallet with KRK', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Setting up: acquiring KRK for staking tests...');
|
|
await mintEth(page, STACK_RPC_URL, accountAddr, '50');
|
|
await buyKrk(page, '10', STACK_RPC_URL, accountKey);
|
|
observations.push('✓ Wallet funded with KRK');
|
|
|
|
addFeedbackStep(feedback, 'setup', observations);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Marcus analyzes staking interface for MEV opportunities', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
page.on('console', msg => console.log(`[BROWSER] ${msg.type()}: ${msg.text()}`));
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
observations.push('Scanning for arbitrage angles, tax rate gaps, snatching opportunities...');
|
|
|
|
// Check if leverage framing is clear
|
|
const hasLeverageInfo = await page.getByText(/leverage|multiplier|amplif/i).isVisible().catch(() => false);
|
|
if (hasLeverageInfo) {
|
|
observations.push('✓ Leverage mechanics visible - can assess risk/reward multiplier');
|
|
} else {
|
|
observations.push('⚠ Leverage framing unclear - hard to calculate edge');
|
|
feedback.overall.friction.push('Leverage mechanics not clearly explained');
|
|
}
|
|
|
|
// Tax rate tooltip
|
|
const taxSelect = page.getByRole('combobox', { name: /tax/i }).first();
|
|
const taxVisible = await taxSelect.isVisible({ timeout: 5_000 }).catch(() => false);
|
|
|
|
if (taxVisible) {
|
|
observations.push('✓ Tax rate selector found');
|
|
|
|
// Look for tooltip or info icon
|
|
const infoIcon = page.locator('svg[data-icon="circle-info"], svg[class*="info"]').first();
|
|
const hasTooltip = await infoIcon.isVisible().catch(() => false);
|
|
|
|
if (hasTooltip) {
|
|
observations.push('✓ Tax rate has tooltip - explains tradeoff');
|
|
await infoIcon.hover();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const tooltipText = await page.locator('[role="tooltip"], .tooltip').textContent().catch(() => '');
|
|
if (tooltipText.toLowerCase().includes('snatch') || tooltipText.toLowerCase().includes('harder')) {
|
|
observations.push('✓ Tooltip explains "higher tax = harder to snatch" - good framing');
|
|
} else {
|
|
observations.push('⚠ Tooltip doesn\'t clearly explain snatch resistance');
|
|
}
|
|
} else {
|
|
observations.push('✗ No tooltip on tax rate - mechanics unclear');
|
|
feedback.overall.friction.push('Tax rate selection lacks explanation');
|
|
}
|
|
} else {
|
|
observations.push('✗ Tax rate selector not found');
|
|
}
|
|
|
|
// Protocol stats visibility
|
|
const hasStats = await page.locator('text=/TVL|total staked|positions/i').isVisible().catch(() => false);
|
|
if (hasStats) {
|
|
observations.push('✓ Protocol stats visible - can gauge competition and pool depth');
|
|
} else {
|
|
observations.push('⚠ No protocol-wide stats - harder to assess meta');
|
|
}
|
|
|
|
// Contract addresses for verification
|
|
const hasContracts = await page.locator('text=/0x[a-fA-F0-9]{40}|contract/i').isVisible().catch(() => false);
|
|
if (hasContracts) {
|
|
observations.push('✓ Contract addresses visible - can verify on-chain before committing');
|
|
} else {
|
|
observations.push('✗ No contract addresses shown - can\'t independently verify');
|
|
feedback.overall.friction.push('No contract addresses for verification');
|
|
}
|
|
|
|
// Look for open positions to snatch
|
|
const positionsList = await page.locator('[class*="position"], [class*="stake-card"]').count();
|
|
if (positionsList > 0) {
|
|
observations.push(`✓ Can see ${positionsList} existing positions - potential snatch targets`);
|
|
} else {
|
|
observations.push('⚠ Can\'t see other stakers\' positions - no snatching meta visible');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'marcus-b');
|
|
mkdirSync(screenshotDir, { recursive: true });
|
|
const screenshotPath = join(screenshotDir, `stake-interface-analysis-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'stake-interface-analysis', observations, screenshotPath);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Marcus executes aggressive low-tax stake', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
// Connect wallet
|
|
await connectWallet(page);
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Going for lowest tax rate - maximum upside, I\'ll just monitor for snatches');
|
|
|
|
// Fill stake form
|
|
const stakeAmountInput = page.getByLabel(/staking amount/i).or(page.locator('input[type="number"]').first());
|
|
await stakeAmountInput.waitFor({ state: 'visible', timeout: 10_000 });
|
|
await stakeAmountInput.fill('100');
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
// Select lowest tax rate (index 0 or value "5")
|
|
const taxSelect = page.getByRole('combobox', { name: /tax/i }).first();
|
|
await taxSelect.selectOption({ index: 0 }); // Pick first option (lowest)
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
const selectedTax = await taxSelect.inputValue();
|
|
observations.push(`Selected tax rate: ${selectedTax}% (lowest available)`);
|
|
|
|
// Screenshot before stake
|
|
const screenshotDir = join('test-results', 'usertest', 'marcus-b');
|
|
const preStakePath = join(screenshotDir, `pre-stake-${Date.now()}.png`);
|
|
await page.screenshot({ path: preStakePath, fullPage: true });
|
|
|
|
// Execute stake
|
|
const stakeButton = page.getByRole('button', { name: /^(stake|snatch)/i }).first();
|
|
const buttonText = await stakeButton.textContent();
|
|
|
|
if (buttonText?.toLowerCase().includes('snatch')) {
|
|
observations.push('✓ Button shows "Snatch and Stake" - clear that I\'m taking someone\'s position');
|
|
} else {
|
|
observations.push('Button shows "Stake" - am I creating new position or snatching?');
|
|
}
|
|
|
|
try {
|
|
await stakeButton.click();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
// Wait for transaction
|
|
const txInProgress = await page.getByRole('button', { name: /sign|waiting|confirm/i }).isVisible({ timeout: 3_000 }).catch(() => false);
|
|
if (txInProgress) {
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(5_000);
|
|
}
|
|
|
|
observations.push('✓ Stake transaction executed');
|
|
} catch (error: any) {
|
|
observations.push(`✗ Stake failed: ${error.message}`);
|
|
feedback.overall.friction.push('Could not complete stake transaction');
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
// Screenshot after stake
|
|
const postStakePath = join(screenshotDir, `post-stake-${Date.now()}.png`);
|
|
await page.screenshot({ path: postStakePath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'execute-stake', observations, postStakePath);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Marcus checks position P&L and monitoring tools', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
await connectWallet(page);
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Checking my position - where\'s the P&L?');
|
|
|
|
// Look for position card/details
|
|
const hasPositionCard = await page.locator('[class*="position"], [class*="your-stake"]').isVisible().catch(() => false);
|
|
if (hasPositionCard) {
|
|
observations.push('✓ Position card visible');
|
|
} else {
|
|
observations.push('⚠ Can\'t find my position display');
|
|
}
|
|
|
|
// P&L visibility
|
|
const hasPnL = await page.locator('text=/profit|loss|P&L|gain|\\+\\$|\\-\\$/i').isVisible().catch(() => false);
|
|
if (hasPnL) {
|
|
observations.push('✓ P&L displayed - can see if I\'m winning');
|
|
} else {
|
|
observations.push('✗ No P&L shown - can\'t tell if this is profitable');
|
|
feedback.overall.friction.push('Position P&L not visible');
|
|
}
|
|
|
|
// Tax accumulation / time held
|
|
const hasTimeMetrics = await page.locator('text=/time|duration|days|hours/i').isVisible().catch(() => false);
|
|
if (hasTimeMetrics) {
|
|
observations.push('✓ Time-based metrics shown - can calculate tax accumulation');
|
|
} else {
|
|
observations.push('⚠ No time held display - harder to estimate when I\'ll be profitable');
|
|
}
|
|
|
|
// Snatch risk indicator
|
|
const hasSnatchRisk = await page.locator('text=/snatch risk|vulnerable|safe/i').isVisible().catch(() => false);
|
|
if (hasSnatchRisk) {
|
|
observations.push('✓ Snatch risk indicator - helps me decide when to exit');
|
|
} else {
|
|
observations.push('⚠ No snatch risk metric - flying blind on when I\'ll get snatched');
|
|
}
|
|
|
|
// Next steps clarity
|
|
const hasActions = await page.getByRole('button', { name: /claim|exit|increase/i }).isVisible().catch(() => false);
|
|
if (hasActions) {
|
|
observations.push('✓ Clear action buttons - know what I can do next');
|
|
} else {
|
|
observations.push('⚠ Not clear what actions I can take with this position');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'marcus-b');
|
|
const screenshotPath = join(screenshotDir, `position-monitoring-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'position-monitoring', observations, screenshotPath);
|
|
|
|
// Marcus's verdict
|
|
const hasEdge = observations.some(o => o.includes('✓ P&L displayed'));
|
|
const canMonitor = hasPnL || hasSnatchRisk;
|
|
|
|
feedback.overall.wouldStake = true; // Marcus is a degen, he'll stake anyway
|
|
feedback.overall.wouldReturn = canMonitor;
|
|
|
|
observations.push(`Marcus verdict: ${hasEdge ? 'Clear edge, will monitor actively' : 'Can\'t calculate edge properly'}`);
|
|
observations.push(`Would return: ${canMonitor ? 'Yes, need to watch for snatches' : 'Maybe, but tooling is weak'}`);
|
|
|
|
} finally {
|
|
await context.close();
|
|
writePersonaFeedback(feedback);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe.serial('Sarah - Yield Farmer ("what are the risks?")', () => {
|
|
let feedback: PersonaFeedback;
|
|
const accountKey = SARAH_KEY;
|
|
const accountAddr = new Wallet(accountKey).address;
|
|
|
|
test.beforeAll(() => {
|
|
feedback = createPersonaFeedback('sarah', 'B', 'staker');
|
|
});
|
|
|
|
test('Sarah pre-funds wallet with KRK', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Funding wallet for conservative staking test...');
|
|
await mintEth(page, STACK_RPC_URL, accountAddr, '50');
|
|
await buyKrk(page, '10', STACK_RPC_URL, accountKey);
|
|
observations.push('✓ Wallet funded');
|
|
|
|
addFeedbackStep(feedback, 'setup', observations);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Sarah evaluates risk disclosure and staking mechanics', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
observations.push('Looking for risk disclosures, worst-case scenarios, and safety features...');
|
|
|
|
// Risk warnings
|
|
const hasRiskWarning = await page.getByText(/risk|warning|caution|loss/i).isVisible().catch(() => false);
|
|
if (hasRiskWarning) {
|
|
observations.push('✓ Risk warning present - shows responsible disclosure');
|
|
} else {
|
|
observations.push('✗ No visible risk warnings - concerning for risk management');
|
|
feedback.overall.friction.push('No risk disclosure on staking interface');
|
|
}
|
|
|
|
// Tax rate explanation with safety framing
|
|
const taxTooltipFound = await page.locator('svg[data-icon="circle-info"], svg[class*="info"]').first().isVisible().catch(() => false);
|
|
if (taxTooltipFound) {
|
|
observations.push('✓ Tax rate info icon found');
|
|
|
|
await page.locator('svg[data-icon="circle-info"], svg[class*="info"]').first().hover();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const tooltipText = await page.locator('[role="tooltip"], .tooltip').textContent().catch(() => '');
|
|
if (tooltipText.toLowerCase().includes('reduce') || tooltipText.toLowerCase().includes('return')) {
|
|
observations.push('✓ Tooltip explains tax impact on returns - good risk education');
|
|
} else {
|
|
observations.push('⚠ Tooltip doesn\'t clearly explain how tax affects my returns');
|
|
}
|
|
} else {
|
|
observations.push('✗ No tooltip on tax rate - critical mechanism unexplained');
|
|
feedback.overall.friction.push('Tax rate mechanism not explained');
|
|
}
|
|
|
|
// Protocol stats for safety assessment
|
|
const hasProtocolStats = await page.locator('text=/TVL|health|utilization/i').isVisible().catch(() => false);
|
|
if (hasProtocolStats) {
|
|
observations.push('✓ Protocol stats visible - can assess overall protocol health');
|
|
} else {
|
|
observations.push('⚠ No protocol health stats - hard to assess systemic risk');
|
|
}
|
|
|
|
// APY/yield projections
|
|
const hasAPY = await page.locator('text=/APY|yield|return|%/i').isVisible().catch(() => false);
|
|
if (hasAPY) {
|
|
observations.push('✓ Yield projections visible - can compare to other protocols');
|
|
} else {
|
|
observations.push('⚠ No clear APY display - can\'t evaluate if returns justify risk');
|
|
feedback.overall.friction.push('No yield projections shown');
|
|
}
|
|
|
|
// Smart contract verification
|
|
const hasContractInfo = await page.locator('text=/0x[a-fA-F0-9]{40}|verified|audit/i').isVisible().catch(() => false);
|
|
if (hasContractInfo) {
|
|
observations.push('✓ Contract info or audit badge visible - can verify safety');
|
|
} else {
|
|
observations.push('⚠ No contract verification info - can\'t independently audit');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'sarah-b');
|
|
mkdirSync(screenshotDir, { recursive: true });
|
|
const screenshotPath = join(screenshotDir, `risk-assessment-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'risk-assessment', observations, screenshotPath);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Sarah executes conservative stake with medium tax rate', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
await connectWallet(page);
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Choosing medium tax rate - balance between returns and safety');
|
|
|
|
// Fill stake form
|
|
const stakeAmountInput = page.getByLabel(/staking amount/i).or(page.locator('input[type="number"]').first());
|
|
await stakeAmountInput.waitFor({ state: 'visible', timeout: 10_000 });
|
|
await stakeAmountInput.fill('50'); // Conservative amount
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
// Select medium tax rate (index 2-3, or 10-15%)
|
|
const taxSelect = page.getByRole('combobox', { name: /tax/i }).first();
|
|
const options = await taxSelect.locator('option').count();
|
|
const midIndex = Math.floor(options / 2);
|
|
await taxSelect.selectOption({ index: midIndex });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
const selectedTax = await taxSelect.inputValue();
|
|
observations.push(`Selected tax rate: ${selectedTax}% (medium - balanced risk/reward)`);
|
|
|
|
// Screenshot before stake
|
|
const screenshotDir = join('test-results', 'usertest', 'sarah-b');
|
|
const preStakePath = join(screenshotDir, `pre-stake-${Date.now()}.png`);
|
|
await page.screenshot({ path: preStakePath, fullPage: true });
|
|
|
|
// Execute stake
|
|
const stakeButton = page.getByRole('button', { name: /^(stake|snatch)/i }).first();
|
|
|
|
try {
|
|
await stakeButton.click();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const txInProgress = await page.getByRole('button', { name: /sign|waiting|confirm/i }).isVisible({ timeout: 3_000 }).catch(() => false);
|
|
if (txInProgress) {
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(5_000);
|
|
}
|
|
|
|
observations.push('✓ Conservative stake executed');
|
|
} catch (error: any) {
|
|
observations.push(`✗ Stake failed: ${error.message}`);
|
|
feedback.overall.friction.push('Stake transaction failed');
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
const postStakePath = join(screenshotDir, `post-stake-${Date.now()}.png`);
|
|
await page.screenshot({ path: postStakePath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'execute-stake', observations, postStakePath);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Sarah evaluates post-stake clarity and monitoring', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
await connectWallet(page);
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Evaluating: Can I clearly see my position, returns, and risks?');
|
|
|
|
// Position visibility
|
|
const hasPosition = await page.locator('[class*="position"], [class*="stake"]').isVisible().catch(() => false);
|
|
if (hasPosition) {
|
|
observations.push('✓ Position card visible');
|
|
} else {
|
|
observations.push('⚠ Position not clearly displayed');
|
|
}
|
|
|
|
// Expected returns
|
|
const hasReturns = await page.locator('text=/daily|weekly|APY|earning/i').isVisible().catch(() => false);
|
|
if (hasReturns) {
|
|
observations.push('✓ Return projections visible - know what to expect');
|
|
} else {
|
|
observations.push('⚠ No clear return projections - don\'t know expected earnings');
|
|
feedback.overall.friction.push('No return projections for active positions');
|
|
}
|
|
|
|
// What happens next
|
|
const hasGuidance = await page.getByText(/next|monitor|check back|claim/i).isVisible().catch(() => false);
|
|
if (hasGuidance) {
|
|
observations.push('✓ Guidance on next steps - know when to check back');
|
|
} else {
|
|
observations.push('⚠ No guidance on what happens next - set and forget?');
|
|
}
|
|
|
|
// Exit options
|
|
const hasExit = await page.getByRole('button', { name: /unstake|exit|withdraw/i }).isVisible().catch(() => false);
|
|
if (hasExit) {
|
|
observations.push('✓ Exit option visible - not locked in permanently');
|
|
} else {
|
|
observations.push('⚠ No clear exit option - am I stuck until snatched?');
|
|
feedback.overall.friction.push('Exit mechanism not clear');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'sarah-b');
|
|
const screenshotPath = join(screenshotDir, `post-stake-clarity-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'post-stake-clarity', observations, screenshotPath);
|
|
|
|
// Sarah's verdict
|
|
const risksExplained = observations.filter(o => o.includes('✓')).length >= 3;
|
|
const canMonitor = hasReturns || hasPosition;
|
|
|
|
feedback.overall.wouldStake = risksExplained;
|
|
feedback.overall.wouldReturn = canMonitor;
|
|
|
|
observations.push(`Sarah verdict: ${risksExplained ? 'Acceptable risk profile' : 'Too many unknowns, won\'t stake'}`);
|
|
observations.push(`Would return: ${canMonitor ? 'Yes, to monitor position' : 'Unclear monitoring requirements'}`);
|
|
|
|
} finally {
|
|
await context.close();
|
|
writePersonaFeedback(feedback);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe.serial('Priya - Institutional ("show me the docs")', () => {
|
|
let feedback: PersonaFeedback;
|
|
const accountKey = PRIYA_KEY;
|
|
const accountAddr = new Wallet(accountKey).address;
|
|
|
|
test.beforeAll(() => {
|
|
feedback = createPersonaFeedback('priya', 'B', 'staker');
|
|
});
|
|
|
|
test('Priya pre-funds wallet with KRK', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Preparing test wallet...');
|
|
await mintEth(page, STACK_RPC_URL, accountAddr, '50');
|
|
await buyKrk(page, '10', STACK_RPC_URL, accountKey);
|
|
observations.push('✓ Wallet funded');
|
|
|
|
addFeedbackStep(feedback, 'setup', observations);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Priya audits documentation and contract transparency', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
observations.push('Looking for: docs, contract addresses, audit reports, technical specs...');
|
|
|
|
// Documentation link
|
|
const hasDocsLink = await page.locator('a[href*="docs"], a[href*="documentation"]').or(page.getByText(/documentation|whitepaper|docs/i)).isVisible().catch(() => false);
|
|
if (hasDocsLink) {
|
|
observations.push('✓ Documentation link visible - can review technical details');
|
|
} else {
|
|
observations.push('✗ No documentation link - cannot perform due diligence');
|
|
feedback.overall.friction.push('No technical documentation accessible');
|
|
}
|
|
|
|
// Contract addresses with copy button
|
|
const contractAddresses = await page.locator('text=/0x[a-fA-F0-9]{40}/').count();
|
|
if (contractAddresses > 0) {
|
|
observations.push(`✓ Found ${contractAddresses} contract address(es) - can verify on Etherscan`);
|
|
|
|
const hasCopyButton = await page.locator('button[title*="copy"], button[aria-label*="copy"]').isVisible().catch(() => false);
|
|
if (hasCopyButton) {
|
|
observations.push('✓ Copy button for addresses - good UX for verification');
|
|
} else {
|
|
observations.push('⚠ No copy button - minor friction for address verification');
|
|
}
|
|
} else {
|
|
observations.push('✗ No contract addresses visible - cannot verify on-chain');
|
|
feedback.overall.friction.push('Contract addresses not displayed');
|
|
}
|
|
|
|
// Audit badge or report
|
|
const hasAudit = await page.locator('a[href*="audit"]').or(page.getByText(/audited|security audit/i)).isVisible().catch(() => false);
|
|
if (hasAudit) {
|
|
observations.push('✓ Audit report accessible - critical for institutional review');
|
|
} else {
|
|
observations.push('✗ No audit report linked - major blocker for institutional capital');
|
|
feedback.overall.friction.push('No audit report accessible from UI');
|
|
}
|
|
|
|
// Protocol parameters visibility
|
|
const hasParams = await page.locator('text=/parameter|config|setting/i').isVisible().catch(() => false);
|
|
if (hasParams) {
|
|
observations.push('✓ Protocol parameters visible - can assess mechanism design');
|
|
} else {
|
|
observations.push('⚠ Protocol parameters not displayed - harder to model behavior');
|
|
}
|
|
|
|
// GitHub or source code link
|
|
const hasGitHub = await page.locator('a[href*="github"]').isVisible().catch(() => false);
|
|
if (hasGitHub) {
|
|
observations.push('✓ GitHub link present - can review source code');
|
|
} else {
|
|
observations.push('⚠ No source code link - cannot independently verify implementation');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'priya-b');
|
|
mkdirSync(screenshotDir, { recursive: true });
|
|
const screenshotPath = join(screenshotDir, `documentation-audit-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'documentation-audit', observations, screenshotPath);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Priya evaluates UI professionalism and data quality', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
await connectWallet(page);
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Evaluating UI quality: precision, accuracy, professionalism...');
|
|
|
|
// Numeric precision
|
|
const numbers = await page.locator('text=/\\d+\\.\\d{2,}/').count();
|
|
if (numbers > 2) {
|
|
observations.push(`✓ Found ${numbers} precise numbers - shows data quality`);
|
|
} else {
|
|
observations.push('⚠ Limited numeric precision - data may be rounded/imprecise');
|
|
}
|
|
|
|
// Real-time data indicators
|
|
const hasLiveData = await page.locator('text=/live|real-time|updated/i').isVisible().catch(() => false);
|
|
if (hasLiveData) {
|
|
observations.push('✓ Real-time data indicators - shows active monitoring');
|
|
} else {
|
|
observations.push('⚠ No indication if data is live or stale');
|
|
}
|
|
|
|
// Error states and edge cases
|
|
observations.push('Testing edge cases: trying to stake 0...');
|
|
const stakeInput = page.getByLabel(/staking amount/i).or(page.locator('input[type="number"]').first());
|
|
await stakeInput.fill('0');
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
const hasValidation = await page.locator('text=/invalid|minimum|required/i').isVisible().catch(() => false);
|
|
if (hasValidation) {
|
|
observations.push('✓ Input validation present - handles edge cases gracefully');
|
|
} else {
|
|
observations.push('⚠ No visible validation for invalid inputs');
|
|
}
|
|
|
|
// Clear labels and units
|
|
const hasUnits = await page.locator('text=/KRK|ETH|%|USD/i').count();
|
|
if (hasUnits >= 3) {
|
|
observations.push('✓ Clear units on all values - professional data presentation');
|
|
} else {
|
|
observations.push('⚠ Some values missing units - could cause confusion');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'priya-b');
|
|
const screenshotPath = join(screenshotDir, `ui-quality-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'ui-quality', observations, screenshotPath);
|
|
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
test('Priya performs test stake and evaluates reporting', async ({ browser }) => {
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: accountKey,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
const observations: string[] = [];
|
|
|
|
try {
|
|
await page.goto(STAKE_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
await connectWallet(page);
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
observations.push('Executing small test stake to evaluate position reporting...');
|
|
|
|
// Fill form
|
|
const stakeInput = page.getByLabel(/staking amount/i).or(page.locator('input[type="number"]').first());
|
|
await stakeInput.fill('25');
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
const taxSelect = page.getByRole('combobox', { name: /tax/i }).first();
|
|
await taxSelect.selectOption({ index: 1 });
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
// Execute
|
|
const stakeButton = page.getByRole('button', { name: /^(stake|snatch)/i }).first();
|
|
|
|
try {
|
|
await stakeButton.click();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const txInProgress = await page.getByRole('button', { name: /sign|waiting|confirm/i }).isVisible({ timeout: 3_000 }).catch(() => false);
|
|
if (txInProgress) {
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(5_000);
|
|
}
|
|
|
|
observations.push('✓ Test stake executed');
|
|
} catch (error: any) {
|
|
observations.push(`✗ Stake failed: ${error.message}`);
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
// Evaluate position reporting
|
|
observations.push('Checking position dashboard for institutional-grade reporting...');
|
|
|
|
// Transaction hash
|
|
const hasTxHash = await page.locator('text=/0x[a-fA-F0-9]{64}|transaction|tx/i').isVisible().catch(() => false);
|
|
if (hasTxHash) {
|
|
observations.push('✓ Transaction hash visible - can verify on Etherscan');
|
|
} else {
|
|
observations.push('⚠ No transaction hash shown - harder to verify on-chain');
|
|
}
|
|
|
|
// Position details
|
|
const hasDetails = await page.locator('text=/amount|tax rate|time|date/i').count();
|
|
if (hasDetails >= 3) {
|
|
observations.push('✓ Comprehensive position details - sufficient for reporting');
|
|
} else {
|
|
observations.push('⚠ Limited position details - insufficient for audit trail');
|
|
}
|
|
|
|
// Export or reporting tools
|
|
const hasExport = await page.getByRole('button', { name: /export|download|csv/i }).isVisible().catch(() => false);
|
|
if (hasExport) {
|
|
observations.push('✓ Export functionality - can generate reports for compliance');
|
|
} else {
|
|
observations.push('✗ No export option - manual record-keeping required');
|
|
feedback.overall.friction.push('No position export for institutional reporting');
|
|
}
|
|
|
|
// Screenshot
|
|
const screenshotDir = join('test-results', 'usertest', 'priya-b');
|
|
const screenshotPath = join(screenshotDir, `position-reporting-${Date.now()}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
|
addFeedbackStep(feedback, 'position-reporting', observations, screenshotPath);
|
|
|
|
// Priya's verdict
|
|
const hasRequiredDocs = observations.filter(o => o.includes('✓')).length >= 4;
|
|
const meetsStandards = !observations.some(o => o.includes('✗ No audit report'));
|
|
|
|
feedback.overall.wouldStake = hasRequiredDocs && meetsStandards;
|
|
feedback.overall.wouldReturn = hasRequiredDocs;
|
|
|
|
observations.push(`Priya verdict: ${feedback.overall.wouldStake ? 'Meets institutional standards' : 'Insufficient documentation/transparency'}`);
|
|
observations.push(`Would recommend: ${meetsStandards ? 'Yes, with caveats' : 'No, needs audit and better docs'}`);
|
|
|
|
} finally {
|
|
await context.close();
|
|
writePersonaFeedback(feedback);
|
|
}
|
|
});
|
|
});
|
|
});
|