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}
-
{stage.label}
{stage.detail}
{#if stage.active}{/if}
{/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 = `{#each data.stages as s}- {s.name}
{/each}
`;
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 = `{#each groups as g}- {#each g.items as item}{item.n}{/each}
{/each}
`;
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/],
[`