import { describe, it, expect } from 'vitest' import { toApiPath, rows, mapMeta, mapLink, cardRows, metricValue, cardVisible, sortCards, permittedCards, renderableCards, visibleRows, overflowCount, } from './dashboardCards' const failuresCard = { id: 'geenforce-failures', render: 'exceptions', severity: 'critical', position: 10, map: { title: 'hostname', detail: 'entryname', meta: [{ key: 'message' }, { key: 'exitcode', label: 'exit' }], link: '/pcs/{computerid}', }, } describe('endpoint paths', () => { it('strips the api prefix the instance already carries', () => { expect(toApiPath('/api/geenforce/dashboard/failures')) .toBe('/geenforce/dashboard/failures') }) it('leaves a relative endpoint alone', () => { expect(toApiPath('/geenforce/x')).toBe('/geenforce/x') }) }) describe('reading rows from whatever shape the endpoint returns', () => { it('accepts a bare array, {rows} or {items}', () => { expect(rows({ _data: [1, 2] })).toEqual([1, 2]) expect(rows({ _data: { rows: [1] } })).toEqual([1]) expect(rows({ _data: { items: [1, 2, 3] } })).toEqual([1, 2, 3]) }) it('treats a failed fetch as no rows rather than throwing', () => { expect(rows({ _data: null })).toEqual([]) }) }) describe('mapping a row', () => { it('builds title, detail, meta and link from the declaration', () => { const card = { ...failuresCard, _data: [{ hostname: 'WJSF1234', entryname: 'Install OpenText', exitcode: 1603, message: 'Fatal error', computerid: 42, }] } const [row] = cardRows(card) expect(row.title).toBe('WJSF1234') expect(row.detail).toBe('Install OpenText') expect(row.meta.map((m) => m.text)).toEqual(['Fatal error', 'exit 1603']) expect(row.link).toBe('/pcs/42') }) it('supports a trailing unit so the text reads as a sentence', () => { const card = { render: 'exceptions', map: { title: 'hostname', meta: [{ key: 'quietdays', label: 'quiet for', suffix: ' days' }] }, _data: [{ hostname: 'QUIETPC', quietdays: 3 }], } expect(cardRows(card)[0].meta[0].text).toBe('quiet for 3 days') }) it('drops empty meta values instead of rendering a stray label', () => { const card = { ...failuresCard, _data: [{ hostname: 'X', message: '', exitcode: null }] } expect(cardRows(card)[0].meta).toEqual([]) }) it('omits the link when the substitution value is missing, keeping the row', () => { // A PC shopdb does not know still reports its failure - that is the bay // most likely to be misconfigured. It must not link to /pcs/undefined. const card = { ...failuresCard, _data: [{ hostname: 'GHOSTPC', computerid: null }] } const [row] = cardRows(card) expect(row.title).toBe('GHOSTPC') expect(row.link).toBeNull() }) }) describe('long lists', () => { const many = (n) => ({ render: 'exceptions', map: { title: 'hostname' }, _data: Array.from({ length: n }, (_v, i) => ({ hostname: `PC${i}` })), }) it('shows at most five rows so one card cannot bury the rest', () => { expect(visibleRows(many(40))).toHaveLength(5) expect(overflowCount(many(40))).toBe(35) }) it('does not claim an overflow when everything fits', () => { expect(visibleRows(many(3))).toHaveLength(3) expect(overflowCount(many(3))).toBe(0) }) }) describe('empty handling', () => { it('hides a card with nothing to report by default', () => { // The whole point: a card saying "nothing wrong" daily trains people to // stop reading the page. expect(cardVisible({ render: 'exceptions', _data: [] })).toBe(false) }) it('keeps a card whose absence is itself news when it opts in', () => { expect(cardVisible({ render: 'exceptions', _data: [], empty: 'line' })).toBe(true) }) it('shows a card that has findings', () => { expect(cardVisible({ render: 'exceptions', _data: [{ a: 1 }] })).toBe(true) }) it('hides a card whose fetch failed rather than drawing it empty', () => { expect(cardVisible({ render: 'exceptions', _data: null })).toBe(false) }) it('hides a zero metric but shows a non-zero one', () => { expect(cardVisible({ render: 'metric', _data: { value: 0 } })).toBe(false) expect(cardVisible({ render: 'metric', _data: { value: 3 } })).toBe(true) expect(metricValue({ render: 'metric', _data: { value: 3 } })).toBe(3) }) }) describe('ordering', () => { it('puts severity before position, so info never sits above a failure', () => { const ordered = sortCards([ { id: 'info-recent', severity: 'info', position: 1 }, { id: 'crit-failures', severity: 'critical', position: 90 }, { id: 'warn-toner', severity: 'warning', position: 50 }, ]) expect(ordered.map((c) => c.id)) .toEqual(['crit-failures', 'warn-toner', 'info-recent']) }) it('falls back to position within one severity', () => { const ordered = sortCards([ { id: 'b', severity: 'critical', position: 20 }, { id: 'a', severity: 'critical', position: 10 }, ]) expect(ordered.map((c) => c.id)).toEqual(['a', 'b']) }) }) describe('gating', () => { it('drops a card the user lacks permission for', () => { // The dashboard must not become a way around RBAC. const has = (name) => name === 'printers.view' const kept = permittedCards([ { id: 'toner', permission: 'printers.view' }, { id: 'enforce', permission: 'geenforce.manage' }, { id: 'open', permission: null }, ], has) expect(kept.map((c) => c.id)).toEqual(['toner', 'open']) }) it('skips a render mode this core does not have', () => { // A plugin built against a newer core degrades instead of leaving a hole. const kept = renderableCards([ { id: 'ok', render: 'exceptions' }, { id: 'future', render: 'sparkline' }, ]) expect(kept.map((c) => c.id)).toEqual(['ok']) }) }) describe('chips and tooltips', () => { const printerCard = { render: 'exceptions', map: { title: 'printername', titletooltip: 'location', chips: 'supplies', link: '/printers/{printerid}', }, _data: [{ printerid: 7, printername: 'WJ-HP-402', location: 'Cell B, north wall', supplies: [ { text: 'Black 4%', title: 'CF226X (high)', level: 'critical' }, { text: 'Cyan 9%', title: 'No part number on file for this model', level: 'low' }, ], }], } it('builds one chip per depleted supply, each with its reorder tooltip', () => { const [row] = cardRows(printerCard) expect(row.chips.map((c) => c.text)).toEqual(['Black 4%', 'Cyan 9%']) expect(row.chips[0].title).toBe('CF226X (high)') expect(row.chips[0].level).toBe('critical') }) it('puts the location on the title as a tooltip, not in the line', () => { const [row] = cardRows(printerCard) expect(row.titletip).toBe('Cell B, north wall') expect(row.detail).toBe('') }) it('has no chips when a card declares none', () => { expect(cardRows({ render: 'exceptions', map: { title: 'x' }, _data: [{ x: 'y' }] })[0].chips) .toEqual([]) }) })