|
| 1 | +// @spec frontend-activity |
| 2 | +// |
| 3 | +// AC-06 shared eventDisplay helpers + adoption across surfaces |
| 4 | + |
| 5 | +import { readFileSync } from 'node:fs'; |
| 6 | +import { resolve } from 'node:path'; |
| 7 | + |
| 8 | +import { describe, expect, test } from 'vitest'; |
| 9 | + |
| 10 | +import { |
| 11 | + relativeTime, |
| 12 | + severityLabel, |
| 13 | + severityTone, |
| 14 | + sourceLabel, |
| 15 | +} from '@/api/eventDisplay'; |
| 16 | + |
| 17 | +const read = (p: string) => readFileSync(resolve(process.cwd(), p), 'utf8'); |
| 18 | + |
| 19 | +describe('frontend-activity — shared event-display helpers', () => { |
| 20 | + // @ac AC-06 |
| 21 | + test('frontend-activity/AC-06 — sourceLabel/severityLabel/severityTone map known + title-case unknown', () => { |
| 22 | + expect(sourceLabel('transaction')).toBe('Compliance'); |
| 23 | + expect(sourceLabel('alert')).toBe('Alert'); |
| 24 | + expect(sourceLabel('monitoring')).toBe('Monitoring'); |
| 25 | + expect(sourceLabel('intelligence')).toBe('Intelligence'); |
| 26 | + expect(sourceLabel('audit')).toBe('Audit'); |
| 27 | + // graceful: unknown source never renders raw lowercase. |
| 28 | + expect(sourceLabel('newfangled')).toBe('Newfangled'); |
| 29 | + |
| 30 | + expect(severityLabel('critical')).toBe('Critical'); |
| 31 | + expect(severityLabel('info')).toBe('Info'); |
| 32 | + expect(severityTone('critical')).toBe('crit'); |
| 33 | + expect(severityTone('high')).toBe('crit'); |
| 34 | + expect(severityTone('medium')).toBe('warn'); |
| 35 | + expect(severityTone('low')).toBe('info'); |
| 36 | + expect(severityTone('info')).toBe('info'); |
| 37 | + }); |
| 38 | + |
| 39 | + // @ac AC-06 |
| 40 | + test('frontend-activity/AC-06 — relativeTime is human + em-dash-free', () => { |
| 41 | + expect(relativeTime(new Date().toISOString())).toBe('just now'); |
| 42 | + const twoH = new Date(Date.now() - 2 * 3_600_000).toISOString(); |
| 43 | + expect(relativeTime(twoH)).toBe('2h ago'); |
| 44 | + // invalid date -> empty string, never an em-dash. |
| 45 | + expect(relativeTime('not-a-date')).toBe(''); |
| 46 | + expect(relativeTime('not-a-date')).not.toContain('—'); |
| 47 | + }); |
| 48 | + |
| 49 | + // @ac AC-06 |
| 50 | + test('frontend-activity/AC-06 — no surface renders a raw source/severity enum; per-surface copies removed', () => { |
| 51 | + const widgets = read('src/pages/dashboard/widgets.tsx'); |
| 52 | + // dashboard widget adopts the shared helpers, not raw fields. |
| 53 | + expect(widgets).toContain('sourceLabel(a.source)'); |
| 54 | + expect(widgets).toContain('severityLabel(a.severity)'); |
| 55 | + expect(widgets).toContain('severityTone(a.severity)'); |
| 56 | + expect(widgets).not.toMatch(/\$\{a\.source\} ·/); // old raw sub |
| 57 | + expect(widgets).not.toMatch(/function sevTone/); |
| 58 | + expect(widgets).not.toMatch(/function timeAgo/); |
| 59 | + |
| 60 | + // ActivityPage no longer RENDERS the bare {a.source} as a JSX child. |
| 61 | + // (The client-side search haystack still references ${a.source} in a |
| 62 | + // template string — that is filtering, not a UI render, so we exclude |
| 63 | + // the `$`-prefixed template usage from the check.) |
| 64 | + const page = read('src/pages/activity/ActivityPage.tsx'); |
| 65 | + expect(page).not.toMatch(/[^$]\{a\.source\}/); |
| 66 | + expect(page).toContain("from '@/api/eventDisplay'"); |
| 67 | + |
| 68 | + // The duplicate helpers are gone; the canonical ones are imported. |
| 69 | + const drawer = read('src/pages/activity/ActivityDrawer.tsx'); |
| 70 | + expect(drawer).not.toMatch(/export function severityTone/); |
| 71 | + expect(drawer).toContain("from '@/api/eventDisplay'"); |
| 72 | + const host = read('src/pages/HostDetailPage.tsx'); |
| 73 | + expect(host).not.toMatch(/function activityRelativeTime/); |
| 74 | + expect(host).toContain("relativeTime(item.occurred_at)"); |
| 75 | + }); |
| 76 | +}); |
0 commit comments