From 3d3103371274c1b3f71a6f0db64faab0da7fe86f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:19:58 +0000 Subject: [PATCH] test(plugin-list): pin all eight reported `columns` spellings on a kind:'html' page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reported defect no longer reproduces on main. Re-measured every one of the eight spellings objectstack#12649 tried: none lands in the reported state (a populated table whose only header is the index column, with zero diagnostics). Six render data columns — forms 1 and 3 render exactly the authored projection, forms 2, 4, 5, 7 and 8b render the block's defaults — and forms 6 and 8a fail loudly with the compile-error block on screen. Three separate mechanisms produced one symptom, and each was fixed elsewhere: the braced literal subset (#6614 / PR #6669), the unauthored-projection handoff (PR #6679), and the grid's default-column derivation (#6677). What none of those pinned is the card's own claim, which is a statement about all eight spellings at once. This adds that matrix, through the real page renderer, the real html-tier compile against the real registry manifest, the real `list-view` registration and the real `object-grid` — so no future change can put any spelling back into the reported state. Tests only; the changeset declares an empty frontmatter accordingly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 --- .changeset/lucky-donkeys-repeat.md | 14 + .../htmlTierColumnSpellings-6598.test.tsx | 248 ++++++++++++++++++ vitest.config.mts | 6 + 3 files changed, 268 insertions(+) create mode 100644 .changeset/lucky-donkeys-repeat.md create mode 100644 packages/plugin-list/src/__tests__/htmlTierColumnSpellings-6598.test.tsx diff --git a/.changeset/lucky-donkeys-repeat.md b/.changeset/lucky-donkeys-repeat.md new file mode 100644 index 0000000000..e1a2c2679e --- /dev/null +++ b/.changeset/lucky-donkeys-repeat.md @@ -0,0 +1,14 @@ +--- +--- + +Tests only — no published behaviour changes. + +objectui#6598 reported that a `kind:'html'` page's `` rendered rows but +no data columns, in all eight `columns` spellings the reporter tried. Re-measured +on the merged ref: the symptom no longer reproduces in any of the eight. The +three mechanisms behind it were fixed by objectui#6614 / PR #6669 (the braced +literal subset), PR #6679 (the unauthored-projection handoff) and objectui#6677 +(the grid's default-column derivation). This adds the matrix pin the card itself +was missing — every one of the eight spellings, end to end through the real live +registration and the real object-grid — so no future change can put any of them +back into the reported state. diff --git a/packages/plugin-list/src/__tests__/htmlTierColumnSpellings-6598.test.tsx b/packages/plugin-list/src/__tests__/htmlTierColumnSpellings-6598.test.tsx new file mode 100644 index 0000000000..3aa9b1b124 --- /dev/null +++ b/packages/plugin-list/src/__tests__/htmlTierColumnSpellings-6598.test.tsx @@ -0,0 +1,248 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * objectui#6598 — the reporter's EIGHT `columns` spellings, end to end on a + * `kind:'html'` page, pinned as a matrix. + * + * ## What was reported, and what this file is for + * + * The production report (objectstack#12649, hotcrm, promo-video recon) tried + * eight ways of getting columns onto a `` in an html-kind page and + * got ONE picture from all eight: the row count ("23 records"), the + * filter/group/sort toolbar, and **no data columns at all — only the index + * column**, with zero diagnostics anywhere. That uniformity is what made the + * card so hard to anchor: it read as a single bug, and it was not one. It was + * three unrelated mechanisms whose failure modes happened to look identical + * from the page: + * + * - a GRAMMAR limit — `interpretBrace` materialized strict JSON only, so the + * single-quoted array every JSX author writes became an inert `{ $expr }` + * marker nothing downstream evaluates (fixed by objectui#6614 / PR #6669); + * - a HANDOFF lie — `ListView` spelled "the author declared nothing" as + * `fields: []`, and an empty array is truthy, so `ObjectGrid` pinned its + * projection at zero and neither default-columns derivation could run + * (fixed by objectui#6598's first half, PR #6679); + * - two spellings that were never the contract at all (`viewName` / `view`), + * plus two that are page-fatal compile errors. + * + * Sibling files pin the mechanisms one at a time: + * `literal-subset-6614.test.ts` and `inert-expression-6598.test.ts` in + * `@object-ui/sdui-parser` own the grammar and its diagnostic; + * `ListView.unauthoredColumnProjection-6598.test.tsx` owns the handoff against + * a stub grid; `htmlTierListViewDefaultColumns-6598.test.tsx` takes the + * unauthored case end to end. + * + * ⚠️ None of them pins THE CARD'S OWN CLAIM, which is a statement about all + * eight spellings at once: *whatever* an author writes for `columns` on this + * tier, the page must never again land in the reported state. That claim can + * only be checked as a matrix, and only through the real live registration — + * so this file runs the real page renderer, the real html-tier compile against + * the real registry manifest, the real `list-view` registration and the real + * `object-grid`, once per spelling. + * + * ## The invariant, and why it is shaped this way + * + * The reported failure had two halves: the page showed no data columns, AND it + * said nothing about why. So the invariant every spelling must satisfy is a + * disjunction — each form either + * + * (a) renders a table with at least one DATA column, or + * (b) fails LOUDLY, with the compile-error block on screen. + * + * What no form may do is what all eight did when the card was filed: render a + * populated table whose only header is the index column. A spelling moving + * between (a) and (b) is a design decision someone can argue about; a spelling + * falling out of both is this card regressing. + * + * Registered in `heavyDomTests` for the setup's `@object-ui/plugin-grid` + * side-effect registration — the same route the sibling end-to-end file takes. + */ +import { describe, it, expect } from 'vitest'; +import { render, waitFor } from '@testing-library/react'; +import React from 'react'; +import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; +import '../index'; + +const OBJECT = 'opportunity'; + +const dataSource = { + find: async () => ({ + data: [ + { id: 'o-1', name: 'Acme expansion', amount: 1000, stage: 'new' }, + { id: 'o-2', name: 'Globex renewal', amount: 2000, stage: 'won' }, + ], + total: 23, + hasMore: true, + }), + findOne: async () => null, + create: async () => ({}), + update: async () => ({}), + delete: async () => ({}), + count: async () => 23, + getObjectSchema: async (name: string) => ({ + name, + label: 'Opportunity', + fields: { + id: { type: 'text', label: 'Id', hidden: true }, + name: { type: 'text', label: 'Opportunity Name' }, + stage: { type: 'text', label: 'Stage' }, + amount: { type: 'currency', label: 'Amount' }, + }, + }), + getObjects: async () => [], + onMutation: () => () => {}, +} as any; + +const INDEX_COLUMN = '#'; + +interface Rendered { + headers: string[]; + dataHeaders: string[]; + compileFailed: boolean; + text: string; +} + +/** + * Render one spelling and settle on whichever terminal state it reaches — a + * table, or the page-level compile-error block. Deliberately NOT + * `waitFor(table)`: two of the eight spellings are page-fatal by design, and a + * helper that can only wait for a table would report those as a timeout rather + * than as the loud failure they are. + */ +async function renderSpelling(source: string): Promise { + const { container } = render( + + + , + ); + await waitFor(() => { + const settled = + container.querySelector('table') !== null || + /failed to compile/i.test(container.textContent || ''); + expect(settled).toBe(true); + }); + const headers = Array.from(container.querySelectorAll('th')).map((th) => (th.textContent || '').trim()); + const text = container.textContent || ''; + return { + headers, + dataHeaders: headers.filter((h) => h !== INDEX_COLUMN), + compileFailed: /failed to compile/i.test(text), + text, + }; +} + +/** The reported state, named once so every case can assert against it. */ +function expectNotTheReportedSymptom(r: Rendered): void { + const renderedDataColumns = !r.compileFailed && r.dataHeaders.length > 0; + const failedLoudly = r.compileFailed; + // Before the fixes: `headers` was exactly ['#'] with `compileFailed` false — + // rows on screen, no data columns, and nothing said so. + expect(renderedDataColumns || failedLoudly).toBe(true); + expect(r.headers).not.toEqual([INDEX_COLUMN]); +} + +describe("#6598 — all eight reported `columns` spellings on a kind:'html' page", () => { + describe('the spellings that render the AUTHORED projection', () => { + it('form 1 — a single-quoted JSX array literal (the spelling the report leads with)', async () => { + // The grammar case objectui#6614/#6669 legalised. Credit is #6669's; this + // is the end-to-end guard that the materialised array survives all the + // way to a header row. + const r = await renderSpelling(``); + + expectNotTheReportedSymptom(r); + expect(r.headers).toEqual([INDEX_COLUMN, 'Opportunity Name', 'Amount']); + expect(r.headers).not.toContain('Stage'); + expect(r.text).toContain('23 records'); + }); + + it('form 3 — an object-array with bare identifier keys', async () => { + const r = await renderSpelling( + ``, + ); + + expectNotTheReportedSymptom(r); + expect(r.headers).toEqual([INDEX_COLUMN, 'Opportunity Name', 'Amount']); + expect(r.headers).not.toContain('Stage'); + }); + }); + + describe("the spellings that declare no projection this tier can read — the block's DEFAULTS render", () => { + // ⚠️ Deliberately "which business columns are present", never an exact + // header list: WHICH defaults a grid derives is a separate, live question + // against `packages/plugin-grid` (objectui#6677) and it moves this list. It + // must not be able to move whether the page shows data columns at all, + // which is the only thing this card is about. + const expectDefaults = (r: Rendered) => { + expectNotTheReportedSymptom(r); + expect(r.headers).toContain('Opportunity Name'); + expect(r.headers).toContain('Amount'); + expect(r.text).toContain('23 records'); + }; + + it('form 7 — no `columns` attribute at all', async () => { + // The only one of the eight that failed with ZERO diagnostics, and the + // half PR #6679 fixed: ListView must hand the grid NO projection when the + // author declared none, so the grid's own defaults can run. + expectDefaults(await renderSpelling(``)); + }); + + it('form 2 — a JSON STRING where an array is declared', async () => { + // ⛔ Not honoured as a projection, and that is contract-first (AGENTS.md + // #0.1), not a gap: `columns` is declared `array`, so a string draws a + // warning-severity `type-mismatch` from the manifest validator and is + // refused. Parsing it anyway would be exactly the consumer-side tolerance + // alias the rule forbids. What matters to THIS card is that refusing it + // no longer costs the page its columns. + const r = await renderSpelling(``); + + expectDefaults(r); + // The string was not silently adopted as a two-column projection: the + // undeclared third field is on screen, so these are the defaults. + expect(r.headers).toContain('Stage'); + }); + + it('form 4 — a `viewName` reference to a saved view', async () => { + // `viewName` is not declared by the live `list-view` registration and is + // not a base prop, so it draws `unknown-prop`. It was never the spelling + // for a saved view on this tier — see objectui#6678, which is about that + // being undiscoverable, not about this page losing its columns. + expectDefaults(await renderSpelling(``)); + }); + + it('form 5 — a `view` reference to a saved view', async () => { + expectDefaults(await renderSpelling(``)); + }); + + it('form 8b — the kebab-case `view-name` variant', async () => { + expectDefaults(await renderSpelling(``)); + }); + }); + + describe('the spellings that are page-fatal — they fail LOUDLY, which is the opposite of the report', () => { + it('form 6 — child elements', async () => { + const r = await renderSpelling( + ``, + ); + + expectNotTheReportedSymptom(r); + expect(r.compileFailed).toBe(true); + // The author is told the actual reason, on the page. + expect(r.text).toContain('column'); + expect(r.headers).toEqual([]); + }); + + it('form 8a — the kebab-case `object-name` variant', async () => { + // `object-name` is not `objectName`, so the required prop is simply + // absent and the page says so by name. + const r = await renderSpelling(``); + + expectNotTheReportedSymptom(r); + expect(r.compileFailed).toBe(true); + expect(r.text).toContain('objectName'); + }); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts index 6151f6f3f6..5c1a76e06c 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -133,6 +133,12 @@ const heavyDomTests = [ // sibling handoff pin registers) is standing in for one side of the // disagreement and cannot see it. Same reason, same route as the entry above. 'packages/plugin-list/src/__tests__/htmlTierListViewDefaultColumns-6598.test.tsx', + // objectui#6598 — the reporter's eight `columns` spellings as a matrix, + // each through the real live registration and the REAL object-grid. Same + // reason and same route as the entry above: the card's own claim is about + // all eight at once, and a stub grid stands in for one side of the + // ListView/ObjectGrid disagreement it has to observe. + 'packages/plugin-list/src/__tests__/htmlTierColumnSpellings-6598.test.tsx', ]; export default defineConfig({