SPB Git

Repository Public

qa-real.spec.ts 11.1 KB 161 lines typescript

1// QA pass of 2026-11-11, kept as a spec: the screens that were merged without ever being opened in a browser, against the2// REAL local stack (real API, real signer with a throwaway in-memory seed, public read-only network access, broadcast OFF).3//4//   npx tsx tests/e2e/qa-real-server.mts --port 8690 --db spw_qa_polish --fork base,arbitrum     (a fresh stack, another terminal)5//   QA_REAL=1 npx playwright test -c tests/e2e/playwright.config.ts qa-agent-fund qa-real6//7// Runs after qa-agent-fund.spec.ts on the same stack (file order); the last test here leaves a signed, never-broadcast8// transfer behind, which blocks the network for anything that would run after it.9import { expect, test, type BrowserContext, type Page } from '@playwright/test';10import { enter, passkeyFile, probe, virtualPasskey, watch } from './qa-helpers.ts';1112const BASE = process.env.QA_BASE ?? 'http://localhost:8690', PORT = Number(new URL(BASE).port);13test.skip(!process.env.QA_REAL, 'needs the real local stack: set QA_REAL=1 (see the header)');14test.use({ baseURL: BASE });15test.describe.configure({ mode: 'serial' });1617async function start(page: Page, context: BrowserContext) {18  const pk = await virtualPasskey(context, page, passkeyFile(PORT));19  const w = watch(page);20  await enter(page, pk, process.env.QA_SETUP_TOKEN ?? 'qa-setup-token');21  return { pk, w };22}23const api = (page: Page, method: string, path: string, body?: unknown) => page.evaluate(async ([m, p, b]) => { const r = await fetch('/api/v1' + p, { method: m as string, credentials: 'same-origin', headers: m === 'GET' ? {} : { 'x-spw-csrf': '1', 'content-type': 'application/json' }, body: m === 'GET' ? undefined : JSON.stringify(b ?? {}) }); return { status: r.status, json: await r.json().catch(() => null) as any }; }, [method, path, body] as const);24const clean = async (page: Page) => { const p = await probe(page); expect(p.overflow, p.wide.join(' | ')).toBe(false); return p; };25const BSC_ETH = '0x2170Ed0880ac9A755fd29B2688956BD959F933F8';2627test('registry of 249 deployments: issuer of pegged tokens, ordering, picker above the navigation, French numbers', async ({ page, context }) => {28  test.setTimeout(180_000);29  const { pk, w } = await start(page, context);30  await expect(page.getByText(/[+−]\d+,\d{2} % aujourd’hui/)).toBeVisible({ timeout: 60_000 });         // decimal comma in French31  // popular assets show the original issue, not a bridged copy32  await expect(page.getByRole('button', { name: /Tether USD/ })).toBeVisible();33  await expect(page.getByRole('main').getByText(/Bridged USDT/)).toHaveCount(0);3435  // a token that keeps the underlying's ticker says who issues it36  await page.goto(`/asset/bsc/${BSC_ETH}`);37  await expect(page.getByRole('heading', { name: 'Binance-Peg Ethereum' })).toBeVisible({ timeout: 30_000 });38  await expect(page.getByTestId('asset-issuer')).toContainText('n’est pas l’émission d’origine de ETH');39  await expect(page.getByTestId('asset-issuer')).toContainText('Binance');40  await clean(page);4142  // the full list: balances first, then majors by market rank; smooth with 249 rows43  await page.goto('/assets');44  const rows = page.locator('main li');45  await expect(rows).toHaveCount(249, { timeout: 30_000 });46  const order = await rows.evaluateAll((els) => els.map((e) => (e as HTMLElement).innerText.replace(/\s+/g, ' ')));47  const at = (s: string) => order.findIndex((x) => x.includes(s));48  expect(at('USDC · Base')).toBeLessThan(3);                                                             // held49  expect(at('BTC · Bitcoin')).toBeGreaterThan(-1);50  expect(at('BTC · Bitcoin')).toBeLessThan(at('APT · Aptos'));                                           // rank, not alphabet51  const ms = await page.evaluate(`(async () => { const i = document.querySelector('input[type=search]'); const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; const t0 = performance.now(); set.call(i, 'usd'); i.dispatchEvent(new Event('input', { bubbles: true })); await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); return performance.now() - t0; })()`) as number;52  expect(ms).toBeLessThan(250);53  await clean(page);5455  // swap: the picker is a sheet ABOVE the bottom navigation, and names the issuer56  await page.goto('/swap');57  await page.getByRole('button', { name: /^Vous recevez/ }).click();58  const dlg = page.getByRole('dialog');59  await dlg.getByRole('searchbox').fill('eth');60  await expect(dlg.getByText(/BNB Smart Chain · adossé — Binance/).first()).toBeVisible();61  const top = await page.evaluate(() => { const e = document.elementFromPoint(window.innerWidth / 2, window.innerHeight - 24); return !!e?.closest('[role=dialog]'); });62  expect(top).toBe(true);63  await dlg.getByRole('button', { name: 'Annuler' }).click();64  // a same-network route says it is executable here; the slippage control stays on one line and is 44 px high65  await page.getByLabel('Montant').fill('25');66  await expect(page.getByText('exécutable ici').first()).toBeVisible({ timeout: 40_000 });67  const slip = await page.getByRole('radio', { name: '0,5 %' }).boundingBox();68  expect(slip!.height).toBeGreaterThanOrEqual(44); expect(slip!.height).toBeLessThan(50);69  const p = await clean(page);70  expect(p.small).toEqual([]);                                                                            // every control on this screen is at least 44 px71  await pk.save();72  expect(w.errors.filter((e) => !/status of 4\d\d/.test(e))).toEqual([]);73});7475test('agent creation: BNB Chain cash leg and representations, warnings kept on the agent page', async ({ page, context }) => {76  test.setTimeout(180_000);77  const { pk, w } = await start(page, context);78  await page.goto('/agents');79  await page.getByRole('button', { name: 'Nouvel agent' }).click();80  const net = page.locator('select').first();81  await expect(net.locator('option')).toHaveCount(8, { timeout: 60_000 });82  await net.selectOption('bsc');83  // the 18-decimal Binance-Peg cash leg is named, with its issuer84  await expect(page.getByTestId('cash-issuer')).toContainText('USDC est « Binance-Peg USD Coin »');85  await expect(page.getByTestId('cash-issuer')).toContainText('18 décimales');86  await page.getByPlaceholder('ETH Trend').fill('QA BNB repr');87  await page.getByRole('checkbox', { name: /^BTC/ }).click();88  // both representations are shown; the default rule is unchanged (WBTC), the liquid one is pointed out89  const group = page.getByTestId('repr-BTC');90  await expect(group.getByRole('radio')).toHaveCount(2);91  await expect(group.getByRole('radio', { name: /WBTC/ })).toHaveAttribute('aria-checked', 'true');92  await expect(group.getByRole('radio', { name: /WBTC/ })).toContainText('choix par défaut');93  await expect(group.getByRole('radio', { name: /BTCB/ })).toContainText('le plus liquide sur BNB Smart Chain');94  await expect(group).toContainText('BTCB est la forme la plus liquide de BTC ; la règle par défaut retient WBTC');95  await group.getByRole('radio', { name: /BTCB/ }).click();96  await expect(group.getByRole('radio', { name: /BTCB/ })).toHaveAttribute('aria-checked', 'true');97  await page.getByRole('checkbox', { name: /^ETH/ }).click();98  await clean(page);99  await page.getByRole('button', { name: 'Créer l’agent' }).click();100  await expect(page.getByRole('heading', { name: 'QA BNB repr' })).toBeVisible({ timeout: 30_000 });101  await expect(page.getByText(/sous forme de BTCB — Binance-Peg BTCB · Émetteur : Binance/)).toBeVisible({ timeout: 30_000 });102  await expect(page.getByText(/sous forme de ETH — Binance-Peg Ethereum · Émetteur : Binance/)).toBeVisible();103  await expect(page.getByText(/sous forme de USDC — Binance-Peg USD Coin/)).toBeVisible();104  await expect(page.getByRole('main').getByText('{native}')).toHaveCount(0);                              // no raw placeholder105  await clean(page);106107  // Ethereum, small orders and a paper-only symbol: what the creation answer warned about stays on the page, in French108  const r = await api(page, 'POST', '/agents', { name: 'QA Eth avert', mission: '', network: 'ethereum', universe: ['ETH', 'EUR'], interval: '1h', capitalUsdc: '100', risk: { maxExposurePct: 50, maxLossPct: 5, maxOrderUsd: 1, maxDailyUsd: 100, maxOrdersPerDay: 12, mandateDays: 30, slippageBps: 50 } });109  expect(r.status).toBe(200);110  expect((r.json.warnings as { code: string }[]).map((x) => x.code)).toContain('paper_only');111  await page.goto(`/agents/${r.json.id}`);112  const warn = page.getByTestId('agent-warnings');113  await expect(warn).toContainText('EUR : le signataire ne peut pas en vérifier le prix', { timeout: 60_000 });114  if ((r.json.warnings as { code: string }[]).some((x) => x.code === 'fees')) await expect(warn).toContainText(/Sur Ethereum, un ordre coûte environ .* \$ US de frais de réseau/);115  await pk.save();116  expect(w.errors.filter((e) => !/status of 4\d\d/.test(e))).toEqual([]);117});118119test('security: the 249 limits can be searched, by network name', async ({ page, context }) => {120  const { pk } = await start(page, context);121  await page.goto('/security');122  await page.getByRole('button', { name: /Tout afficher \(249\)/ }).click();123  await expect(page.locator('main [role=progressbar]')).toHaveCount(249);124  await page.getByRole('searchbox').fill('bnb');125  await expect(page.getByText(/\d+ sur 249/)).toBeVisible();126  expect(await page.locator('main [role=progressbar]').count()).toBeLessThan(40);127  await expect(page.getByText('USDC · BNB Smart Chain')).toBeVisible();128  await expect(page.getByRole('main').getByText('· Bsc')).toHaveCount(0);129  await clean(page);130  await pk.save();131});132133test('broadcast off: a transfer ends as "signed, not sent" — never as sent', async ({ page, context }) => {134  test.setTimeout(240_000);135  const { pk } = await start(page, context);136  await page.goto('/send');137  await page.locator('textarea').fill('0x1234');138  await page.getByPlaceholder('0.00').fill('5');139  await page.getByRole('button', { name: 'Vérifier' }).click();140  await expect(page.getByRole('alert')).toHaveText('Adresse invalide pour ce réseau.');                   // no English server sentence appended141  await page.locator('textarea').fill('0x70997970C51812dc3A010C7d01b50e0d17dc79C8');142  // the API allows 240 requests a minute; the tests before this one can have used them up: wait and try again143  const review = page.getByRole('heading', { name: 'Vérification' }), limited = page.getByRole('alert').filter({ hasText: 'Trop de requêtes' });144  for (let k = 0; k < 5; k++) {145    await page.getByRole('button', { name: 'Vérifier' }).click();146    await review.or(limited).first().waitFor({ timeout: 40_000 });147    if (await review.isVisible()) break;148    await page.waitForTimeout(20_000);149  }150  await expect(review).toBeVisible();151  await page.getByRole('checkbox').check();152  await page.getByRole('button', { name: /Approuver/ }).click();153  await expect(page.getByText('Signé, non envoyé (test)')).toBeVisible({ timeout: 40_000 });154  await expect(page.getByText(/Mode test : la diffusion est désactivée sur ce serveur/)).toBeVisible();155  await expect(page.getByText('Voir dans l’explorateur')).toHaveCount(0);156  await page.goto('/activity');157  await expect(page.getByText('Signé, non envoyé (test)').first()).toBeVisible({ timeout: 30_000 });158  await clean(page);159  await pk.save();160});161
Files main
Menu

Theme