Files
shopdb-flask/frontend/src/components/dashboardCards.spec.js
2026-08-11 15:58:15 -04:00

229 lines
7.8 KiB
JavaScript

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([])
})
})
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')
})
})