|
| 1 | +/** |
| 2 | + * ObjectUI |
| 3 | + * Copyright (c) 2024-present ObjectStack Inc. |
| 4 | + * |
| 5 | + * This source code is licensed under the MIT license found in the |
| 6 | + * LICENSE file in the root directory of this source tree. |
| 7 | + */ |
| 8 | + |
| 9 | +/** |
| 10 | + * objectui#10959 — what an unread `header-bar` key and an unread `body` drew, |
| 11 | + * stated for BOTH tiers. The sibling of objectui#10928, with a different code. |
| 12 | + * |
| 13 | + * Two families of published text said an authored value drew "no error, no |
| 14 | + * warning": |
| 15 | + * |
| 16 | + * - the `HeaderBarSchema` refusals of objectui#10387 (`title`, `logo`, `nav`, |
| 17 | + * `left`, `center`, `right`, `sticky`, `height`) and objectui#10286 |
| 18 | + * (`variant`), on the zod face (message and `.describe()` are one string); |
| 19 | + * - the `body?: never` docblocks objectui#8284 put on the nine nodes whose |
| 20 | + * renderer reads `children` (`box`, `span`, `container`, `flex`, `stack`, |
| 21 | + * `grid`, `scroll-area` in `layout.ts`; `toggle`, `form` in `form.ts`), |
| 22 | + * which ship in the emitted `.d.ts`. |
| 23 | + * |
| 24 | + * The render half is true: the value changed nothing on the page and logged |
| 25 | + * nothing. The warning half was false. The parser tier's `validateTree` |
| 26 | + * (`@object-ui/sdui-parser`) answers a key its registration does not declare |
| 27 | + * with `unknown-prop`, and `header-bar` declares only `crumbs`, `search`, |
| 28 | + * `actions` and `rightContent`. It answers `body` the same way on these nine, |
| 29 | + * whatever its shape, because each of them declares the `children` input: |
| 30 | + * `checkRetiredBodyDialect` gives `not-a-container` only where that input is |
| 31 | + * missing, which is why objectui#10928's clause would be false here. So the |
| 32 | + * clause now names `unknown-prop`: |
| 33 | + * |
| 34 | + * refusal strings: "no render-time error or warning and no element; only the |
| 35 | + * parser tier's `unknown-prop` warning noticed it" |
| 36 | + * (`variant` says "no class" where the others say |
| 37 | + * "no element", as it did before) |
| 38 | + * docblocks: "rendered an EMPTY element with no render-time error or |
| 39 | + * warning; only the parser tier's `unknown-prop` warning |
| 40 | + * noticed it" |
| 41 | + * |
| 42 | + * ## Why this file pins the prose, and where the parser half lives |
| 43 | + * |
| 44 | + * `@object-ui/types` declares no workspace dependency and its tests import |
| 45 | + * none, so `@object-ui/sdui-parser` is not importable here without declaring |
| 46 | + * it (the same reason as objectui#10928's pin). The `body` half of the parser |
| 47 | + * behaviour is pinned in `packages/sdui-parser`: "the retired `body` spelling |
| 48 | + * follows the SAME predicate" in `containment-declared-slot-9910.test.ts`, and |
| 49 | + * "a `body` child list under a CONTAINER draws `unknown-prop` naming |
| 50 | + * `children`" in `body-dialect-6771.test.ts`. The `header-bar` half is the |
| 51 | + * generic undeclared-key branch of `validateTree`; no pin names `header-bar` |
| 52 | + * there. The live-registry reading taken for this change (every key above, on |
| 53 | + * `header-bar` and `ui:header-bar` and on the bare and `ui:` key of the nine, |
| 54 | + * with a render leg and a `not-a-container` control) is on objectui#10959's |
| 55 | + * pull request: a record of one run, not re-derived here. |
| 56 | + * |
| 57 | + * ## What is pinned, and what is deliberately not |
| 58 | + * |
| 59 | + * The clause as a substring. On the zod face: one `header-bar` key of each |
| 60 | + * clause form, read through its refusal with its code and path, then every |
| 61 | + * `header-bar` member whose refusal is one of these two retirements. On the TS |
| 62 | + * face: the `body` docblock of `BoxSchema` and of `FormSchema`, then every |
| 63 | + * docblock in `layout.ts` and `form.ts` that carries objectui#8284's sentence. |
| 64 | + * The rest of each string is NOT pinned; `header-bar-unread-keys-10387`, |
| 65 | + * `mirror-groups-cd-10286` and the objectui#8284 pins read it. |
| 66 | + */ |
| 67 | + |
| 68 | +import { describe, it, expect } from 'vitest'; |
| 69 | +import { readFileSync } from 'node:fs'; |
| 70 | +import { join } from 'node:path'; |
| 71 | +import { z } from 'zod'; |
| 72 | +import { HeaderBarSchema } from '../zod/navigation.zod'; |
| 73 | + |
| 74 | +/** The clause the parser tier makes true — every site below ends on it. */ |
| 75 | +const TAIL = "only the parser tier's `unknown-prop` warning noticed it"; |
| 76 | + |
| 77 | +/** The refusal strings' clause, in its two forms. */ |
| 78 | +const ELEMENT_CLAUSE = `no render-time error or warning and no element; ${TAIL}`; |
| 79 | +const CLASS_CLAUSE = `no render-time error or warning and no class; ${TAIL}`; |
| 80 | + |
| 81 | +/** The docblocks' clause. */ |
| 82 | +const DOCBLOCK_CLAUSE = `rendered an EMPTY element with no render-time error or warning; ${TAIL}`; |
| 83 | + |
| 84 | +/** The clauses they replaced — false about the parser tier. */ |
| 85 | +const RETIRED_STRING_CLAUSE = 'no error, no warning'; |
| 86 | +const RETIRED_DOCBLOCK_CLAUSE = 'with no error and no warning'; |
| 87 | + |
| 88 | +/** objectui#10928's code, which would be false on every site here. */ |
| 89 | +const WRONG_CODE = '`not-a-container`'; |
| 90 | + |
| 91 | +/** The sentence that marks a docblock as one of objectui#8284's `body` refusals. */ |
| 92 | +const MARK_8284 = 'Before objectui#8284 tombstoned it here, `body` was inherited-and-optional'; |
| 93 | + |
| 94 | +/** |
| 95 | + * The repo root, derived from THIS FILE's own location and never from the cwd |
| 96 | + * (objectui#7799): bare `import.meta.url`, taken apart by hand (the spelling |
| 97 | + * landed for objectui#7791, PR #7796). |
| 98 | + */ |
| 99 | +const SELF_DEPTH_BELOW_REPO_ROOT = 5; // packages / types / src / __tests__ / this file |
| 100 | +const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname) |
| 101 | + .split('/') |
| 102 | + .slice(0, -SELF_DEPTH_BELOW_REPO_ROOT) |
| 103 | + .join('/'); |
| 104 | +const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), 'utf8'); |
| 105 | + |
| 106 | +const LAYOUT = 'packages/types/src/layout.ts'; |
| 107 | +const FORM = 'packages/types/src/form.ts'; |
| 108 | + |
| 109 | +/** A docblock's text with its ` * ` continuations joined, as a reader sees it. */ |
| 110 | +const joinDocblock = (doc: string): string => doc.replace(/\s*\n\s*\*\s?/g, ' '); |
| 111 | + |
| 112 | +/** The docblock on `member` inside `export interface name`, joined. */ |
| 113 | +function memberDocblock(src: string, name: string, member: string): string { |
| 114 | + const start = src.indexOf(`export interface ${name} `); |
| 115 | + expect(start, `interface ${name} not found`).toBeGreaterThan(-1); |
| 116 | + const end = src.indexOf('\n}', start); |
| 117 | + const body = src.slice(start, end); |
| 118 | + const at = body.search(new RegExp(`\\n\\s*${member}\\?:`)); |
| 119 | + expect(at, `member ${name}.${member} not found`).toBeGreaterThan(-1); |
| 120 | + const before = body.slice(0, at); |
| 121 | + const open = before.lastIndexOf('/**'); |
| 122 | + expect(open, `no docblock before ${name}.${member}`).toBeGreaterThan(-1); |
| 123 | + return joinDocblock(before.slice(open)); |
| 124 | +} |
| 125 | + |
| 126 | +describe('objectui#10959 — a header-bar refusal names the parser tier\'s `unknown-prop`', () => { |
| 127 | + const NODE = { type: 'header-bar' as const, crumbs: [{ label: 'Home' }] }; |
| 128 | + const ARMS = [ |
| 129 | + ['title', 'My App', ELEMENT_CLAUSE], |
| 130 | + ['variant', 'floating', CLASS_CLAUSE], |
| 131 | + ] as const; |
| 132 | + |
| 133 | + it.each(ARMS)('`%s` is refused by name, and the refusal names `unknown-prop`', (key, value, clause) => { |
| 134 | + const result = HeaderBarSchema.safeParse({ ...NODE, [key]: value }); |
| 135 | + expect(result.success).toBe(false); |
| 136 | + const issue = result.success ? undefined : result.error.issues.find((i) => i.path.length === 1 && i.path[0] === key); |
| 137 | + expect(issue?.code).toBe('invalid_type'); |
| 138 | + expect(issue?.path).toEqual([key]); |
| 139 | + expect(issue?.message).toContain(clause); |
| 140 | + expect(issue?.message).not.toContain(RETIRED_STRING_CLAUSE); |
| 141 | + expect(issue?.message).not.toContain(WRONG_CODE); |
| 142 | + }); |
| 143 | +}); |
| 144 | + |
| 145 | +describe('objectui#10959 — every header-bar retirement carries the clause', () => { |
| 146 | + const shape = HeaderBarSchema.shape as Record<string, z.ZodType>; |
| 147 | + const members = Object.entries(shape) |
| 148 | + .map(([key, schema]) => ({ key, text: schema.description ?? '' })) |
| 149 | + .filter((m) => /^REFUSED \(objectui#(?:10387|10286), ADR-0049\)/.test(m.text)); |
| 150 | + |
| 151 | + it('the walk reaches both retirements (non-vacuity, by name rather than by count)', () => { |
| 152 | + const keys = members.map((m) => m.key); |
| 153 | + expect(keys).toContain('title'); |
| 154 | + expect(keys).toContain('variant'); |
| 155 | + }); |
| 156 | + |
| 157 | + it('none of them still says "no error, no warning", and each names `unknown-prop`', () => { |
| 158 | + const stale = members.filter((m) => m.text.includes(RETIRED_STRING_CLAUSE) || !m.text.includes(TAIL)); |
| 159 | + expect(stale.map((m) => m.key)).toEqual([]); |
| 160 | + }); |
| 161 | +}); |
| 162 | + |
| 163 | +describe('objectui#10959 — the `body?: never` docblocks name the parser tier\'s `unknown-prop`', () => { |
| 164 | + const sources = [ |
| 165 | + [LAYOUT, read(LAYOUT)], |
| 166 | + [FORM, read(FORM)], |
| 167 | + ] as const; |
| 168 | + |
| 169 | + it.each([ |
| 170 | + [LAYOUT, 'BoxSchema'], |
| 171 | + [FORM, 'FormSchema'], |
| 172 | + ])('%s — `%s.body` carries the clause', (path, name) => { |
| 173 | + const doc = memberDocblock(read(path), name, 'body'); |
| 174 | + expect(doc).toContain(MARK_8284); |
| 175 | + expect(doc).toContain(DOCBLOCK_CLAUSE); |
| 176 | + expect(doc).not.toContain(RETIRED_DOCBLOCK_CLAUSE); |
| 177 | + expect(doc).not.toContain(WRONG_CODE); |
| 178 | + }); |
| 179 | + |
| 180 | + it('every objectui#8284 `body` docblock in both files carries it (non-vacuity: both files reached)', () => { |
| 181 | + const stale: string[] = []; |
| 182 | + for (const [path, src] of sources) { |
| 183 | + const docs = (src.match(/\/\*\*[\s\S]*?\*\//g) ?? []).map(joinDocblock).filter((d) => d.includes(MARK_8284)); |
| 184 | + expect(docs.length, `${path}: no objectui#8284 docblock found`).toBeGreaterThan(0); |
| 185 | + docs.forEach((d, i) => { |
| 186 | + if (d.includes(RETIRED_DOCBLOCK_CLAUSE) || !d.includes(DOCBLOCK_CLAUSE)) stale.push(`${path} #${i}`); |
| 187 | + }); |
| 188 | + } |
| 189 | + expect(stale).toEqual([]); |
| 190 | + }); |
| 191 | +}); |
0 commit comments