Files
universe/e2e/51-institutional-baseline.spec.ts
zeekayandHanzo Dev f84d398e19 e2e: the staking route is gone, so stop declaring it
The stLUX vault page was removed from the console — no vault is configured for
any brand, so the surface only ever said so, and staking returns are served by
/dashboard/calculator and /dashboard/validators from the P-Chain. lux.cloud is
a catch-all origin, so the old path still paints the console's "No such
section"; a bodyLen assertion against that shell passes and proves nothing.
It moves to NOT_SERVED, where the registry keeps retired paths visible.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-08 05:56:54 -07:00

319 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, Page, Response } from '@playwright/test'
import { mkdirSync, writeFileSync } from 'fs'
import { join } from 'path'
import { env, SHOT_DIR, viewportName } from './env'
import { signIn, snap } from './helpers'
// ---------------------------------------------------------------------------
// THE INSTITUTIONAL BASELINE — what lux.cloud is like for an institution today
// ---------------------------------------------------------------------------
// This spec does not decide whether the product is good. It MEASURES the six
// things an institutional custody/infrastructure console is judged on, screen
// by screen, and writes the evidence next to the screenshots so the score in
// the review can be checked against something other than an opinion:
//
// 1. Does the screen render at all (nav status, not just "the SPA is 200").
// 2. Does it say what it is (an h1 / page heading).
// 3. How much of it is a ZERO or a placeholder — and is that zero EXPLAINED.
// Mainnet C-Chain is genuinely idle right now, so a flat number there is
// CORRECT; "0/5 CONNECTED" and 0.0000 uptime are defects. An institution
// cannot tell those apart unless the screen says which it is, so we record
// every zero WITH THE LABEL NEXT TO IT and let the review adjudicate.
// 4. Did any data call actually fail (4xx/5xx XHR, console error). A zero
// with a failed fetch behind it is a BUG; a zero with a clean 200 behind
// it is the chain being quiet.
// 5. Is it still loading after it has had its time (a permanent skeleton is
// the worst of the three states — it never resolves to either answer).
// 6. Is there a trust signal at all: a block height, a timestamp, a
// "last updated", a network name, an explorer link to verify against.
//
// It runs at all three viewports (playwright projects) because "institutional"
// includes the phone an operator approves from.
//
// LUX_SHOT_SET=institutional-baseline LUX_E2E_PASSWORD=… \
// npx playwright test e2e/51-institutional-baseline.spec.ts
//
// Read-only. It signs in and it looks. It never submits a form, never signs,
// never broadcasts — every value-moving control on these screens is
// owner-gated and this suite must never be the thing that trips one.
const CLOUD = env.urls.cloud
interface Screen {
id: string
path: string
group: string
// Built and routable, but absent from the signed-in navigation — an
// institutional user cannot reach it without being told the URL.
unlinked?: boolean
// No session required; these are the two screens an investor sees first.
anon?: boolean
}
// Groups mirror the signed-in sidebar exactly (measured 2026-07-26), so a gap
// in this list is a gap in the product, not in the test.
const SCREENS: Screen[] = [
{ id: '00-landing', path: '/', group: 'Pre-auth', anon: true },
{ id: '01-login', path: '/login', group: 'Pre-auth', anon: true },
{ id: '10-overview', path: '/dashboard', group: 'Overview' },
{ id: '11-analytics', path: '/dashboard/analytics', group: 'Overview' },
{ id: '20-validators', path: '/dashboard/validators', group: 'Infrastructure' },
{ id: '21-rpc', path: '/dashboard/rpc', group: 'Infrastructure' },
{ id: '22-chains', path: '/dashboard/chains', group: 'Infrastructure' },
{ id: '23-storage', path: '/dashboard/storage', group: 'Infrastructure' },
{ id: '24-deploy', path: '/dashboard/deploy', group: 'Infrastructure' },
{ id: '31-calculator', path: '/dashboard/calculator', group: 'Assets & DeFi' },
{ id: '32-treasury', path: '/dashboard/treasury', group: 'Assets & DeFi' },
{ id: '33-safes', path: '/dashboard/safes', group: 'Assets & DeFi' },
{ id: '34-mpc', path: '/dashboard/mpc', group: 'Assets & DeFi' },
{ id: '35-dex', path: '/dashboard/dex', group: 'Assets & DeFi' },
{ id: '36-bridge', path: '/dashboard/bridge', group: 'Assets & DeFi' },
{ id: '40-explorer', path: '/dashboard/explorer', group: 'Observability' },
{ id: '41-monitoring', path: '/dashboard/monitoring', group: 'Observability' },
{ id: '50-iam', path: '/dashboard/iam', group: 'Identity & Billing' },
{ id: '51-billing', path: '/dashboard/billing', group: 'Identity & Billing' },
// Routes that EXIST in the app but are not linked from the signed-in shell.
// Several of them are exactly what the institutional bar asks for (wallets,
// API keys, team/roles), so their absence from the nav is itself a finding.
{ id: '60-wallets', path: '/dashboard/wallets', group: 'Unlinked', unlinked: true },
{ id: '61-assets', path: '/dashboard/assets', group: 'Unlinked', unlinked: true },
{ id: '62-api-keys', path: '/dashboard/api-keys', group: 'Unlinked', unlinked: true },
{ id: '63-team', path: '/dashboard/organization/team', group: 'Unlinked', unlinked: true },
{ id: '64-settings', path: '/dashboard/settings', group: 'Unlinked', unlinked: true },
{ id: '65-webhooks', path: '/dashboard/webhooks', group: 'Unlinked', unlinked: true },
{ id: '66-usage', path: '/dashboard/usage', group: 'Unlinked', unlinked: true },
{ id: '67-admin', path: '/admin', group: 'Unlinked', unlinked: true },
]
// ── What counts as a zero ──────────────────────────────────────────────────
// A number that is entirely zero, a currency zero, or a typographic stand-in
// for "we have no value". `0.0000` and `0/5` are the two that burned us, so
// both are matched explicitly rather than by a loose /0/.
const ZERO = /(?:\$\s?0(?:\.0+)?|\b0(?:\.0+)?\s?%|\b0(?:\.0+)?\b|\b0\s*\/\s*\d+\b|—|–|\bN\/?A\b|--)/g
// The label a zero is sitting under, so the review can say WHICH zero.
// 48 chars of preceding text is enough to carry "Total Value Locked" or
// "UPTIME" without dragging in the whole card.
function zerosWithLabels(text: string): { label: string; value: string }[] {
const out: { label: string; value: string }[] = []
for (const m of text.matchAll(ZERO)) {
const before = text.slice(Math.max(0, m.index! - 48), m.index!).trim()
out.push({ label: before.slice(-48), value: m[0] })
}
return out
}
// A screen earns trust by letting you check it against the chain. Any ONE of
// these is enough: a block height, a freshness stamp, a chain id, or a link out
// to the explorer where the same number can be read independently.
const TRUST = [
/block\s*#?\s*\d/i,
/chain\s*id\s*\d/i,
/\b\d+\s*(?:seconds?|secs?|minutes?|mins?|hours?)\s+ago\b/i,
/last\s+(?:updated|synced|refreshed)/i,
/explorer/i,
]
// Copy that EXPLAINS an absence instead of just printing 0. This is the single
// highest-leverage thing missing from the product, so it gets measured directly.
const EXPLAINED_EMPTY = [
/no\s+\w+\s+yet/i,
/nothing\s+to\s+show/i,
/network\s+is\s+idle/i,
/\bidle\b/i,
/get\s+started/i,
/create\s+your\s+first/i,
/not\s+(?:yet\s+)?(?:available|deployed|configured|connected)/i,
/unavailable/i,
/couldn'?t\s+load|failed\s+to\s+load|unable\s+to\s+load/i,
]
// Still-rendering after we have already waited. A skeleton that never resolves
// reads as a hang, and an institution reads a hang as an outage.
//
// The verbs are the ones this product actually prints. The first version of
// this list only had loading/fetching/please-wait, and it passed the DEX page
// at 390px while the screenshot plainly said "Reading pools, reserves, and
// markets from chain…" — a detector that misses the real copy is just a
// slower way of not looking.
const STILL_LOADING =
/\b(loading|fetching|reading|checking|resolving|syncing)\b[^.]{0,60}…|\bplease\s+wait\b|^…$/i
interface Row {
id: string
group: string
path: string
unlinked: boolean
viewport: string
status: number
heading: string
chars: number
zeros: { label: string; value: string }[]
zeroCount: number
trustSignals: string[]
explainedEmpty: string[]
stillLoading: boolean
failedCalls: { url: string; status: number }[]
consoleErrors: string[]
shot: string
}
// Only the app's OWN data calls matter — a blocked third-party beacon is not a
// defect in the console. Host-matched, not substring-matched.
function isOwnCall(url: string): boolean {
try {
const h = new URL(url).host
return /(^|\.)lux\.(cloud|network|id|exchange|build)$/.test(h)
|| /(^|\.)(hanzo|zoo)\.(ai|network|ngo|id)$/.test(h)
} catch { return false }
}
async function measure(page: Page, s: Screen): Promise<Row> {
const failedCalls: { url: string; status: number }[] = []
const consoleErrors: string[] = []
const onResponse = (r: Response) => {
if (r.status() >= 400 && isOwnCall(r.url())) {
failedCalls.push({ url: r.url().slice(0, 160), status: r.status() })
}
}
const onConsole = (m: { type(): string; text(): string }) => {
if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 200))
}
page.on('response', onResponse)
page.on('console', onConsole)
let status = 0
try {
const res = await page.goto(`${CLOUD}${s.path}`, {
waitUntil: 'domcontentloaded', timeout: 60_000,
})
status = res?.status() ?? 0
} catch { status = 0 }
// Hydration, then the data calls, then a beat for them to paint. Generous on
// purpose: a screen judged "stuck loading" must have genuinely had its time.
await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {})
await page.waitForTimeout(4000)
const text = ((await page.textContent('body').catch(() => '')) || '').replace(/\s+/g, ' ')
const heading = (await page.locator('h1, h2').first().textContent().catch(() => ''))?.trim() || ''
const shot = `institutional-${s.id}`
await snap(page, shot)
page.off('response', onResponse)
page.off('console', onConsole)
const zeros = zerosWithLabels(text)
return {
id: s.id,
group: s.group,
path: s.path,
unlinked: !!s.unlinked,
viewport: viewportName(page.viewportSize()),
status,
heading,
chars: text.length,
zeros: zeros.slice(0, 40),
zeroCount: zeros.length,
trustSignals: TRUST.filter((re) => re.test(text)).map(String),
explainedEmpty: EXPLAINED_EMPTY.filter((re) => re.test(text)).map(String),
stillLoading: STILL_LOADING.test(text),
failedCalls,
consoleErrors,
shot: `${shot}-${viewportName(page.viewportSize())}.png`,
}
}
test.describe('Institutional baseline — lux.cloud, signed in', () => {
// One sign-in, then the whole surface in that session. Serial by file, so the
// session established by the first test is the session the rest use.
test.describe.configure({ mode: 'serial' })
const rows: Row[] = []
test('walk every screen an institutional user can reach', async ({ page }) => {
// 27 screens × (nav + networkidle + 4s + shot). Generous, measured ~9s each.
test.setTimeout(20 * 60_000)
// The two pre-auth screens first — an investor's first impression is formed
// before there is a session, and it is part of the baseline.
for (const s of SCREENS.filter((x) => x.anon)) {
rows.push(await measure(page, s))
}
const jwt = await signIn(page)
// Assert the POSITIVE signal. "no error string" gave two false passes.
const claims = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'))
expect(claims.iss, 'token not issued by the IdP we signed in at').toBe(env.urls.id)
await expect(page.locator(':text-matches("sign\\\\s*out", "i")').first())
.toHaveCount(1, { timeout: 15_000 })
await expect(page.getByText(/Nodes & Validators/i).first()).toBeAttached({ timeout: 15_000 })
await expect(page.getByText(/Chains/i).first()).toBeAttached({ timeout: 15_000 })
for (const s of SCREENS.filter((x) => !x.anon)) {
rows.push(await measure(page, s))
// Every authenticated screen must still be an authenticated screen — a
// silent session drop mid-walk would make the rest of the run measure the
// logged-out shell and call it the product.
expect(await page.locator(':text-matches("sign\\\\s*out", "i")').count(),
`session lost at ${s.path}`).toBeGreaterThan(0)
}
})
// ── The two defects that are never a judgement call ──────────────────────
// Everything else on these screens is a design opinion and belongs in the
// review, not in a red build. These two are not opinions:
//
// • A screen still saying "Reading … from chain…" after domcontentloaded +
// networkidle + 4s has not rendered. Measured: /dashboard/dex at 390px
// showed exactly that while 26 of its RPC calls came back 429.
// • A route that silently renders a DIFFERENT page. The deployed console's
// router ends in a catch-all redirect to /dashboard, so /dashboard/
// api-keys shows the Overview under the Overview nav highlight. An
// institution that bookmarks the key-management URL lands on a page that
// looks legitimate and is not the one it asked for. Silence is worse than
// a 404 here: a 404 is information.
test('no screen hangs, and no route silently renders another page', async () => {
const overview = rows.find((r) => r.id === '10-overview')!
const hung = rows.filter((r) => r.stillLoading).map((r) => `${r.path} (${r.viewport})`)
const misrouted = rows
.filter((r) => r.id !== '10-overview' && r.heading && r.heading === overview.heading)
.map((r) => `${r.path} → "${r.heading}"`)
expect(hung, 'screens that never finished rendering').toEqual([])
expect(misrouted, 'routes that silently rendered a different page').toEqual([])
})
test.afterAll(async () => {
if (!rows.length) return
mkdirSync(SHOT_DIR, { recursive: true })
const vp = rows[0].viewport
const file = join(SHOT_DIR, `baseline-${vp}.json`)
writeFileSync(file, JSON.stringify({ capturedAgainst: CLOUD, env: env.name, viewport: vp, rows }, null, 2))
// The run states its own verdict, so a green tick is never the whole story.
console.log(`\n ── institutional baseline (${vp}) ──`)
for (const r of rows) {
const flags = [
r.status >= 400 || r.status === 0 ? `HTTP:${r.status}` : '',
r.stillLoading ? 'STUCK-LOADING' : '',
r.failedCalls.length ? `FAILED:${r.failedCalls.length}` : '',
r.zeroCount && !r.explainedEmpty.length ? `BARE-ZEROS:${r.zeroCount}` : '',
!r.trustSignals.length ? 'NO-TRUST-SIGNAL' : '',
r.unlinked ? 'UNLINKED' : '',
].filter(Boolean).join(' ')
console.log(` ${r.id.padEnd(16)} ${String(r.chars).padStart(6)}ch ${flags || 'ok'}`)
}
console.log(` evidence: ${file}\n`)
})
})