Every remaining fixture-only surface now renders reconciled gitea data,
with the fixture kept only as the no-config demo fallback.
- lib/views/project-data.ts: ProjectData — the uniform input (reconciled
backlog + deps + timelines + calibration + workers + today) that AppShell
assembles once and every view builder consumes.
- lib/views/{issue-detail,gantt-view,deps-graph,standup-view,inbox-view}.ts:
pure builders, ProjectData → the fixture-shaped object each screen already
renders. Real signals only; honest degradation where a signal isn't derivable
(buffered p80 vs per-issue Monte Carlo; flat "idle in review"; no fabricated
inbox mentions/outages).
- Screens take an optional `data?` and fall back to the fixture; AppShell wires
the real view whenever the backlog is reconciled. Issue "blocks" chips and the
rail inbox badge now resolve from real data too.
- Dev-only rail surfaces (First run / States / Primitives) gated on
import.meta.env.DEV || demo — shown in dev + e2e, hidden in a packaged app.
Rail host label reflects the connected instance.
- Extended the live onboarding e2e to click Board → Gantt → Deps → Issue
sidecar → Standup → Inbox on real gitea data with a pageerror guard; 14
fixture e2e stay green, typecheck + prod build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.7 KiB
TypeScript
86 lines
3.7 KiB
TypeScript
import { dirname, join } from 'node:path'
|
|
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { _electron as electron, expect, test } from '@playwright/test'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const MAIN = join(here, '..', 'out', 'main', 'index.js')
|
|
|
|
/** Read the real token from .env.local (walking up) so the test can fill the form. */
|
|
function readToken(): string | null {
|
|
let dir = here
|
|
for (let i = 0; i < 6; i++) {
|
|
try {
|
|
const m = /^GITEA_TOKEN\s*=\s*(.+?)\s*$/m.exec(readFileSync(join(dir, '.env.local'), 'utf8'))
|
|
if (m) return m[1].trim()
|
|
} catch {
|
|
/* keep walking */
|
|
}
|
|
dir = dirname(dir)
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Opt-in (GITEA_LIVE=1). Forces the onboarding path (COMMITEA_NO_ENV_LOCAL) with a
|
|
// fresh userData, fills the real connection form, and asserts it lands on real data.
|
|
test.describe('live onboarding', () => {
|
|
test('connect form → real board', async () => {
|
|
const token = process.env.GITEA_LIVE ? readToken() : null
|
|
test.skip(!token, 'GITEA_LIVE + a .env.local token required')
|
|
test.setTimeout(90_000)
|
|
|
|
const app = await electron.launch({
|
|
args: [MAIN],
|
|
env: { ...process.env, COMMITEA_NO_ENV_LOCAL: '1', COMMITEA_USERDATA: mkdtempSync(join(tmpdir(), 'commitea-')) },
|
|
})
|
|
const win = await app.firstWindow()
|
|
await win.waitForLoadState('domcontentloaded')
|
|
|
|
// Any uncaught render error in a real-data view builder must fail the test —
|
|
// this is the "it actually works on real data" guarantee.
|
|
const pageErrors: string[] = []
|
|
win.on('pageerror', (e) => pageErrors.push(e.message))
|
|
|
|
// the connection gate, not the app
|
|
await expect(win.getByText(/Connect your Gitea/)).toBeVisible({ timeout: 15000 })
|
|
await win.getByPlaceholder('your-org').fill('christian')
|
|
await win.getByPlaceholder('your-repo').fill('commitea')
|
|
await win.getByPlaceholder(/gitea PAT/).fill(token!)
|
|
await win.getByRole('button', { name: 'Connect' }).click()
|
|
|
|
const rail = win.getByRole('navigation', { name: 'Primary' })
|
|
const realTitle = /Model router|SQLite cache bootstrap|Purity\/rebuild/
|
|
|
|
// Board (real reconciled columns) — real issue titles prove the sync landed.
|
|
await rail.getByRole('button', { name: 'The pot' }).click()
|
|
await expect(win.getByText(realTitle).first()).toBeVisible({ timeout: 30000 })
|
|
|
|
// Gantt tab — scheduler-derived bars over the real open backlog.
|
|
await win.getByRole('tab', { name: 'Gantt' }).click()
|
|
await expect(win.getByText(realTitle).first()).toBeVisible()
|
|
|
|
// Dependencies tab — real dep graph. Just needs to render without throwing.
|
|
await win.getByRole('tab', { name: 'Dependencies' }).click()
|
|
await win.waitForTimeout(300)
|
|
|
|
// Issue drill-in — the machine-derived sidecar (lifecycle/forecast/deps) on a real issue.
|
|
await win.getByRole('tab', { name: 'Board' }).click()
|
|
await win.getByText(realTitle).first().click()
|
|
await expect(win.getByText('Machine-derived')).toBeVisible()
|
|
await win.getByRole('button', { name: 'Back' }).click()
|
|
|
|
// Standup + Inbox — scheduler plan / nag / inbox feed over real data.
|
|
await rail.getByRole('button', { name: 'Standup' }).click()
|
|
await expect(win.getByRole('heading', { name: 'Morning standup' })).toBeVisible()
|
|
await rail.getByRole('button', { name: 'Inbox' }).click()
|
|
await expect(win.getByRole('heading', { name: 'Inbox' })).toBeVisible()
|
|
|
|
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-onboarding.png'), fullPage: true, animations: 'disabled' })
|
|
expect(pageErrors, `uncaught render errors: ${pageErrors.join(' · ')}`).toEqual([])
|
|
|
|
await app.close()
|
|
})
|
|
})
|