Files
universe/playwright.config.ts
T
zeekayandHanzo Dev f8895b9a54 e2e: one environment model, viewport projects, and a login proof that can fail
LUX_ENV=local|devnet|testnet|mainnet is now the only environment knob. e2e/env.ts
resolves every base URL, RPC endpoint, network ID and chain ID from NETWORKS.yaml
at runtime; playwright.config.ts and helpers.ts derive from it instead of keeping
their own copies. Those copies had already drifted — the config resolved the
testnet explorer to test-explore.lux.network, which is NXDOMAIN, while helpers.ts
used the real explore-test.lux.network. The retired two-axis ENV/LUX_NETWORK
model now fails loudly rather than being half-honoured.

Projects are viewports (mobile 390x844, tablet 834x1194, laptop 1440x900), so
"mobile and desktop" is the same three frames every run. Screenshots go to
~/Desktop/lux-e2e/<env>/<surface>-<viewport>.png on pass, and shot-reporter.ts
copies a failing test's dying state next to them — a failure you cannot look at
is a failure you cannot fix.

50-login.spec.ts proves sign-in for real: lux.cloud -> lux.id -> /dashboard, with
the password from LUX_E2E_PASSWORD only. It refuses the cheap answers, because
each one lies here — /dashboard is an SPA that returns 200 and keeps its URL when
anonymous, and /auth/callback renders "Sign-in failed" at 200. The proof is the
token: a JWT in sessionStorage whose iss is the IdP and whose email is the
account typed, which lux.id/v1/iam/oauth/userinfo accepts (401 bare, 401 for a
junk token). That is the leg that breaks under invalid_client. It also asserts
the handoff is a public PKCE client and carries a negative control so the
positive assertions cannot go vacuous. Verified with a wrong password: red.

Measured while building this, not assumed:
  - workers are per-env. Mainnet is CDN-fronted and ran the gate at 4 with
    342/342; testnet hits the raw LB and lost 66 cases to throttling at 4 that
    all passed at 2.
  - `locator('text=/block/i').first()` in two specs resolved to a hidden Radix
    menu trigger, so both tracked the nav's collapse rather than the blocks
    table. Replaced by one shared assertion on the section AND a block link.
  - the token check runs inside the page: page.request.get attaches request
    headers to a thrown error, which printed a live bearer token into the log.
  - 40-lux-financial carried a plaintext password; it now comes from the
    environment.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 15:46:04 -07:00

86 lines
3.4 KiB
TypeScript
Raw 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 { defineConfig } from '@playwright/test'
import { env, envBanner, VIEWPORTS, SHOT_DIR } from './e2e/env'
// ---------------------------------------------------------------------------
// The multi-environment e2e suite.
// ---------------------------------------------------------------------------
// LUX_ENV=local | devnet | testnet | mainnet (default: mainnet)
//
// make e2e # mainnet, all three viewports
// make e2e-testnet # same specs, testnet endpoints
// make e2e-login # just the sign-in proof
// npx playwright test --project=mobile
//
// Every env-shaped value comes from e2e/env.ts, which reads NETWORKS.yaml — this
// config declares NO urls, ports or chain IDs of its own. It used to keep a
// third copy of those tables (and resolved the testnet explorer to the NXDOMAIN
// host test-explore.lux.network).
//
// PROJECTS ARE VIEWPORTS. "Mobile and desktop" therefore means the same three
// frames on every run: mobile 390x844, tablet 834x1194, laptop 1440x900. Run one
// with --project=<name>; screenshots carry the frame name so they never collide.
//
// The branded web3-surface matrix keeps its own config (playwright.branded.
// config.ts) with its own 4-breakpoint grid and matrix reporter — one way to run
// each suite, so this one testIgnores it.
export default defineConfig({
testDir: './e2e',
testIgnore: '**/branded/**',
// File-level parallelism only (fullyParallel:false keeps tests inside a file
// serial, which is where the ordering assumptions live). The worker count is
// per-environment and measured — see WORKERS in e2e/env.ts. On mainnet 1
// worker and 4 gave an IDENTICAL result set (154 passed / 2 failed) in 5.9m vs
// 2.3m; testnet throttles at 4 and is clean at 2. Everything but the login
// spec is read-only against live surfaces, and rpcCall() backs off on 429.
fullyParallel: false,
forbidOnly: !!process.env.CI,
// One retry locally, so a page that was slow to render under 4-worker load is
// reported as FLAKY rather than either failing the run or being silently
// green. Measured: with 4 workers, `09-defi-flow › explorer shows recent
// blocks` failed once at laptop and passed on its own in 13s. Flaky is a
// status, not a pass — a genuinely broken surface still fails twice and goes
// red. Never raise this to paper over a real failure.
retries: process.env.CI ? 2 : 1,
workers: env.workers,
timeout: 120_000,
expect: { timeout: 15_000 },
reporter: [
// Relocates every FAILURE's screenshot to SHOT_DIR so a red run is
// immediately viewable next to the deliberate snap() shots.
['./e2e/shot-reporter.ts'],
...(process.env.CI
? [['github'] as const, ['junit', { outputFile: 'e2e/results.xml' }] as const]
: [['list'] as const]),
['html', { open: 'never' }] as const,
],
use: {
baseURL: env.urls.exploreLux,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
actionTimeout: 15_000,
navigationTimeout: 60_000,
ignoreHTTPSErrors: true,
},
projects: VIEWPORTS.map((v) => ({
name: v.name,
use: { viewport: { width: v.width, height: v.height } },
})),
outputDir: './e2e/test-results',
metadata: {
banner: envBanner(),
env: env.name,
network: env.network,
networkId: env.networkId,
chainIds: env.chainIds,
workers: env.workers,
shots: SHOT_DIR,
urls: env.urls,
},
})