Files
app/e2e/gui.spec.ts
zeekayandHanzo Dev 5c1562944c Build the governance interface on our own design system
lux.vote was served by an interface derived from Decent DAO under AGPL-3.0.
This is a fresh app on @hanzo/gui with no Decent lineage — the replaced app's
route table was read to enumerate what the interface must do, and nothing else.

No Tailwind anywhere. @luxfi/ui is not a dependency: its prebuilt components
carry arbitrary-utility class strings and are the only reason the exchange
scans for them. Dropping it means replacing two things it provided —
createGui, which GuiProvider alone does not call and without which the tree
throws and the document is blank, and luxUi()'s aliases, now read from
node_modules/@hanzogui at config time so an upgrade cannot leave a package
undeduplicated.

Twelve screens over the Governor, the votes token, the Timelock, the work
market, Roles, Karma and the gauges, wired to luxfi/standard's Foundry
artifacts rather than to its stale ABI barrel. The contracts are not what the
prose says: Bounty has no escrow, arbiter or dispute state; MultiDAOGovernor
swaps Executed and Expired; Karma emits no Transfer so no holder list exists.
The Governor's clock is mode=timestamp, which is asked for rather than assumed
because 96369 produces blocks on demand and reading seconds as blocks turns one
day into months.

Every read reports one of four outcomes — deployed, not deployed, not read, no
record. A contract with no code answers with empty data rather than an error,
so an absent contract and an idle one arrive identically at the call site;
only one of them is a statement about governance. /deployment asks eth_getCode
for every address on every chain on load, because the written records disagree
with the chain and with each other.

Endpoints are the /v1/bc/C/rpc form. The bare host serves the same chain to
curl and refuses a browser — 405 on preflight, no allow-origin header — which
would report every Governor unreadable while the Governor was fine.

Wallet login is @luxwallet/connect, imported by subpath: its root barrel pulls
a connector factory naming all seven chains, and a bundler follows every branch
into optional peers the dev server never resolves. It signs a CAIP-122 message
and offers no provider, transactions or chain switching, so viem is built over
our own EIP-6963 discovery for the calls that write.

Telemetry off: the design system posts to api.hanzo.ai on load by default, and
who reads a proposal is not this interface's to report.

Gates ported and made real. The class check ran against the Suspense fallback
until a negative control caught it passing on a route with an injected utility
class; every gate now waits for the screen's own h1.

22 unit tests, 135 browser tests against the built output.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 13:18:35 -07:00

146 lines
5.8 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { SCREENS, ready } from './screens'
/**
* What the rendered page proves that a grep cannot.
*
* A grep over source says what this repository wrote. It says nothing about
* what a component library put on the page underneath — and the whole claim
* here is about the page: that it is drawn by the design system's engine
* through style props, and not by a stylesheet of utility classes. This build
* ships no utility framework at all, so a hit is either a dependency bringing
* one in or somebody adding one back.
*/
/**
* Names the engine and the design system own are not utilities and never count:
* the engine stamps `is_Text`, `_display-flex` and similar style hashes on
* everything it draws.
*/
const OURS = /^(is_|_|t_|hz-|lux-|font_|dark$|light$)/
/**
* Shape-based rather than a dictionary, because the vocabulary is hundreds of
* tokens deep and a list of them rots. A utility is a lowercase word carrying a
* `-`, `:` or `[` — `items-center`, `hover:bg-transparent`, `text-[10px]`.
*
* The second alternative is for the one-word utilities that shape alone cannot
* catch. `flex` and `block` carry no separator and would otherwise pass, which a
* negative control found: a deliberately injected `class="flex items-center"`
* was reported with two of its three names.
*/
const UTIL = '^-?[a-z][a-z0-9]*(?:[-:/][a-z0-9[\\]().,%#_-]*)+$|^[a-z-]+\\[' +
'|^(flex|grid|block|inline|hidden|absolute|relative|fixed|sticky|static|container|truncate|italic|underline|uppercase|lowercase|capitalize|invisible|visible)$'
test.describe('the page is drawn by the engine, not by a stylesheet', () => {
for (const path of SCREENS) {
test(`${path} carries no utility classes`, async ({ page }) => {
await page.goto(path)
await ready(page)
const found = await page.evaluate(
(src) => {
const re = new RegExp(src.ours)
const util = new RegExp(src.util)
const out: string[] = []
for (const el of Array.from(document.querySelectorAll<HTMLElement>('[class]'))) {
const cls = typeof el.className === 'string' ? el.className : ''
const hits = cls
.trim()
.split(/\s+/)
.filter(Boolean)
.filter((t) => !re.test(t))
.filter((t) => util.test(t))
if (hits.length) out.push(`${el.tagName.toLowerCase()}: ${hits.slice(0, 6).join(' ')}`)
}
return out.slice(0, 12)
},
{ ours: OURS.source, util: UTIL },
)
expect(found, `${path} renders utility classes: ${found.join(' | ')}`).toEqual([])
})
}
})
test('no stylesheet in the document declares a utility class', async ({ page }) => {
await page.goto('/')
await ready(page)
// Reads the rules the browser actually parsed, so a framework arriving inside
// a dependency's CSS is caught as well as one imported here.
const suspects = await page.evaluate(() => {
const out: string[] = []
for (const sheet of Array.from(document.styleSheets)) {
let rules: CSSRuleList
try {
rules = sheet.cssRules
} catch {
continue
}
for (const rule of Array.from(rules)) {
const text = (rule as CSSStyleRule).selectorText
if (!text) continue
if (/\.(flex|grid|hidden|block|items-|justify-|text-|bg-|p[xytblr]?-\d|m[xytblr]?-\d|w-\d|h-\d)/.test(text)) {
out.push(text.slice(0, 80))
}
}
}
return out.slice(0, 10)
})
expect(suspects, `a stylesheet declares utility classes: ${suspects.join(' | ')}`).toEqual([])
})
test.describe('every screen says what it is', () => {
for (const path of SCREENS) {
test(`${path} renders and names itself`, async ({ page }) => {
const broke: string[] = []
page.on('pageerror', (e) => broke.push(String(e)))
await page.goto(path)
// A screen that threw while rendering can still show a heading, because
// React keeps what it committed before the throw. The console is where
// that shows.
const mounted = await page.locator('#root > *').count()
expect(mounted, `${path} mounted nothing`).toBeGreaterThan(0)
const h1 = page.getByRole('heading', { level: 1 })
await expect(h1, `${path} has no level-one heading, so nothing says what the screen is`).toBeVisible()
expect(await h1.count(), `${path} has more than one level-one heading`).toBe(1)
expect(broke, `${path} threw while rendering`).toEqual([])
})
}
})
test.describe('everything reachable has a name', () => {
for (const path of SCREENS) {
test(`${path} names every control`, async ({ page }) => {
await page.goto(path)
await ready(page)
// A control with no accessible name is unreadable to a screen reader, and
// on these screens the controls carry addresses and vote counts.
const unnamed = await page.evaluate(() => {
const sel = 'a[href], button, input, select, textarea, [role="button"], [role="link"]'
const out: string[] = []
for (const el of Array.from(document.querySelectorAll<HTMLElement>(sel))) {
const s = getComputedStyle(el)
if (s.display === 'none' || s.visibility === 'hidden') continue
if ((el as HTMLInputElement).type === 'hidden') continue
const named =
(el.textContent || '').trim() ||
el.getAttribute('aria-label') ||
el.getAttribute('title') ||
(el.getAttribute('id') && document.querySelector(`label[for="${el.getAttribute('id')}"]`)) ||
el.closest('label')
if (!named) out.push(`${el.tagName.toLowerCase()}${el.getAttribute('type') ? `[${el.getAttribute('type')}]` : ''}`)
}
return out.slice(0, 8)
})
expect(unnamed, `${path} has unnamed controls: ${unnamed.join(', ')}`).toEqual([])
})
}
})