diff --git a/apps/web/src/app/inspector/page.tsx b/apps/web/src/app/inspector/page.tsx index f3bbfd8..bf34f61 100644 --- a/apps/web/src/app/inspector/page.tsx +++ b/apps/web/src/app/inspector/page.tsx @@ -285,7 +285,7 @@ export default function InspectorPage() { {/* Failures */} {analysis.failures.length > 0 && ( -
+

Failures ({analysis.failures.length})

diff --git a/apps/web/tests/inspector.spec.ts b/apps/web/tests/inspector.spec.ts index cb47bf4..9fc18b4 100644 --- a/apps/web/tests/inspector.spec.ts +++ b/apps/web/tests/inspector.spec.ts @@ -39,8 +39,12 @@ test.describe('Inspector', () => { await page.getByRole('button', { name: 'failed-auth' }).click(); await page.getByRole('button', { name: 'Analyze' }).click(); - // Wait for failure to appear - await expect(page.getByText('FAILED_AUTHORIZATION')).toBeVisible({ + // Wait for failure to appear — scope to the failures section to avoid + // matching the trace JSON in the textarea. Use .first() because the + // failed-auth fixture produces multiple FAILED_AUTHORIZATION failures. + await expect( + page.locator('[data-testid="failure-summary"]').getByText('FAILED_AUTHORIZATION').first(), + ).toBeVisible({ timeout: 10000, }); }); diff --git a/packages/toolkit/src/react/components.ts b/packages/toolkit/src/react/components.ts deleted file mode 100644 index c304aea..0000000 --- a/packages/toolkit/src/react/components.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * React component stubs for @ocpp-debugkit/toolkit/react. - * - * v0.2.0: These are stub implementations — presentational components - * that will be fully extracted from the Inspector in Phase 4. - * They are functional but minimal, using no external CSS framework. - */ - -import type { - SessionTimelineProps, - MessageInspectorProps, - FailureSummaryProps, - ReportViewerProps, - ReplayControlsProps, -} from './types.js'; - -export function SessionTimeline({ events, selectedEventId, onSelectEvent }: SessionTimelineProps) { - return { - type: 'SessionTimeline', - props: { events, selectedEventId, onSelectEvent }, - } as const; -} - -export function MessageInspector({ event }: MessageInspectorProps) { - return { - type: 'MessageInspector', - props: { event }, - } as const; -} - -export function FailureSummary({ failures }: FailureSummaryProps) { - return { - type: 'FailureSummary', - props: { failures }, - } as const; -} - -export function ReportViewer({ html }: ReportViewerProps) { - return { - type: 'ReportViewer', - props: { html }, - } as const; -} - -export function ReplayControls({ - isPlaying, - currentIndex, - totalEvents, - onPlay, - onPause, - onStep, - onStepBack, - onJump, - speed, - onSpeedChange, -}: ReplayControlsProps) { - return { - type: 'ReplayControls', - props: { - isPlaying, - currentIndex, - totalEvents, - onPlay, - onPause, - onStep, - onStepBack, - onJump, - speed, - onSpeedChange, - }, - } as const; -} diff --git a/packages/toolkit/src/react/components.tsx b/packages/toolkit/src/react/components.tsx new file mode 100644 index 0000000..46d6261 --- /dev/null +++ b/packages/toolkit/src/react/components.tsx @@ -0,0 +1,325 @@ +/** + * React components for @ocpp-debugkit/toolkit/react. + * + * These are presentational components — they accept data as props and + * render UI. They are SSR-safe (no window/document access at module level). + * No external CSS framework — uses inline styles for zero-dependency rendering. + */ + +import type { + SessionTimelineProps, + MessageInspectorProps, + FailureSummaryProps, + ReportViewerProps, + ReplayControlsProps, +} from './types.js'; + +const SEVERITY_COLORS: Record = { + critical: '#dc2626', + warning: '#f59e0b', + info: '#3b82f6', +}; + +const styles = { + container: { + fontFamily: 'system-ui, -apple-system, sans-serif', + color: '#1f2937', + }, + list: { + listStyle: 'none' as const, + padding: 0, + margin: 0, + }, + listItem: { + padding: '8px 12px', + borderBottom: '1px solid #e5e7eb', + cursor: 'pointer', + fontSize: '13px', + } as const, + selectedItem: { + backgroundColor: '#dbeafe', + }, + badge: { + display: 'inline-block', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '11px', + fontWeight: 600, + color: '#fff', + marginRight: '6px', + } as const, + code: { + fontFamily: 'monospace', + fontSize: '12px', + backgroundColor: '#f3f4f6', + padding: '2px 4px', + borderRadius: '3px', + }, + section: { + marginBottom: '16px', + }, + heading: { + fontSize: '14px', + fontWeight: 600, + marginBottom: '8px', + color: '#374151', + }, + empty: { + color: '#9ca3af', + fontStyle: 'italic' as const, + padding: '12px', + }, + button: { + padding: '4px 12px', + border: '1px solid #d1d5db', + borderRadius: '4px', + background: '#fff', + cursor: 'pointer', + fontSize: '13px', + marginRight: '4px', + }, + buttonActive: { + background: '#3b82f6', + color: '#fff', + borderColor: '#3b82f6', + }, + rangeInput: { + width: '100%', + margin: '8px 0', + }, +}; + +function formatTimestamp(ts: number | null): string { + if (ts === null) return '—'; + return new Date(ts).toISOString().slice(11, 19); +} + +export function SessionTimeline({ events, selectedEventId, onSelectEvent }: SessionTimelineProps) { + if (events.length === 0) { + return
No events in trace
; + } + + return ( +
+
Timeline ({events.length} events)
+
    + {events.map((event, i) => ( +
  • onSelectEvent?.(event.id)} + style={{ + ...styles.listItem, + ...(selectedEventId === event.id ? styles.selectedItem : {}), + }} + > + {i} + + {formatTimestamp(event.timestamp)} + + {event.messageType} + {event.action && ( + {event.action} + )} + {event.direction} +
  • + ))} +
+
+ ); +} + +export function MessageInspector({ event }: MessageInspectorProps) { + if (!event) { + return
Select an event to inspect
; + } + + return ( +
+
Message Inspector
+
+ ID: {event.id} +
+
+ Message ID: {event.messageId} +
+
+ Type: {event.messageType} +
+
+ Action: {event.action ?? '—'} +
+
+ Direction: {event.direction} +
+
+ Timestamp: {formatTimestamp(event.timestamp)} +
+
+ Payload: +
+          {JSON.stringify(event.payload, null, 2)}
+        
+
+
+ Raw: +
+          {JSON.stringify(event.rawMessage, null, 2)}
+        
+
+
+ ); +} + +export function FailureSummary({ failures }: FailureSummaryProps) { + if (failures.length === 0) { + return ( +
+
Failures
+
No failures detected
+
+ ); + } + + return ( +
+
Failures ({failures.length})
+
    + {failures.map((failure, i) => ( +
  • + + {failure.severity.toUpperCase()} + + {failure.code} +
    + {failure.description} +
    + {failure.suggestedSteps.length > 0 && ( +
    + Suggested steps: +
      + {failure.suggestedSteps.map((step, j) => ( +
    • {step}
    • + ))} +
    +
    + )} +
  • + ))} +
+
+ ); +} + +export function ReportViewer({ html }: ReportViewerProps) { + return ( +
+
Report
+