mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
fix(live): textContent disambiguation handles missing inter-element whitespace
While driving the new live loop end-to-end against the repeated-aside
fixture, --text disambiguation silently fell back to first-match instead
of landing on the picked card.
Root cause: `el.textContent` concatenates child text nodes without
inserting whitespace, so `<h1>Hero Two</h1><p>Second card body copy.</p>`
reads as "Hero TwoSecond card body copy." — but the source has whitespace
between </h1> and <p>. The single-space normalization on both sides
missed the join boundary; substring comparison failed; filterByText
returned [] and the caller fell through to first-match.
Fix: filterByText now compares both single-space AND no-whitespace
normalizations on each side, accepting the candidate if EITHER matches.
Bumped the minimum-target-length threshold from 6 to 8 to compensate
for the slightly looser comparison.
Plus two doc clarifications surfaced during the same session:
- live.md now warns that variant CSS using bare `:scope { ... }` styles
the variant wrapper div, not the picked element. Always use a
descendant combinator (`:scope > .card`, `:scope .hero-title`, etc.) —
the fake test agent's CSS is the canonical template.
- live.md documents the agent-side abort path. Aborting an in-flight
generate via `live-accept --discard` only mutates source — the browser
bar stays in GENERATING forever. Use `live-poll --reply EVENT_ID error
"msg"` instead so the browser receives the error SSE and resets.
Test coverage:
- New unit test in live-wrap.test.mjs covering the textContent-without-
inter-element-whitespace shape (three identical <aside> branches each
with <h1> + <p>, picks the second by --text).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
54d9f05ea5
commit
9ec904302b
@@ -534,6 +534,49 @@ describe('live-wrap — JSX / TSX correctness', () => {
|
||||
assert.ok(!inside.includes('Gamma card'), 'did not wrap Gamma');
|
||||
});
|
||||
|
||||
it('disambiguates when the picked element has multiple text-node children (textContent has no inter-element whitespace)', () => {
|
||||
// Real-world regression caught while driving a live loop in the browser.
|
||||
// textContent concatenates child text without inserting whitespace, so
|
||||
// an <aside><h1>Hero Two</h1><p>Second card body copy.</p></aside> reads
|
||||
// as "Hero TwoSecond card body copy." — but the source has whitespace
|
||||
// between </h1> and <p>. A single-space normalization on both sides
|
||||
// misses the join boundary; a no-whitespace normalization catches it.
|
||||
const tsx = `export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
<aside className="card">
|
||||
<h1 className="hero-title">Hero One</h1>
|
||||
<p className="hero-hook">First card body copy.</p>
|
||||
</aside>
|
||||
<aside className="card">
|
||||
<h1 className="hero-title">Hero Two</h1>
|
||||
<p className="hero-hook">Second card body copy.</p>
|
||||
</aside>
|
||||
<aside className="card">
|
||||
<h1 className="hero-title">Hero Three</h1>
|
||||
<p className="hero-hook">Third card body copy.</p>
|
||||
</aside>
|
||||
</main>
|
||||
);
|
||||
}`;
|
||||
writeFileSync(join(tmp, 'Page.tsx'), tsx);
|
||||
|
||||
// Note: --text is the textContent the BROWSER produced — no space between
|
||||
// "Two" and "Second" because textContent has no inter-element whitespace.
|
||||
execSync(
|
||||
`node source/skills/impeccable/scripts/live-wrap.mjs --id concat1 --count 3 --classes "card" --tag "aside" --text "Hero TwoSecond card body copy." --file "${join(tmp, 'Page.tsx')}"`,
|
||||
{ cwd: process.cwd(), encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
const modified = readFileSync(join(tmp, 'Page.tsx'), 'utf-8');
|
||||
const originalMatch = modified.match(/data-impeccable-variant="original"[\s\S]*?<\/div>/);
|
||||
assert.ok(originalMatch, 'original wrapper present');
|
||||
const inside = originalMatch[0];
|
||||
assert.ok(inside.includes('Hero Two'), 'wrapped Hero Two (the picked card)');
|
||||
assert.ok(!inside.includes('Hero One'), 'did not wrap Hero One');
|
||||
assert.ok(!inside.includes('Hero Three'), 'did not wrap Hero Three');
|
||||
});
|
||||
|
||||
it('falls back to first-match when --text is not literally present in source (e.g. {title})', () => {
|
||||
// textContent the browser sends is the rendered text, but the source uses
|
||||
// a JSX expression. No candidate's source body contains the literal
|
||||
|
||||
Reference in New Issue
Block a user