32-work-boards: work.zoo + work.pars now both serve the redesigned Dework board (luxfi/work-board). Replace the stale literal assertions (BountyV1 / #0 / "N bounties" — removed by the redesign, the 3 false failures) with the real structural contract via one parameterized factory: Dework chrome (icon-rail + w-64 <aside> sidebar + Overview / Community Suggestions / Leaderboards / Combined Board nav), inline-<svg> BrandMark (the logo is not an <img>), the Bounties Space-category grid, the To Do / In Progress / In Review / Done kanban columns, the emerald LIVE badge (not the amber FIXTURE fallback) + org chainId, and a UI<->chain check (the rendered treasury truncation equals on-chain BountyV1.treasury()). Un-skip Pars (now live, chain 494949); Lux/Hanzo stay skip-gated (HTTP-unreachable, brands.ts bounty==0x0). 31-vote: the vote shells are retiring -> work.<org>.network. Arm a hanzo.vote shell-health guard that hammers cold loads and fails on the intermittent blank-#root white-screen (Buffer-crash class) OR the "Made with love by Lux" cross-brand leak — a real, known-flagged regression, kept armed (not muted/skipped). lux/zoo/pars green. work-boards: 1 -> 10 passing (3 false failures fixed, Pars un-skipped, 2 skip). vote: hanzo.vote deterministically flagged; lux/zoo/pars (6) green, 4 skip. Co-authored-by: Hanzo Dev <[email protected]>
150 lines
7.6 KiB
TypeScript
150 lines
7.6 KiB
TypeScript
import { test, expect, Page, Browser } from '@playwright/test'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Vote surfaces — the 4 <org>.vote governance front-ends
|
|
// ---------------------------------------------------------------------------
|
|
// RETIRING → work.<org>.network. Owner direction: the vote shells (Decent-fork,
|
|
// reading ZERO chain) converge onto the WORK BOARD, which is the genuinely-live
|
|
// governance surface (see 32-work-boards). Until each shell is replaced, these stay
|
|
// as regression guards. For the three healthy shells (lux/zoo/pars) that means:
|
|
// • HTTP 200 + correct brand in <title> and rendered DOM.
|
|
// • ZERO cross-brand leakage — a surface must never render another org's brand
|
|
// (nor a forbidden competitor term). The white-label invariant.
|
|
// • NOT a white-screen — #root has real descendants and the body has real text.
|
|
// • The Connect Wallet control is present in the DOM.
|
|
// The "reads a real on-chain proposal/DAO tile" assertion stays SKIPPED (with the
|
|
// exact flip condition) so the spec is SAVED and ready the moment a surface goes live.
|
|
//
|
|
// hanzo.vote — ARMED shell-health guard, EXPECTED RED (real, flagged defects).
|
|
// hanzo.vote is a broken, retiring shell with TWO intermittent defects (both tied to
|
|
// an unstable Decent-fork render, verified 2026-07-17 — hanzo ~50% blank, lux/zoo/pars
|
|
// 0/N):
|
|
// 1. blank #root white-screen — HTTP 200 but an EMPTY #root, from
|
|
// `Cannot read properties of undefined (reading 'address')` after a 403 (the
|
|
// Buffer-crash class).
|
|
// 2. a "Made with love by Lux" footer — Lux's name on a Hanzo surface, a
|
|
// cross-brand white-label violation.
|
|
// A single load is a coin-flip, so the guard HAMMERS several fresh loads and fails on
|
|
// ANY white-screen or ANY cross-brand leak. It is NOT muted or skipped — it stays red
|
|
// until work.hanzo replaces the shell.
|
|
|
|
interface Surface {
|
|
org: string
|
|
brand: string
|
|
url: string
|
|
foreign: string[] // other org brands + competitor terms that must NEVER appear
|
|
}
|
|
|
|
const ORG_BRANDS = ['Lux', 'Zoo', 'Hanzo', 'Pars']
|
|
// Competitor / forbidden lineage terms (owner rules: no Avalanche, no "snowman").
|
|
const FORBIDDEN = ['Avalanche', 'Snowman', 'Ava Labs']
|
|
|
|
const SURFACES: Surface[] = ORG_BRANDS.map((brand) => ({
|
|
org: brand.toLowerCase(),
|
|
brand,
|
|
url: `https://${brand.toLowerCase()}.vote`,
|
|
foreign: [...ORG_BRANDS.filter((b) => b !== brand), ...FORBIDDEN],
|
|
}))
|
|
|
|
async function render(page: Page, url: string) {
|
|
const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
|
await page.waitForTimeout(4000) // let the SPA hydrate
|
|
const title = await page.title()
|
|
const rootKids = await page.evaluate(() => {
|
|
const r = document.getElementById('root') || document.getElementById('app') || document.getElementById('__next')
|
|
return r ? r.querySelectorAll('*').length : -1
|
|
})
|
|
const bodyText = await page.evaluate(() => document.body.innerText || '')
|
|
return { status: res?.status() ?? 0, title, rootKids, bodyText }
|
|
}
|
|
|
|
// One cold load in a FRESH context (no cache/cookies) — how the blank-crash reproduces.
|
|
// Only HTTP 200 responses count toward the checks; a nav/infra failure (goto throws /
|
|
// non-200) is retried and is NOT the defect.
|
|
async function coldLoad(browser: Browser, url: string, foreign: string[]) {
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
const ctx = await browser.newContext()
|
|
const page = await ctx.newPage()
|
|
try {
|
|
const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
|
const status = res?.status() ?? 0
|
|
await page.waitForTimeout(4000)
|
|
const data = await page.evaluate((foreign) => {
|
|
const r = document.getElementById('root') || document.getElementById('app') || document.getElementById('__next')
|
|
const bt = document.body.innerText || ''
|
|
return {
|
|
rootKids: r ? r.querySelectorAll('*').length : -1,
|
|
bodyLen: bt.trim().length,
|
|
foreignHits: foreign.filter((t) => new RegExp(`\\b${t}\\b`, 'i').test(bt)),
|
|
}
|
|
}, foreign)
|
|
await ctx.close()
|
|
if (status !== 200) continue // infra hiccup, not the defect — retry
|
|
return { status, ...data }
|
|
} catch {
|
|
await ctx.close() // nav error — retry
|
|
}
|
|
}
|
|
return { status: 0, rootKids: -1, bodyLen: 0, foreignHits: [] as string[] }
|
|
}
|
|
|
|
for (const s of SURFACES) {
|
|
test.describe(`Vote Surface — ${s.brand} (${s.org}.vote)`, () => {
|
|
if (s.org === 'hanzo') {
|
|
// ARMED guard — expected RED. See file header. Do NOT mute or skip: a green
|
|
// result means hanzo.vote stopped blank-crashing AND leaking (or work.hanzo
|
|
// replaced it).
|
|
test('shell-health guard — no white-screen, no cross-brand leak (RETIRING → work.hanzo)', async ({ browser }) => {
|
|
const ATTEMPTS = 6
|
|
const whiteScreens: number[] = []
|
|
const leaks = new Set<string>()
|
|
for (let i = 0; i < ATTEMPTS; i++) {
|
|
const r = await coldLoad(browser, s.url, s.foreign)
|
|
const blank = r.status === 200 && (r.rootKids <= 20 || r.bodyLen < 100)
|
|
if (blank) whiteScreens.push(i + 1)
|
|
r.foreignHits.forEach((t) => leaks.add(t))
|
|
console.log(
|
|
` hanzo.vote load ${i + 1}/${ATTEMPTS}: HTTP ${r.status} · #root kids=${r.rootKids} · bodyLen=${r.bodyLen}` +
|
|
`${blank ? ' <== WHITE-SCREEN' : ''}${r.foreignHits.length ? ' <== LEAK: ' + r.foreignHits.join(',') : ''}`,
|
|
)
|
|
}
|
|
const defects: string[] = []
|
|
if (whiteScreens.length) defects.push(`white-screened on loads [${whiteScreens.join(',')}]/${ATTEMPTS} (blank #root — Buffer-crash class)`)
|
|
if (leaks.size) defects.push(`leaked foreign brand(s): ${[...leaks].join(', ')} (white-label violation)`)
|
|
expect(
|
|
defects,
|
|
`hanzo.vote is broken — ${defects.join(' · ')}. KNOWN-FLAGGED regression; retiring → work.hanzo.`,
|
|
).toEqual([])
|
|
})
|
|
} else {
|
|
test('renders with correct brand and is not a white-screen', async ({ page }) => {
|
|
const { status, title, rootKids, bodyText } = await render(page, s.url)
|
|
const clean = bodyText.replace(/\s+/g, ' ').trim()
|
|
console.log(` ${s.org}.vote: HTTP ${status} · title=${JSON.stringify(title)} · #root kids=${rootKids} · bodyLen=${clean.length}`)
|
|
|
|
expect(status).toBe(200)
|
|
// Brand in the document title AND in the rendered body.
|
|
expect(title).toMatch(new RegExp(s.brand, 'i'))
|
|
expect(title).toMatch(/DAO/i)
|
|
expect(clean).toMatch(new RegExp(`\\b${s.brand}\\b`, 'i'))
|
|
// Not a white-screen: real DOM under #root and real visible text.
|
|
expect(rootKids).toBeGreaterThan(20)
|
|
expect(clean.length).toBeGreaterThan(100)
|
|
// Connect Wallet control present.
|
|
await expect(page.getByText(/connect wallet/i).first()).toBeVisible()
|
|
})
|
|
|
|
test('has ZERO cross-brand / competitor leakage', async ({ page }) => {
|
|
const { bodyText } = await render(page, s.url)
|
|
const hits = s.foreign.filter((term) => new RegExp(`\\b${term}\\b`, 'i').test(bodyText))
|
|
console.log(` ${s.org}.vote foreign hits: ${hits.length ? hits.join(', ') : 'NONE'}`)
|
|
expect(hits, `${s.org}.vote leaked foreign brand(s): ${hits.join(', ')}`).toEqual([])
|
|
})
|
|
}
|
|
|
|
test('reads a live on-chain proposal / DAO tile', async () => {
|
|
test.skip(true, `live after CTO agent deploy — ${s.org}.vote is a Decent-fork shell reading zero chain (memory 2026-07-17); flip when it renders an on-chain proposal/DAO tile. Owner direction: converge to work.${s.org}.network.`)
|
|
})
|
|
})
|
|
}
|