test: Created OutputInfoBar with color-coded stats, wired Use This butt…
- "app/src/components/OutputInfoBar.tsx" - "app/src/components/__tests__/OutputInfoBar.test.tsx" - "app/src/views/ImportConvert.tsx" - "app/src/App.css" GSD-Task: S01/T04
This commit is contained in:
parent
fc63195d68
commit
a97629c390
9 changed files with 377 additions and 22 deletions
|
|
@ -16,3 +16,4 @@
|
||||||
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T05:05:22.658Z","actor":"agent","hash":"59aebe24d8f53b7a","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T05:05:22.658Z","actor":"agent","hash":"59aebe24d8f53b7a","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T02"},"ts":"2026-03-26T05:07:29.861Z","actor":"agent","hash":"a3980272c7b74afa","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T02"},"ts":"2026-03-26T05:07:29.861Z","actor":"agent","hash":"a3980272c7b74afa","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T03"},"ts":"2026-03-26T05:15:38.849Z","actor":"agent","hash":"51de22a58ca5b075","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T03"},"ts":"2026-03-26T05:15:38.849Z","actor":"agent","hash":"51de22a58ca5b075","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
|
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T04"},"ts":"2026-03-26T05:17:44.460Z","actor":"agent","hash":"fd1cf932b3152ba6","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
- Estimate: 1h30m
|
- Estimate: 1h30m
|
||||||
- Files: app/src/hooks/useDebouncedTrace.ts, app/src/components/ParameterSliders.tsx, app/src/components/SvgPreview.tsx, app/src/views/ImportConvert.tsx, app/src/hooks/__tests__/useDebouncedTrace.test.ts
|
- Files: app/src/hooks/useDebouncedTrace.ts, app/src/components/ParameterSliders.tsx, app/src/components/SvgPreview.tsx, app/src/views/ImportConvert.tsx, app/src/hooks/__tests__/useDebouncedTrace.test.ts
|
||||||
- Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
- Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||||
- [ ] **T04: Add output info bar, Use This flow, error states, and verify full integration** — Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.
|
- [x] **T04: Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors** — Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
|
|
|
||||||
16
.gsd/milestones/M002/slices/S01/tasks/T03-VERIFY.json
Normal file
16
.gsd/milestones/M002/slices/S01/tasks/T03-VERIFY.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T03",
|
||||||
|
"unitId": "M002/S01/T03",
|
||||||
|
"timestamp": 1774502143859,
|
||||||
|
"passed": true,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd app",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 3,
|
||||||
|
"verdict": "pass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
81
.gsd/milestones/M002/slices/S01/tasks/T04-SUMMARY.md
Normal file
81
.gsd/milestones/M002/slices/S01/tasks/T04-SUMMARY.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
---
|
||||||
|
id: T04
|
||||||
|
parent: S01
|
||||||
|
milestone: M002
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["app/src/components/OutputInfoBar.tsx", "app/src/components/__tests__/OutputInfoBar.test.tsx", "app/src/views/ImportConvert.tsx", "app/src/App.css"]
|
||||||
|
key_decisions: ["Use This button always visible but disabled when no SVG output — avoids layout shift vs conditional render"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "npx tsc --noEmit: zero errors. npx vitest run --reporter=verbose: 23/23 tests pass across 3 test files (9 API client, 7 OutputInfoBar, 7 useDebouncedTrace)."
|
||||||
|
completed_at: 2026-03-26T05:17:44.406Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T04: Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors
|
||||||
|
|
||||||
|
> Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T04
|
||||||
|
parent: S01
|
||||||
|
milestone: M002
|
||||||
|
key_files:
|
||||||
|
- app/src/components/OutputInfoBar.tsx
|
||||||
|
- app/src/components/__tests__/OutputInfoBar.test.tsx
|
||||||
|
- app/src/views/ImportConvert.tsx
|
||||||
|
- app/src/App.css
|
||||||
|
key_decisions:
|
||||||
|
- Use This button always visible but disabled when no SVG output — avoids layout shift vs conditional render
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-26T05:17:44.415Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T04: Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors
|
||||||
|
|
||||||
|
**Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Built the OutputInfoBar component displaying trace metadata (Path Count, Total Nodes, Open Paths, Processing Time) with color-coded indicators: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0. Null metadata shows a muted placeholder. Warnings render below the stats. Updated ImportConvert to render OutputInfoBar below the preview and changed the Use This button to always-visible with disabled state (disabled when no SVG output or loading). Added CSS for the info bar. Wrote 7 tests covering all color coding thresholds and edge cases. Verified empty/error/loading states from T03's SvgPreview remain correct.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
npx tsc --noEmit: zero errors. npx vitest run --reporter=verbose: 23/23 tests pass across 3 test files (9 API client, 7 OutputInfoBar, 7 useDebouncedTrace).
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 6400ms |
|
||||||
|
| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2500ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Use This button changed from conditionally rendered to always-visible with disabled attribute — better UX (no layout shift). Empty/error/loading states already complete from T03.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `app/src/components/OutputInfoBar.tsx`
|
||||||
|
- `app/src/components/__tests__/OutputInfoBar.test.tsx`
|
||||||
|
- `app/src/views/ImportConvert.tsx`
|
||||||
|
- `app/src/App.css`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
Use This button changed from conditionally rendered to always-visible with disabled attribute — better UX (no layout shift). Empty/error/loading states already complete from T03.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
None.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"exported_at": "2026-03-26T05:15:38.848Z",
|
"exported_at": "2026-03-26T05:17:44.459Z",
|
||||||
"milestones": [
|
"milestones": [
|
||||||
{
|
{
|
||||||
"id": "M001",
|
"id": "M001",
|
||||||
|
|
@ -990,19 +990,26 @@
|
||||||
"milestone_id": "M002",
|
"milestone_id": "M002",
|
||||||
"slice_id": "S01",
|
"slice_id": "S01",
|
||||||
"id": "T04",
|
"id": "T04",
|
||||||
"title": "Add output info bar, Use This flow, error states, and verify full integration",
|
"title": "Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors",
|
||||||
"status": "pending",
|
"status": "complete",
|
||||||
"one_liner": "",
|
"one_liner": "Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors",
|
||||||
"narrative": "",
|
"narrative": "Built the OutputInfoBar component displaying trace metadata (Path Count, Total Nodes, Open Paths, Processing Time) with color-coded indicators: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0. Null metadata shows a muted placeholder. Warnings render below the stats. Updated ImportConvert to render OutputInfoBar below the preview and changed the Use This button to always-visible with disabled state (disabled when no SVG output or loading). Added CSS for the info bar. Wrote 7 tests covering all color coding thresholds and edge cases. Verified empty/error/loading states from T03's SvgPreview remain correct.",
|
||||||
"verification_result": "",
|
"verification_result": "npx tsc --noEmit: zero errors. npx vitest run --reporter=verbose: 23/23 tests pass across 3 test files (9 API client, 7 OutputInfoBar, 7 useDebouncedTrace).",
|
||||||
"duration": "",
|
"duration": "",
|
||||||
"completed_at": null,
|
"completed_at": "2026-03-26T05:17:44.406Z",
|
||||||
"blocker_discovered": false,
|
"blocker_discovered": false,
|
||||||
"deviations": "",
|
"deviations": "Use This button changed from conditionally rendered to always-visible with disabled attribute — better UX (no layout shift). Empty/error/loading states already complete from T03.",
|
||||||
"known_issues": "",
|
"known_issues": "None.",
|
||||||
"key_files": [],
|
"key_files": [
|
||||||
"key_decisions": [],
|
"app/src/components/OutputInfoBar.tsx",
|
||||||
"full_summary_md": "",
|
"app/src/components/__tests__/OutputInfoBar.test.tsx",
|
||||||
|
"app/src/views/ImportConvert.tsx",
|
||||||
|
"app/src/App.css"
|
||||||
|
],
|
||||||
|
"key_decisions": [
|
||||||
|
"Use This button always visible but disabled when no SVG output — avoids layout shift vs conditional render"
|
||||||
|
],
|
||||||
|
"full_summary_md": "---\nid: T04\nparent: S01\nmilestone: M002\nkey_files:\n - app/src/components/OutputInfoBar.tsx\n - app/src/components/__tests__/OutputInfoBar.test.tsx\n - app/src/views/ImportConvert.tsx\n - app/src/App.css\nkey_decisions:\n - Use This button always visible but disabled when no SVG output — avoids layout shift vs conditional render\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T05:17:44.415Z\nblocker_discovered: false\n---\n\n# T04: Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors\n\n**Created OutputInfoBar with color-coded stats, wired Use This button with disabled state, added 7 component tests — 23/23 tests pass, zero TS errors**\n\n## What Happened\n\nBuilt the OutputInfoBar component displaying trace metadata (Path Count, Total Nodes, Open Paths, Processing Time) with color-coded indicators: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0. Null metadata shows a muted placeholder. Warnings render below the stats. Updated ImportConvert to render OutputInfoBar below the preview and changed the Use This button to always-visible with disabled state (disabled when no SVG output or loading). Added CSS for the info bar. Wrote 7 tests covering all color coding thresholds and edge cases. Verified empty/error/loading states from T03's SvgPreview remain correct.\n\n## Verification\n\nnpx tsc --noEmit: zero errors. npx vitest run --reporter=verbose: 23/23 tests pass across 3 test files (9 API client, 7 OutputInfoBar, 7 useDebouncedTrace).\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 6400ms |\n| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2500ms |\n\n\n## Deviations\n\nUse This button changed from conditionally rendered to always-visible with disabled attribute — better UX (no layout shift). Empty/error/loading states already complete from T03.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/components/OutputInfoBar.tsx`\n- `app/src/components/__tests__/OutputInfoBar.test.tsx`\n- `app/src/views/ImportConvert.tsx`\n- `app/src/App.css`\n",
|
||||||
"description": "Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.\n\n## Steps\n\n1. Create `app/src/components/OutputInfoBar.tsx`:\n - Accepts `metadata: TraceMetadata | null`\n - When metadata is null: render nothing or a muted placeholder\n - Render stats: Path Count, Total Nodes, Open Paths, Processing Time\n - Color coding: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0\n - Show warnings from metadata.warnings array if any\n2. Add 'Use This' button to `ImportConvert.tsx`:\n - Disabled when no SVG output is available\n - On click: call `onUseThis(svgOutput, metadata)` prop, which App.tsx uses to transition view state to 'canvas'\n - Pass the SVG string forward (stored in App.tsx state for S02 to consume)\n3. Add empty/error states throughout:\n - No file selected: show upload prompt in preview area\n - API error: show error message in preview area with status code\n - Loading: show spinner overlay on preview\n4. Write `app/src/components/__tests__/OutputInfoBar.test.tsx` — test color coding logic:\n - Normal metadata → green indicators\n - High node count → yellow indicator\n - Open paths > 0 → red indicator\n - Null metadata → no crash, placeholder shown\n5. Final verification: ensure `npx tsc --noEmit` passes with zero errors, `npx vitest run` passes all tests, and all components are wired together end-to-end.",
|
"description": "Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.\n\n## Steps\n\n1. Create `app/src/components/OutputInfoBar.tsx`:\n - Accepts `metadata: TraceMetadata | null`\n - When metadata is null: render nothing or a muted placeholder\n - Render stats: Path Count, Total Nodes, Open Paths, Processing Time\n - Color coding: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0\n - Show warnings from metadata.warnings array if any\n2. Add 'Use This' button to `ImportConvert.tsx`:\n - Disabled when no SVG output is available\n - On click: call `onUseThis(svgOutput, metadata)` prop, which App.tsx uses to transition view state to 'canvas'\n - Pass the SVG string forward (stored in App.tsx state for S02 to consume)\n3. Add empty/error states throughout:\n - No file selected: show upload prompt in preview area\n - API error: show error message in preview area with status code\n - Loading: show spinner overlay on preview\n4. Write `app/src/components/__tests__/OutputInfoBar.test.tsx` — test color coding logic:\n - Normal metadata → green indicators\n - High node count → yellow indicator\n - Open paths > 0 → red indicator\n - Null metadata → no crash, placeholder shown\n5. Final verification: ensure `npx tsc --noEmit` passes with zero errors, `npx vitest run` passes all tests, and all components are wired together end-to-end.",
|
||||||
"estimate": "45m",
|
"estimate": "45m",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -1312,6 +1319,28 @@
|
||||||
"verdict": "✅ pass",
|
"verdict": "✅ pass",
|
||||||
"duration_ms": 2800,
|
"duration_ms": 2800,
|
||||||
"created_at": "2026-03-26T05:15:38.801Z"
|
"created_at": "2026-03-26T05:15:38.801Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"task_id": "T04",
|
||||||
|
"slice_id": "S01",
|
||||||
|
"milestone_id": "M002",
|
||||||
|
"command": "cd app && npx tsc --noEmit",
|
||||||
|
"exit_code": 0,
|
||||||
|
"verdict": "✅ pass",
|
||||||
|
"duration_ms": 6400,
|
||||||
|
"created_at": "2026-03-26T05:17:44.406Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"task_id": "T04",
|
||||||
|
"slice_id": "S01",
|
||||||
|
"milestone_id": "M002",
|
||||||
|
"command": "cd app && npx vitest run --reporter=verbose",
|
||||||
|
"exit_code": 0,
|
||||||
|
"verdict": "✅ pass",
|
||||||
|
"duration_ms": 2500,
|
||||||
|
"created_at": "2026-03-26T05:17:44.406Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -324,6 +324,77 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Output Info Bar */
|
||||||
|
.output-info-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-bar--empty {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-placeholder {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat-value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat--green .output-stat-value {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat--yellow .output-stat-value {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-stat--red .output-stat-value {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-warnings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-info-warning {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
/* Placeholder views */
|
/* Placeholder views */
|
||||||
.placeholder-view {
|
.placeholder-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
68
app/src/components/OutputInfoBar.tsx
Normal file
68
app/src/components/OutputInfoBar.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { TraceMetadata } from '../types/engine';
|
||||||
|
|
||||||
|
interface OutputInfoBarProps {
|
||||||
|
metadata: TraceMetadata | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatColor = 'green' | 'yellow' | 'red';
|
||||||
|
|
||||||
|
function getNodeCountColor(count: number): StatColor {
|
||||||
|
if (count > 5000) return 'yellow';
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenPathsColor(count: number): StatColor {
|
||||||
|
if (count > 0) return 'red';
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OutputInfoBar({ metadata }: OutputInfoBarProps) {
|
||||||
|
if (!metadata) {
|
||||||
|
return (
|
||||||
|
<div className="output-info-bar output-info-bar--empty">
|
||||||
|
<span className="output-info-placeholder">
|
||||||
|
Trace an image to see output stats
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeColor = getNodeCountColor(metadata.node_count_total);
|
||||||
|
const openPathsColor = getOpenPathsColor(metadata.open_paths);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="output-info-bar">
|
||||||
|
<div className="output-info-stats">
|
||||||
|
<span className="output-stat output-stat--green" data-testid="stat-paths">
|
||||||
|
<span className="output-stat-label">Paths</span>
|
||||||
|
<span className="output-stat-value">{metadata.path_count}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`output-stat output-stat--${nodeColor}`}
|
||||||
|
data-testid="stat-nodes"
|
||||||
|
>
|
||||||
|
<span className="output-stat-label">Nodes</span>
|
||||||
|
<span className="output-stat-value">{metadata.node_count_total.toLocaleString()}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`output-stat output-stat--${openPathsColor}`}
|
||||||
|
data-testid="stat-open-paths"
|
||||||
|
>
|
||||||
|
<span className="output-stat-label">Open Paths</span>
|
||||||
|
<span className="output-stat-value">{metadata.open_paths}</span>
|
||||||
|
</span>
|
||||||
|
<span className="output-stat output-stat--green" data-testid="stat-time">
|
||||||
|
<span className="output-stat-label">Time</span>
|
||||||
|
<span className="output-stat-value">{metadata.processing_ms}ms</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{metadata.warnings.length > 0 && (
|
||||||
|
<div className="output-info-warnings" data-testid="warnings">
|
||||||
|
{metadata.warnings.map((w, i) => (
|
||||||
|
<span key={i} className="output-info-warning">⚠ {w}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
app/src/components/__tests__/OutputInfoBar.test.tsx
Normal file
88
app/src/components/__tests__/OutputInfoBar.test.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import OutputInfoBar from '../OutputInfoBar';
|
||||||
|
import type { TraceMetadata } from '../../types/engine';
|
||||||
|
|
||||||
|
function makeMetadata(overrides: Partial<TraceMetadata> = {}): TraceMetadata {
|
||||||
|
return {
|
||||||
|
format: 'svg',
|
||||||
|
path_count: 42,
|
||||||
|
node_count_total: 1200,
|
||||||
|
open_paths: 0,
|
||||||
|
island_count: 3,
|
||||||
|
warnings: [],
|
||||||
|
processing_ms: 150,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OutputInfoBar', () => {
|
||||||
|
it('shows placeholder when metadata is null', () => {
|
||||||
|
const { container } = render(<OutputInfoBar metadata={null} />);
|
||||||
|
expect(container.querySelector('.output-info-bar--empty')).toBeTruthy();
|
||||||
|
expect(screen.getByText(/trace an image/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders stats with green indicators for normal metadata', () => {
|
||||||
|
const meta = makeMetadata();
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const pathsStat = screen.getByTestId('stat-paths');
|
||||||
|
expect(pathsStat).toHaveClass('output-stat--green');
|
||||||
|
expect(pathsStat).toHaveTextContent('42');
|
||||||
|
|
||||||
|
const nodesStat = screen.getByTestId('stat-nodes');
|
||||||
|
expect(nodesStat).toHaveClass('output-stat--green');
|
||||||
|
expect(nodesStat).toHaveTextContent('1,200');
|
||||||
|
|
||||||
|
const openStat = screen.getByTestId('stat-open-paths');
|
||||||
|
expect(openStat).toHaveClass('output-stat--green');
|
||||||
|
expect(openStat).toHaveTextContent('0');
|
||||||
|
|
||||||
|
const timeStat = screen.getByTestId('stat-time');
|
||||||
|
expect(timeStat).toHaveClass('output-stat--green');
|
||||||
|
expect(timeStat).toHaveTextContent('150ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows yellow indicator when node_count_total > 5000', () => {
|
||||||
|
const meta = makeMetadata({ node_count_total: 8500 });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const nodesStat = screen.getByTestId('stat-nodes');
|
||||||
|
expect(nodesStat).toHaveClass('output-stat--yellow');
|
||||||
|
expect(nodesStat).toHaveTextContent('8,500');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows red indicator when open_paths > 0', () => {
|
||||||
|
const meta = makeMetadata({ open_paths: 3 });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const openStat = screen.getByTestId('stat-open-paths');
|
||||||
|
expect(openStat).toHaveClass('output-stat--red');
|
||||||
|
expect(openStat).toHaveTextContent('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows both yellow and red indicators simultaneously', () => {
|
||||||
|
const meta = makeMetadata({ node_count_total: 6000, open_paths: 5 });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('stat-nodes')).toHaveClass('output-stat--yellow');
|
||||||
|
expect(screen.getByTestId('stat-open-paths')).toHaveClass('output-stat--red');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays warnings when present', () => {
|
||||||
|
const meta = makeMetadata({ warnings: ['Too many paths', 'Complex geometry'] });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
const warnings = screen.getByTestId('warnings');
|
||||||
|
expect(warnings).toHaveTextContent('Too many paths');
|
||||||
|
expect(warnings).toHaveTextContent('Complex geometry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render warnings container when no warnings', () => {
|
||||||
|
const meta = makeMetadata({ warnings: [] });
|
||||||
|
render(<OutputInfoBar metadata={meta} />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('warnings')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,7 @@ import FileUpload from '../components/FileUpload';
|
||||||
import PresetSelector from '../components/PresetSelector';
|
import PresetSelector from '../components/PresetSelector';
|
||||||
import ParameterSliders from '../components/ParameterSliders';
|
import ParameterSliders from '../components/ParameterSliders';
|
||||||
import SvgPreview from '../components/SvgPreview';
|
import SvgPreview from '../components/SvgPreview';
|
||||||
|
import OutputInfoBar from '../components/OutputInfoBar';
|
||||||
import { useDebouncedTrace } from '../hooks/useDebouncedTrace';
|
import { useDebouncedTrace } from '../hooks/useDebouncedTrace';
|
||||||
import styles from './ImportConvert.module.css';
|
import styles from './ImportConvert.module.css';
|
||||||
|
|
||||||
|
|
@ -93,15 +94,14 @@ export default function ImportConvert({ onUseThis }: ImportConvertProps) {
|
||||||
params={currentParams}
|
params={currentParams}
|
||||||
onChange={handleParamsChange}
|
onChange={handleParamsChange}
|
||||||
/>
|
/>
|
||||||
{svgOutput && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="use-this-btn"
|
className="use-this-btn"
|
||||||
|
disabled={!svgOutput || isLoading}
|
||||||
onClick={handleUseThis}
|
onClick={handleUseThis}
|
||||||
>
|
>
|
||||||
Use This →
|
Use This →
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightPanel}>
|
<div className={styles.rightPanel}>
|
||||||
<SvgPreview
|
<SvgPreview
|
||||||
|
|
@ -110,6 +110,7 @@ export default function ImportConvert({ onUseThis }: ImportConvertProps) {
|
||||||
error={error}
|
error={error}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
/>
|
/>
|
||||||
|
<OutputInfoBar metadata={metadata} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue