ui: unify aero chrome surfaces and shared hero across kanban/graph
This commit is contained in:
parent
c8d7f8eb0d
commit
e6317594b6
18 changed files with 540 additions and 995 deletions
|
|
@ -61,6 +61,8 @@ test('extracted graph section has viewport and legend', async () => {
|
|||
assert.match(graphSection, /Read left to right/, 'legend should include plain directional hint');
|
||||
assert.match(graphSection, /Left = blockers/, 'legend should include left/right dependency meaning');
|
||||
assert.match(graphSection, /Right = work unblocked by this task/, 'legend should include downstream meaning');
|
||||
assert.match(graphSection, /min-h-\[24rem\]/, 'graph container should enforce bounded minimum height');
|
||||
assert.match(graphSection, /md:min-h-\[35rem\]/, 'graph container should scale minimum height on desktop');
|
||||
});
|
||||
|
||||
test('graph node card supports tooltips and actionable glow', async () => {
|
||||
|
|
@ -73,3 +75,8 @@ test('graph node card supports tooltips and actionable glow', async () => {
|
|||
assert.match(nodeCard, /isDimmed/, 'should support dimming non-chain nodes');
|
||||
assert.match(nodeCard, /Ready to work/, 'actionable tooltip text');
|
||||
});
|
||||
|
||||
test('graph edges expose explicit relation labels', async () => {
|
||||
const graphPage = await read('src/components/graph/dependency-graph-page.tsx');
|
||||
assert.match(graphPage, /label:\s*'BLOCKS'/, 'edges should include plain-language relation labels');
|
||||
});
|
||||
|
|
|
|||
50
tests/guards/ui-foundation-contract.test.mjs
Normal file
50
tests/guards/ui-foundation-contract.test.mjs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
async function read(relativePath) {
|
||||
return fs.readFile(path.join(ROOT, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('layout uses Noto Sans without JetBrains Mono dependency', async () => {
|
||||
const layout = await read('src/app/layout.tsx');
|
||||
|
||||
assert.match(layout, /Noto_Sans/, 'should use Noto Sans font family');
|
||||
assert.match(layout, /--font-ui/, 'should expose ui font css variable');
|
||||
assert.doesNotMatch(layout, /JetBrains_Mono/, 'should not include JetBrains Mono');
|
||||
assert.doesNotMatch(layout, /--font-mono/, 'should not expose mono font css variable from layout');
|
||||
});
|
||||
|
||||
test('global stylesheet defines aero chrome foundation tokens and anti-banding layers', async () => {
|
||||
const css = await read('src/app/globals.css');
|
||||
|
||||
assert.match(css, /--bg-base:/, 'should define matte base token');
|
||||
assert.match(css, /--glass-base:/, 'should define glass surface token');
|
||||
assert.match(css, /--edge-top:/, 'should define top chrome edge token');
|
||||
assert.match(css, /--font-ui-stack:/, 'should define ui font stack token');
|
||||
assert.match(css, /--font-mono-stack:/, 'should define mono font stack token');
|
||||
assert.match(css, /font-family:\s*var\(--font-ui-stack\)/, 'body should consume ui font token');
|
||||
assert.match(css, /body::before/, 'should define anti-banding grid layer');
|
||||
assert.match(css, /body::after/, 'should define anti-banding noise layer');
|
||||
assert.match(css, /--status-rdy-glow:/, 'should define ready glow token');
|
||||
assert.match(css, /--status-blk-glow:/, 'should define blocked glow token');
|
||||
assert.match(css, /--status-wip-glow:/, 'should define in-progress glow token');
|
||||
assert.match(css, /--elevation-tight:/, 'should define tight elevation token');
|
||||
assert.match(css, /--elevation-ambient:/, 'should define ambient elevation token');
|
||||
assert.match(css, /\.glass-panel/, 'should define reusable glass panel primitive');
|
||||
});
|
||||
|
||||
test('kanban and graph surfaces apply semantic typography classes', async () => {
|
||||
const kanbanControls = await read('src/components/kanban/kanban-controls.tsx');
|
||||
const kanbanCard = await read('src/components/kanban/kanban-card.tsx');
|
||||
const graphCardGrid = await read('src/components/graph/task-card-grid.tsx');
|
||||
const graphNodeCard = await read('src/components/graph/graph-node-card.tsx');
|
||||
|
||||
assert.match(kanbanControls, /ui-text/, 'kanban controls should use ui-text class');
|
||||
assert.match(kanbanCard, /system-data/, 'kanban card should use system-data class for metadata');
|
||||
assert.match(graphCardGrid, /ui-text/, 'graph task grid should use ui-text class for prose labels');
|
||||
assert.match(graphNodeCard, /system-data/, 'graph node card should use system-data class for machine data');
|
||||
});
|
||||
|
|
@ -71,10 +71,10 @@ test('buildKanbanColumns groups by core statuses and sorts by priority ascending
|
|||
const columns = buildKanbanColumns(issues);
|
||||
|
||||
assert.deepEqual(Object.keys(columns), KANBAN_STATUSES);
|
||||
assert.deepEqual(columns.ready.map((x) => x.id), ['bb-4', 'bb-5', 'bb-6']);
|
||||
assert.deepEqual(columns.ready.map((x) => x.id), ['bb-2', 'bb-4', 'bb-1']);
|
||||
assert.equal(columns.ready.some((x) => x.issue_type === 'epic'), false);
|
||||
assert.deepEqual(columns.in_progress.map((x) => x.id), ['bb-3']);
|
||||
assert.deepEqual(columns.blocked.map((x) => x.id), ['bb-2', 'bb-1']);
|
||||
assert.deepEqual(columns.blocked.map((x) => x.id), ['bb-5', 'bb-6']);
|
||||
assert.equal(columns.closed.length, 0);
|
||||
});
|
||||
|
||||
|
|
@ -105,12 +105,12 @@ test('buildBlockedByTree returns compact blocker tree with depth and total', ()
|
|||
issue({ id: 'bb-4', title: 'Nested blocker', dependencies: [{ type: 'blocks', target: 'bb-2' }] }),
|
||||
];
|
||||
|
||||
const tree = buildBlockedByTree(issues, 'bb-1');
|
||||
const tree = buildBlockedByTree(issues, 'bb-4');
|
||||
|
||||
assert.equal(tree.total, 3);
|
||||
assert.equal(tree.total, 2);
|
||||
assert.deepEqual(
|
||||
tree.nodes.map((node) => `${node.id}:${node.level}`),
|
||||
['bb-2:1', 'bb-3:1', 'bb-4:2'],
|
||||
['bb-2:1', 'bb-1:2'],
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -136,27 +136,28 @@ test('pickNextActionableIssue is deterministic by priority asc, unblocks desc, u
|
|||
status: 'open',
|
||||
priority: 1,
|
||||
updated_at: '2026-02-10T01:00:00Z',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-10' }],
|
||||
}),
|
||||
issue({
|
||||
id: 'bb-2',
|
||||
status: 'open',
|
||||
priority: 1,
|
||||
updated_at: '2026-02-10T02:00:00Z',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-11' }, { type: 'blocks', target: 'bb-12' }],
|
||||
}),
|
||||
issue({
|
||||
id: 'bb-3',
|
||||
id: 'bb-10',
|
||||
status: 'open',
|
||||
priority: 1,
|
||||
updated_at: '2026-02-10T02:00:00Z',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-13' }, { type: 'blocks', target: 'bb-14' }],
|
||||
dependencies: [{ type: 'blocks', target: 'bb-1' }],
|
||||
}),
|
||||
issue({
|
||||
id: 'bb-11',
|
||||
status: 'open',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-2' }],
|
||||
}),
|
||||
issue({
|
||||
id: 'bb-12',
|
||||
status: 'open',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-2' }],
|
||||
}),
|
||||
issue({ id: 'bb-10', status: 'blocked' }),
|
||||
issue({ id: 'bb-11', status: 'blocked' }),
|
||||
issue({ id: 'bb-12', status: 'blocked' }),
|
||||
issue({ id: 'bb-13', status: 'blocked' }),
|
||||
issue({ id: 'bb-14', status: 'blocked' }),
|
||||
];
|
||||
|
||||
const columns = buildKanbanColumns(issues);
|
||||
|
|
@ -187,7 +188,9 @@ test('buildUnblocksCountByIssue counts unique blocks dependencies per issue', ()
|
|||
|
||||
const map = buildUnblocksCountByIssue(issues);
|
||||
|
||||
assert.equal(map.get('bb-1'), 2);
|
||||
assert.equal(map.get('bb-1'), 0);
|
||||
assert.equal(map.get('bb-2'), 1);
|
||||
assert.equal(map.get('bb-3'), 1);
|
||||
});
|
||||
|
||||
test('buildExecutionChecklist evaluates owner, blockers, quality signal, and execution-compatible lane', () => {
|
||||
|
|
@ -197,8 +200,9 @@ test('buildExecutionChecklist evaluates owner, blockers, quality signal, and exe
|
|||
status: 'open',
|
||||
owner: 'dev-a',
|
||||
description: 'Implements acceptance criteria with rollback notes',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-2' }],
|
||||
}),
|
||||
issue({ id: 'bb-2', status: 'closed', dependencies: [{ type: 'blocks', target: 'bb-1' }] }),
|
||||
issue({ id: 'bb-2', status: 'closed' }),
|
||||
];
|
||||
|
||||
const checklist = buildExecutionChecklist(issues[0], issues);
|
||||
|
|
@ -208,3 +212,21 @@ test('buildExecutionChecklist evaluates owner, blockers, quality signal, and exe
|
|||
[true, true, true, true],
|
||||
);
|
||||
});
|
||||
|
||||
test('buildExecutionChecklist fails blocker check when blocker is still open', () => {
|
||||
const issues = [
|
||||
issue({
|
||||
id: 'bb-1',
|
||||
status: 'open',
|
||||
owner: 'dev-a',
|
||||
description: 'Implements acceptance criteria with rollback notes',
|
||||
dependencies: [{ type: 'blocks', target: 'bb-2' }],
|
||||
}),
|
||||
issue({ id: 'bb-2', status: 'open' }),
|
||||
];
|
||||
|
||||
const checklist = buildExecutionChecklist(issues[0], issues);
|
||||
const blockerItem = checklist.find((item) => item.key === 'no_open_blockers');
|
||||
|
||||
assert.equal(blockerItem?.passed, false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue