|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import { describe, it, expect } from 'vitest'; |
| 4 | +import { RecordLineItemsProps } from '@objectstack/spec/ui'; |
| 5 | + |
| 6 | +import { InvoiceDocumentPage } from '../src/ui/pages/index.js'; |
| 7 | +import { InvoiceLine } from '../src/data/objects/invoice.object.js'; |
| 8 | + |
| 9 | +/** |
| 10 | + * The invoice print page heads its line-items table with labels. |
| 11 | + * |
| 12 | + * `record:line_items` draws a column exactly as declared. Unlike the |
| 13 | + * master-detail grid, it does not fill a column from the child object's |
| 14 | + * field, so a column with no `label` is headed by its raw field name: the |
| 15 | + * printed invoice read `description` / `quantity` / `unit_price` / `amount`. |
| 16 | + * |
| 17 | + * The pin holds each column to the label its own child field declares, and |
| 18 | + * never to the field name. Deriving the expected heading from the field keeps |
| 19 | + * the print in step with the master-detail grid, which hydrates the same |
| 20 | + * labels for the same fields. |
| 21 | + */ |
| 22 | + |
| 23 | +type Column = Record<string, unknown>; |
| 24 | +type Component = { type?: unknown; properties?: Record<string, unknown> }; |
| 25 | + |
| 26 | +const page = InvoiceDocumentPage as unknown as { regions?: { components?: Component[] }[] }; |
| 27 | + |
| 28 | +const lineItemBlocks = (page.regions ?? []) |
| 29 | + .flatMap((region) => region.components ?? []) |
| 30 | + .filter((component) => component.type === 'record:line_items'); |
| 31 | + |
| 32 | +const columnsOf = (block: Component | undefined): Column[] => |
| 33 | + (Array.isArray(block?.properties?.columns) ? block!.properties!.columns : []) as Column[]; |
| 34 | + |
| 35 | +const childFields = (InvoiceLine.fields ?? {}) as Record<string, { label?: unknown }>; |
| 36 | + |
| 37 | +describe('Invoice print page — the line-items table is headed by labels, not field names', () => { |
| 38 | + const [block, ...rest] = lineItemBlocks; |
| 39 | + |
| 40 | + it('carries one line-items block over the invoice lines', () => { |
| 41 | + expect(block, 'the invoice print page must carry its line-items table').toBeTruthy(); |
| 42 | + expect(rest).toHaveLength(0); |
| 43 | + expect(block.properties?.childObject).toBe(InvoiceLine.name); |
| 44 | + expect(columnsOf(block).map((c) => c.name)).toEqual(['description', 'quantity', 'unit_price', 'amount']); |
| 45 | + }); |
| 46 | + |
| 47 | + it('heads every column with its child field\'s own label, never the field name', () => { |
| 48 | + for (const column of columnsOf(block)) { |
| 49 | + const name = column.name as string; |
| 50 | + const fieldLabel = childFields[name]?.label; |
| 51 | + expect(typeof fieldLabel, `showcase_invoice_line.${name} must declare a label`).toBe('string'); |
| 52 | + expect(column.label, `column '${name}' must declare its heading`).toBe(fieldLabel); |
| 53 | + expect(column.label, `column '${name}' must not be headed by its field name`).not.toBe(name); |
| 54 | + } |
| 55 | + }); |
| 56 | + |
| 57 | + it('parses against the spec\'s `record:line_items` row with the labels intact', () => { |
| 58 | + const result = RecordLineItemsProps.safeParse(block.properties); |
| 59 | + expect(result.success, JSON.stringify(result.error?.issues)).toBe(true); |
| 60 | + expect(result.data).toEqual(block.properties); |
| 61 | + }); |
| 62 | +}); |
0 commit comments