, so block.start sits 2 spaces
// deeper than the original element. Using block.start as the deindent
// base on JSX accept/discard pushes every restored line 2 spaces too far
// right. The fix anchors the indent on `replaceRange.start` (the outer
// wrapper line), which is at the original element's indent level for
// both HTML and JSX.
it('discard restores JSX content at the original indent (no 2-space drift from marker-inside layout)', () => {
// Run the real wrap CLI so we exercise the JSX-marker-inside-wrapper
// layout end to end, not a hand-rolled approximation.
const tsx = `export default function App() {
return (
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
execFileSync('node', [WRAP, '--id', 'INDENTDISC', '--count', '3', '--classes', 'card', '--tag', 'aside', '--file', join(tmp, 'App.tsx')], {
cwd: tmp,
encoding: 'utf-8',
});
runAccept(tmp, ['--id', 'INDENTDISC', '--discard']);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
// The aside opener should land at exactly 6 spaces — same as the
// original — and the
child at 8 (preserved relative depth).
// The earlier 6/6/6 collapse was caused by `originalLines.map(l =>
// indent + ' ' + l.trimStart())` in live-wrap stripping ALL
// leading whitespace before reindenting; the fix strips only the
// COMMON minimum so the relative structure is preserved.
assert.match(after, /^
// never matched the depth-zero condition; replace-range stopped at
// block.end (the marker comment), leaving the wrapper's outer
// orphaned in the file after accept/discard — and worse, an
// unrelated
right after the wrapper got
// its own
mis-counted as the wrapper close.
const tsx = `export default function App() {
return (
After
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
execFileSync('node', [WRAP, '--id', 'MULTILINESC', '--count', '3', '--classes', 'card', '--tag', 'aside', '--file', join(tmp, 'App.tsx')], {
cwd: tmp,
encoding: 'utf-8',
});
const result = runAccept(tmp, ['--id', 'MULTILINESC', '--discard']);
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
// The wrapper scaffold must be fully gone — no orphan from
// the outer wrapper, and no impeccable markers/data attributes.
assert.ok(!after.includes('data-impeccable-variants'),
`outer wrapper div must be fully removed; got:\n${after}`);
assert.ok(!after.includes('data-impeccable-variant'),
`original-div wrapper must be fully removed; got:\n${after}`);
assert.ok(!after.includes('impeccable-variants-start'),
`start marker must be removed; got:\n${after}`);
// The unrelated
After
sibling
// must survive intact — Bugbot's worst-case scenario was the depth
// walk eating its as the wrapper close.
assert.ok(after.includes('
After
'),
`unrelated next-card sibling must be preserved; got:\n${after}`);
// The multi-line self-closing div inside the original element must
// survive too.
assert.match(after, //m,
`multi-line self-closing inside original must survive; got:\n${after}`);
});
it('expandReplaceRange finds JSX wrapper openers with long multi-line attributes', () => {
const extraAttrs = Array.from({ length: 18 }, (_, i) => ` data-extra-${i}="x"`).join('\n');
const tsx = `export default function App() {
return (
{/* impeccable-variants-start LONGOPEN */}
{/* Original */}
{/* Variants: insert below this line */}
{/* impeccable-variants-end LONGOPEN */}
After
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
const result = runAccept(tmp, ['--id', 'LONGOPEN', '--discard']);
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
assert.doesNotMatch(after, /data-impeccable-variants/);
assert.doesNotMatch(after, /impeccable-variants-start/);
assert.match(after, /