-
- {#if showLearnBtn}
-
+{#if fired && !highlight}
+
+
+
+
+
+ {#if expanded}
+
+
Trigger {event.trigger?.type ?? '?'} #{event.trigger?.number ?? '?'} v={event.trigger?.value ?? '?'}{event.trigger?.device ? ` (${event.trigger.device})` : ''}
+
Action {event.action?.type ?? '?'}: {event.action?.summary ?? '?'}
+
Result {firedResult?.text ?? '?'}
+
Latency {event.latency_us != null ? formatLatency(event.latency_us) : '—'}
+
Mode {event.mode ?? '?'}
+
Time {formatAbsoluteTime(event.timestamp)}
+
{/if}
-
- {#if expanded}
-
- {#if fired}
-
Trigger {event.trigger?.type ?? '?'} #{event.trigger?.number ?? '?'} v={event.trigger?.value ?? '?'}{event.trigger?.device ? ` (${event.trigger.device})` : ''}
-
Action {event.action?.type ?? '?'}: {event.action?.summary ?? '?'}
-
Result {event.result ?? '?'}{event.error ? ` — ${event.error}` : ''}
-
Latency {event.latency_us != null ? formatLatency(event.latency_us) : '—'}
-
Mode {event.mode ?? '?'}
-
Time {formatAbsoluteTime(event.timestamp)}
- {:else}
-
Device {event.device_id ?? 'Unknown'}
-
Channel {event.channel ?? '—'}
-
Type {event.event_type}
-
Time {formatAbsoluteTime(event.timestamp)}
+{:else}
+
+
+
+
+ {#if showLearnBtn}
+
{/if}
- {/if}
-
+
+ {#if expanded}
+
+
Device {event.device_id ?? 'Unknown'}
+
Channel {event.channel ?? '—'}
+
Type {event.event_type}
+
Time {formatAbsoluteTime(event.timestamp)}
+
+ {/if}
+
+{/if}
diff --git a/conductor-gui/ui/src/lib/components/EventRow.test.ts b/conductor-gui/ui/src/lib/components/EventRow.test.ts
index 15f4aab..80b27e6 100644
--- a/conductor-gui/ui/src/lib/components/EventRow.test.ts
+++ b/conductor-gui/ui/src/lib/components/EventRow.test.ts
@@ -1,5 +1,5 @@
/**
- * EventRow tests (GUI v2 Phase 4 + ADR-014 Phase 3A)
+ * EventRow tests (GUI v2 Phase 4 + ADR-014)
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
@@ -21,6 +21,24 @@ vi.mock('$lib/utils/event-helpers', () => ({
if (t === 'encoder') return 'encoder';
return 'unknown';
},
+ getFiredTriggerLabel: (e) => {
+ if (typeof e !== 'object' || e === null || e._type !== 'mapping_fired') return '?';
+ const t = e?.trigger;
+ const type = t?.type ?? '?';
+ const num = t?.number;
+ const label = type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ return num != null ? `${label} ${num}` : label;
+ },
+ getFiredActionLabel: (e) => {
+ if (typeof e !== 'object' || e === null || e._type !== 'mapping_fired') return '?';
+ return e?.action?.summary ?? e?.action?.type ?? 'Action';
+ },
+ getFiredResultLabel: (e) => {
+ if (typeof e !== 'object' || e === null || e._type !== 'mapping_fired') return { text: '?', isError: false };
+ const isError = e?.result === 'error' || !!e?.error;
+ if (isError) return { text: `✗ ${e.error || 'fail'}`, isError: true };
+ return { text: `✓ ${e.result || 'OK'}`, isError: false };
+ },
}));
vi.mock('$lib/stores/events.js', async () => {
@@ -32,6 +50,8 @@ vi.mock('$lib/stores/events.js', async () => {
eventChannelFilter: writable('all'),
eventDeviceFilter: writable('all'),
eventFiredFilter: writable(true),
+ toastsEnabled: writable(true),
+ toastsContinuous: writable(false),
eventStreamVisible: writable(true),
learnSessionActive: writable(false),
autoScroll: writable(true),
@@ -77,7 +97,7 @@ describe('EventRow', () => {
return render(EventRow, { props });
}
- // --- Existing functionality ---
+ // --- Raw event functionality ---
it('renders event type label', async () => {
await importAndRender({ event: RAW_EVENT });
@@ -116,7 +136,7 @@ describe('EventRow', () => {
await fireEvent.click(btn);
});
- // --- Expand/Collapse (ADR-014 Phase 3A) ---
+ // --- Expand/Collapse ---
it('starts collapsed by default', async () => {
const { container } = await importAndRender({ event: RAW_EVENT });
@@ -127,7 +147,6 @@ describe('EventRow', () => {
it('expands on click to show detail panel', async () => {
const { container } = await importAndRender({ event: RAW_EVENT });
const toggle = container.querySelector('.event-toggle')!;
- expect(toggle).toBeTruthy();
await fireEvent.click(toggle);
expect(container.querySelector('.event-row.expanded')).toBeTruthy();
expect(container.querySelector('.event-detail-panel')).toBeTruthy();
@@ -136,14 +155,13 @@ describe('EventRow', () => {
it('collapses on second click', async () => {
const { container } = await importAndRender({ event: RAW_EVENT });
const toggle = container.querySelector('.event-toggle')!;
- expect(toggle).toBeTruthy();
await fireEvent.click(toggle);
expect(container.querySelector('.expanded')).toBeTruthy();
await fireEvent.click(toggle);
expect(container.querySelector('.expanded')).toBeNull();
});
- it('shows chevron element', async () => {
+ it('shows chevron element for raw events', async () => {
const { container } = await importAndRender({ event: RAW_EVENT });
const chevron = container.querySelector('.event-chevron');
expect(chevron).toBeTruthy();
@@ -155,7 +173,6 @@ describe('EventRow', () => {
const toggle = container.querySelector('.event-toggle')!;
await fireEvent.click(toggle);
const chevron = container.querySelector('.event-chevron')!;
- expect(chevron).toBeTruthy();
expect(chevron.textContent).toBe('▾');
});
@@ -163,7 +180,6 @@ describe('EventRow', () => {
const { container } = await importAndRender({ event: RAW_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
expect(panel.textContent).toContain('Device');
expect(panel.textContent).toContain('Mikro');
});
@@ -172,7 +188,6 @@ describe('EventRow', () => {
const { container } = await importAndRender({ event: RAW_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
expect(panel.textContent).toContain('Channel');
expect(panel.textContent).toContain('1');
});
@@ -181,50 +196,87 @@ describe('EventRow', () => {
const { container } = await importAndRender({ event: RAW_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
expect(panel.textContent).toContain('Time');
expect(panel.textContent).toContain('12:34:56.789');
});
- // --- Fired event styling (ADR-014 Phase 3A) ---
+ // --- Fired event styling (Option D mockup) ---
- it('renders fired icon ⚡ for mapping_fired events', async () => {
+ it('renders colored lightning bolt icon for fired events', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
- const icon = container.querySelector('.event-fired-icon')!;
+ const icon = container.querySelector('.event-fired-icon.note');
expect(icon).toBeTruthy();
expect(icon.textContent).toBe('⚡');
+ // No plain dot for fired events
+ expect(container.querySelector('.event-dot')).toBeNull();
});
- it('does not render standard dot for fired events', async () => {
+ it('renders "Fired" label with fired-type class', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
- expect(container.querySelector('.event-dot')).toBeNull();
+ const firedType = container.querySelector('.fired-type');
+ expect(firedType).toBeTruthy();
+ expect(firedType.textContent).toBe('Fired');
});
- it('renders "Fired" label for mapping_fired events', async () => {
+ it('uses mapping-fired class on event-row for fired events', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
- const typeLabel = container.querySelector('.event-type')!;
- expect(typeLabel).toBeTruthy();
- expect(typeLabel.textContent).toBe('Fired');
+ expect(container.querySelector('.mapping-fired')).toBeTruthy();
});
- it('applies fired class on event-row for fired events', async () => {
+ it('renders structured trigger → action content', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
- expect(container.querySelector('.event-row.fired')).toBeTruthy();
+ const trigger = container.querySelector('.fired-trigger');
+ const action = container.querySelector('.fired-action');
+ const arrow = container.querySelector('.fired-arrow');
+ expect(trigger).toBeTruthy();
+ expect(trigger.textContent).toBe('Note 36');
+ expect(arrow).toBeTruthy();
+ expect(arrow.textContent).toBe('→');
+ expect(action).toBeTruthy();
+ expect(action.textContent).toBe('⌘+C');
+ });
+
+ it('renders result pill for fired events', async () => {
+ const { container } = await importAndRender({ event: FIRED_EVENT });
+ const result = container.querySelector('.fired-result');
+ expect(result).toBeTruthy();
+ expect(result.textContent).toBe('✓ ok');
+ expect(result.classList.contains('ok')).toBe(true);
});
- it('shows trigger→action content for fired events', async () => {
- const { container } = await importAndRender({ event: FIRED_EVENT });
- const detail = container.querySelector('.event-detail')!;
- expect(detail).toBeTruthy();
- expect(detail.textContent).toContain('note');
- expect(detail.textContent).toContain('⌘+C');
+ it('renders error result pill for error events', async () => {
+ const errorEvent = { ...FIRED_EVENT, result: 'error', error: 'App not found' };
+ const { container } = await importAndRender({ event: errorEvent });
+ const result = container.querySelector('.fired-result');
+ expect(result).toBeTruthy();
+ expect(result.textContent).toBe('✗ App not found');
+ expect(result.classList.contains('err')).toBe(true);
});
- it('has left border colored by trigger type for fired events', async () => {
+ it('applies action-failed class for error fired events', async () => {
+ const errorEvent = { ...FIRED_EVENT, result: 'error', error: 'App not found' };
+ const { container } = await importAndRender({ event: errorEvent });
+ expect(container.querySelector('.mapping-fired.action-failed')).toBeTruthy();
+ });
+
+ it('has green background for success fired events', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
- const row = container.querySelector('.event-row.fired');
+ const row = container.querySelector('.mapping-fired');
expect(row).toBeTruthy();
- // The fired class presence implies border styling via CSS
+ // mapping-fired class implies green-08 background via CSS
+ expect(row.classList.contains('action-failed')).toBe(false);
+ });
+
+ it('shows chevron for fired events (same as raw)', async () => {
+ const { container } = await importAndRender({ event: FIRED_EVENT });
+ const chevron = container.querySelector('.event-chevron');
+ expect(chevron).toBeTruthy();
+ expect(chevron.textContent).toBe('▸');
+ });
+
+ it('shows relative time for fired events', async () => {
+ await importAndRender({ event: FIRED_EVENT });
+ expect(screen.getByText('now')).toBeTruthy();
});
// --- Fired event detail panel ---
@@ -232,7 +284,7 @@ describe('EventRow', () => {
it('fired detail panel shows trigger info', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
- const panel = container.querySelector('.event-detail-panel')!;
+ const panel = container.querySelector('.fired-detail-panel')!;
expect(panel).toBeTruthy();
expect(panel.textContent).toContain('Trigger');
expect(panel.textContent).toContain('note');
@@ -242,8 +294,7 @@ describe('EventRow', () => {
it('fired detail panel shows action info', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
- const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
+ const panel = container.querySelector('.fired-detail-panel')!;
expect(panel.textContent).toContain('Action');
expect(panel.textContent).toContain('⌘+C');
});
@@ -251,8 +302,7 @@ describe('EventRow', () => {
it('fired detail panel shows result', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
- const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
+ const panel = container.querySelector('.fired-detail-panel')!;
expect(panel.textContent).toContain('Result');
expect(panel.textContent).toContain('ok');
});
@@ -260,8 +310,7 @@ describe('EventRow', () => {
it('fired detail panel shows latency', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
- const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
+ const panel = container.querySelector('.fired-detail-panel')!;
expect(panel.textContent).toContain('Latency');
expect(panel.textContent).toContain('0.5ms');
});
@@ -269,23 +318,16 @@ describe('EventRow', () => {
it('fired detail panel shows mode', async () => {
const { container } = await importAndRender({ event: FIRED_EVENT });
await fireEvent.click(container.querySelector('.event-toggle')!);
- const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
+ const panel = container.querySelector('.fired-detail-panel')!;
expect(panel.textContent).toContain('Mode');
expect(panel.textContent).toContain('Default');
});
it('fired detail panel shows error message when result is error', async () => {
- const errorEvent = {
- ...FIRED_EVENT,
- result: 'error',
- error: 'App not found',
- };
+ const errorEvent = { ...FIRED_EVENT, result: 'error', error: 'App not found' };
const { container } = await importAndRender({ event: errorEvent });
await fireEvent.click(container.querySelector('.event-toggle')!);
- const panel = container.querySelector('.event-detail-panel')!;
- expect(panel).toBeTruthy();
- expect(panel.textContent).toContain('error');
+ const panel = container.querySelector('.fired-detail-panel')!;
expect(panel.textContent).toContain('App not found');
});
@@ -293,46 +335,54 @@ describe('EventRow', () => {
const { container } = await importAndRender({ event: RAW_EVENT, showLearnBtn: true });
const btn = screen.getByText('Learn');
await fireEvent.click(btn);
- // stopPropagation should prevent expand
expect(container.querySelector('.expanded')).toBeNull();
});
- // --- Keyboard accessibility (native