|
| 1 | +import { describe, expect, it } from 'vitest'; |
| 2 | +import { compile, generateDts, manifestFromConfigs } from '../index.js'; |
| 3 | + |
| 4 | +// A tiny public-tier manifest, shaped exactly like getAllConfigs() output. |
| 5 | +const manifest = manifestFromConfigs([ |
| 6 | + { type: 'flex', namespace: 'ui', isContainer: true, inputs: [ |
| 7 | + { name: 'direction', type: 'enum', enum: ['row', 'col'] }, |
| 8 | + { name: 'gap', type: 'number' }, |
| 9 | + { name: 'wrap', type: 'boolean' }, |
| 10 | + ] }, |
| 11 | + { type: 'card', namespace: 'ui', isContainer: true, inputs: [ |
| 12 | + { name: 'title', type: 'string' }, |
| 13 | + ] }, |
| 14 | + { type: 'object-table', namespace: 'plugin-grid', isContainer: false, inputs: [ |
| 15 | + { name: 'object', type: 'string', required: true, binding: 'object' }, |
| 16 | + { name: 'columns', type: 'array' }, |
| 17 | + { name: 'pageSize', type: 'number' }, |
| 18 | + ] }, |
| 19 | +]); |
| 20 | + |
| 21 | +describe('compile (parse + validate)', () => { |
| 22 | + it('compiles valid JSX to a tree and reports requires + bindings', () => { |
| 23 | + const r = compile( |
| 24 | + `<flex direction="row" gap={4} wrap> |
| 25 | + <object-table object="account" columns={["name","amount"]} pageSize={25} /> |
| 26 | + </flex>`, |
| 27 | + manifest, |
| 28 | + ); |
| 29 | + expect(r.ok).toBe(true); |
| 30 | + expect(r.diagnostics).toEqual([]); |
| 31 | + expect(r.tree).toMatchObject({ |
| 32 | + type: 'flex', |
| 33 | + direction: 'row', |
| 34 | + gap: 4, |
| 35 | + wrap: true, |
| 36 | + children: [{ type: 'object-table', object: 'account', pageSize: 25 }], |
| 37 | + }); |
| 38 | + expect(r.requires.sort()).toEqual(['plugin-grid', 'ui']); |
| 39 | + expect(r.bindings).toEqual([ |
| 40 | + { tag: 'object-table', input: 'object', kind: 'object', value: 'account' }, |
| 41 | + ]); |
| 42 | + }); |
| 43 | + |
| 44 | + it('rejects unknown components (whitelist = manifest tags)', () => { |
| 45 | + const r = compile(`<flex><script>alert(1)</script></flex>`, manifest); |
| 46 | + expect(r.ok).toBe(false); |
| 47 | + expect(r.diagnostics.map((d) => d.code)).toContain('forbidden-tag'); |
| 48 | + }); |
| 49 | + |
| 50 | + it('flags a missing required prop (completeness)', () => { |
| 51 | + const r = compile(`<object-table columns={[]} />`, manifest); |
| 52 | + expect(r.ok).toBe(false); |
| 53 | + expect(r.diagnostics).toContainEqual( |
| 54 | + expect.objectContaining({ code: 'missing-required-prop' }), |
| 55 | + ); |
| 56 | + }); |
| 57 | + |
| 58 | + it('flags an illegal enum value and a coarse type mismatch', () => { |
| 59 | + const r = compile(`<flex direction="diagonal" gap="big" />`, manifest); |
| 60 | + expect(r.diagnostics.map((d) => d.code)).toEqual( |
| 61 | + expect.arrayContaining(['invalid-enum', 'type-mismatch']), |
| 62 | + ); |
| 63 | + }); |
| 64 | + |
| 65 | + it('rejects event handlers and raw-html injection', () => { |
| 66 | + const r = compile(`<card onClick="steal()" dangerouslySetInnerHTML={{}} />`, manifest); |
| 67 | + expect(r.diagnostics.filter((d) => d.code === 'forbidden-attr')).toHaveLength(2); |
| 68 | + }); |
| 69 | +}); |
| 70 | + |
| 71 | +describe('generateDts (the JSX type surface)', () => { |
| 72 | + it('emits a JSX.IntrinsicElements augmentation from the manifest', () => { |
| 73 | + const dts = generateDts(manifest); |
| 74 | + expect(dts).toContain('"object-table": ObjectTableProps;'); |
| 75 | + expect(dts).toContain('export interface ObjectTableProps extends SduiBaseProps'); |
| 76 | + expect(dts).toContain('object: string;'); // required → not optional |
| 77 | + expect(dts).toContain('pageSize?: number;'); // optional |
| 78 | + expect(dts).toContain('direction?: "row" | "col";'); // enum → union |
| 79 | + }); |
| 80 | +}); |
0 commit comments