From aa20ab0102b5cc952f3b44010d6fea24069f8a09 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 12:32:17 +0000 Subject: [PATCH] fix(plugin-grid): retire the FIELD-meta-dead `titleFormat` relational key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectGrid`'s `RELATIONAL_META_KEYS` copied eight keys off the object-schema field def onto every relational column's `fieldMeta`, at all three of `generateColumns`'s column-building call sites. `titleFormat` was one of them and has ZERO readers on a field meta. This is a different zero from the one objectui#6711 acted on, and a stronger one: `titleFormat` is a real, live key with many readers — it just has none on a field meta. The sweep did not fail to find readers; it found every member read of the identifier across `packages/` and `apps/` and classified each by receiver. All of them are OBJECT schemas (`objectDef` / `objectSchema` / `objSchema`, and `refObjectSchema?.titleFormat` in `LookupField` — the referenced object's schema, which is what this grid's own inline picker reads). The one remaining receiver is `param.titleFormat` on a resolved `ActionParamDef`. So copying `reference_to` is what makes `titleFormat` work on this path; copying `titleFormat` reached nothing. Nothing renders differently, and that does not rest on the member sweep alone: the only computed access to the meta bag in `@object-ui/fields` or `plugin-grid` is `applyRelationalMeta`'s own write, so no consumer can pick the key up dynamically. Removing a key nothing reads changes no rendering, so the suite proves nothing by staying green; the absence is pinned instead, at all three call sites, with a presence assertion on the seven survivors as the control against vacuity. Also re-syncs the two `plugin-dashboard` descriptions of the grid's copy set that objectui#6711 had already falsified — comments only; that seam's assertions still pin its own three-key boundary and are untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .changeset/6874-retire-titleformat.md | 57 ++++++ .../lookupRelationalMeta-6694.test.tsx | 13 +- .../plugin-dashboard/src/recordFields.tsx | 20 +- packages/plugin-grid/src/ObjectGrid.tsx | 74 +++++-- .../relationalMetaCopySet-6711.test.tsx | 11 +- .../relationalMetaCopySet-6874.test.tsx | 184 ++++++++++++++++++ 6 files changed, 324 insertions(+), 35 deletions(-) create mode 100644 .changeset/6874-retire-titleformat.md create mode 100644 packages/plugin-grid/src/__tests__/relationalMetaCopySet-6874.test.tsx diff --git a/.changeset/6874-retire-titleformat.md b/.changeset/6874-retire-titleformat.md new file mode 100644 index 0000000000..d8c5cab1e8 --- /dev/null +++ b/.changeset/6874-retire-titleformat.md @@ -0,0 +1,57 @@ +--- +'@object-ui/plugin-grid': patch +--- + +`ObjectGrid` no longer copies `titleFormat` onto a relational column's `fieldMeta` +(objectui#6874). + +`RELATIONAL_META_KEYS` listed eight keys that `applyRelationalMeta` copies off the +object-schema field def onto the built `fieldMeta`, at all three of `generateColumns`'s +column-building call sites. `titleFormat` was one of them and had **zero FIELD-meta +readers**. + +This is a zero of a different kind from objectui#6711's, and a stronger one: `titleFormat` +is a real, live key with plenty of readers — it just has none on a field meta. The sweep +did not fail to find readers. It found every member read of the identifier across +`packages/` and `apps/` (tests included) and classified each by its receiver: + +- `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`, + `components/renderers/layout/containers.tsx`, `plugin-detail/DetailView.tsx`, + `plugin-kanban/ObjectKanban.tsx`, `plugin-calendar/ObjectCalendar.tsx`, + `react/hooks/useRecordSearch.ts`. An OBJECT schema, every one. +- `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the REFERENCED + object's schema, fetched by `getSchema(referenceTo)`. Also an OBJECT schema, and the one + that decides this case — it is what the grid's own inline picker reads. +- `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved `ActionParamDef`. + The field-def read beside it is `field.title_format`, a different spelling on a different + surface. + +`RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the repo's single +`titleFormat=` pass is `titleFormat={refTitleFormat}` — object-schema sourced. So copying +`reference_to` is what makes `titleFormat` work on this path, and copying `titleFormat` +onto the meta reached nothing. + +Nothing renders differently, and the argument does not rest on the member sweep alone: the +only computed access to the meta bag anywhere in `@object-ui/fields` or `plugin-grid` is +`applyRelationalMeta`'s own write, so no consumer can pick the key up dynamically. The key +is also not a member of any declared type on this path — `applyRelationalMeta` writes into +a `Record` and the bag reaches cell renderers through an `as any` cast. + +`plugin-dashboard/src/recordFields.tsx` had already recorded this exact measurement as its +reason for not copying the key into that seam, so it was a measured no-op in two seams and +retired from only one. Same defect class as objectui#6625 (`FieldMeta.decimals`), +objectui#6597 (`FieldMeta.referenceTo`) and objectui#6711 (`reference_to_field`), and the +same disposition as objectui#6711 on this very list. + +⚠️ **What the measurement bounds.** The sweep covers this repo and the producer repo. A +host application outside them could still be reading `titleFormat` off the `fieldMeta` a +cell renderer receives; that was never a declared promise this renderer made, and this +repo's own contract is what the retirement is about — but the world was not measured, and a +host reading the key off a field meta gets `undefined` after this change. The supported +source is unchanged and unaffected: the referenced object's schema. + +Because the key had no readers on this path, the suite stays green whether or not the +removal is correct, so the absence is pinned directly instead +(`__tests__/relationalMetaCopySet-6874.test.tsx`): all three call sites, each with a +presence assertion on the seven surviving keys as the control against a fixture that passes +by never reaching the copy path. diff --git a/packages/plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx b/packages/plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx index d79f5f1ada..8b836f96ea 100644 --- a/packages/plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx @@ -237,7 +237,10 @@ describe('objectui#6694 — RecordDetailDrawer lookup rows carry their reference /** * The copy-set boundary. * - * `ObjectGrid`'s `applyRelationalMeta` copies NINE keys; this seam copies THREE, + * `ObjectGrid`'s `applyRelationalMeta` copies SEVEN keys — it copied NINE until + * objectui#6711 and objectui#6874 retired `reference_to_field` and `titleFormat` + * from its list, both on the reader measurement this seam had already recorded. + * This seam copies THREE, * and the difference is measured rather than preferred: the grid's cells are * EDITABLE, so its extra keys feed the inline picker (`LookupField` / `UserField` * read `id_field`, `description_field`, `lookup_filters`, `lookupFilters`). @@ -245,7 +248,7 @@ describe('objectui#6694 — RecordDetailDrawer lookup rows carry their reference * renderer — and `packages/fields/src/index.tsx` reads exactly three relational * keys off a cell's `field` prop. * - * ⛔ This is what stops the omitted six from being added back "for parity": a + * ⛔ This is what stops the omitted keys from being added back "for parity": a * `FieldMeta` member written on every call and read by nothing is precisely what * objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`) retired from this * same file. If these widgets ever gain inline editing, that is the event that @@ -257,7 +260,11 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys reference_to: 'project', reference: 'project', display_field: 'project_code', - // The six the grid also copies, which have no reader on this path: + // Six keys with no reader on this path. FOUR of them the grid still copies + // (its picker-only keys); the other two it has since retired as well — + // `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874). + // All six stay on the fixture on purpose: the assertion below pins THIS + // seam's boundary, which does not move when the grid's list does. reference_to_field: 'x', id_field: 'x', description_field: 'x', diff --git a/packages/plugin-dashboard/src/recordFields.tsx b/packages/plugin-dashboard/src/recordFields.tsx index f8de11b91a..da500e028f 100644 --- a/packages/plugin-dashboard/src/recordFields.tsx +++ b/packages/plugin-dashboard/src/recordFields.tsx @@ -92,11 +92,15 @@ export const NUMERIC_FIELD_TYPES = new Set([ * widgets funnel through, which is what this module exists for (see the file * header: the two surfaces must never drift). * - * ## ⚠️ The copy set is DELIBERATELY 3 of the grid's 9 — measured, per key + * ## ⚠️ The copy set is DELIBERATELY 3 of the grid's 7 — measured, per key * - * `RELATIONAL_META_KEYS` is `reference_to`, `reference`, `reference_to_field`, - * `display_field`, `id_field`, `description_field`, `lookup_filters`, - * `lookupFilters`, `titleFormat`. The grid needs all nine because its cells are + * `RELATIONAL_META_KEYS` is `reference_to`, `reference`, `display_field`, + * `id_field`, `description_field`, `lookup_filters`, `lookupFilters`. It listed + * NINE until the two keys this file had already measured as reader-less were + * retired from it as well — `reference_to_field` (objectui#6711) and + * `titleFormat` (objectui#6874). + * + * The grid needs the remaining seven because its cells are * EDITABLE — its own docblock says the extra keys "drive the inline picker's * query (LookupField reads reference_to/reference, display_field, id_field, * description_field, lookup_filters)", and the defect that earned them was an @@ -114,15 +118,17 @@ export const NUMERIC_FIELD_TYPES = new Set([ * mentions in that module; read only by `fields/src/widgets/LookupField.tsx` * and `UserField.tsx`, both EDITORS. ⛔ NOT copied. * - `reference_to_field` — ZERO member reads anywhere in the repo. ⛔ NOT - * copied. + * copied. ⭐ The grid has since retired it from its own list too + * (objectui#6711); this measurement is what that retirement acted on. * - `titleFormat` — never read off a FIELD meta at all; every reader takes it * off the OBJECT schema (`getRecordDisplayName` in `@object-ui/core`, * `containers.tsx`). On this path that object schema arrives through * `useRefObjectSchema(reference_to)` — so copying `reference_to` is what * makes `titleFormat` work, and copying `titleFormat` here would reach - * nothing. ⛔ NOT copied. + * nothing. ⛔ NOT copied. ⭐ The grid has since retired it too + * (objectui#6874), on exactly this reading. * - * ⛔ Do not "restore parity" by widening this to the grid's nine. A member + * ⛔ Do not "restore parity" by widening this to the grid's seven. A member * written from the schema def on every call and read by nothing is exactly what * objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`) retired from this * very file. Add a key when a reader on THIS path is measured, not before; if diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 0356ce42d4..e263b832dc 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -430,42 +430,74 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null { * raw id after moving to another row. Copy them from the object-schema field * definition onto the built `fieldMeta` for every column-building path. * - * ## ⛔ `reference_to_field` was in this list and is RETIRED (objectui#6711) + * ## ⛔ Two keys were in this list and are RETIRED * * Every key here has to have a measured reader on this grid's own render path — * the cell renderers and inline editors in `@object-ui/fields` that - * `getCellRenderer` dispatches into. `reference_to_field` had none: swept across - * `packages/` and `apps/` (and again across the producer repo), the only - * occurrences of the identifier anywhere were this array literal — the write — - * and prose recording that nothing reads it. No member access, no destructuring, - * no bracket read. `@objectstack/spec`'s FieldSchema does not declare it either, - * so nothing authorable produces it. + * `getCellRenderer` dispatches into. Two keys had none, for two different + * reasons, and each retirement was its own adjudication. * - * The control that makes that zero a reading, not an artefact of how the sweep - * was written: the same sweep over its list-mates finds real readers for each of - * them — `reference_to` / `reference` / `display_field` in `LookupCellRenderer`, - * `id_field` / `description_field` / `lookup_filters` / `lookupFilters` in - * `LookupField` / `UserField`. ⚠️ One exception, measured and deliberately NOT - * acted on here: `titleFormat` has no FIELD-meta reader either — every reader - * takes it off the OBJECT schema, which reaches the picker through - * `useRefObjectSchema(reference_to)` (`plugin-dashboard/src/recordFields.tsx` - * records the same measurement). Retiring it is a separate adjudication. + * ### `reference_to_field` — objectui#6711 + * + * Swept across `packages/` and `apps/` (and again across the producer repo), the + * only occurrences of the identifier anywhere were this array literal — the + * write — and prose recording that nothing reads it. No member access, no + * destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not + * declare it either, so nothing authorable produces it. + * + * ### `titleFormat` — objectui#6874 + * + * A zero of a different kind, and a stronger one. `titleFormat` is a real, live + * key with plenty of readers — it simply has no FIELD-meta reader. The sweep did + * not fail to find readers; it found every member read of the identifier across + * `packages/` and `apps/` (tests included) and classified each one by receiver: + * + * - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`, + * `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`, + * `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`. + * OBJECT schema, every one. + * - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the + * REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an + * OBJECT schema, and the one that matters here — it is what this grid's own + * inline picker reads. + * - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved + * `ActionParamDef`; the field-def read next to it is `field.title_format`, + * a different spelling on a different surface. + * + * `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the + * repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` — + * object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat` + * work on this path; copying `titleFormat` onto the meta reached nothing. + * `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first + * and declined to copy the key, so it was a measured no-op in two seams and had + * been retired from only one. + * + * ### The control that makes both zeros a reading + * + * Not an artefact of how the sweep was written: the same sweep over the + * surviving list-mates finds a real FIELD-meta reader for every one of them — + * `reference_to` / `reference` / `display_field` off the cell's `field` prop in + * `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` / + * `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…` + * in `LookupField` / `UserField`. There is no third reader-less key: all seven + * survivors are read off a field meta. * * ⚠️ The sweep bounds these two repos. A host application outside them could - * still be reading the key off `fieldMeta`; the repo's own contract is what this - * retirement is about. + * still be reading either key off `fieldMeta`; the repo's own contract is what + * these retirements are about. * * ⛔ Do not re-add a key for symmetry with the object-schema field def. A * member written from the def on every column build and read by nothing is * exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`) * retired from the sibling producer. Add a key when a reader on THIS path is - * measured, not before. The absence is pinned in - * `__tests__/relationalMetaCopySet-6711.test.tsx`. + * measured, not before. Both absences are pinned, at all three call sites — + * `__tests__/relationalMetaCopySet-6711.test.tsx` and + * `__tests__/relationalMetaCopySet-6874.test.tsx`. */ const RELATIONAL_META_KEYS = [ 'reference_to', 'reference', 'display_field', 'id_field', 'description_field', - 'lookup_filters', 'lookupFilters', 'titleFormat', + 'lookup_filters', 'lookupFilters', ] as const; /** diff --git a/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx b/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx index dd9168c7a9..75e4c3ae68 100644 --- a/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx +++ b/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6711.test.tsx @@ -15,7 +15,7 @@ * * ## The control against vacuity lives in the same assertions * - * Each case also asserts that the eight SURVIVING keys do arrive on the same + * Each case also asserts that the seven SURVIVING keys do arrive on the same * meta. An absence assertion on its own passes for the wrong reason as soon as * the fixture stops reaching the copy path at all (a renamed helper, a column * path that no longer resolves this renderer, a def the grid never reads); the @@ -60,16 +60,19 @@ const MANAGER_DEF = { description_field: 'title', lookup_filters: [['active', '=', true]], lookupFilters: [['active', '=', true]], + // Also retired, in objectui#6874, and pinned in its own file + // (`relationalMetaCopySet-6874.test.tsx`). Kept on the fixture so this file's + // survivor control stays a list of keys the grid really does still copy. titleFormat: '{name}', // The retired key (objectui#6711). Kept on the fixture on purpose. reference_to_field: 'MUST_NOT_BE_COPIED', }; -/** The eight keys that survive the retirement — the control. */ +/** The seven keys that survive both retirements — the control. */ const SURVIVING_KEYS = [ 'reference_to', 'reference', 'display_field', 'id_field', 'description_field', - 'lookup_filters', 'lookupFilters', 'titleFormat', + 'lookup_filters', 'lookupFilters', ] as const; const ROWS = [{ id: 'r1', name: 'Tower T1', manager: 'u1' }]; @@ -158,7 +161,7 @@ describe('objectui#6711 — ObjectGrid no longer copies `reference_to_field` ont expect(meta).not.toHaveProperty('reference_to_field'); }); - it(`still copies the eight surviving relational keys (${name})`, async () => { + it(`still copies the seven surviving relational keys (${name})`, async () => { const meta = await renderAndCaptureMeta(schemaExtra); for (const key of SURVIVING_KEYS) { expect(meta).toHaveProperty(key); diff --git a/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6874.test.tsx b/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6874.test.tsx new file mode 100644 index 0000000000..d750d0b199 --- /dev/null +++ b/packages/plugin-grid/src/__tests__/relationalMetaCopySet-6874.test.tsx @@ -0,0 +1,184 @@ +/** + * objectui#6874 — `ObjectGrid`'s relational copy set must NOT carry + * `titleFormat`. + * + * ## ⚠️ Why this is a pin and not a behaviour test + * + * `titleFormat` is a real, live key — but it has ZERO readers on a FIELD meta. + * Every read of the identifier in this repo takes it off an OBJECT schema + * (`objectDef` / `objectSchema` / `objSchema` in `record-title.ts`, + * `containers.tsx`, `DetailView.tsx`, `ObjectKanban.tsx`, `ObjectCalendar.tsx`, + * `useRecordSearch.ts`), and the one that this grid's own inline picker uses is + * `refObjectSchema?.titleFormat` in `LookupField` — the REFERENCED object's + * schema, fetched by `getSchema(referenceTo)`. So `reference_to` is what makes + * `titleFormat` work on this path, and the copy reached nothing. + * + * Removing it therefore changes no rendering at all: every other test in this + * package stays green whether or not the removal is correct. A green suite + * proves nothing here. What CAN be asserted is the thing that was actually + * measured — the key is no longer WRITTEN onto the `fieldMeta` a cell renderer + * receives — so this file asserts that absence directly, at all three of + * `generateColumns`'s column-building call sites, and goes red the moment the + * key is re-added to `RELATIONAL_META_KEYS`. + * + * ## The control against vacuity lives in the same assertions + * + * Each case also asserts that the seven SURVIVING keys do arrive on the same + * meta. An absence assertion on its own passes for the wrong reason as soon as + * the fixture stops reaching the copy path at all (a renamed helper, a column + * path that no longer resolves this renderer, a def the grid never reads); the + * presence half is what makes each `not.toHaveProperty` a measurement rather + * than a tautology. + * + * The probe replaces the registered `lookup` cell renderer, which is exactly + * how the real `LookupCellRenderer` receives this bag — `getCellRenderer` checks + * the registry first — so what it captures is the `field` prop the shipped + * renderer would have been handed. + * + * Same disposition, same list, same author path as objectui#6711, which retired + * `reference_to_field` from this array; that absence is pinned in + * `relationalMetaCopySet-6711.test.tsx`. + */ +import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; +import { render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; + +import { ObjectGrid } from '../ObjectGrid'; +import { + registerAllFields, + registerFieldRenderer, + getCellRenderer, + type CellRendererProps, +} from '@object-ui/fields'; +import { ActionProvider, SchemaRendererProvider } from '@object-ui/react'; + +registerAllFields(); + +const OBJECT = 'os_6874_report'; + +/** + * One field def carrying EVERY relational key the grid has ever copied, + * including both retired ones. A def that omitted `titleFormat` could not tell + * "the grid stopped copying it" apart from "the fixture never offered it". + */ +const MANAGER_DEF = { + type: 'lookup', + label: 'Manager', + reference_to: 'users', + reference: 'users', + display_field: 'name', + id_field: 'id', + description_field: 'title', + lookup_filters: [['active', '=', true]], + lookupFilters: [['active', '=', true]], + // The key this file pins as retired (objectui#6874). Kept on the fixture on + // purpose — a def without it would make the assertion vacuous. + titleFormat: 'MUST_NOT_BE_COPIED', + // Retired earlier, in objectui#6711. Kept for the same reason. + reference_to_field: 'x', +}; + +/** The seven keys that survive both retirements — the control. */ +const SURVIVING_KEYS = [ + 'reference_to', 'reference', + 'display_field', 'id_field', 'description_field', + 'lookup_filters', 'lookupFilters', +] as const; + +const ROWS = [{ id: 'r1', name: 'Tower T1', manager: 'u1' }]; + +const captured: Record[] = []; + +function ProbeCell({ value, field }: CellRendererProps): React.ReactElement { + captured.push(field as unknown as Record); + return {String(value ?? '')}; +} + +let originalLookupRenderer: React.FC; + +beforeAll(() => { + if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = vi.fn() as any; + } + originalLookupRenderer = getCellRenderer('lookup'); + registerFieldRenderer('lookup', ProbeCell); +}); + +afterAll(() => { + registerFieldRenderer('lookup', originalLookupRenderer); +}); + +function makeDataSource() { + return { + find: vi.fn(async () => ({ data: ROWS, total: ROWS.length, hasMore: false, pageSize: 50 })), + getObjectSchema: async (name: string) => ({ + name, + fields: { + id: { type: 'text' }, + name: { type: 'text', label: 'Name' }, + manager: { ...MANAGER_DEF }, + }, + }), + } as any; +} + +async function renderAndCaptureMeta(schemaExtra: Record) { + captured.length = 0; + const ds = makeDataSource(); + const schema: any = { + type: 'object-grid', + objectName: OBJECT, + data: ROWS, + pagination: { pageSize: 50 }, + ...schemaExtra, + }; + render( + + + + + , + ); + // ⚠️ Wait for the ENRICHED meta, not merely the first one. The object schema + // arrives from an async fetch, so the first paint hands the renderer a bare + // `{ name, type }` — on which `not.toHaveProperty('titleFormat')` passes for + // the wrong reason. `label` is the signal because it is written from the same + // `objectDefField` block, immediately BEFORE `applyRelationalMeta`, and is not + // itself one of the keys under test — so the wait cannot manufacture the + // assertions below. + await waitFor(() => { + expect(captured.length).toBeGreaterThan(0); + expect(captured[captured.length - 1]).toHaveProperty('label'); + }); + return captured[captured.length - 1]; +} + +/** + * The three call sites of `applyRelationalMeta`, reached by the three shapes + * `generateColumns` branches on: ListColumn objects, a string array, and the + * inline-data path (rows handed down + an authored `fields` projection). + */ +const CALL_SITES: Array<[string, Record]> = [ + ['ListColumn objects', { columns: [{ field: 'manager', label: 'Manager', type: 'lookup' }] }], + ['string columns', { columns: ['manager'] }], + ['inline data + fields projection', { fields: ['manager'] }], +]; + +describe('objectui#6874 — ObjectGrid no longer copies `titleFormat` onto fieldMeta', () => { + for (const [name, schemaExtra] of CALL_SITES) { + it(`does not copy it (${name})`, async () => { + const meta = await renderAndCaptureMeta(schemaExtra); + expect(meta).not.toHaveProperty('titleFormat'); + }); + + it(`still copies the seven surviving relational keys (${name})`, async () => { + const meta = await renderAndCaptureMeta(schemaExtra); + for (const key of SURVIVING_KEYS) { + expect(meta).toHaveProperty(key); + } + expect(meta.reference_to).toBe('users'); + expect(meta.display_field).toBe('name'); + }); + } +});