|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import { describe, it, expect } from 'vitest'; |
| 4 | +import { ComponentPropsMap } from '@objectstack/spec/ui'; |
| 5 | + |
| 6 | +import { CommandCenterPage } from '../src/ui/pages/index.js'; |
| 7 | + |
| 8 | +/** |
| 9 | + * The Command Center's KPI tiles write `filter` in the form their own contract |
| 10 | + * declares. |
| 11 | + * |
| 12 | + * The page is built through local helpers (`kpi()`, `band()`, `panel()`), and |
| 13 | + * `kpi()` spreads its `filter` argument straight into the `object-metric` |
| 14 | + * node's `properties`. `PageComponent.properties` is an open bag, so nothing in |
| 15 | + * `definePage()` judges what lands there: four tiles wrote the MongoDB-style |
| 16 | + * record form (`{ status: 'active' }`) that `ComponentPropsMap['object-metric']` |
| 17 | + * refuses by name, and the page still loaded. `os validate` reports it only as |
| 18 | + * an advisory warning, so a green run did not mean a clean page. |
| 19 | + * |
| 20 | + * This pin evaluates the page as authored (helpers included), finds every |
| 21 | + * `object-metric` node wherever it sits in the tree, and parses its WHOLE |
| 22 | + * `properties` bag against the row: the verdict on a `filter` value is a value |
| 23 | + * verdict, so the full parse has to be green, not just free of unknown keys. |
| 24 | + */ |
| 25 | + |
| 26 | +type AnyRec = Record<string, unknown>; |
| 27 | + |
| 28 | +const isRec = (v: unknown): v is AnyRec => !!v && typeof v === 'object' && !Array.isArray(v); |
| 29 | + |
| 30 | +/** Every `object-metric` node in the page, at any depth. */ |
| 31 | +function metricNodes(root: unknown): AnyRec[] { |
| 32 | + const out: AnyRec[] = []; |
| 33 | + const visit = (v: unknown): void => { |
| 34 | + if (Array.isArray(v)) { |
| 35 | + for (const item of v) visit(item); |
| 36 | + return; |
| 37 | + } |
| 38 | + if (!isRec(v)) return; |
| 39 | + if (v.type === 'object-metric') out.push(v); |
| 40 | + for (const child of Object.values(v)) visit(child); |
| 41 | + }; |
| 42 | + visit(root); |
| 43 | + return out; |
| 44 | +} |
| 45 | + |
| 46 | +/** The tiles that scope their count by a filter; dropping one changes what the tile counts. */ |
| 47 | +const FILTERED_TILES = ['cc_k1', 'cc_k2', 'cc_k3', 'cc_k4']; |
| 48 | + |
| 49 | +const metricRow = ComponentPropsMap['object-metric']; |
| 50 | + |
| 51 | +describe('Command Center — object-metric properties parse against their ComponentPropsMap row', () => { |
| 52 | + const nodes = metricNodes(CommandCenterPage.regions); |
| 53 | + |
| 54 | + it('finds the KPI tiles through the helpers, filtered ones included', () => { |
| 55 | + expect(nodes.length).toBeGreaterThan(0); |
| 56 | + const filtered = nodes.filter((n) => isRec(n.properties) && n.properties.filter !== undefined); |
| 57 | + expect(filtered.map((n) => n.id).sort()).toEqual(expect.arrayContaining(FILTERED_TILES)); |
| 58 | + }); |
| 59 | + |
| 60 | + it('every object-metric properties bag parses clean', () => { |
| 61 | + const failures: string[] = []; |
| 62 | + for (const node of nodes) { |
| 63 | + const result = metricRow.safeParse(node.properties); |
| 64 | + if (result.success) continue; |
| 65 | + for (const issue of result.error.issues) { |
| 66 | + failures.push(`${String(node.id)} › properties.${issue.path.join('.')}: ${issue.code}`); |
| 67 | + } |
| 68 | + } |
| 69 | + expect(failures).toEqual([]); |
| 70 | + }); |
| 71 | + |
| 72 | + it('the filtered tiles carry a non-empty ViewFilterRule array', () => { |
| 73 | + for (const id of FILTERED_TILES) { |
| 74 | + const node = nodes.find((n) => n.id === id); |
| 75 | + const filter = isRec(node?.properties) ? node.properties.filter : undefined; |
| 76 | + expect(Array.isArray(filter) && filter.length > 0, `${id}: filter is a non-empty array`).toBe(true); |
| 77 | + } |
| 78 | + }); |
| 79 | +}); |
0 commit comments