/** * JinjaAdapter — conditional-spread / nullish-omission / hyphenated-key * regression tests (#textarea / #checkbox Phase 2b parity). * * Ported from `packages/adapter-xslate/src/__tests__/xslate-spread-attrs.test.ts`, * translating each expected template string to Jinja syntax. The shared * adapter-conformance fixtures (`textarea`, `checkbox`) only exercise the * falsy branch of the conditional spread and the unset branch of the * optional attr, so these unit tests pin the truthy / present branches the * fixtures can't reach. * * One test (`hyphenated child attr hash key`) is NOT a byte-for-byte * translation: Jinja dict-literal keys are ALWAYS quoted (`jinjaHashKey` * unconditionally single-quotes, unlike Kolon's bareword-key sugar — * `{key: value}` in Jinja/Python means "look up variable `key`", not the * string `"key"` — see `lib/jinja-naming.ts`'s docstring), so there is no * "stays unquoted" case to assert for a bare-identifier-safe prop name like * `size`; only that BOTH keys are correctly quoted. */ import { test, expect, describe } from 'bun:test' import { compileJSX } from '@barefootjs/jsx' import type { ComponentIR } from '@barefootjs/jsx' import { JinjaAdapter } from '../adapter' function compileToIR(source: string, adapter?: JinjaAdapter): ComponentIR { const result = compileJSX(source.trimStart(), 'test.tsx', { adapter: adapter ?? new JinjaAdapter(), outputIR: true, }) const irFile = result.files.find(f => f.type === 'ir') if (!irFile) throw new Error('No IR output') return JSON.parse(irFile.content) as ComponentIR } function compileAndGenerate(source: string, adapter?: JinjaAdapter) { const a = adapter ?? new JinjaAdapter() const ir = compileToIR(source, a) return a.generate(ir) } describe('JinjaAdapter - conditional inline-object spread (textarea aria-describedby)', () => { // `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to a Jinja inline // ternary of dicts so the falsy `{}` branch OMITS the key // (bf.spread_attrs does not emit empty entries). The shared fixture only // exercises the falsy branch; this pins the truthy one. test('emits a Jinja inline ternary of dicts through bf.spread_attrs', () => { const { template } = compileAndGenerate(` function Box({ describedBy }: { describedBy?: string }) { return
} `) expect(template).toContain( "bf.spread_attrs(({'aria-describedby': describedBy} if bf.truthy(describedBy) else {}))", ) }) test('resolves the value reference and preserves the static key for a second prop', () => { const { template } = compileAndGenerate(` function Box({ label }: { label: string }) { return } `) expect(template).toContain( "bf.spread_attrs(({'data-label': label} if bf.truthy(label) else {}))", ) }) test('falls back to BF101 for a computed (non-static) object key', () => { const adapter = new JinjaAdapter() const ir = compileToIR(` function Box({ k, v }: { k?: string; v?: string }) { return } `, adapter) adapter.generate(ir) const errs = (adapter as unknown as { errors: { code: string }[] }).errors expect(errs.some(e => e.code === 'BF101')).toBe(true) }) }) describe('JinjaAdapter - local-const conditional-spread resolution (#checkbox icon)', () => { // A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, spread as a bare // identifier (`{...attrs}`), resolves through the same Jinja // ternary-of-dicts lowering as the inline form. CheckIcon's // `const sizeAttrs = size ? {…} : {}` is exactly this shape. test('resolves a bare-identifier spread of a function-scope conditional const', () => { const { template } = compileAndGenerate(` function Box({ flag }: { flag?: boolean }) { const attrs = flag ? { 'data-on': 'yes' } : {} return } `) expect(template).toContain( "bf.spread_attrs(({'data-on': 'yes'} if bf.truthy(flag) else {}))", ) }) // A const that aliases another bare identifier must NOT be forwarded (loop // guard): the resolver bails, so the spread falls through to the standard // lowering emitting the bare `attrs` variable. test('does not forward a const that aliases another identifier (loop guard)', () => { const { template } = compileAndGenerate(` function Box({ other }: { other?: object }) { const attrs = other return } `) // The fallthrough reads the computed const as its initializer (#3494). expect(template).toContain('bf.spread_attrs(other)') }) }) describe('JinjaAdapter - Record