The nine screens the owner named read the chain, and the render check clears at
2.5s. MEASURED on /dashboard/treasury at 3/8/15/25/40s: at 2.5s it still shows
"…" and "Reading token balances…", and it has settled by 3s — no permanent
skeleton, just a check that was looking too early. So the evidence screenshots
were of a half-loaded page and the chain-context assertion was reading an
ellipsis where the address had not arrived yet.
One 3s settle in the economy block only. It does not soften the assertion —
/dashboard/{safes,mpc,bridge,dex,deploy} still show no network, chainId,
address or height at 3/3 viewports.
Co-authored-by: Hanzo Dev <[email protected]>
177 lines
9.3 KiB
TypeScript
177 lines
9.3 KiB
TypeScript
import { test, expect, Page } from '@playwright/test'
|
|
import { env } from './env'
|
|
import { CLOUD_DASHBOARD, CLOUD_ECONOMY, CLOUD_PUBLIC } from './routes'
|
|
import { expectRendered, renderRoute, signIn, snap } from './helpers'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// THE ADMIN DASH, SIGNED IN — all 44 dashboard routes
|
|
// ---------------------------------------------------------------------------
|
|
// lux.cloud ships 44 routes under /dashboard and, until 51-institutional-
|
|
// baseline landed this morning, not one of them had ever been opened by this
|
|
// suite with a session. Everything green was anonymous: the SPA shell answers
|
|
// 200 for /dashboard/zzz-bogus, so anonymous coverage of a dashboard route
|
|
// proves the router exists and nothing else.
|
|
//
|
|
// This is the sweep that opens all of them as the owner. 51-institutional-
|
|
// baseline is the deep read of 27 screens against the six institutional
|
|
// criteria and writes a scored report; this is the BREADTH pass — every route,
|
|
// three viewports, does it render as a signed-in page. Two different questions,
|
|
// two files, no overlap: that one grades, this one enumerates.
|
|
//
|
|
// SESSION: signed in once per project (viewport) via helpers.signIn(), which is
|
|
// the same flow 50-login proves end to end. There is no second copy of the
|
|
// click path here — the second copy is how a run ends up somewhere else
|
|
// entirely when a button's accessible name changes.
|
|
//
|
|
// NO CREDENTIAL, NO GREEN. If LUX_E2E_PASSWORD is unset this whole file FAILS
|
|
// rather than skipping. A skipped authenticated sweep reports the same colour
|
|
// as a passing one, and the entire point of the file is that nobody had checked.
|
|
//
|
|
// SAFETY: read-only. Every screen here can move value — /dashboard/treasury,
|
|
// /dashboard/safes, /dashboard/mpc, /dashboard/bridge. This spec navigates and
|
|
// screenshots. It submits no form, prepares no transaction, signs nothing,
|
|
// broadcasts nothing, and re-keys nothing. Those paths are owner-gated and this
|
|
// suite must never be the thing that trips one.
|
|
|
|
// NOT `mode: 'serial'`. Serial skips every remaining test in the file the
|
|
// moment one fails, which for a 60-route sweep means one broken screen hides
|
|
// the state of the other fifty-nine — measured: the first run stopped at
|
|
// /dashboard/safes and never reported the eight economy screens behind it. The
|
|
// file already runs in one worker in file order (fullyParallel:false), so the
|
|
// shared signed-in page works without it, and every route now gets a verdict.
|
|
const CLOUD = env.urls.cloud
|
|
const CLOUD_HOST = new URL(CLOUD).host
|
|
|
|
let page: Page
|
|
|
|
test.beforeAll(async ({ browser }, testInfo) => {
|
|
const vp = testInfo.project.use.viewport ?? undefined
|
|
const ctx = await browser.newContext({ viewport: vp, ignoreHTTPSErrors: true })
|
|
page = await ctx.newPage()
|
|
await signIn(page)
|
|
console.log(` signed in as ${env.urls.cloud} → ${page.url()}`)
|
|
})
|
|
|
|
test.afterAll(async () => {
|
|
await page?.context().close()
|
|
})
|
|
|
|
// ── The session is real ────────────────────────────────────────────────────
|
|
// Asserted with POSITIVE signals only. Two false passes today came from
|
|
// checking that an error string was absent; a page that never rendered has no
|
|
// error string on it either.
|
|
test.describe('Session', () => {
|
|
test('the browser is signed in and on lux.cloud, not the IdP', async () => {
|
|
await renderRoute(page, `${CLOUD}/dashboard`)
|
|
const host = new URL(page.url()).host
|
|
console.log(` landed on ${page.url()}`)
|
|
expect(host, `still on the identity provider — the code→token exchange did not complete`)
|
|
.toBe(CLOUD_HOST)
|
|
|
|
// Any ONE of these is proof of a signed-in shell. All three are measured
|
|
// present on lux.cloud/dashboard; requiring all three would make the test
|
|
// track the nav's layout instead of the session.
|
|
const signals = {
|
|
signOut: await page.getByText(/sign out|log out/i).first().isVisible({ timeout: 10_000 }).catch(() => false),
|
|
nodes: await page.getByText(/nodes & validators|validators/i).first().isVisible({ timeout: 5_000 }).catch(() => false),
|
|
chains: await page.getByText(/chains/i).first().isVisible({ timeout: 5_000 }).catch(() => false),
|
|
}
|
|
console.log(` signals: ${JSON.stringify(signals)}`)
|
|
expect(Object.values(signals).some(Boolean),
|
|
`no signed-in signal on ${page.url()} — expected "Sign out", "Nodes & Validators" or `
|
|
+ `"Chains" and found none of them`).toBe(true)
|
|
await snap(page, 'console-session')
|
|
})
|
|
})
|
|
|
|
// ── Every dashboard route ──────────────────────────────────────────────────
|
|
test.describe(`Console — ${CLOUD_DASHBOARD.length} dashboard routes`, () => {
|
|
for (const route of CLOUD_DASHBOARD) {
|
|
const name = route.replace(/^\/dashboard\/?/, '') || 'overview'
|
|
test(`${route}`, async () => {
|
|
const r = await expectRendered(page, CLOUD + route, `signed-in ${route}`)
|
|
|
|
// Still signed in — a route that silently bounces to the IdP would
|
|
// otherwise "render" perfectly and pass.
|
|
expect(new URL(r.url).host,
|
|
`${route} bounced the session to ${r.url}`).toBe(CLOUD_HOST)
|
|
|
|
// A permanent skeleton is worse than an empty state: it never resolves to
|
|
// either answer, so an operator cannot tell "no data" from "broken". Give
|
|
// it a further 6s, then require it to have settled.
|
|
const stillLoading = async () => await page
|
|
.locator('[class*="skeleton"], [class*="Skeleton"], [aria-busy="true"]')
|
|
.first().isVisible({ timeout: 1_000 }).catch(() => false)
|
|
if (await stillLoading()) {
|
|
await page.waitForTimeout(6_000)
|
|
expect(await stillLoading(),
|
|
`${route} is still showing loading skeletons ~9s after load — it never resolves to `
|
|
+ `data OR to an empty state`).toBe(false)
|
|
}
|
|
|
|
console.log(` ${route.padEnd(42)} ${String(r.textLen).padStart(6)} chars `
|
|
+ `${r.heading ? `“${r.heading}”` : '(no heading)'}`)
|
|
await snap(page, `console-${name.replace(/\//g, '-')}`)
|
|
})
|
|
}
|
|
})
|
|
|
|
// ── The economy screens, held to a higher bar ──────────────────────────────
|
|
// The owner's ask, verbatim: "manage bridge fees; wallets; mpc; treasury deploy
|
|
// and see all contracts safe, nodes explorer exchange bridge shit all that jazz
|
|
// and adjust and manage all of our economy". These nine screens ARE that ask.
|
|
// Rendering is not enough for them — an institutional operator has to be able
|
|
// to tell, on the screen, WHICH network and WHICH state they are looking at
|
|
// before they touch anything. A treasury page with no chain context is how
|
|
// somebody signs on the wrong network.
|
|
test.describe('Economy screens — the owner\'s named surfaces', () => {
|
|
for (const route of CLOUD_ECONOMY) {
|
|
test(`${route} names what it is and which chain it is on`, async () => {
|
|
const r = await expectRendered(page, CLOUD + route, `economy screen ${route}`)
|
|
|
|
// These nine screens read the chain, so give them a beat past the 2.5s
|
|
// paint check before judging what is on them. MEASURED: /dashboard/treasury
|
|
// shows "…" and "Reading token balances…" at 2.5s and has settled by 3s
|
|
// (checked at 3/8/15/25/40s — no permanent skeleton). Without this the
|
|
// owner's evidence screenshots are of a half-loaded page and the context
|
|
// check below reads an ellipsis instead of an address.
|
|
await page.waitForTimeout(3_000)
|
|
|
|
// Says what it is.
|
|
expect(r.heading.length,
|
|
`${route} renders with no heading — an operator cannot tell what screen they are on`)
|
|
.toBeGreaterThan(0)
|
|
|
|
// Says which chain / network / address it is talking about. Any of a
|
|
// network name, a chainId, a block height or a 0x address counts; a
|
|
// value-bearing screen with none of them has no trust signal at all.
|
|
const body = (await page.textContent('body')) ?? ''
|
|
const context = {
|
|
network: /\b(mainnet|testnet|devnet)\b/i.test(body),
|
|
chainId: new RegExp(`\\b${env.chainIds.lux}\\b`).test(body),
|
|
address: /0x[a-fA-F0-9]{6,}/.test(body),
|
|
height: /\b\d{1,3}(,\d{3})+\b/.test(body),
|
|
}
|
|
console.log(` ${route.padEnd(28)} “${r.heading}” context=${JSON.stringify(context)}`)
|
|
expect(Object.values(context).some(Boolean),
|
|
`${route} shows no network, chainId, address or height — nothing on this screen tells `
|
|
+ `an operator which chain they are about to act on`).toBe(true)
|
|
|
|
await snap(page, `economy-${route.replace(/^\/dashboard\//, '').replace(/\//g, '-')}`)
|
|
})
|
|
}
|
|
})
|
|
|
|
// ── Sign-out is reachable ──────────────────────────────────────────────────
|
|
// Not exercised — clicking it would end the session for the serial file. Its
|
|
// PRESENCE is the assertion: an institutional console that cannot be left is a
|
|
// finding of its own.
|
|
test.describe('Public routes still serve with a session', () => {
|
|
for (const route of CLOUD_PUBLIC) {
|
|
test(`${route} renders for a signed-in browser`, async () => {
|
|
const r = await expectRendered(page, CLOUD + route, `signed-in public ${route}`)
|
|
console.log(` ${route.padEnd(12)} ${r.textLen} chars`)
|
|
})
|
|
}
|
|
})
|