test(pen-core): static guard against CSS-side padding shorthands

Greps every builder source for paddingTop/Right/Bottom/Left as a
property key and fails if any reappear. The unified padding field is
the only form resolvePadding reads; the CSS-side siblings render with
zero inset, which slipped past type checks because ElementTree is
Record<string, unknown>. JSDoc / line-comment mentions stay allowed
so builders can still document the trap inline.
This commit is contained in:
Fini 2026-04-27 08:25:00 +08:00
parent c5655000fd
commit b79ce5233d

View file

@ -0,0 +1,68 @@
/**
* Static guard: element-builder source files must not use the CSS-side
* padding shorthands `paddingTop` / `paddingRight` / `paddingBottom`
* / `paddingLeft` as object property assignments.
*
* The layout engine's `resolvePadding` (`packages/pen-core/src/layout/
* engine.ts`) only reads the unified `padding` field
* (`number | [T,R] | [T,R,B,L] | string`); the CSS-side siblings
* are silently dropped, so nodes that declared them rendered with
* zero insets. Codex caught this on `add_sidebar_nav_v0` (commit
* 5c88aca6) and a 14-builder follow-up sweep landed in 5c91ff19.
*
* This test makes the trap loud — any future builder that re-introduces
* the shorthand fails CI before reaching review. References to the
* field names in JSDoc / line comments are still allowed (`* `, `//`)
* because that's how the warning is documented in builder doc blocks.
*/
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
const BUILDERS_DIR = join(__dirname, '..', 'element-builders');
const FORBIDDEN = ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'] as const;
function isCommentLine(line: string): boolean {
const trimmed = line.trimStart();
return (
trimmed.startsWith('//') ||
trimmed.startsWith('*') ||
trimmed.startsWith('/*') ||
trimmed.startsWith('*/')
);
}
describe('element-builders — no CSS-side padding shorthands', () => {
it('every builder uses padding: [T,R,B,L] / [T,R] / number, never paddingTop/Right/Bottom/Left', () => {
const offenders: string[] = [];
const files = readdirSync(BUILDERS_DIR).filter(
(f) => f.endsWith('.ts') && !f.endsWith('.d.ts'),
);
for (const file of files) {
const src = readFileSync(join(BUILDERS_DIR, file), 'utf-8');
const lines = src.split('\n');
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
if (isCommentLine(line)) continue;
for (const field of FORBIDDEN) {
// Match `field:` as a property key (allow leading whitespace,
// optional quotes). Avoids matching inside strings of comments
// already filtered above; identifier boundary in front prevents
// matching `notPaddingTop`.
const re = new RegExp(`(?:^|[\\s,{('"\`])${field}\\s*:`);
if (re.test(line)) {
offenders.push(`${file}:${i + 1}: ${line.trim()}`);
}
}
}
}
expect(
offenders,
`Builders using CSS-side padding shorthand silently render with 0 inset. ` +
`Switch to the unified padding field (number | [T,R] | [T,R,B,L]).\n` +
offenders.join('\n'),
).toEqual([]);
});
});