mirror of
https://github.com/luxdao/app.git
synced 2026-10-30 18:14:05 +00:00
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]>
146 lines
5.8 KiB
TypeScript
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([])
|
|
})
|
|
}
|
|
})
|