import { describe, it, expect } from 'vitest' import { toApiPath, rows, mapMeta, mapLink, cardRows, metricValue, cardVisible, sortCards, permittedCards, renderableCards, visibleRows, overflowCount, queueRows, queueOverflow, contextCards, severityTotals, QUEUEPERSOURCE, QUEUEMAX, singularise, cardUnavailable, unavailableCards, sourceFacets, } 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([]) }) }) describe('stacked rows', () => { const card = { render: 'list', layout: 'stacked', map: { title: 'typename', detail: 'message', detailtooltip: 'fullmessage' }, _data: [{ typename: 'Recertification', message: 'Short text', fullmessage: 'Short text' }], } it('offers no tooltip when the detail is already whole', () => { // A tooltip repeating what is on screen is noise, and it makes the cursor // change for no reason. expect(cardRows(card)[0].detailtip).toBe('') }) it('offers the full text when the detail was trimmed', () => { const trimmed = { ...card, _data: [{ typename: 'General', message: 'Start of a long message...', fullmessage: 'Start of a long message that continues well past the card', }] } expect(cardRows(trimmed)[0].detailtip) .toBe('Start of a long message that continues well past the card') }) }) // --------------------------------------------------------------------------- // The merged queue. These exist because the old grid failed in ways that were // invisible: a card truncated at 5 of 50 and nothing said where the rest went, // and severity was declared but the layout ignored it. // --------------------------------------------------------------------------- const critA = { id: 'endpoints-stability', title: 'Endpoints with crashes', render: 'exceptions', severity: 'critical', position: 20, viewall: '/pcs', map: { title: 'hostname' }, _data: [{ hostname: 'PC-CMM-TEST' }, { hostname: 'HOST-7' }], } const critB = { id: 'geenforce-failures', title: 'Enforcement failures', render: 'exceptions', severity: 'critical', position: 10, viewall: '/geenforce', map: { title: 'hostname' }, _data: [{ hostname: 'WJPC0615' }], } const warnA = { id: 'computers-quiet', title: 'PCs gone silent', render: 'exceptions', severity: 'warning', position: 30, map: { title: 'hostname' }, _data: [{ hostname: 'FFBWTH63' }], } const infoA = { id: 'warranty-expiring', title: 'Warranties expiring', render: 'exceptions', severity: 'info', position: 40, viewall: '/reports/warranty', map: { title: 'assetnumber' }, _data: Array.from({ length: 50 }, (_, i) => ({ assetnumber: 'A' + i })), } describe('queueRows', () => { it('merges every queue card into one list, worst first', () => { const queue = queueRows([warnA, critA, critB]) expect(queue.map((r) => r.severity)).toEqual( ['critical', 'critical', 'critical', 'warning']) }) it('orders by declared position within a severity', () => { // geenforce is position 10, endpoints 20: a failure outranks a crash. const queue = queueRows([critA, critB]) expect(queue[0].title).toBe('WJPC0615') }) it('keeps the source on every row, so a merged row can still say why', () => { const queue = queueRows([critB]) expect(queue[0].source).toBe('Enforcement failures') expect(queue[0].sourceid).toBe('geenforce-failures') expect(queue[0].viewall).toBe('/geenforce') }) it('leaves info cards out - they are context, not work', () => { const queue = queueRows([critB, infoA]) expect(queue).toHaveLength(1) expect(queue.every((r) => r.severity !== 'info')).toBe(true) }) it('caps one source so a noisy check cannot bury a rare one', () => { const noisy = { ...critA, _data: Array.from({ length: 40 }, (_, i) => ({ hostname: 'H' + i })) } const queue = queueRows([noisy, warnA]) expect(queue.filter((r) => r.sourceid === 'endpoints-stability')).toHaveLength(QUEUEPERSOURCE) // the warning still made it onto the board expect(queue.some((r) => r.sourceid === 'computers-quiet')).toBe(true) }) it('caps the whole board', () => { const many = Array.from({ length: 6 }, (_, i) => ({ ...critA, id: 'c' + i, position: i, _data: Array.from({ length: 5 }, (_, j) => ({ hostname: 'H' + i + j })), })) expect(queueRows(many)).toHaveLength(QUEUEMAX) }) it('gives every row a stable key', () => { const keys = queueRows([critA, critB]).map((r) => r.key) expect(new Set(keys).size).toBe(keys.length) }) }) describe('queueOverflow', () => { it('reports what was hidden, per source, with somewhere to go', () => { const noisy = { ...critA, _data: Array.from({ length: 12 }, (_, i) => ({ hostname: 'H' + i })) } const [entry] = queueOverflow([noisy]) expect(entry.hidden).toBe(7) // 12 rows, 5 shown expect(entry.viewall).toBe('/pcs') expect(entry.title).toBe('Endpoints with crashes') }) it('says nothing when everything is on the board', () => { expect(queueOverflow([critB, warnA])).toEqual([]) }) it('ignores info cards, which are not in the queue to overflow from', () => { expect(queueOverflow([infoA])).toEqual([]) }) it('lists the biggest backlog first', () => { const a = { ...critA, id: 'a', _data: Array.from({ length: 8 }, (_, i) => ({ hostname: 'x' + i })) } const b = { ...critA, id: 'b', _data: Array.from({ length: 20 }, (_, i) => ({ hostname: 'y' + i })) } expect(queueOverflow([a, b])[0].id).toBe('b') }) }) describe('contextCards', () => { it('is the info cards, sorted', () => { const context = contextCards([critA, infoA, warnA]) expect(context.map((c) => c.id)).toEqual(['warranty-expiring']) }) }) describe('severityTotals', () => { it('counts ROWS, not cards', () => { // Three cards holding one row each is three things to do, not three cards. const totals = severityTotals([critA, critB, warnA, infoA]) expect(totals).toEqual({ critical: 3, warning: 1, info: 50 }) }) it('counts a metric card by its value', () => { const metric = { id: 'm', render: 'metric', severity: 'warning', _data: { value: 7 } } expect(severityTotals([metric]).warning).toBe(7) }) it('treats an undeclared severity as info, matching the sort', () => { const bare = { id: 'b', render: 'exceptions', _data: [{}, {}] } expect(severityTotals([bare]).info).toBe(2) }) }) describe('singularise', () => { it('drops the s at exactly one', () => { expect(singularise(' days', 1)).toBe(' day') expect(singularise(' events', 1)).toBe(' event') }) it('leaves plurals alone', () => { expect(singularise(' days', 0)).toBe(' days') expect(singularise(' days', 2)).toBe(' days') expect(singularise(' days', 19)).toBe(' days') }) it('singularises the first word and keeps what follows', () => { // The card that made this matter: ' events (48h)' read "1 events (48h)" // five rows running once the queue merged. expect(singularise(' events (48h)', 1)).toBe(' event (48h)') expect(singularise(' events (48h)', 2)).toBe(' events (48h)') }) it('handles a trailing word rather than refusing it', () => { // "1 day ago" is correct; an earlier version left this plural out of // caution about chopping the wrong token. expect(singularise(' days ago', 1)).toBe(' day ago') }) it('leaves a unit that merely ends in s', () => { expect(singularise(' ms', 1)).toBe(' ms') expect(singularise('%', 1)).toBe('%') }) it('handles no suffix', () => { expect(singularise('', 1)).toBe('') expect(singularise(undefined, 1)).toBe('') }) }) describe('cardUnavailable', () => { // "Nothing is low" and "we cannot tell whether anything is low" are different // answers. The printer supplies card returned the same empty for both, so a // dead Zabbix rendered as "Low Supplies 0" - reassurance, not information. it('reads the reason a card gives for having no rows', () => { const card = { _data: { rows: [], unavailable: 'Supply monitoring is not reachable' } } expect(cardUnavailable(card)).toBe('Supply monitoring is not reachable') }) it('is empty for a card that simply has nothing to report', () => { expect(cardUnavailable({ _data: { rows: [] } })).toBe('') expect(cardUnavailable({ _data: [] })).toBe('') expect(cardUnavailable({ _data: null })).toBe('') }) it('ignores a non-string, so bad data cannot render as a reason', () => { expect(cardUnavailable({ _data: { rows: [], unavailable: true } })).toBe('') }) it('shows an unavailable card even when its empty rule says hide', () => { const card = { empty: 'hide', render: 'exceptions', _data: { rows: [], unavailable: 'Supply monitoring is not reachable' }, } expect(cardVisible(card)).toBe(true) }) it('still hides a card that is merely empty', () => { expect(cardVisible({ empty: 'hide', render: 'exceptions', _data: { rows: [] } })).toBe(false) }) it('still hides a card whose fetch failed, which is not the same thing', () => { expect(cardVisible({ empty: 'hide', render: 'exceptions', _data: null })).toBe(false) }) it('picks out the unavailable ones', () => { const blind = { id: 'a', _data: { rows: [], unavailable: 'not reachable' } } const fine = { id: 'b', _data: { rows: [{}] } } expect(unavailableCards([blind, fine]).map((c) => c.id)).toEqual(['a']) }) }) describe('sourceFacets', () => { it('is one chip per contributing check, worst first', () => { const facets = sourceFacets([warnA, critA, critB]) expect(facets.map((f) => f.key)).toEqual( ['geenforce-failures', 'endpoints-stability', 'computers-quiet']) }) it('labels with the shortname, since the title is a heading', () => { const card = { ...critA, shortname: 'Crashes' } expect(sourceFacets([card])[0].label).toBe('Crashes') }) it('falls back to the title when no shortname is declared', () => { expect(sourceFacets([critA])[0].label).toBe('Endpoints with crashes') }) it('counts every row, not just the ones the queue had room for', () => { // The chip says how many there are; the queue caps what it shows. A chip // reading 5 above a "3 more" line would be its own small lie. const noisy = { ...critA, _data: Array.from({ length: 12 }, (_, i) => ({ hostname: 'H' + i })) } expect(sourceFacets([noisy])[0].count).toBe(12) }) it('leaves out checks with nothing to show', () => { const empty = { ...critA, id: 'empty', _data: [] } expect(sourceFacets([empty])).toEqual([]) }) it('leaves out info cards, which are not in the queue', () => { expect(sourceFacets([infoA])).toEqual([]) }) })