import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { parse, compile } from 'svelte/compiler'; import { analyzeSvelteMarkup, buildPropsScriptV2, collectRootIdentifiers, derivePropName, restoreSvelteMarkup, } from '../skill/scripts/live/svelte-ast.mjs'; const PIT_BOARD = `
    {#each stages as stage, i}
  1. {stage.label}

    {stage.detail}

    {#if stage.active}{/if}
  2. {/each}
`; describe('svelte AST scaffolding', () => { it('turns a free each collection into one structured prop and keeps the loop intact', () => { const res = analyzeSvelteMarkup(PIT_BOARD, parse); assert.equal(res.ok, true, res.reason); const collection = res.contract.find((c) => c.kind === 'collection'); assert.equal(collection.prop, 'stages'); assert.equal(collection.expr, 'stages'); // The loop body survives verbatim: bound expressions are not props. assert.match(res.markupWithProps, /\{#each stages as stage, i\}/); assert.match(res.markupWithProps, /\{stage\.label\}/); assert.match(res.markupWithProps, /\{#if stage\.active\}/); // No prop entries for bound expressions. assert.equal(res.contract.some((c) => c.expr.includes('stage.')), false); }); it('records the item hydration description for each blocks', () => { const res = analyzeSvelteMarkup(PIT_BOARD, parse); const collection = res.contract.find((c) => c.kind === 'collection'); assert.equal(collection.item.rootTag, 'li'); assert.deepEqual(collection.item.rootClasses, ['stage']); assert.deepEqual(collection.item.textSlots.map((s) => s.key), ['label', 'detail']); }); it('extracts free text and attribute expressions as props', () => { const res = analyzeSvelteMarkup(PIT_BOARD, parse); const props = Object.fromEntries(res.contract.map((c) => [c.expr, c])); assert.equal(props['footerNote'].kind, 'text'); assert.equal(props['activeId'].kind, 'text'); assert.match(res.markupWithProps, /data-active=\{activeId\}/); }); it('names collection props from member tails and restores them precisely', () => { const src = ``; const res = analyzeSvelteMarkup(src, parse); assert.equal(res.ok, true, res.reason); assert.match(res.markupWithProps, /\{#each stages as s\}/); const restored = restoreSvelteMarkup(res.markupWithProps, res.contract, parse); assert.equal(restored.ok, true); assert.equal(restored.markup, src); }); it('classifies event handler expressions as handler props', () => { const src = ``; const res = analyzeSvelteMarkup(src, parse); assert.equal(res.ok, true, res.reason); const handler = res.contract.find((c) => c.expr === 'handleGo'); assert.equal(handler.kind, 'handler'); }); it('deduplicates repeated expressions and disambiguates name collisions', () => { const src = `

{a.title}

{b.title}

{a.title}

`; const res = analyzeSvelteMarkup(src, parse); assert.equal(res.ok, true, res.reason); const names = res.contract.map((c) => c.prop); assert.deepEqual(names, ['title', 'title2']); }); it('supports independent nested each blocks but rejects bound nested ones', () => { const independent = `
{#each rows as r}

{r.x}

{/each}{#each cols as c}

{c.y}

{/each}
`; const okRes = analyzeSvelteMarkup(independent, parse); assert.equal(okRes.ok, true, okRes.reason); assert.equal(okRes.contract.filter((c) => c.kind === 'collection').length, 2); const nested = ``; const badRes = analyzeSvelteMarkup(nested, parse); assert.equal(badRes.ok, false); assert.match(badRes.reason, /nested/); }); it('falls back for constructs a detached preview cannot support', () => { const cases = [ [`
`, /component tag/], [``, /bind:/], [`
{t}
`, /use:/], [`
{#await p}

wait

{:then v}

{v}

{/await}
`, /await/], [`