|
70 | 70 | * everywhere, which is precisely why the ALLOWED rows could never have caught |
71 | 71 | * this. |
72 | 72 | * |
73 | | - * ## Scope, measured — what this does NOT claim |
| 73 | + * ## Scope — the whole group, since objectui#6236 |
74 | 74 | * |
75 | | - * The renderer treats `section-divider` as a presentational ROW and holds no |
76 | | - * association between it and the fields that follow it, so a false predicate |
77 | | - * removes the HEADING and leaves the section's fields rendering. The console |
78 | | - * renderer (`apps/console/src/components/FormPage.tsx:1819`) drops the whole |
79 | | - * `<section>`, fields included. That divergence is real and is filed |
80 | | - * separately — it needs a renderer-side grouping contract, not another line in |
81 | | - * a layout. The `stillRendersItsFields` case below pins the CURRENT behaviour |
82 | | - * honestly rather than letting the file imply a guarantee it does not deliver. |
| 75 | + * objectui#6236 (maintainer ruling 2026-08-27) closed the divergence this |
| 76 | + * header used to record: the renderer now holds a real divider-to-field |
| 77 | + * association — the divider's membership claim (`FormField.fields`, stamped by |
| 78 | + * every synthesis site above from the RESOLVED member list) — and a false |
| 79 | + * section predicate hides the heading AND the claimed fields, matching the |
| 80 | + * console renderer. So every DENIED row below asserts both halves: the heading |
| 81 | + * (the #6111 deliverable) and the gated member field (the #6236 wiring). The |
| 82 | + * hidden members skip client-side validation and their values still submit; |
| 83 | + * those semantics are pinned at the renderer in |
| 84 | + * `packages/components/src/renderers/form/__tests__/section-grouping-6236.test.tsx` |
| 85 | + * — this file pins that each LAYOUT's synthesis site actually stamps the claim |
| 86 | + * (an unstamped site reverts to heading-only, invisible to every other suite). |
| 87 | + * |
| 88 | + * Two derived-fieldGroup synthesis sites (ModalForm / DrawerForm |
| 89 | + * `derivedSections`) also stamp the claim for uniformity, but the spec |
| 90 | + * `fieldGroups` vocabulary has no section-predicate slot, so no authoring path |
| 91 | + * can turn their gate on today and no DENIED row can discriminate them; they |
| 92 | + * stay fail-open until that vocabulary grows a predicate. |
83 | 93 | */ |
84 | 94 |
|
85 | 95 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
@@ -192,6 +202,12 @@ const renderModalFormDirect = async (scope: Record<string, unknown>) => { |
192 | 202 | /** The gated section's heading — `null` when the layout hid it. */ |
193 | 203 | const gatedHeading = () => screen.queryByText('Compensation'); |
194 | 204 |
|
| 205 | +/** |
| 206 | + * The gated section's claimed MEMBER field (#6236) — `null` when the layout's |
| 207 | + * synthesis site stamped the membership claim and the gate took the group. |
| 208 | + */ |
| 209 | +const gatedField = () => screen.queryByLabelText(/salary/i); |
| 210 | + |
195 | 211 | /** |
196 | 212 | * Every layout, by name, with the mount that reaches its own synthesis site. |
197 | 213 | * Named individually because a fix applied to five of six sites still passes a |
@@ -221,25 +237,33 @@ const LAYOUTS: { label: string; mount: (scope: Record<string, unknown>) => Promi |
221 | 237 | ]; |
222 | 238 |
|
223 | 239 | describe('#6111 — an authored section `visibleWhen` reaches an evaluator in every layout', () => { |
224 | | - describe('DENIED — the predicate resolves FALSE, so the section heading is HIDDEN', () => { |
225 | | - // ⚠️ THE deliverable. Green here is the only observation that separates |
226 | | - // "the predicate arrived and was evaluated" from "it never arrived" — |
227 | | - // every other row in this file is green on unfixed code. |
| 240 | + describe('DENIED — the predicate resolves FALSE, so the whole section is HIDDEN', () => { |
| 241 | + // ⚠️ THE deliverable, in two halves. The heading half (#6111) separates |
| 242 | + // "the predicate arrived and was evaluated" from "it never arrived"; the |
| 243 | + // member half (#6236) separates "this layout's synthesis site stamped the |
| 244 | + // membership claim" from "the divider arrived claim-less and the gate |
| 245 | + // stayed heading-only" — an unstamped site fails ONLY here, in the SHOWN |
| 246 | + // direction, because a claim-less divider is valid compat behaviour |
| 247 | + // everywhere else. |
228 | 248 | for (const layout of LAYOUTS) { |
229 | 249 | it(layout.label, async () => { |
230 | 250 | await layout.mount(DENIED); |
231 | 251 | expect(gatedHeading()).toBeNull(); |
| 252 | + expect(gatedField()).toBeNull(); |
232 | 253 | }); |
233 | 254 | } |
234 | 255 | }); |
235 | 256 |
|
236 | 257 | describe('ALLOWED — the SAME predicate text, a user it admits ⇒ still SHOWN', () => { |
237 | 258 | // The control. Without it, "hidden" is satisfied by a layout that dropped |
238 | 259 | // the heading for an unrelated reason — a worse defect, invisible above. |
| 260 | + // The member assertion keeps the same honesty for the group half: a gate |
| 261 | + // that hides claimed fields unconditionally would pass every DENIED row. |
239 | 262 | for (const layout of LAYOUTS) { |
240 | 263 | it(layout.label, async () => { |
241 | 264 | await layout.mount(ALLOWED); |
242 | 265 | expect(gatedHeading()).not.toBeNull(); |
| 266 | + expect(gatedField()).not.toBeNull(); |
243 | 267 | }); |
244 | 268 | } |
245 | 269 | }); |
@@ -274,26 +298,27 @@ describe('#6111 — an authored section `visibleWhen` reaches an evaluator in ev |
274 | 298 | ); |
275 | 299 | await waitFor(() => expect(screen.getByText('Always')).toBeTruthy()); |
276 | 300 | expect(gatedHeading()).not.toBeNull(); |
| 301 | + expect(gatedField()).not.toBeNull(); |
277 | 302 | }); |
278 | 303 | } |
279 | 304 | }); |
280 | 305 |
|
281 | | - it('measured scope: a hidden section still renders its FIELDS (objectui#6111 follow-up)', async () => { |
282 | | - // NOT an endorsement — an honest pin of what this chain does and does not |
283 | | - // deliver. The renderer-side grouping contract EXISTS since objectui#6236 |
284 | | - // (maintainer ruling 2026-08-27): a `section-divider` that claims its |
285 | | - // members (`FormField.fields: string[]`) gates the whole group — heading |
286 | | - // and fields together, hidden members skip client-side validation, values |
287 | | - // still submit (pinned in `packages/components/.../section-grouping-6236. |
288 | | - // test.tsx`). What is still missing is THIS CHAIN's half: the layouts' |
289 | | - // divider synthesis sites (`ObjectForm.tsx` and siblings) copy |
290 | | - // `visibleWhen` onto the pseudo-field but do not yet stamp the membership |
291 | | - // claim, so an authored section predicate still hides only the heading |
292 | | - // here — the fields stay, exactly as below. The console renderer drops |
293 | | - // the whole `<section>`. This assertion turning red is the SIGNAL that |
294 | | - // the synthesis wiring (the remaining half of objectui#6236) landed. |
| 306 | + it('measured scope: a hidden section hides its FIELDS too (objectui#6236 wiring landed)', async () => { |
| 307 | + // FLIPPED — deliberately, and exactly once. From #6111 until the #6236 |
| 308 | + // wiring landed, this case pinned the honest limitation: the divider was a |
| 309 | + // presentational row, so a false section predicate removed the heading and |
| 310 | + // LEFT THE FIELDS RENDERING (`expect(getByLabelText(/salary/i)).toBeTruthy()` |
| 311 | + // stood here). #6111 wrote it so that its red flip would be the SIGNAL the |
| 312 | + // grouping contract landed rather than a broken test — and that is what |
| 313 | + // happened: the synthesis sites now stamp the membership claim |
| 314 | + // (`fields: [...names]`) onto the divider, the renderer gates the whole |
| 315 | + // group, and this case now pins the NEW contract on the same chain, the |
| 316 | + // same mount, the same predicate: heading gone AND claimed field gone, |
| 317 | + // matching the console renderer at last. Values still submit and hidden |
| 318 | + // members skip client-side validation — pinned at the renderer in |
| 319 | + // section-grouping-6236.test.tsx. |
295 | 320 | await renderObjectForm(DENIED, { formType: 'simple' }); |
296 | 321 | expect(gatedHeading()).toBeNull(); |
297 | | - expect(screen.getByLabelText(/salary/i)).toBeTruthy(); |
| 322 | + expect(gatedField()).toBeNull(); |
298 | 323 | }); |
299 | 324 | }); |
0 commit comments