SPB Git

Repository Public

futures-collateral.spec.ts 15.2 KB 210 lines typescript

1import { spawn, type ChildProcess } from 'node:child_process';2import { mkdirSync } from 'node:fs';3import { dirname, resolve } from 'node:path';4import { fileURLToPath } from 'node:url';5import { expect, test, type Page } from '@playwright/test';67// « Déposer » / « Retirer » of a venue account, in a browser, against the in-browser mock (`?mock=1`, development build only):8// no backend, no chain, no venue, no signer, a simulated passkey — apps/web/src/derivatives/mockCollateral.ts says what is real9// in it (pinned route, floor, fee, canonical withdrawal and its hash, withdraw3 action, passkey challenge) and what is not (time10// stands in for the chain and the venue). What this spec checks is the SCREEN: what it shows before the passkey, that it never11// announces a deposit before the venue credited it nor a withdrawal before it arrived, and what it says when it is late.12//   npx playwright test -c tests/e2e/playwright.config.ts futures-collateral.spec.ts        (FUTURES_SHOTS=1 writes docs/v4/screens/collateral-*.png)13const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');14const PORT = 5191;15const BASE = process.env.FUTURES_URL ?? `http://localhost:${PORT}`;16const SHOTS = resolve(ROOT, 'docs/v4/screens');17const BRIDGE = '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7';18let vite: ChildProcess | null = null;1920test.beforeAll(async () => {21  if (process.env.FUTURES_URL) return;22  vite = spawn(resolve(ROOT, 'apps/web/node_modules/.bin/vite'), ['--port', String(PORT), '--strictPort'], { cwd: resolve(ROOT, 'apps/web'), stdio: 'ignore' });23  for (let i = 0; i < 80; i++) {24    try { if ((await fetch(`${BASE}/`)).ok) return; } catch { /* not up yet */ }25    await new Promise((r) => setTimeout(r, 250));26  }27  throw new Error('the Vite dev server did not start');28});29test.afterAll(() => { vite?.kill(); });3031const live = (page: Page, name: string, ...args: unknown[]) => page.evaluate(([n, a]) => (window as any).__spwMock.live[n as string](...(a as unknown[])), [name, args] as const);32const coll = (page: Page, name: string, ...args: unknown[]) => page.evaluate(([n, a]) => (window as any).__spwMock.live.collateral[n as string](...(a as unknown[])), [name, args] as const);33const errorsOf = (page: Page) => { const errors: string[] = []; page.on('console', (m) => { if (m.type() === 'error' && !/Failed to load resource/.test(m.text())) errors.push(m.text()); }); page.on('pageerror', (e) => errors.push(e.message)); return errors; };3435for (const size of [36  { name: 'phone', use: { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true } },37  { name: 'desktop', use: { viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1, isMobile: false, hasTouch: false } },38] as const) {39  test.describe(`futures collateral — ${size.name}`, () => {40    test.use({ ...size.use, locale: 'fr-CA' });41    const shot = async (page: Page, name: string) => { if (process.env.FUTURES_SHOTS !== '1') return; mkdirSync(SHOTS, { recursive: true }); await page.screenshot({ path: `${SHOTS}/collateral-${size.name}-${name}.png`, fullPage: false }); };42    /** a real-money account that never deposited (the mock's server setting is on), with 40 USDC in the wallet on Arbitrum */43    const open = async (page: Page, network: 'mainnet' | 'testnet' = 'mainnet') => {44      await page.goto(`${BASE}/futures?v=accounts&mock=1&mainnet=1`);45      await expect(page.getByTestId('live-card')).toBeVisible();46      await live(page, 'fund', network, '0');47      await page.getByTestId(`live-create-${network}`).getByRole('button').click();48      const card = page.getByTestId(`live-account-${network}`);49      await expect(card.getByTestId('collateral-panel')).toBeVisible();50      return card;51    };5253    test('deposit: minimum refused, review says everything, waits for the venue, then credited; withdrawal: fee, own address, arrives', async ({ page }) => {54      test.setTimeout(120_000);55      const errors = errorsOf(page), card = await open(page), panel = card.getByTestId('collateral-panel');56      await expect(card.getByTestId('mode-band')).toHaveAttribute('data-tone', 'mainnet');57      await expect(panel.getByTestId('collateral-venue-balance')).toContainText(/0,00\s\$/);58      await expect(card.getByTestId('funding-box').getByRole('button')).toHaveCount(0);59      await panel.getByTestId('collateral-deposit').click();60      const sheet = page.getByTestId('deposit-sheet');61      await expect(sheet).toHaveAttribute('data-step', 'form');62      // the real-money band is in the sheet and cannot be dismissed63      await expect(sheet.getByTestId('mode-band')).toContainText(/Réel — argent réel/i);64      await expect(sheet.getByTestId('mode-band').getByRole('button')).toHaveCount(0);65      await expect(sheet.getByTestId('deposit-minimum')).toContainText('perdu pour toujours');66      // below the minimum: refused before anything is prepared67      await sheet.getByTestId('deposit-amount').fill('5');68      await expect(sheet.getByTestId('deposit-problem')).toContainText('Refusé : sous 6 USDC');69      await expect(sheet.getByTestId('deposit-review')).toBeDisabled();70      await sheet.getByTestId('deposit-amount').fill('60');71      await expect(sheet.getByTestId('deposit-problem')).toContainText('n’a que 40 USDC');72      await shot(page, '01-deposit-form');73      await sheet.getByTestId('deposit-amount').fill('10');74      await expect(sheet.getByTestId('deposit-problem')).toHaveCount(0);75      await sheet.getByTestId('deposit-review').click();76      await expect(sheet).toHaveAttribute('data-step', 'review');77      const facts = sheet.getByTestId('deposit-facts'), says = sheet.getByTestId('deposit-says');78      await expect(facts).toContainText('10 USDC');79      await expect(facts).toContainText('USDC natif sur Arbitrum One → pont de Hyperliquid → votre compte');80      await expect(facts).toContainText('moins de 60 s');81      await expect(facts).toContainText('6 USDC');82      await expect(says).toContainText('La plateforme crédite l’adresse qui envoie');83      await expect(says).toContainText(BRIDGE.slice(0, 8));84      await expect(says).toContainText('Sous 5 USDC, un dépôt n’est pas crédité et est perdu');85      await expect(says).toContainText('en attente du crédit par la plateforme');86      await expect(sheet.getByTestId('collateral-integrity')).toHaveCount(0);87      await expect(sheet.getByTestId('mode-band')).toBeVisible();88      await shot(page, '02-deposit-review');89      await sheet.getByTestId('deposit-approve').click();90      await expect(sheet).toHaveAttribute('data-step', 'progress', { timeout: 10_000 });91      const op = sheet.getByTestId('collateral-op');92      // sent, then confirmed on-chain: NOT done, the balance after is "not yet"93      await expect(op).toHaveAttribute('data-state', /^(sent|awaiting_credit)$/);94      await expect(op).toHaveAttribute('data-done', '0');95      await expect(op).toContainText('pas encore — en attente');96      await expect(op).not.toContainText('Crédité :');97      await shot(page, '03-deposit-waiting');98      // the venue credits: only now is it announced99      await expect(op).toHaveAttribute('data-state', 'credited', { timeout: 20_000 });100      await expect(op).toHaveAttribute('data-done', '1');101      await expect(op.getByTestId('collateral-op-words')).toContainText('Crédité : la plateforme a inscrit ce dépôt sur votre compte.');102      await expect(op).toContainText(/Solde sur la plateforme, après\s*10 USDC/);103      await shot(page, '04-deposit-credited');104      await sheet.getByRole('button', { name: 'Fermer' }).last().click();105      await expect(panel.getByTestId('collateral-venue-balance')).toContainText(/10,00\s\$/, { timeout: 15_000 });106      await expect(panel.getByTestId('collateral-ops').getByTestId('collateral-op').first()).toHaveAttribute('data-state', 'credited');107108      // withdrawal109      await panel.getByTestId('collateral-withdraw').click();110      const ws = page.getByTestId('withdraw-sheet');111      await expect(ws.getByTestId('mode-band')).toContainText(/Réel — argent réel/i);112      await ws.getByTestId('withdraw-amount').fill('1');113      await expect(ws.getByTestId('withdraw-problem')).toContainText('dépasser les frais de 1 USDC');114      await ws.getByTestId('withdraw-amount').fill('11');115      await expect(ws.getByTestId('withdraw-problem')).toContainText('10 USDC retirables');116      await ws.getByTestId('withdraw-amount').fill('4');117      await ws.getByTestId('withdraw-review').click();118      await expect(ws).toHaveAttribute('data-step', 'review');119      await expect(ws.getByTestId('withdraw-facts')).toContainText(/Frais de la plateforme\s*1 USDC/);120      await expect(ws.getByTestId('withdraw-facts')).toContainText(/Vous recevrez\s*3 USDC/);121      await expect(ws.getByTestId('withdraw-says')).toContainText('l’adresse de ce portefeuille, et aucune autre');122      await expect(ws.getByTestId('withdraw-says')).toContainText('withdraw3 (Mainnet): 4 USDC to 0x5ad0c0ffee');123      await expect(ws.getByTestId('collateral-integrity')).toHaveCount(0);124      await shot(page, '05-withdraw-review');125      await ws.getByTestId('withdraw-approve').click();126      await expect(ws).toHaveAttribute('data-step', 'progress', { timeout: 10_000 });127      const wop = ws.getByTestId('collateral-op');128      await expect(wop).toHaveAttribute('data-state', 'submitted');129      await expect(wop).toHaveAttribute('data-done', '0');130      await expect(wop.getByTestId('collateral-op-words')).toContainText('l’USDC n’est pas encore arrivé sur ce portefeuille');131      await expect(wop).toHaveAttribute('data-state', 'completed', { timeout: 20_000 });132      await expect(wop.getByTestId('collateral-op-words')).toContainText('Arrivé : ce portefeuille a reçu l’USDC du pont.');133      await expect(wop).toContainText(/Reçu sur le portefeuille\s*3 USDC/);134      await shot(page, '06-withdraw-arrived');135      await ws.getByRole('button', { name: 'Fermer' }).last().click();136      await expect(panel.getByTestId('collateral-venue-balance')).toContainText(/6,00\s\$/, { timeout: 15_000 });137      expect(errors).toEqual([]);138    });139140    test('a credit that does not come is never announced: after ten minutes the screen says what to check; a wrong passkey sends nothing', async ({ page }) => {141      test.setTimeout(90_000);142      const errors = errorsOf(page), card = await open(page), panel = card.getByTestId('collateral-panel');143      // wrong passkey first: the operation ends, nothing left the wallet144      await live(page, 'passkey', 'bad');145      await panel.getByTestId('collateral-deposit').click();146      let sheet = page.getByTestId('deposit-sheet');147      await sheet.getByTestId('deposit-amount').fill('10');148      await sheet.getByTestId('deposit-review').click();149      await sheet.getByTestId('deposit-approve').click();150      await expect(sheet).toHaveAttribute('data-step', 'progress', { timeout: 10_000 });151      await expect(sheet.getByTestId('collateral-op')).toHaveAttribute('data-state', 'failed');152      await expect(sheet.getByTestId('collateral-op')).toHaveAttribute('data-done', '0');153      await expect(sheet.getByTestId('collateral-op-words')).toContainText('Le dépôt n’a pas eu lieu. Rien n’a atteint la plateforme.');154      await sheet.getByRole('button', { name: 'Fermer' }).last().click();155      // now a deposit whose credit never comes, seen eleven minutes later156      await coll(page, 'creditAfter', null); await coll(page, 'age', 11 * 60_000);157      await panel.getByTestId('collateral-deposit').click();158      sheet = page.getByTestId('deposit-sheet');159      await sheet.getByTestId('deposit-amount').fill('12');160      await sheet.getByTestId('deposit-review').click();161      await sheet.getByTestId('deposit-approve').click();162      await expect(sheet).toHaveAttribute('data-step', 'progress', { timeout: 10_000 });163      const op = sheet.getByTestId('collateral-op');164      await expect(op).toHaveAttribute('data-state', 'awaiting_credit');165      await expect(op).toHaveAttribute('data-done', '0');166      await expect(op.getByTestId('collateral-op-words')).toContainText('Le dépôt n’est PAS annoncé comme arrivé');167      await expect(op.getByTestId('collateral-overdue')).toContainText('À vérifier, dans l’ordre');168      await expect(op.getByTestId('collateral-overdue')).toContainText(BRIDGE);169      await expect(op.getByTestId('collateral-overdue').getByRole('listitem')).toHaveCount(5);170      await expect(op).toContainText('pas encore — en attente');171      await shot(page, '07-deposit-overdue');172      expect(await page.getByRole('dialog').evaluate((d) => d.scrollWidth <= d.clientWidth + 1 && [...d.querySelectorAll('*')].every((e) => e.scrollWidth <= e.clientWidth + 1 || getComputedStyle(e).overflowX !== 'visible' || e.clientWidth === 0))).toBe(true);173      await sheet.getByRole('button', { name: 'Fermer' }).last().click();174      // the account card keeps saying so, and a second deposit is refused while this one is on its way175      await expect(panel.getByTestId('collateral-ops').getByTestId('collateral-op').first()).toHaveAttribute('data-state', 'awaiting_credit');176      await expect(panel.getByTestId('collateral-venue-balance')).toContainText(/0,00\s\$/);177      await panel.getByTestId('collateral-deposit').click();178      sheet = page.getByTestId('deposit-sheet');179      await sheet.getByTestId('deposit-amount').fill('10');180      await sheet.getByTestId('deposit-review').click();181      await expect(sheet.getByTestId('collateral-error')).toContainText('Un dépôt est déjà en route');182      expect(errors).toEqual([]);183    });184185    test('USDC on another network: nothing is bridged silently, a pre-filled link to the cross-network transfer; testnet has no deposit', async ({ page }) => {186      await page.goto(`${BASE}/futures?v=accounts&mock=1&mainnet=1`);187      await expect(page.getByTestId('live-card')).toBeVisible();188      await live(page, 'fund', 'mainnet', '0');189      await coll(page, 'wallet', '2', [{ network: 'base', name: 'Base', usdc: '31.5' }]);    // 2 USDC on Arbitrum, 31.5 on Base190      await page.getByTestId('live-create-mainnet').getByRole('button').click();191      const panel = page.getByTestId('live-account-mainnet').getByTestId('collateral-panel');192      await expect(panel.getByTestId('deposit-why')).toHaveAttribute('data-reason', 'usdc_elsewhere');193      await expect(panel.getByTestId('collateral-deposit')).toBeDisabled();194      await expect(panel.getByTestId('deposit-why')).toContainText('Rien n’est transféré automatiquement');195      const link = panel.getByTestId('collateral-bridge-links').getByRole('link');196      await expect(link).toHaveAttribute('href', '/swap?bridgeFrom=base&bridgeTo=arbitrum');197      await expect(link).toContainText('Amener de l’USDC de Base vers Arbitrum One (31,5 USDC disponibles)');198      await shot(page, '08-usdc-elsewhere');199      await live(page, 'fund', 'testnet', '100');200      await page.getByTestId('live-create-testnet').getByRole('button').click();201      const t = page.getByTestId('live-account-testnet').getByTestId('collateral-panel');202      await expect(t.getByTestId('collateral-deposit')).toBeDisabled();203      await expect(t.getByTestId('deposit-why')).toHaveAttribute('data-reason', 'no_route');204      await expect(t.getByTestId('collateral-withdraw')).toBeEnabled();205      // no horizontal overflow on any size: a long address must wrap206      expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1)).toBe(true);207    });208  });209}210
Files main
Menu

Theme