SPB Git

Repository Public

futures-live.spec.ts 31.2 KB 438 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 Locator, type Page } from '@playwright/test';67// Controlled LIVE trading screens of the Futures terminal against the in-browser mock (`?mock=1`, development build only):8// no backend, no venue, no signer, a simulated passkey — see apps/web/src/derivatives/mockLive.ts for what is real in it9// (canonical objects and hashes, acknowledgement text, risk engine, order of the backend's checks) and what is not.10// The spec starts its own Vite dev server (or uses FUTURES_URL). FUTURES_SHOTS=1 writes docs/v4/screens/live-*.png.11//   npx playwright test -c tests/e2e/playwright.config.ts futures-live.spec.ts12const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');13const PORT = 5190;14const BASE = process.env.FUTURES_URL ?? `http://localhost:${PORT}`;15const SHOTS = resolve(ROOT, 'docs/v4/screens');16let vite: ChildProcess | null = null;1718test.beforeAll(async () => {19  if (process.env.FUTURES_URL) return;20  vite = spawn(resolve(ROOT, 'apps/web/node_modules/.bin/vite'), ['--port', String(PORT), '--strictPort'], { cwd: resolve(ROOT, 'apps/web'), stdio: 'ignore' });21  for (let i = 0; i < 80; i++) {22    try { if ((await fetch(`${BASE}/`)).ok) return; } catch { /* not up yet */ }23    await new Promise((r) => setTimeout(r, 250));24  }25  throw new Error('the Vite dev server did not start');26});27test.afterAll(() => { vite?.kill(); });2829const 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(String(e))); return errors; };30const mark = (page: Page, id = 'hyperliquid:BTC') => page.evaluate(async (i) => Number((await (await fetch(`/api/v1/derivatives/market?instrument=${encodeURIComponent(i)}`)).json()).ticker.mark), id);31type Hook = 'mainnet' | 'fund' | 'passkey' | 'venue' | 'signerOffsetBps' | 'signerDown' | 'ttl' | 'diverge' | 'settle' | 'unreadable';32const hook = (page: Page, name: Hook, ...args: unknown[]) => page.evaluate(([n, a]) => (window as any).__spwMock.live[n as string](...(a as unknown[])), [name, args] as const);33const liveAccounts = (page: Page): Promise<{ id: string; network: string; mode: string; orders: number }[]> => page.evaluate(() => (window as any).__spwMock.live.accounts());3435const SIZES = [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;3940for (const size of SIZES) {41  const phone = size.name === 'phone';42  test.describe(`futures live — ${size.name}`, () => {43    test.use({ ...size.use, locale: 'fr-CA' });44    const shot = async (page: Page, name: string, fullPage = false) => {45      if (process.env.FUTURES_SHOTS !== '1') return;46      mkdirSync(SHOTS, { recursive: true });47      await page.screenshot({ path: `${SHOTS}/live-${size.name}-${name}.png`, fullPage });48    };49    const panel = async (page: Page, name: 'Graphique' | 'Carnet' | 'Ordre' | 'Positions') => { if (phone) await page.getByRole('tab', { name: new RegExp(`^${name}( \\(\\d+\\))?$`) }).click(); };50    const view = async (page: Page, name: 'Terminal' | 'Comptes') => { await page.getByRole('tablist', { name: 'Vues' }).getByRole('tab', { name }).click(); };51    const blotterTab = (page: Page, name: RegExp) => page.getByTestId('blotter').getByRole('tab', { name }).click();52    const openAccounts = async (page: Page) => { await page.goto(`${BASE}/futures?v=accounts&mock=1`); await expect(page.getByTestId('live-card')).toBeVisible(); await expect(page.getByTestId('live-create-testnet')).toBeVisible(); };53    /** the limits form with room for the test orders, then the review, the acknowledgement and the (simulated) passkey */54    const authorize = async (page: Page, card: Locator, o: { shots?: boolean } = {}) => {55      await card.getByTestId('authorize').click();56      const sheet = page.getByTestId('authorize-sheet');57      await expect(sheet).toHaveAttribute('data-step', 'form');58      if (o.shots) await shot(page, '03-authorize-form');59      // the per-order limit may not exceed the per-position limit: the form says so and offers nothing to approve60      await sheet.getByRole('textbox', { name: 'Valeur maximale par ordre' }).fill('200');61      await expect(sheet.getByTestId('policy-problem')).toContainText('ne peut pas dépasser la limite par position');62      await expect(sheet.getByRole('button', { name: /Continuer/ })).toBeDisabled();63      await sheet.getByRole('textbox', { name: 'Position maximale par marché' }).fill('400');64      await sheet.getByRole('textbox', { name: 'Exposition brute maximale' }).fill('600');65      await sheet.getByRole('button', { name: /Continuer/ }).click();66      await expect(sheet).toHaveAttribute('data-step', 'review');67      // 1. the jurisdiction text, in full, in the screen's language; nothing can be approved before it is acknowledged68      await expect(sheet.getByTestId('ack-text')).toContainText('Hyperliquid — conditions de la plateforme et votre juridiction (Québec, Canada)');69      await expect(sheet.getByTestId('ack-text')).toContainText('En approuvant, vous confirmez avoir lu ce résumé');70      await expect(sheet.getByTestId('auth-approve')).toBeDisabled();71      // 2. what is signed, 3. the trading key, what it can and cannot do, 4. the limits as the server recorded them72      await expect(sheet.getByTestId('what-is-signed')).toContainText('approveAgent');73      await expect(sheet.getByTestId('agent-can')).toContainText('placer des ordres');74      await expect(sheet.getByTestId('agent-can')).toContainText('retirer des fonds');75      await expect(sheet.getByTestId('policy-rows')).toContainText(/200,00\s\$/);76      await expect(sheet.getByTestId('integrity')).toHaveCount(0);77      if (o.shots) await shot(page, '04-authorize-review');78      await sheet.getByTestId('ack-check').check();79      if (o.shots) { await sheet.getByTestId('auth-approve').scrollIntoViewIfNeeded(); await shot(page, '05-authorize-ready'); }80      await sheet.getByTestId('auth-approve').click();81    };82    const setup = async (page: Page, o: { shots?: boolean } = {}) => {83      await openAccounts(page);84      await page.getByTestId('live-create-testnet').getByRole('button', { name: 'Créer le compte testnet' }).click();85      const card = page.getByTestId('live-account-testnet');86      await expect(card).toHaveAttribute('data-mode', 'READ_ONLY');87      await authorize(page, card, o);88      await expect(card).toHaveAttribute('data-mode', 'MANUAL_LIVE', { timeout: 10_000 });89      return card;90    };91    const toTerminal = async (page: Page) => {92      await view(page, 'Terminal');93      await expect(page.getByTestId('terminal')).toHaveAttribute('data-tone', 'testnet');94      await panel(page, 'Ordre');95      await expect(page.getByTestId('order-panel')).toHaveAttribute('data-account-kind', 'venue');96    };97    const form = async (page: Page, o: { kind?: 'Au marché' | 'À cours limité'; price?: string; size: string; side?: 'Short' }) => {98      const p = page.getByTestId('order-panel');99      if (o.side) await p.getByRole('radio', { name: o.side }).click();100      if (o.kind) await p.getByRole('radio', { name: o.kind, exact: true }).click();101      if (o.price !== undefined) await p.getByRole('textbox', { name: /Prix limite/ }).fill(o.price);102      await p.getByRole('textbox', { name: 'Taille' }).fill(o.size);103    };104    const review = async (page: Page) => {105      await expect(page.getByTestId('preview')).toContainText('Valeur notionnelle');106      await expect(page.getByTestId('live-review')).toBeEnabled();107      await page.getByTestId('live-review').click();108      const sheet = page.getByTestId('approval-sheet');109      await expect(sheet).toHaveAttribute('data-phase', 'ready');110      return sheet;111    };112    const approve = async (sheet: Locator) => { await sheet.getByRole('button', { name: /Approuver avec la clé d’accès/ }).click(); };113    const limitPrice = async (page: Page, factor: number) => String(Math.round((await mark(page)) * factor));114115    test('testnet: create → authorise → limit order open → cancel → market → position → halt blocks a new order, allows the close → revoke', async ({ page }) => {116      test.setTimeout(150_000);117      const errors = errorsOf(page);118      await openAccounts(page);119      // before anything: off, mainnet shown and blocked with the reason, nothing that pretends120      const live = page.getByTestId('live-card');121      await expect(live.getByTestId('live-status')).toContainText('désactivée');122      await expect(live.getByTestId('mainnet-setting')).toContainText('n’est pas un interrupteur de cette interface');123      await expect(live.getByTestId('live-create-mainnet').getByRole('button', { name: 'Créer le compte réel' })).toBeDisabled();124      await expect(live.getByTestId('mainnet-blocked')).toContainText('le serveur garde le réseau principal désactivé');125      await live.scrollIntoViewIfNeeded();126      await shot(page, '01-accounts-off');127      await live.getByRole('button', { name: 'Créer le compte testnet' }).click();128      const card = page.getByTestId('live-account-testnet');129      await expect(card).toHaveAttribute('data-mode', 'READ_ONLY');130      await expect(card.getByTestId('mode-band')).toHaveAttribute('data-tone', 'testnet');131      await expect(card.getByTestId('mode-band')).toContainText(/Testnet — argent fictif/i);132      await expect(card.getByTestId('venue-state')).toContainText(/1\s000,00\s\$/);133      await expect(card.getByTestId('venue-state')).toContainText('plateforme');134      await expect(card.getByTestId('trading-key')).toContainText('0x7e57');135      await expect(card.getByTestId('trading-key')).toContainText('Elle ne peut ni retirer des fonds');136      // funding is explained, never offered: deposits and withdrawals are not built137      await expect(card.getByTestId('funding-box')).toContainText('« Déposer » et « Retirer » se font ci-dessous');138      await expect(card.getByTestId('funding-box').getByRole('button')).toHaveCount(0);139      await card.scrollIntoViewIfNeeded();140      await shot(page, '02-account-readonly', true);141      await authorize(page, card, { shots: true });142      await expect(card).toHaveAttribute('data-mode', 'MANUAL_LIVE', { timeout: 10_000 });143      await expect(live.getByTestId('live-status')).toContainText('autorisée');144      await expect(card.getByTestId('live-authorization')).toContainText('BTC');145      await card.scrollIntoViewIfNeeded();146      await shot(page, '06-account-authorized', true);147148      // ---- the terminal: the band says which money, permanently149      await toTerminal(page);150      const band = page.getByTestId('terminal').getByTestId('mode-band').first();151      await expect(band).toContainText(/Testnet — argent fictif/i);152      await expect(band.getByRole('button')).toHaveCount(0);                                  // nothing dismisses it153      await expect(page.getByTestId('paper-banner')).toContainText('Compte TESTNET');154      await expect(page.getByTestId('order-panel').getByTestId('free-collateral')).toContainText(/1\s000,00\s\$/);155156      // ---- limit order: preview → proposal → the canonical order → passkey → open (from the server)157      await form(page, { price: await limitPrice(page, 0.99), size: '0,001' });158      await expect(page.getByTestId('preview')).toContainText('estimation');159      await shot(page, '07-order-preview', phone);160      let sheet = await review(page);161      await expect(page.getByTestId('live-review')).toBeDisabled();                           // a proposal is pending: no second one162      const canonical = sheet.getByTestId('canonical');163      await expect(canonical).toContainText('hyperliquid:BTC');164      await expect(canonical).toContainText('Achat');165      await expect(canonical).toContainText('Limite');166      await expect(canonical).toContainText('0,001 BTC');167      await expect(canonical).toContainText('jusqu’à annulation (GTC)');168      await expect(canonical).toContainText('testnet');169      await expect(sheet.getByTestId('countdown')).toContainText(/Approbation possible encore 1:\d\d/);170      await expect(sheet.getByTestId('mode-band')).toHaveAttribute('data-tone', 'testnet');171      await shot(page, '08-approve-sheet');172      await approve(sheet);173      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'open', { timeout: 10_000 });174      await expect(sheet.getByTestId('live-outcome')).toContainText('Ordre ouvert sur la plateforme');175      await shot(page, '09-order-open');176      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();177      await expect(page.getByTestId('order-panel').getByTestId('live-outcome')).toHaveAttribute('data-state', 'open');178      await panel(page, 'Positions');179      const blotter = page.getByTestId('blotter');180      await blotterTab(page, /^Ordres ouverts \(1\)/);181      await expect(blotter).toContainText('ouvert');182      await expect(blotter).toContainText('plateforme');183      await shot(page, '10-open-order', phone);184      await blotter.getByRole('button', { name: 'Annuler l’ordre sur BTC' }).click();          // no passkey185      await expect(blotter).toContainText('Aucun ordre ouvert.');186      await blotterTab(page, /^Historique/);187      await expect(blotter.locator('[data-state="cancelled"]').first()).toBeVisible();188189      // ---- market order: it is a limit IOC at its worst price, and the sheet says so190      await panel(page, 'Ordre');191      await form(page, { kind: 'Au marché', size: '0,002' });192      sheet = await review(page);193      await expect(sheet.getByTestId('canonical')).toContainText('ordre au marché = limite « immédiat ou annulé » au pire prix');194      await expect(sheet.getByTestId('canonical')).toContainText('Pire prix accepté');195      await expect(sheet.getByTestId('canonical')).toContainText('immédiat ou annulé (IOC)');196      await approve(sheet);197      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'filled', { timeout: 10_000 });198      await expect(sheet.getByTestId('live-outcome')).toContainText(/Ordre exécuté\s: 0,002 BTC à/);199      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();200      await panel(page, 'Positions');201      await blotterTab(page, /^Positions \(1\)/);202      await expect(blotter).toContainText('Long');203      await shot(page, '11-position', phone);204      await blotterTab(page, /^Exécutions/);205      await expect(blotter).toContainText('Achat');206207      // ---- halt: a new order is refused, the close still passes208      await panel(page, 'Ordre');209      await page.getByTestId('live-controls').getByRole('button', { name: 'Suspendre les nouveaux ordres' }).click();210      await expect(page.getByTestId('halt-banner').first()).toContainText('Négociation suspendue sur ce compte.');211      await form(page, { kind: 'À cours limité', price: await limitPrice(page, 0.99), size: '0,001' });212      await expect(page.getByTestId('risk-refusal')).toContainText('La négociation est suspendue sur ce compte');213      await expect(page.getByTestId('live-review')).toBeDisabled();214      await page.getByTestId('halt-banner').first().scrollIntoViewIfNeeded();215      await shot(page, '12-halted', phone);216      await panel(page, 'Positions');217      await blotterTab(page, /^Positions \(1\)/);218      await blotter.getByRole('button', { name: 'Fermer 100 % de la position BTC' }).click();219      sheet = page.getByTestId('approval-sheet');220      await expect(sheet).toHaveAttribute('data-phase', 'ready');221      await expect(sheet.getByTestId('canonical')).toContainText('Vente');222      await expect(sheet.getByTestId('canonical')).toContainText(/Réduction seulement\s*oui/);223      await shot(page, '13-close-sheet');224      await approve(sheet);225      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'filled', { timeout: 10_000 });226      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();227      await expect(blotter).toContainText('Aucune position.');228      await blotterTab(page, /^Risque/);229      await expect(blotter).toContainText('Négociation suspendue');230      await panel(page, 'Ordre');231      await page.getByTestId('halt-banner').first().getByRole('button', { name: 'Reprendre la négociation' }).click();232      await expect(page.getByTestId('halt-banner')).toHaveCount(0);233234      // ---- revoke: no passkey, the account is read-only again235      await view(page, 'Comptes');236      await page.getByTestId('live-account-testnet').getByTestId('revoke').click();237      const rv = page.getByTestId('revoke-sheet');238      await expect(rv).toContainText('pas même une annulation');239      await expect(rv.getByTestId('revoke-cancel-first')).toBeChecked();240      await shot(page, '14-revoke');241      await rv.getByRole('button', { name: 'Révoquer maintenant' }).click();242      await expect(rv.getByTestId('revoke-done')).toContainText('Autorisation révoquée');243      await rv.getByRole('button', { name: 'Fermer', exact: true }).last().click();244      await expect(page.getByTestId('live-account-testnet')).toHaveAttribute('data-mode', 'READ_ONLY');245      await view(page, 'Terminal');246      await panel(page, 'Ordre');247      await expect(page.getByTestId('live-readonly')).toContainText('révoquée');248      await expect(page.getByTestId('order-panel').getByRole('textbox')).toHaveCount(0);249      expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);250      expect(errors).toEqual([]);251    });252253    test('refusals: price band (risk engine, then signer), order notional, cancelled passkey, expired proposal', async ({ page }) => {254      test.setTimeout(150_000);255      await setup(page);256      await toTerminal(page);257      // far from the market: the risk engine refuses in the preview, and nothing can be reviewed258      await form(page, { price: await limitPrice(page, 0.9), size: '0,001' });259      await expect(page.getByTestId('risk-refusal')).toContainText('price_deviation');260      await expect(page.getByTestId('live-review')).toBeDisabled();261      // above the notional the signer will sign per order262      await form(page, { price: await limitPrice(page, 0.99), size: '0,01' });263      await expect(page.getByTestId('risk-refusal')).toContainText('order_notional_exceeded');264      await expect(page.getByTestId('risk-refusal')).toContainText('Le signataire plafonne chaque ordre');265      await expect(page.getByTestId('live-review')).toBeDisabled();266      await shot(page, '15-refused-notional', phone);267      // the signer's own price is elsewhere: the API accepts, the signer refuses at approval, the proposal is consumed268      await hook(page, 'signerOffsetBps', 1500);269      await form(page, { price: await limitPrice(page, 0.99), size: '0,001' });270      let sheet = await review(page);271      await approve(sheet);272      await expect(sheet).toHaveAttribute('data-phase', 'void', { timeout: 10_000 });273      await expect(sheet.getByTestId('live-error')).toHaveAttribute('data-code', 'signer_price');274      await expect(sheet.getByTestId('live-error')).toContainText('sort de la bande de prix');275      await expect(sheet.getByRole('button', { name: /Approuver avec la clé d’accès/ })).toHaveCount(0);276      await shot(page, '16-refused-signer');277      await hook(page, 'signerOffsetBps', 0);278      // a dismissed passkey prompt: nothing was sent, the proposal is withdrawn, "prepare again" makes a new one279      await hook(page, 'passkey', 'cancel');280      await sheet.getByTestId('prepare-again').click();281      await expect(sheet).toHaveAttribute('data-phase', 'ready');282      await approve(sheet);283      await expect(sheet.getByTestId('live-error')).toHaveAttribute('data-code', 'passkey_cancelled');284      await expect(sheet.getByTestId('live-error')).toContainText('Rien n’a été signé ni envoyé');285      await shot(page, '17-passkey-cancelled');286      await sheet.getByTestId('prepare-again').click();287      await expect(sheet).toHaveAttribute('data-phase', 'ready');288      await approve(sheet);289      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'open', { timeout: 10_000 });290      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();291      // an expired proposal cannot be approved: the button goes away, "prepare again" is offered292      await hook(page, 'ttl', 7000);293      await form(page, { price: await limitPrice(page, 0.99), size: '0,001' });294      sheet = await review(page);295      await expect(sheet).toHaveAttribute('data-phase', 'void', { timeout: 12_000 });296      await expect(sheet.getByTestId('live-error')).toHaveAttribute('data-code', 'proposal_expired');297      await expect(sheet.getByRole('button', { name: /Approuver avec la clé d’accès/ })).toHaveCount(0);298      await shot(page, '18-expired');299      await hook(page, 'ttl', 90_000);300      await sheet.getByRole('button', { name: 'Annuler', exact: true }).click();301      // every proposal of this test has its own key; the three that were given up are withdrawn or refused, one order rests302      await panel(page, 'Positions');303      await blotterTab(page, /^Ordres ouverts \(1\)/);304      await blotterTab(page, /^Historique/);305      const states = await page.getByTestId('blotter').locator('[data-state]').evaluateAll((els) => els.map((e) => e.getAttribute('data-state')));306      expect(states.filter((s) => s === 'open')).toHaveLength(1);307      expect(states.filter((s) => s === 'risk_checked' || s === 'proposed')).toHaveLength(0);308    });309310    test('venue answers: unknown → reconciled, a venue refusal in its own words; double click makes one proposal; leverage change', async ({ page }) => {311      test.setTimeout(120_000);312      await setup(page);313      await toTerminal(page);314      const before = (await liveAccounts(page))[0]!.orders;315      await form(page, { price: await limitPrice(page, 0.99), size: '0,001' });316      await expect(page.getByTestId('preview')).toContainText('Valeur notionnelle');317      await expect(page.getByTestId('live-review')).toBeEnabled();318      await page.getByTestId('live-review').dblclick();                                        // two clicks, one review, one Idempotency-Key319      let sheet = page.getByTestId('approval-sheet');320      await expect(sheet).toHaveAttribute('data-phase', 'ready');321      expect((await liveAccounts(page))[0]!.orders).toBe(before + 1);322      await hook(page, 'venue', 'unknown');323      await sheet.getByRole('button', { name: /Approuver avec la clé d’accès/ }).dblclick();     // two clicks, one prompt, one approval324      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'unknown', { timeout: 10_000 });325      await expect(sheet.getByTestId('live-outcome')).toContainText('En cours de vérification auprès de la plateforme — ne pas renvoyer');326      await expect(sheet.getByTestId('prepare-again')).toHaveCount(0);                        // nothing that could send it again327      await shot(page, '19-unknown');328      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'open', { timeout: 15_000 });   // resolved by reconciliation, from the server329      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();330      expect((await liveAccounts(page))[0]!.orders).toBe(before + 1);331      // the venue refuses: its words are shown332      await hook(page, 'venue', 'reject');333      await form(page, { price: await limitPrice(page, 0.985), size: '0,001' });334      sheet = await review(page);335      await approve(sheet);336      await expect(sheet.getByTestId('live-outcome')).toHaveAttribute('data-state', 'rejected', { timeout: 10_000 });337      await expect(sheet.getByTestId('live-outcome')).toContainText('Order has invalid price.');338      await shot(page, '20-venue-rejected');339      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();340      // leverage: a venue setting, changed with the passkey, read back from the venue341      const lev = page.getByTestId('venue-leverage');342      await expect(lev).toContainText('20×');343      await lev.getByRole('button', { name: 'Modifier…' }).click();344      const ls = page.getByTestId('leverage-sheet');345      await ls.getByRole('slider').fill('2');346      await ls.getByRole('button', { name: 'Préparer le changement' }).click();347      await expect(ls).toHaveAttribute('data-phase', 'ready');348      await expect(ls.getByTestId('canonical')).toContainText('2×');349      await shot(page, '21-leverage');350      await ls.getByRole('button', { name: /Approuver avec la clé d’accès/ }).click();351      await expect(ls.getByTestId('leverage-result')).toHaveAttribute('data-state', 'confirmed', { timeout: 10_000 });352      await ls.getByRole('button', { name: 'Fermer', exact: true }).last().click();353      await expect(lev).toContainText('2×', { timeout: 10_000 });354    });355356    test('an unfunded account: the venue refuses the authorization, in its words, with the funding explanation', async ({ page }) => {357      await openAccounts(page);358      await hook(page, 'fund', 'testnet', '0');359      await page.getByTestId('live-create-testnet').getByRole('button', { name: 'Créer le compte testnet' }).click();360      const card = page.getByTestId('live-account-testnet');361      await expect(card.getByTestId('venue-state')).toContainText(/0,00\s\$/);362      await authorize(page, card);363      const sheet = page.getByTestId('authorize-sheet');364      await expect(sheet).toHaveAttribute('data-step', 'refused', { timeout: 10_000 });365      await expect(sheet.getByTestId('venue-words')).toContainText('Must deposit before performing actions. User: 0x5ad0');366      await expect(sheet.getByTestId('auth-refused')).toContainText('La plateforme a refusé d’enregistrer la clé de négociation.');367      await expect(sheet.getByTestId('auth-refused')).toContainText('n’accepte aucune action d’un compte qui n’a jamais reçu de dépôt');368      await expect(sheet.getByTestId('funding-box')).toContainText('robinet');369      await shot(page, '22-unfunded');370      await sheet.getByRole('button', { name: 'Fermer', exact: true }).last().click();371      await expect(card).toHaveAttribute('data-mode', 'READ_ONLY');372      await expect(page.getByTestId('live-status')).toContainText('désactivée');373    });374375    test('mainnet: real-money band when enabled; blocked with the reason when the server setting is off; divergence; English', async ({ page }) => {376      test.setTimeout(120_000);377      await page.goto(`${BASE}/futures?v=accounts&mock=1&mainnet=1`);                         // the mock starts with the server setting ON378      await expect(page.getByTestId('live-create-mainnet').getByRole('button', { name: 'Créer le compte réel' })).toBeEnabled();379      await expect(page.getByTestId('mainnet-setting')).toHaveCount(0);380      await page.getByTestId('live-create-mainnet').getByRole('button', { name: 'Créer le compte réel' }).click();381      const card = page.getByTestId('live-account-mainnet');382      await expect(card.getByTestId('mode-band')).toHaveAttribute('data-tone', 'mainnet');383      await expect(card.getByTestId('mode-band')).toContainText(/Réel — argent réel/i);384      await authorize(page, card);385      await expect(card).toHaveAttribute('data-mode', 'MANUAL_LIVE', { timeout: 10_000 });386      await view(page, 'Terminal');387      await expect(page.getByTestId('terminal')).toHaveAttribute('data-tone', 'mainnet');388      await expect(page.getByTestId('terminal').getByTestId('mode-band').first()).toContainText(/Réel — argent réel/i);389      await panel(page, 'Ordre');390      await expect(page.getByTestId('live-foot')).toContainText('ARGENT RÉEL');391      await form(page, { price: await limitPrice(page, 0.99), size: '0,001' });392      const sheet = await review(page);393      await expect(sheet.getByTestId('mode-band')).toHaveAttribute('data-tone', 'mainnet');394      await expect(sheet.getByRole('button', { name: 'Approuver avec la clé d’accès — ARGENT RÉEL' })).toBeVisible();395      await shot(page, '23-real-approve');396      await sheet.getByRole('button', { name: 'Annuler', exact: true }).click();              // withdraws the proposal: nothing is signed397      await expect(sheet).toHaveCount(0);398      if (phone) await page.getByRole('tab', { name: /^Graphique/ }).click();399      await shot(page, '24-real-terminal');400      // a difference with the venue stops new orders; "accept the venue's state" is explained and confirmed401      await panel(page, 'Ordre');402      await hook(page, 'diverge', 'mainnet', 'position');403      const hb = page.getByTestId('halt-banner').first();404      await expect(hb).toHaveAttribute('data-kind', 'divergence', { timeout: 15_000 });405      await expect(hb).toContainText('ne concordent pas');406      await hb.scrollIntoViewIfNeeded();407      await shot(page, '25-divergence', phone);408      await hb.getByRole('button', { name: 'Revérifier maintenant' }).click();409      await expect(hb).toContainText('l’écart est toujours là');410      await hb.getByRole('button', { name: 'Accepter l’état de la plateforme…' }).click();411      await page.getByRole('dialog', { name: 'Accepter l’état de la plateforme' }).getByRole('button', { name: /Oui, prendre l’état de la plateforme/ }).click();412      await expect(page.getByTestId('halt-banner')).toHaveCount(0, { timeout: 10_000 });413      // the server setting goes off: the account is shown, every action is blocked with the reason414      await hook(page, 'mainnet', false);415      await expect(page.getByTestId('order-panel').getByTestId('mainnet-off')).toContainText('Réseau principal désactivé sur le serveur.', { timeout: 30_000 });416      await expect(page.getByTestId('order-panel').getByRole('textbox')).toHaveCount(0);417      const refused = await page.evaluate(async () => { const id = (window as any).__spwMock.live.accounts().find((a: any) => a.network === 'mainnet').id;418        const r = await fetch(`/api/v1/derivatives/live/accounts/${id}/orders/preview`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-spw-csrf': '1' }, body: JSON.stringify({ instrument: 'hyperliquid:BTC', side: 'buy', type: 'limit', size: '0.001', price: '70000' }) });419        return { status: r.status, error: (await r.json()).error }; });420      expect(refused).toEqual({ status: 403, error: 'mainnet_disabled' });421      await shot(page, '26-mainnet-off', phone);422      await view(page, 'Comptes');423      await expect(page.getByTestId('live-account-mainnet').getByTestId('mainnet-off')).toBeVisible();424      await expect(page.getByTestId('live-account-mainnet').getByTestId('revoke')).toBeEnabled();   // stopping is always possible425      // English426      await page.getByRole('navigation').getByRole('button', { name: 'Réglages' }).click();427      await page.getByRole('radio', { name: 'English' }).click();428      await page.getByRole('navigation').getByRole('button', { name: 'Swap' }).click();429      await page.getByRole('tab', { name: 'Futures' }).click();430      await expect(page.getByTestId('terminal').getByTestId('mode-band').first()).toContainText(/Real — real money/i);431      await page.getByRole('tablist', { name: 'Views' }).getByRole('tab', { name: 'Accounts' }).click();432      await expect(page.getByTestId('live-account-mainnet')).toContainText('Main network disabled on the server.');433      await page.getByTestId('live-account-mainnet').scrollIntoViewIfNeeded();434      await shot(page, '27-english', true);435    });436  });437}438
Files main
Menu

Theme