- surfaces.ts: SURFACES table covering all D-09 surfaces (home/wallet, apps, marketplace, discover, cloud, mesh, server, web5, fleet, chat) plus four secondary screens (AppDetails, MarketplaceAppDetails, CloudFolder, OpenWrtGateway) and the located wallet-send modal. Navigation is via real RouterLink/button clicks (never page.goto) so revisit measures actual Vue Router client-side transitions. - measure.ts: measureSurface() records first-visit vs revisit timing, an RPC method+timing trace (no bodies), a dataset-stamp remount probe, and derives maxConcurrentRpc/rpcWallClockMs via sweep-line so serial waterfalls are distinguishable from parallel fan-outs. Includes a dismiss-and-retry click guard for stray modals (Companion app intro) that would otherwise cascade failures across surfaces. - surface-perf.spec.ts: logs in via the existing app-launch flow, walks every SURFACES row, writes results + a run header to ARCHY_PERF_OUT. Verified end-to-end against the local mock-backend + vite dev server (14/15 surfaces measured cleanly across 3 runs each; the 15th, Mesh, times out only because the mock backend never reports a connected mesh device — expected to resolve against a real node in Task 2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
// Re-runnable surface-perf harness (02-01-PLAN.md Task 1). Logs in using the
|
|
// exact flow from app-launch.spec.ts, walks every SURFACES row via
|
|
// measureSurface(), and writes the full result array + a run header to
|
|
// ARCHY_PERF_OUT (defaulting to e2e/test-results/surface-perf.json).
|
|
//
|
|
// Redaction is structural, not a cleanup pass: measure.ts's RpcTracker only
|
|
// ever records method name + timing (T-02-06) — request/response bodies,
|
|
// page text and screenshots are never captured into the artifact.
|
|
import { execSync } from 'node:child_process'
|
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { expect, test, type Page } from '@playwright/test'
|
|
import { measureSurface, type SurfaceMeasurement } from './measure'
|
|
import { SURFACES } from './surfaces'
|
|
|
|
const PASSWORD = process.env.ARCHY_PASSWORD ?? 'password123'
|
|
const RUNS = process.env.ARCHY_PERF_RUNS ? Number(process.env.ARCHY_PERF_RUNS) : 3
|
|
const OUT_PATH = resolve(process.cwd(), process.env.ARCHY_PERF_OUT ?? 'e2e/test-results/surface-perf.json')
|
|
|
|
async function login(page: Page): Promise<void> {
|
|
// Mirrors e2e/app-launch.spec.ts's login() verbatim — do not invent a
|
|
// second auth path.
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' })
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('neode_intro_seen', '1')
|
|
localStorage.setItem('neode_onboarding_complete', '1')
|
|
})
|
|
await page.goto('/login', { waitUntil: 'networkidle' })
|
|
|
|
const passwordInput = page.locator('input[type="password"]').first()
|
|
await passwordInput.waitFor({ timeout: 15_000 })
|
|
await passwordInput.fill(PASSWORD)
|
|
await page
|
|
.locator('button:has-text("Login"), button:has-text("Unlock"), button:has-text("Continue"), button[type="submit"]')
|
|
.first()
|
|
.click()
|
|
await page.waitForURL('**/dashboard**', { timeout: 20_000 })
|
|
}
|
|
|
|
function currentCommit(): string {
|
|
try {
|
|
return execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim()
|
|
} catch {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
test('surface-perf: measure every D-09 surface and write the baseline artifact', async ({ page, baseURL }) => {
|
|
test.setTimeout(20 * 60 * 1000) // 15 surfaces x 3 runs x network round-trips can run long on a real node
|
|
|
|
await login(page)
|
|
|
|
const results: SurfaceMeasurement[] = []
|
|
const skipped: string[] = []
|
|
|
|
for (const surface of SURFACES) {
|
|
try {
|
|
const measurement = await measureSurface(page, surface, { runs: RUNS })
|
|
results.push(measurement)
|
|
if (measurement.error) skipped.push(`${surface.id}: ${measurement.error}`)
|
|
} catch (err) {
|
|
// A surface that throws outside measureSurface's own per-run try/catch
|
|
// (e.g. login state got corrupted) is still recorded, never dropped —
|
|
// an unmeasured surface must never silently disappear from the array.
|
|
results.push({
|
|
id: surface.id,
|
|
label: surface.label,
|
|
path: surface.path,
|
|
kind: surface.kind,
|
|
runs: RUNS,
|
|
samples: [],
|
|
firstVisitMs: null,
|
|
revisitMs: null,
|
|
firstVisitRpcCount: null,
|
|
revisitRpcCount: null,
|
|
revisitRpcCalls: [],
|
|
maxConcurrentRpc: null,
|
|
rpcWallClockMs: null,
|
|
remounted: null,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
})
|
|
skipped.push(`${surface.id}: ${err instanceof Error ? err.message : String(err)}`)
|
|
}
|
|
}
|
|
|
|
const header = {
|
|
baseUrl: baseURL ?? process.env.ARCHY_BASE_URL ?? 'http://192.168.1.228',
|
|
takenAt: new Date().toISOString(),
|
|
commit: currentCommit(),
|
|
runs: RUNS,
|
|
notes: skipped.length > 0 ? `Skipped/errored surfaces: ${skipped.join('; ')}` : 'All surfaces measured cleanly.',
|
|
}
|
|
|
|
const artifact = { ...header, results }
|
|
|
|
mkdirSync(dirname(OUT_PATH), { recursive: true })
|
|
writeFileSync(OUT_PATH, JSON.stringify(artifact, null, 2))
|
|
|
|
expect(results.length).toBe(SURFACES.length)
|
|
})
|