Files
pbakaus_impeccable/tests/live-svelte-ast.test.mjs
T
Paul BakausandClaude Code a6f965e8bf fix: address second round of PR review bot findings
cursor[bot]:
- style: directives with dynamic values now fall back to source-preview
  instead of being scaffolded as boolean condition props that falsified
  the style in the detached preview.
- class: directives carry a className probe, so v2 hydration answers the
  condition from the live DOM instead of always defaulting to false.
- The existing-wrapper remount path now checks the mount result; a failed
  remount keeps the error card instead of advancing to a CYCLING bar over
  a page where nothing rendered.

greptile-apps[bot]:
- The repo-root live pointer records every booted app (most recent
  first) and resolution prefers the app whose helper server is alive, so
  a helper run from the repo root of a two-app monorepo can no longer be
  redirected onto the wrong app's session store by the last boot. Legacy
  single-value pointers still read.

This work was produced with AI assistance (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-27 15:34:57 -07:00

239 lines
10 KiB
JavaScript

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 = `<ol class="stages" data-active={activeId}>
{#each stages as stage, i}
<li class="stage">
<span class="label">{stage.label}</span>
<p class="detail">{stage.detail}</p>
{#if stage.active}<span class="dot"></span>{/if}
</li>
{/each}
<p class="footer">{footerNote}</p>
</ol>`;
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 = `<ul>{#each data.stages as s}<li>{s.name}</li>{/each}</ul>`;
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 = `<button onclick={handleGo} class="go">{label}</button>`;
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 = `<div><h1>{a.title}</h1><h2>{b.title}</h2><p>{a.title}</p></div>`;
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 = `<div>{#each rows as r}<p>{r.x}</p>{/each}{#each cols as c}<p>{c.y}</p>{/each}</div>`;
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 = `<ul>{#each groups as g}<li>{#each g.items as item}<span>{item.n}</span>{/each}</li>{/each}</ul>`;
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 = [
[`<div><Card title={t} /></div>`, /component tag/],
[`<input bind:value={query} />`, /bind:/],
[`<div use:tooltip>{t}</div>`, /use:/],
[`<div>{#await p}<p>wait</p>{:then v}<p>{v}</p>{/await}</div>`, /await/],
[`<div><script>let x = 1;<\/script><p>{t}</p></div>`, /script/],
[`<div {...rest}>{t}</div>`, /spread/],
];
for (const [src, reason] of cases) {
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, false, `expected fallback for: ${src}`);
assert.match(res.reason, reason, src);
}
});
it('treats class directives as condition props', () => {
const src = `<div class:active={isActive}><span>{label}</span></div>`;
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, true, res.reason);
const cond = res.contract.find((c) => c.expr === 'isActive');
assert.equal(cond.kind, 'condition');
});
it('round-trips every supported corpus snippet', () => {
const corpus = [
PIT_BOARD,
`<ul>{#each data.stages as s, i (s.id)}<li>{s.name}</li>{/each}</ul>`,
`<section>{#if user.loggedIn}<p>{user.name}</p>{:else}<p>guest</p>{/if}</section>`,
`<div>{@html bodyHtml}</div>`,
`<article>{#each posts as post}<h2>{post.title}</h2>{#if post.pinned}<em>pinned</em>{/if}{/each}</article>`,
`<p title={hint}>{copy} and {copy}</p>`,
`<div>{@const label = row.name}<span>{label}</span></div>`,
`<nav>{#each links as link}<a href={link.href}>{link.text}</a>{/each}</nav>`,
];
for (const src of corpus) {
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, true, `${res.reason} in: ${src}`);
const restored = restoreSvelteMarkup(res.markupWithProps, res.contract, parse);
assert.equal(restored.ok, true, src);
assert.equal(restored.markup, src, `round trip failed for: ${src}`);
}
});
it('generates a props script that compiles with real svelte', () => {
const res = analyzeSvelteMarkup(PIT_BOARD, parse);
const component = `${buildPropsScriptV2(res.contract)}\n${res.markupWithProps}\n<style>\n .stage { display: flex; }\n</style>\n`;
const compiled = compile(component, { generate: 'client' });
assert.ok(compiled.js.code.length > 0);
const fatal = (compiled.warnings || []).filter((w) => /error/i.test(w.code || ''));
assert.deepEqual(fatal, []);
});
it('collectRootIdentifiers skips member properties and object keys', () => {
const ast = parse(`<p>{fmt(user.name, { width: cols })}</p>`, { modern: true });
const tag = ast.fragment.nodes[0].fragment.nodes[0];
const roots = collectRootIdentifiers(tag.expression);
assert.deepEqual([...roots].sort(), ['cols', 'fmt', 'user']);
});
it('derivePropName picks stable tails', () => {
assert.equal(derivePropName('stages'), 'stages');
assert.equal(derivePropName('data.stages'), 'stages');
assert.equal(derivePropName('rows[0].label'), 'label');
assert.equal(derivePropName('a + b'), 'value');
});
});
describe('keyed each blocks', () => {
it('records a keyField for member keys and keeps the key in the scaffold', () => {
const src = `<ul>{#each expenses as expense, i (expense.id)}<li class="row"><strong>{expense.name}</strong></li>{/each}</ul>`;
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, true, res.reason);
const collection = res.contract.find((c) => c.kind === 'collection');
assert.equal(collection.item.keyField, 'id');
assert.match(res.markupWithProps, /\(expense\.id\)/);
const restored = restoreSvelteMarkup(res.markupWithProps, res.contract, parse);
assert.equal(restored.markup, src);
});
it('needs no keyField when the key is the item or the index', () => {
for (const src of [
`<ul>{#each rows as r (r)}<li>{r.x}</li>{/each}</ul>`,
`<ul>{#each rows as r, i (i)}<li>{r.x}</li>{/each}</ul>`,
]) {
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, true, `${res.reason} in ${src}`);
const collection = res.contract.find((c) => c.kind === 'collection');
assert.equal(collection.item.keyField, undefined, src);
}
});
it('falls back for keys a detached preview cannot hydrate distinctly', () => {
const cases = [
[`<ul>{#each rows as r (globalThing)}<li>{r.x}</li>{/each}</ul>`, /not derived from the loop item/],
[`<ul>{#each rows as r (makeKey(r))}<li>{r.x}</li>{/each}</ul>`, /complex each key/],
[`<ul>{#each rows as r (r.name)}<li>{r.name}</li>{/each}</ul>`, /also a displayed field/],
];
for (const [src, reason] of cases) {
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, false, `expected fallback for ${src}`);
assert.match(res.reason, reason, src);
}
});
});
describe('review regressions: reserved prop names', () => {
it('never derives a JS reserved word as a prop name (M5)', () => {
for (const [src, expected] of [
[`<div>{item.class}</div>`, 'classValue'],
[`<div>{cfg.default}</div>`, 'defaultValue'],
[`<div>{a.for}</div>`, 'forValue'],
]) {
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, true, res.reason);
assert.equal(res.contract[0].prop, expected, src);
// The generated stub must actually compile.
const stub = `${buildPropsScriptV2(res.contract)}\n${res.markupWithProps}\n`;
const compiled = compile(stub, { generate: 'client' });
assert.ok(compiled.js.code.length > 0, src);
// And restore back to the original expression.
const restored = restoreSvelteMarkup(res.markupWithProps, res.contract, parse);
assert.equal(restored.markup, src);
}
});
});
describe('review regressions: directives', () => {
it('class directives carry a className probe for live hydration', () => {
const src = `<div class:active={isActive}><span>{label}</span></div>`;
const res = analyzeSvelteMarkup(src, parse);
assert.equal(res.ok, true, res.reason);
const cond = res.contract.find((c) => c.expr === 'isActive');
assert.deepEqual(cond.probe, { className: 'active' });
});
it('style directives with dynamic values fall back to source-preview', () => {
const res = analyzeSvelteMarkup(`<div style:opacity={fade}>x</div>`, parse);
assert.equal(res.ok, false);
assert.match(res.reason, /style:opacity/);
});
it('style directives with static values stay supported', () => {
const res = analyzeSvelteMarkup(`<div style:color="red">{note}</div>`, parse);
assert.equal(res.ok, true, res.reason);
});
});