SPB Git

Repository Public

wallet.spec.ts 9.2 KB 139 lines typescript

1import { readFileSync } from 'node:fs';2import { expect, test } from '@playwright/test';34// Requires scripts/dev-stack.sh (throwaway signer keystore, broadcast off). Uses a virtual passkey via CDP.5const SHOTS = 'test-results/shots';6test('owner setup, lock/unlock, every screen on a phone viewport, send guarded by real chain checks', async ({ page, context }) => {7  const cdp = await context.newCDPSession(page);8  await cdp.send('WebAuthn.enable');9  await cdp.send('WebAuthn.addVirtualAuthenticator', { options: { protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true } });10  const errors: string[] = [];11  page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });12  page.on('pageerror', (e) => errors.push(String(e)));1314  await page.goto('/');15  await expect(page.getByText('Configuration initiale')).toBeVisible();16  await page.screenshot({ path: `${SHOTS}/01-setup.png` });17  await page.getByRole('textbox').fill('definitely-wrong-token');18  await page.getByRole('button', { name: /Créer ma clé/ }).click();19  await expect(page.getByRole('alert')).toContainText(/Jeton de configuration invalide/);20  await page.getByRole('textbox').fill(readFileSync(`${process.env.SPW_DEV_DIR ?? '/tmp/spw-dev'}/setup-token`, 'utf8').trim());21  await page.getByRole('button', { name: /Créer ma clé/ }).click();22  await expect(page.getByText('Valeur totale estimée')).toBeVisible({ timeout: 30_000 });23  await expect(page.getByRole('heading', { name: 'Actifs' })).toBeVisible();24  await expect(page.getByText(/21\/21 réseaux opérationnels/)).toBeVisible({ timeout: 90_000 });25  await page.waitForTimeout(1500);26  await page.screenshot({ path: `${SHOTS}/02-home.png`, fullPage: true });2728  // dedicated asset page: price, chart, balance, details29  await page.getByRole('button', { name: /USD Coin/ }).first().click();30  await expect(page.getByText('Votre solde')).toBeVisible();31  await expect(page.getByText('Contrat du jeton')).toBeVisible();32  await page.waitForTimeout(2500);33  await page.screenshot({ path: `${SHOTS}/02b-asset.png`, fullPage: true });34  await page.getByRole('button', { name: 'Retour' }).click();35  await page.getByRole('button', { name: /Tous les actifs/ }).click();36  await page.getByRole('searchbox').fill('pepe');37  await expect(page.getByRole('button', { name: /Pepe/ })).toHaveCount(1);38  await page.screenshot({ path: `${SHOTS}/02c-assets.png` });39  await page.getByRole('button', { name: 'Retour' }).click();4041  // no horizontal overflow on a phone42  expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);4344  // lock, then unlock with the passkey45  await page.getByRole('button', { name: 'Verrouiller' }).first().click();46  await expect(page.getByRole('button', { name: /Déverrouiller/ })).toBeVisible();47  await page.screenshot({ path: `${SHOTS}/03-lock.png` });48  await page.getByRole('button', { name: /Déverrouiller/ }).click();49  await expect(page.getByText('Valeur totale estimée')).toBeVisible({ timeout: 30_000 });5051  // receive: signer-derived address + QR52  await page.getByRole('navigation').getByRole('button', { name: 'Accueil' }).click();53  await page.getByRole('main').getByRole('button', { name: 'Recevoir' }).click();54  await expect(page.locator('img[alt^="QR 0x"]')).toBeVisible();55  await expect(page.getByText(/Envoyez uniquement ETH \/ USDC \/ .*sur le réseau Base/)).toBeVisible();56  await page.screenshot({ path: `${SHOTS}/04-receive.png`, fullPage: true });57  await page.getByRole('radio', { name: 'Solana' }).click();58  await expect(page.getByText(/Envoyez uniquement SOL \/ USDC \/ .*sur le réseau Solana/)).toBeVisible();59  await page.getByRole('radio', { name: 'Bitcoin', exact: true }).click();60  await expect(page.locator('img[alt^="QR bc1q"]')).toBeVisible();61  for (const [name, prefix] of [['Bitcoin Cash', 'bitcoincash:q'], ['Dogecoin', 'D'], ['Litecoin', 'ltc1q'], ['TRON', 'T'], ['XRP Ledger', 'r'], ['Sui', '0x'], ['Aptos', '0x'], ['Cosmos Hub', 'cosmos1'], ['NEAR Protocol', ''], ['TON', 'UQ'], ['Stellar', 'G']] as const) {62    await page.getByRole('radio', { name, exact: true }).click();63    await expect(page.locator(`img[alt^="QR ${prefix}"]`)).toBeVisible();64  }65  await expect(page.getByText(/un compte doit détenir au moins 1 XLM/)).toBeVisible();66  await page.screenshot({ path: `${SHOTS}/04b-receive-stellar.png`, fullPage: true });6768  // swap: route comparison screen (no provider is wired in this build: it must say so, and execution must be visibly disabled)69  await page.getByRole('navigation').getByRole('button', { name: 'Échanger' }).click();70  await expect(page.getByText(/Devis en direct de plusieurs fournisseurs/)).toBeVisible();71  await page.getByLabel('Montant').fill('25');72  // live quotes from real providers (USDC -> ETH on Base); the empty test wallet cannot execute, and must be told why73  await expect(page.getByText('kyberswap').first()).toBeVisible({ timeout: 30_000 });74  await expect(page.getByText(/min\. garanti/).first()).toBeVisible();75  await page.screenshot({ path: `${SHOTS}/04c-swap.png`, fullPage: true });76  await page.getByRole('button', { name: /Échanger via KyberSwap/ }).click();77  await expect(page.getByRole('alert')).toContainText(/Solde insuffisant/, { timeout: 30_000 });78  expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);7980  // send: invalid address is refused, then an empty wallet cannot draft a transfer (live mainnet balance check)81  await page.getByRole('navigation').getByRole('button', { name: 'Envoyer' }).click();82  await page.locator('textarea').fill('0x1234');83  await page.getByRole('radio', { name: 'USDC', exact: true }).click();84  await page.getByPlaceholder('0.00').fill('5');85  await page.getByRole('button', { name: 'Vérifier' }).click();86  await expect(page.getByRole('alert')).toContainText(/Adresse invalide/);87  await page.locator('textarea').fill('0x70997970C51812dc3A010C7d01b50e0d17dc79C8');88  await page.getByRole('button', { name: 'Vérifier' }).click();89  await expect(page.getByRole('alert')).toContainText(/Solde insuffisant/, { timeout: 30_000 });90  await page.screenshot({ path: `${SHOTS}/05-send.png`, fullPage: true });9192  await page.getByRole('navigation').getByRole('button', { name: 'Activité' }).click();93  await expect(page.getByText('Aucune activité')).toBeVisible();94  await page.getByRole('banner').getByRole('button', { name: 'Sécurité' }).click();95  await expect(page.getByText('Signature active')).toBeVisible();96  await expect(page.getByText('Chaîne d’empreintes intacte')).toBeVisible();97  await page.screenshot({ path: `${SHOTS}/06-security.png`, fullPage: true });9899  // English + light theme100  await page.getByRole('navigation').getByRole('button', { name: 'Réglages' }).click();101  await page.getByRole('radio', { name: 'English' }).click();102  await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();103  await page.getByRole('radio', { name: 'Dark' }).click();104  await page.screenshot({ path: `${SHOTS}/07-settings-en-dark.png`, fullPage: true });105  await page.getByRole('navigation').getByRole('button', { name: 'Home' }).click();106  await page.waitForTimeout(600);107  await page.screenshot({ path: `${SHOTS}/08-home-dark.png`, fullPage: true });108  await page.getByRole('navigation').getByRole('button', { name: 'Settings' }).click();109  await page.getByRole('radio', { name: 'Light' }).click();110  await page.getByRole('radio', { name: 'Français' }).click();111112  // emergency halt from the phone113  await page.getByRole('banner').getByRole('button', { name: 'Sécurité' }).click();114  await page.getByRole('button', { name: 'Arrêt d’urgence' }).click();115  await page.getByLabel('STOP').fill('stop');116  await page.getByRole('button', { name: 'Arrêt d’urgence' }).click();117  await expect(page.getByText('Signature arrêtée')).toBeVisible();118  await page.getByRole('navigation').getByRole('button', { name: 'Accueil' }).click();119  await expect(page.getByText(/Arrêt d’urgence actif/)).toBeVisible();120  await expect(page.getByRole('main').getByRole('button', { name: 'Envoyer' })).toBeDisabled();121122  expect(errors.filter((e) => !/status of 4\d\d/.test(e))).toEqual([]);123});124125test('a second browser without the passkey cannot get in', async ({ browser }) => {126  const ctx = await browser.newContext();127  const page = await ctx.newPage();128  const cdp = await ctx.newCDPSession(page);129  await cdp.send('WebAuthn.enable');130  await cdp.send('WebAuthn.addVirtualAuthenticator', { options: { protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true } });131  await page.goto('/');132  await expect(page.getByRole('button', { name: /Déverrouiller/ })).toBeVisible();133  await expect(page.getByText('Configuration initiale')).toHaveCount(0); // setup is closed for good134  for (const p of ['/api/v1/portfolio', '/api/v1/accounts', '/api/v1/security/audit', '/api/v1/export/activity.csv'])135    expect((await page.request.get(p)).status()).toBe(401);136  expect((await page.request.post('/api/v1/auth/setup/options', { headers: { 'x-spw-csrf': '1', origin: process.env.SPW_E2E_BASE ?? 'http://localhost:8650' }, data: {} })).status()).toBe(403);137  await ctx.close();138});139
Files main
Menu

Theme