360 lines
14 KiB
JavaScript
360 lines
14 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
toApiPath, subst, rows, mapTitle, mapBadge, mapMeta, mapBody, manageLink,
|
|
keyvalueFields, tableColumns, badges, panelVisible, tabSections, rollupStatus,
|
|
rowAction, facets, facetedRows,
|
|
} from './pluginAssetPanels'
|
|
|
|
const warrantyPanel = {
|
|
id: 'warranty', title: 'Warranty', render: 'list',
|
|
map: {
|
|
title: 'vendor',
|
|
badge: {
|
|
label: 'status', color: 'statuscolor',
|
|
labelmap: { active: 'Active', expired: 'Expired' },
|
|
},
|
|
meta: [
|
|
{ key: 'servicelevel' },
|
|
{ key: 'enddate', label: 'Ends', format: 'date' },
|
|
{ key: 'servicetag', label: 'Tag', mono: true },
|
|
],
|
|
},
|
|
empty: 'No warranty on record.',
|
|
manage: { to: '/warranties?addfor={assetid}', label: 'Add / manage' },
|
|
_data: [
|
|
{
|
|
vendor: 'Dell', status: 'active', statuscolor: '#4CAF50',
|
|
servicelevel: 'ProSupport', enddate: '2027-01-01', servicetag: 'ABC123',
|
|
},
|
|
],
|
|
}
|
|
|
|
describe('endpoint + subst', () => {
|
|
it('substitutes {assetid} and strips the leading /api', () => {
|
|
expect(toApiPath('/api/warranty/asset/{assetid}', 5)).toBe('/warranty/asset/5')
|
|
expect(subst('/warranties?addfor={assetid}', 9)).toBe('/warranties?addfor=9')
|
|
})
|
|
it('only strips a leading /api segment, not /apixyz', () => {
|
|
expect(toApiPath('/apixyz/thing', 1)).toBe('/apixyz/thing')
|
|
})
|
|
})
|
|
|
|
describe('list render mapping (warranty)', () => {
|
|
it('maps title, badge (labelmap + color), and formatted meta', () => {
|
|
const item = rows(warrantyPanel)[0]
|
|
expect(mapTitle(warrantyPanel, item)).toBe('Dell')
|
|
|
|
const badge = mapBadge(warrantyPanel, item)
|
|
expect(badge.label).toBe('Active')
|
|
expect(badge.color).toBe('#4CAF50')
|
|
|
|
const meta = mapMeta(warrantyPanel, item)
|
|
expect(meta[0].text).toBe('ProSupport')
|
|
expect(meta[1].text).toMatch(/^Ends /) // date formatted + labelled
|
|
expect(meta[2]).toEqual({ text: 'Tag ABC123', mono: true })
|
|
})
|
|
|
|
it('drops empty meta values', () => {
|
|
const item = { vendor: 'HP', status: 'expired', statuscolor: '#F44336' }
|
|
const meta = mapMeta(warrantyPanel, { ...item })
|
|
expect(meta).toEqual([]) // no servicelevel/enddate/servicetag
|
|
expect(mapBadge(warrantyPanel, item).label).toBe('Expired')
|
|
})
|
|
|
|
it('builds the manage link with the assetid', () => {
|
|
expect(manageLink(warrantyPanel, 7)).toBe('/warranties?addfor=7')
|
|
})
|
|
it('is visible when it has rows or an empty message', () => {
|
|
expect(panelVisible(warrantyPanel)).toBe(true)
|
|
expect(panelVisible({ ...warrantyPanel, _data: [] })).toBe(true) // has empty
|
|
expect(panelVisible({ render: 'list', _data: [] })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('expandable row body (activity feed)', () => {
|
|
const activityPanel = {
|
|
id: 'endpoints-activity', render: 'list',
|
|
map: { title: 'title', body: 'body' },
|
|
}
|
|
|
|
it('returns the body only for rows that carry one', () => {
|
|
expect(mapBody(activityPanel, { title: 'Crash', body: 'Faulting module' }))
|
|
.toBe('Faulting module')
|
|
expect(mapBody(activityPanel, { title: 'Crash' })).toBeNull()
|
|
expect(mapBody(activityPanel, { title: 'Crash', body: '' })).toBeNull()
|
|
})
|
|
|
|
it('returns null when the panel declares no body key', () => {
|
|
expect(mapBody(warrantyPanel, { body: 'ignored' })).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('list row filtering (tracked services vs processes)', () => {
|
|
const feedPanel = {
|
|
id: 'endpoints-tracked-services', render: 'list',
|
|
filter: { key: 'kind', equals: 'service' },
|
|
map: { title: 'name' },
|
|
_data: [
|
|
{ name: 'UDC MTConnect Agent', kind: 'service' },
|
|
{ name: 'DncMain', kind: 'process' },
|
|
{ name: 'Spooler', kind: 'SERVICE' },
|
|
],
|
|
}
|
|
|
|
it('keeps only rows whose filter key matches, case-insensitively', () => {
|
|
const kept = rows(feedPanel).map((r) => r.name)
|
|
expect(kept).toEqual(['UDC MTConnect Agent', 'Spooler'])
|
|
})
|
|
|
|
it('filters the same feed to processes when equals is process', () => {
|
|
const kept = rows({ ...feedPanel, filter: { key: 'kind', equals: 'process' } })
|
|
expect(kept.map((r) => r.name)).toEqual(['DncMain'])
|
|
})
|
|
|
|
it('returns every row when no filter is declared', () => {
|
|
const kept = rows({ ...feedPanel, filter: undefined })
|
|
expect(kept).toHaveLength(3)
|
|
})
|
|
})
|
|
|
|
describe('keyvalue + table + badge', () => {
|
|
it('keyvalue reads a fields array or a plain object', () => {
|
|
const fromFields = keyvalueFields({
|
|
render: 'keyvalue', _data: { fields: [{ label: 'Cal due', value: '2026-01-01', format: 'date' }] },
|
|
})
|
|
expect(fromFields[0].label).toBe('Cal due')
|
|
|
|
const fromObject = keyvalueFields({ render: 'keyvalue', _data: { hostname: 'PC1', online: true } })
|
|
expect(fromObject).toEqual([
|
|
{ label: 'Hostname', value: 'PC1' },
|
|
{ label: 'Online', value: 'Yes' },
|
|
])
|
|
})
|
|
|
|
it('table uses declared columns, else infers from row keys', () => {
|
|
const declared = tableColumns({ columns: [{ key: 'a', label: 'Alpha' }], _data: [{ a: 1 }] })
|
|
expect(declared).toEqual([{ key: 'a', label: 'Alpha' }])
|
|
|
|
const inferred = tableColumns({ _data: [{ vendor: 'x', model: 'y' }] })
|
|
expect(inferred.map((c) => c.label)).toEqual(['Vendor', 'Model'])
|
|
})
|
|
|
|
it('badge reads an array or a badges field', () => {
|
|
expect(badges({ render: 'badge', _data: [{ label: 'A', color: '#111' }] })).toEqual([{ label: 'A', color: '#111' }])
|
|
expect(badges({ render: 'badge', _data: { badges: [{ label: 'B', color: '#222' }] } })).toEqual([{ label: 'B', color: '#222' }])
|
|
})
|
|
})
|
|
|
|
describe('tabSections', () => {
|
|
const panel = {
|
|
render: 'tabs',
|
|
_data: {
|
|
sections: [
|
|
{ label: 'General', fields: [{ label: 'Cnc', value: 'OKUMA' }] },
|
|
{ label: 'eFocas', fields: [{ label: 'IpAddr', value: '192.168.1.1', mono: true }] },
|
|
// A section whose fields all dropped out must not leave an empty tab.
|
|
{ label: 'NTSHR', fields: [] },
|
|
],
|
|
},
|
|
}
|
|
|
|
it('returns one entry per non-empty section', () => {
|
|
expect(tabSections(panel).map((s) => s.label)).toEqual(['General', 'eFocas'])
|
|
})
|
|
|
|
it('formats field values and carries mono through', () => {
|
|
const efocas = tabSections(panel)[1]
|
|
expect(efocas.fields[0]).toEqual({ label: 'IpAddr', value: '192.168.1.1', mono: true })
|
|
})
|
|
|
|
it('is empty for a payload with no sections', () => {
|
|
expect(tabSections({ render: 'tabs', _data: null })).toEqual([])
|
|
})
|
|
|
|
it('drives panel visibility', () => {
|
|
expect(panelVisible(panel)).toBe(true)
|
|
expect(panelVisible({ render: 'tabs', _data: { sections: [] } })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('rollupStatus', () => {
|
|
const status = {
|
|
key: 'statecolor',
|
|
levels: { green: 'ok', yellow: 'warning', red: 'critical' },
|
|
}
|
|
|
|
it('returns null when the panel declares no status', () => {
|
|
expect(rollupStatus({ _data: [{ statecolor: 'green' }] })).toBeNull()
|
|
})
|
|
|
|
it('returns null when there are no rows', () => {
|
|
expect(rollupStatus({ status, _data: [] })).toBeNull()
|
|
})
|
|
|
|
it('is ok (green) when every row is ok', () => {
|
|
const result = rollupStatus({ status, _data: [{ statecolor: 'green' }, { statecolor: 'green' }] })
|
|
expect(result).toEqual({ level: 'ok', color: '#2dce89' })
|
|
})
|
|
|
|
it('is warning (yellow) when a warning is present but no critical', () => {
|
|
const result = rollupStatus({ status, _data: [{ statecolor: 'green' }, { statecolor: 'yellow' }] })
|
|
expect(result).toEqual({ level: 'warning', color: '#ffc107' })
|
|
})
|
|
|
|
it('is critical (red) when any row is stopped', () => {
|
|
const result = rollupStatus({ status, _data: [{ statecolor: 'yellow' }, { statecolor: 'red' }] })
|
|
expect(result).toEqual({ level: 'critical', color: '#f5365c' })
|
|
})
|
|
|
|
it('ignores unmapped values without raising the level', () => {
|
|
const result = rollupStatus({ status, _data: [{ statecolor: 'gray' }, { statecolor: 'green' }] })
|
|
expect(result).toEqual({ level: 'ok', color: '#2dce89' })
|
|
})
|
|
|
|
it('returns null when no row value maps to a known level', () => {
|
|
expect(rollupStatus({ status, _data: [{ statecolor: 'gray' }] })).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('rowAction (inline MTConnect toggle)', () => {
|
|
const panel = { id: 'endpoints-services', render: 'list', commandendpoint: '/api/endpoints/5/commands' }
|
|
|
|
it('returns null for rows without an action', () => {
|
|
expect(rowAction(panel, { name: 'Some Service' })).toBeNull()
|
|
expect(rowAction(panel, { name: 'x', action: { status: 'mtconnect_status' } })).toBeNull()
|
|
})
|
|
|
|
it('offers Stop when the row is running', () => {
|
|
const info = rowAction(panel, {
|
|
name: 'MTConnect',
|
|
action: { running: true, start: 'mtconnect_start', stop: 'mtconnect_stop', status: 'mtconnect_status' },
|
|
})
|
|
expect(info.running).toBe(true)
|
|
expect(info.toggle).toBe('mtconnect_stop')
|
|
expect(info.toggleLabel).toBe('Stop')
|
|
expect(info.endpoint).toBe('/api/endpoints/5/commands')
|
|
})
|
|
|
|
it('offers Start when the row is not running', () => {
|
|
const info = rowAction(panel, {
|
|
name: 'MTConnect',
|
|
action: { running: false, start: 'mtconnect_start', stop: 'mtconnect_stop', status: 'mtconnect_status' },
|
|
})
|
|
expect(info.running).toBe(false)
|
|
expect(info.toggle).toBe('mtconnect_start')
|
|
expect(info.toggleLabel).toBe('Start')
|
|
})
|
|
})
|
|
|
|
describe('facet chips on a card that shows a health dot', () => {
|
|
// The case that prompted this: the Activity card shows a RED dot because a
|
|
// critical event is in the data, while the visible rows are all info because
|
|
// the critical is older and sits further down. The dot and the list appear to
|
|
// disagree. Chips resolve it: the count sits under the dot, and one click
|
|
// brings the critical rows to the surface.
|
|
const activity = {
|
|
id: 'endpoints-activity',
|
|
render: 'list',
|
|
status: {
|
|
key: 'severity',
|
|
levels: { info: 'ok', warning: 'warning', critical: 'critical' },
|
|
},
|
|
_data: [
|
|
{ title: 'Successful logon', severity: 'info' },
|
|
{ title: 'Successful logon', severity: 'info' },
|
|
{ title: 'Scheduled task created', severity: 'warning' },
|
|
{ title: 'Unexpected shutdown', severity: 'critical' },
|
|
],
|
|
}
|
|
|
|
it('derives chips from the status declaration, so no panel declares filters twice', () => {
|
|
const chips = facets(activity)
|
|
expect(chips.map((c) => c.value)).toEqual(['critical', 'warning', 'info'])
|
|
})
|
|
|
|
it('orders worst first rather than alphabetically', () => {
|
|
expect(facets(activity)[0].value).toBe('critical')
|
|
})
|
|
|
|
it('counts from the same rows the dot is computed from, so they cannot disagree', () => {
|
|
const chips = facets(activity)
|
|
expect(chips.find((c) => c.value === 'critical').count).toBe(1)
|
|
expect(chips.find((c) => c.value === 'info').count).toBe(2)
|
|
expect(rollupStatus(activity).level).toBe('critical')
|
|
})
|
|
|
|
it('labels a colour-keyed panel by its level, not by the colour name', () => {
|
|
const services = {
|
|
id: 'svc',
|
|
render: 'list',
|
|
status: { key: 'statecolor', levels: { green: 'ok', red: 'critical' } },
|
|
_data: [{ statecolor: 'green' }, { statecolor: 'red' }],
|
|
}
|
|
const labels = facets(services).map((c) => c.label)
|
|
expect(labels).toContain('Critical')
|
|
expect(labels).not.toContain('Red')
|
|
})
|
|
|
|
it('shows everything when nothing is selected, rather than hiding the card', () => {
|
|
expect(facetedRows(activity, []).length).toBe(4)
|
|
expect(facetedRows(activity, undefined).length).toBe(4)
|
|
})
|
|
|
|
it('filters to the selected severities, and allows more than one', () => {
|
|
expect(facetedRows(activity, ['critical']).map((r) => r.title))
|
|
.toEqual(['Unexpected shutdown'])
|
|
expect(facetedRows(activity, ['critical', 'warning']).length).toBe(2)
|
|
})
|
|
|
|
it('offers no chips for a panel with no status rollup', () => {
|
|
expect(facets({ id: 'x', render: 'list', _data: [{ a: 1 }] })).toEqual([])
|
|
})
|
|
|
|
it('prefers an explicit facets declaration over the status rollup', () => {
|
|
// Services carry BOTH: a statecolor status dot and a statelabel facet.
|
|
// Chips must follow the declaration, because a service is Running or
|
|
// Stopped - "OK" and "Critical" is not how anybody describes one.
|
|
const services = {
|
|
id: 'endpoints-services',
|
|
render: 'list',
|
|
status: { key: 'statecolor', levels: { green: 'ok', red: 'critical' } },
|
|
facets: { key: 'statelabel', order: ['Running', 'Stopped', 'Paused'] },
|
|
_data: [
|
|
{ statelabel: 'Running', statecolor: 'green' },
|
|
{ statelabel: 'Stopped', statecolor: 'red' },
|
|
{ statelabel: 'Paused', statecolor: 'red' },
|
|
],
|
|
}
|
|
const chips = facets(services)
|
|
expect(chips.map((c) => c.label)).toEqual(['Running', 'Stopped', 'Paused'])
|
|
expect(chips.map((c) => c.label)).not.toContain('Critical')
|
|
})
|
|
|
|
it('gives an unlisted state its own chip rather than dropping the row', () => {
|
|
// Windows has transitional states, and an agent can report something the
|
|
// order list never anticipated. Those rows must stay reachable.
|
|
const services = {
|
|
id: 'svc2',
|
|
render: 'list',
|
|
facets: { key: 'statelabel', order: ['Running', 'Stopped'] },
|
|
_data: [{ statelabel: 'Running' }, { statelabel: 'Start Pending' }],
|
|
}
|
|
const chips = facets(services)
|
|
expect(chips.map((c) => c.value)).toEqual(['Running', 'Start Pending'])
|
|
expect(facetedRows(services, ['Start Pending']).length).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('panelVisible and empty', () => {
|
|
it('keeps a table panel that declares an empty message', () => {
|
|
// It is the only card in the Software tab, so hiding it hides the tab and
|
|
// the reader cannot tell "nothing reported" from "feature missing".
|
|
const panel = { render: 'table', _data: { items: [] }, empty: 'No installed software reported.' }
|
|
expect(panelVisible(panel)).toBe(true)
|
|
})
|
|
|
|
it('still hides a table panel with nothing to say', () => {
|
|
expect(panelVisible({ render: 'table', _data: { items: [] } })).toBe(false)
|
|
})
|
|
})
|