Files
pbakaus_impeccable/tests/live-svelte-adapter-deepseek.test.mjs
T
5b5e487a4f Improve live mode configure bar and pick UX (#242)
* Fix: tear down annotation overlay when Escape exits live pick mode.

The configure prompt auto-focuses and bypasses the global Escape handler, so its local path must hide the annot overlay; togglePick off now does the same as a safety net.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Improve live mode steer pill typing affordance.

Show a visible caret and placeholder when focused, expand on pointerdown, and drop the muddy border so the graphite surface carries the affordance alone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Improve live mode configure bar layout and pill styling.

Align pills and input on a shared text track, refine muted pill chrome with a quiet action border, and center the row with symmetric inset so spacing reads evenly in the 36px bar.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add x1 to live mode variant count picker.

The configure bar count pill now cycles 1→2→3→4→1 so users can request a single variant.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Polish live mode configure bar, edit badge, and action picker.

Refine selection pill layout and tooltips, shrink edit copy to an icon aligned with the outline, right-align the action picker, and sync demo styles and regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix live mode element nav when configure input is focused.

Passthrough empty arrow keys from the configure and steer prompts so handleKeyDown can move between pickable elements without breaking autofocus typing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove accidental live.js inject from Base.astro.

Strip the localhost helper script tag left over from local live mode iteration so the PR ships only intentional UI changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix review findings: pick-cursor state sync, anchor recovery, e2e selectors.

Code review of this branch surfaced ten confirmed bugs plus three smaller
ones; this commit fixes all of them.

- Route every interaction-state transition through a new setLiveState()
  helper that re-syncs the pick-mode crosshair, fixing four confirmed
  cursor bugs: never appearing on pick toggle (sync ran before the state
  change), sticking through the configure phase, surviving teardown
  page-wide, and the style mounting inside the adapter's shadow root
  where it can't match the host document (now document.head).
- Anchor recovery: a matching id is decisive again (hashed class names
  and component tags broke recovery), empty-text elements can no longer
  match the fuzzy text passes (".includes('')" hole plus shortest-text
  preference), and the dead 2-class-subset fallback is removed.
- Selection pill: drop the hover-only "armed" guard so keyboard
  activation works; the pill arms on focus as well as hover.
- Configure chrome: remove the configure-bar tooltip on teardown, align
  restorePickerBarChrome padding with initBar (5px), share the
  configure-input stylesheet with the insert row, and sync the
  ui-core.mjs surface inventory with live-browser.js.
- Site demos: delete the stale duplicate .live-demo-ctx-selection rule
  that killed the teal pill on dark pages, and keep the configure-phase
  demo bar on the overlay's dark surface in light mode so the near-white
  prompt text stays readable.
- E2E/contract tests: match the icon-only submit button by aria-label
  ("Generate variants") instead of the removed "Go" text, and update
  source-contract pins for setLiveState and buildConfigureSubmitButton.

Verified: bun run test green, live-mode E2E 23/23 across all fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Wire insert voice button into syncVoiceUi listening state.

Voice on the insert configure row runs through the same 'configure' mode,
but syncVoiceUi only stamped data-listening/aria state on the replace
row's #impeccable-live-configure-voice, so the insert button never pulsed
while listening. Target whichever of the two row buttons is mounted, the
same either-row pattern syncConfigureInputChrome uses.

Addresses Bugbot review comment on PR #242.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Reinject from source when the session wrapper lands during anchor recovery.

The anchor-recovery observer stood down as soon as the session's variant
wrapper appeared in the DOM, without running injectVariantsFromSource.
A wrapper can land incomplete (wrap HMR landed, variant insert did not),
which is exactly the case injectVariantsFromSource's existing-wrapper
replace path handles - so recovery ended with the bar stuck and no
variants. Route both the anchor-found and wrapper-landed cases through
injectVariantsFromSource, which owns wrapper replacement, recovery-flag
clearing, and variant display.

Addresses Bugbot review comment on PR #242.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Restore inline edit drafts before configure chrome teardown disables editing.

teardownConfigureChrome called disableInlineEdit() ahead of hideBar(),
wiping inlineEditRows and the impeccableOriginalText metadata that
hideBar()'s EDITING-state restoreInlineEditDrafts() needs - so turning
Pick off mid "Edit copy" left edited DOM text in place, neither saved
nor canceled. Let hideBar() own the sequence: it restores drafts first,
then disables inline edit.

Addresses Bugbot review comment on PR #242.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:18:45 -07:00

875 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createLlmAgent, resolveLlmAgentConfig } from './live-e2e/agents/llm-agent.mjs';
import { bootFixtureSession, FIXTURES_DIR, REPO_ROOT } from './live-e2e/session.mjs';
import { runSteerSmoke } from './live-e2e/steer.mjs';
import { runPreActions } from './live-e2e/preactions.mjs';
import {
assertAnnotationUploadEvent,
assertApplyDockVisible,
clickAccept,
clickApplyEdits,
clickDiscard,
clickEditCopy,
clickExitLiveMode,
clickGo,
clickNext,
clickPrev,
clickSaveEdit,
drawAnnotationPinAndStroke,
editTextLeaf,
getVisibleVariant,
installLiveQueryHelpers,
pickElement,
runInsertFlow,
runLiveChromeBottomBarSmoke,
waitForApplyDockHidden,
waitForBarHidden,
waitForCycling,
waitForHandshake,
} from './live-e2e/ui.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const FIXTURE_NAME = 'vite8-sveltekit-stateful';
const ROUTE_FILE = 'src/routes/+page.svelte';
const LAYOUT_FILE = 'src/routes/+layout.svelte';
const APP_HTML = 'src/app.html';
const EXPECTED_INSERT_PROMPT =
'Insert a concise muted footnote below the empty expenses card explaining that shared expenses sync automatically.';
const artifactRoot = createArtifactRoot();
let playwright;
let browser;
if (!process.env.DEEPSEEK_API_KEY) {
throw new Error('DEEPSEEK_API_KEY is required. This Svelte adapter sweep must never fall back to fake/mock AI.');
}
describe('Svelte live adapter DeepSeek browser sweep', () => {
before(async () => {
playwright = await import('playwright');
browser = await playwright.chromium.launch({ headless: true });
});
after(async () => {
if (browser) await browser.close();
});
it('covers live chrome, replace, edit, annotations, insert, refresh recovery, accept/discard, and exit', async (t) => {
const fixture = loadConsolidatedFixture();
const llmConfig = resolveLlmAgentConfig({ provider: 'deepseek' });
assert.equal(llmConfig.provider, 'deepseek');
const agent = await createLlmAgent({
config: llmConfig,
log: (m) => t.diagnostic('[deepseek] ' + m),
});
assert.ok(agent, 'DeepSeek agent must be available');
const session = await bootFixtureSession({
name: FIXTURE_NAME,
fixture,
browser,
agent,
wrapTarget: wrapTargetFromPickedElement,
log: (m) => t.diagnostic(m),
});
const { page, tmp, live, consoleErrors, teardown } = session;
const evidence = createEvidenceWriter({ page, tmp, live, consoleErrors });
try {
await evidence.capture('00-boot');
await assertSvelteAdapterInjection(tmp);
await waitForHandshake(page);
await assertShadowChrome(page);
await assertHostileCssIsolation(page);
await evidence.capture('01-handshake');
await runLiveChromeBottomBarSmoke(page, {
expectDetectMinCount: 1,
designRawText: 'Design System: Stateful Expense System',
});
await evidence.capture('02-bottom-bar');
await runSteerSmoke(page, tmp, fixture, (m) => t.diagnostic(m), {
unlockTimeoutMs: 120_000,
selectorTimeoutMs: 60_000,
runPreActions,
});
await evidence.capture('03-steer');
await runPreActions(page, fixture.runtime.preActions);
await assertOpenCount(page);
const discardSession = await runReplaceDiscardRecoveryFlow({ page, tmp, evidence });
await evidence.capture('04-replace-discard-recovered');
await assertDiscardedSession(tmp, discardSession.id);
await runEditCopyFlow({ page, tmp, live, evidence });
await evidence.capture('05-edit-copy-applied');
await runAnnotationGenerateFlow({ page, tmp, evidence });
await evidence.capture('06-annotation-discarded');
const acceptedReplace = await runAcceptReplaceFlow({ page, tmp, evidence });
await assertCompletedRealSource(tmp, acceptedReplace.id);
await evidence.capture('07-replace-accepted');
const acceptedInsert = await runInsertFlowWithRecovery({ page, tmp, evidence });
await assertCompletedRealSource(tmp, acceptedInsert.id);
await assertFinalSourceClean(tmp);
await evidence.capture('08-insert-accepted');
const beforeExit = await globalBarSnapshot(page);
await clickExitLiveMode(page);
const afterExit = await page.evaluate(() => ({
liveInit: window.__IMPECCABLE_LIVE_INIT__,
hasGlobalBar: Boolean(window.__impeccableLiveQuery?.('#impeccable-live-global-bar')),
}));
assert.ok(beforeExit.hasBar, 'global bottom bar exists before Exit');
assert.equal(afterExit.liveInit, false, 'live init flag clears after Exit');
assert.equal(afterExit.hasGlobalBar, false, 'global bottom bar is removed immediately after Exit');
await evidence.capture('09-exit');
assertNoConsoleErrors(consoleErrors);
} catch (err) {
await evidence.capture('failure');
throw err;
} finally {
await teardown();
}
});
});
function loadConsolidatedFixture() {
const fixturePath = join(FIXTURES_DIR, FIXTURE_NAME, 'fixture.json');
const fixture = JSON.parse(readFileSync(fixturePath, 'utf-8'));
fixture.runtime = {
styling: 'plain-css',
install: ['npm', 'install', '--no-audit', '--no-fund', '--loglevel=error'],
devCommand: ['npx', 'vite', 'dev', '--host', '127.0.0.1'],
readyPattern: 'Local:\\s+https?://[^:]+:(\\d+)',
readyTimeoutMs: 120000,
pickSelector: "[data-testid='expense-row']",
acceptedSourcePattern: '<article[^>]*class="[^\"]*\\bexpense-row\\b',
preActions: [
{ type: 'click', selector: "[data-testid='add-expense']" },
{ type: 'wait', selector: "[data-testid='expense-row']" },
],
stateProbe: {
textSelector: "[data-testid='open-count']",
expectedText: '1 offen',
windowProperty: '__impeccableStatefulMounts',
expectWindowUnchanged: true,
},
steer: {
message: 'make the title more direct for the expense dashboard',
sourceFile: ROUTE_FILE,
target: { classes: 'hero-title', tag: 'h1' },
expectSelector: 'h1.hero-title[data-impeccable-steer="e2e"]',
expectSourceContains: 'data-impeccable-steer="e2e"',
},
probe: {
expectLiveInit: true,
expectConsoleClean: true,
},
};
return fixture;
}
async function runReplaceDiscardRecoveryFlow({ page, tmp, evidence }) {
const stateBeforeCycling = await readStateProbe(page);
await pickElement(page, "[data-testid='expense-row']", { resetPickMode: true });
await assertSelectedElementChrome(page);
await selectAction(page, 'Polish');
const sourceBefore = readFileSync(join(tmp, ROUTE_FILE), 'utf-8');
await clickGo(page);
await waitForVisibleCycling(page, 3, { timeout: 240_000 });
const session = await currentSveltePreviewSession(page, tmp);
assert.equal(session.previewMode, 'svelte-component');
assert.match(session.previewFile, /^node_modules\/\.impeccable-live\/[^/]+\/manifest\.json$/);
assert.ok(existsSync(join(tmp, dirname(session.previewFile), 'v1.svelte')));
assert.ok(existsSync(join(tmp, dirname(session.previewFile), 'v2.svelte')));
assert.ok(existsSync(join(tmp, dirname(session.previewFile), 'v3.svelte')));
assert.equal(readFileSync(join(tmp, ROUTE_FILE), 'utf-8'), sourceBefore, 'real Svelte source is not mutated while cycling');
await assertStateProbeUnchanged(page, stateBeforeCycling, 'variant cycling should not remount Svelte app state');
await cycleTo(page, 2);
await cycleTo(page, 3);
await cycleTo(page, 2);
await assertVariantCounter(page, 2, 3);
await evidence.capture('replace-cycle-variant-2');
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForHandshake(page);
await assertRecoverableMessage(page, 2, 3);
await revealExpenseRow(page);
await waitForVisibleCycling(page, 3, { timeout: 60_000 });
await assertVariantCounter(page, 2, 3);
await clickDiscard(page);
await waitForBarHidden(page);
await page.waitForSelector("[data-testid='expense-row']", { state: 'visible', timeout: 20_000 });
await assertNoCyclingZero(page);
await assertNoRawSvelteExpressions(page);
return session;
}
async function runEditCopyFlow({ page, tmp, live, evidence }) {
await runPreActions(page, [{ type: 'click', selector: "[data-testid='add-expense']" }, { type: 'wait', selector: "[data-testid='expense-row']" }]);
await pickElement(page, "[data-testid='expense-row']", { resetPickMode: true });
const sourceBeforeCancel = readFileSync(join(tmp, ROUTE_FILE), 'utf-8');
await clickEditCopy(page);
await editTextLeaf(page, '.expense-row strong', 'Design snack cancelled');
await clickEditBadgeAction(page, 'Cancel');
await page.waitForFunction(
() => !window.__impeccableLiveQuery('[data-impeccable-editable="true"]'),
null,
{ timeout: 5_000 },
);
assert.equal(readFileSync(join(tmp, ROUTE_FILE), 'utf-8'), sourceBeforeCancel, 'Cancel does not mutate source');
await assertPendingDockCount(live, 0);
await clickEditCopy(page);
await editTextLeaf(page, '.expense-row strong', 'Design snack staged');
await clickSaveEdit(page);
await assertApplyDockVisible(page, 1);
await assertPendingDockCount(live, 1);
await clickPendingTrash(page);
await waitForApplyDockHidden(page);
await assertPendingDockCount(live, 0);
await page.waitForFunction(
() => document.querySelector('.expense-row strong')?.textContent?.includes('Design snack'),
null,
{ timeout: 10_000 },
);
await clickEditCopy(page);
await editTextLeaf(page, '.expense-row strong', 'Design snack approved');
await clickSaveEdit(page);
await assertApplyDockVisible(page, 1);
await evidence.capture('edit-pending-dock');
await clickApplyEdits(page);
await waitForPendingDockCleared(live, { timeout: 240_000 });
await waitForApplyDockHidden(page, { timeout: 60_000 });
const sourceAfter = readFileSync(join(tmp, ROUTE_FILE), 'utf-8');
assert.match(sourceAfter, /Design snack approved/, 'DeepSeek Apply writes edited copy to source');
assert.doesNotMatch(sourceAfter, /contenteditable|data-impeccable-editable|data-impeccable-original-text|impeccable-variants/, 'manual Apply does not leak live scaffolding');
}
async function runAnnotationGenerateFlow({ page, tmp, evidence }) {
await pickElement(page, '.hero-title', { resetPickMode: true });
await drawAnnotationPinAndStroke(page, { comment: 'Make the page title easier to scan' });
await selectAction(page, 'Polish');
await clickGo(page);
await waitForCycling(page, 3, { timeout: 180_000 });
const generateEvent = latestJournalEvent(tmp, (event) => event.type === 'generate' && event.screenshotPath);
await assertAnnotationUploadEvent(generateEvent);
assert.ok(existsSync(generateEvent.screenshotPath), 'annotation screenshot file exists');
await clickNext(page);
await assertVariantCounter(page, 2, 3);
await evidence.capture('annotation-cycle');
await clickDiscard(page);
await waitForBarHidden(page);
}
async function runAcceptReplaceFlow({ page, tmp, evidence }) {
await runPreActions(page, [{ type: 'click', selector: "[data-testid='add-expense']" }, { type: 'wait', selector: "[data-testid='expense-row']" }]);
await pickElement(page, "[data-testid='expense-row']", { resetPickMode: true });
await selectAction(page, 'Polish');
await clickGo(page);
await waitForVisibleCycling(page, 3, { timeout: 240_000 });
await cycleTo(page, 3);
const session = await currentSveltePreviewSession(page, tmp);
await evidence.capture('replace-before-accept');
await clickAccept(page, { expectedVariant: 3 });
await waitForSvelteAcceptComplete(tmp, session, { timeout: 120_000 });
await waitForBarHidden(page, { timeout: 20_000 }).catch(() => {});
const source = readFileSync(join(tmp, ROUTE_FILE), 'utf-8');
assert.match(source, /expense-row/, 'accepted replace stays in real Svelte source');
assert.doesNotMatch(source, /data-impeccable-variants|impeccable-variants-start|data-impeccable-variant=/, 'accepted replace source is clean');
assert.equal(existsSync(join(tmp, dirname(session.previewFile))), false, 'temp Svelte preview folder deleted after accept');
return session;
}
async function runInsertFlowWithRecovery({ page, tmp, evidence }) {
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForHandshake(page);
await page.waitForSelector('.empty-card', { state: 'visible', timeout: 20_000 });
await runInsertFlow(page, {
anchorSelector: '.empty-card',
position: 'after',
prompt: EXPECTED_INSERT_PROMPT,
});
const insertEvent = await waitForLatestJournalEvent(tmp, (event) => event.type === 'generate' && event.mode === 'insert');
assert.equal(insertEvent.mode, 'insert');
assert.equal(insertEvent.count, 3);
assert.equal(insertEvent.insert?.position, 'after');
assert.match(insertEvent.insert?.anchor?.outerHTML || '', /empty-card/);
await waitForCycling(page, 3, { timeout: 240_000 });
await cycleTo(page, 2);
await cycleTo(page, 3);
await cycleTo(page, 2);
await cycleTo(page, 3);
await assertVariantCounter(page, 3, 3);
const session = await currentSveltePreviewSession(page, tmp);
assert.equal(JSON.parse(readFileSync(join(tmp, session.previewFile), 'utf-8')).mode, 'insert');
await evidence.capture('insert-cycle-variant-3');
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForHandshake(page);
await waitForCycling(page, 3, { timeout: 60_000 });
await assertVariantCounter(page, 3, 3);
await page.waitForFunction(
() => Boolean(document.querySelector('.empty-card + *')),
null,
{ timeout: 20_000 },
);
await clickAccept(page, { expectedVariant: 3 });
await waitForSvelteAcceptComplete(tmp, session, { timeout: 120_000 });
await waitForBarHidden(page, { timeout: 20_000 }).catch(() => {});
const source = readFileSync(join(tmp, ROUTE_FILE), 'utf-8');
assert.match(source, /automatisch|synchron|sync|abgeglichen|shared expenses/i, 'accepted insert content lands in real source');
assert.doesNotMatch(source, /data-impeccable-variants|impeccable-variants-start|data-impeccable-variant=/, 'accepted insert source is clean');
assert.equal(existsSync(join(tmp, dirname(session.previewFile))), false, 'insert temp Svelte preview folder deleted after accept');
return session;
}
async function assertSvelteAdapterInjection(tmp) {
const appHtml = readFileSync(join(tmp, APP_HTML), 'utf-8');
const layout = readFileSync(join(tmp, LAYOUT_FILE), 'utf-8');
assert.doesNotMatch(appHtml, /live\.js|impeccable-live-start/, 'Svelte adapter leaves src/app.html untouched');
assert.match(layout, /ImpeccableLiveRoot|impeccable-live-svelte-start/, 'Svelte adapter patches layout with live root');
}
async function assertShadowChrome(page) {
const result = await page.evaluate(() => {
const host = document.getElementById('impeccable-live-root');
const root = host?.shadowRoot;
return {
hasHost: Boolean(host),
hasShadowRoot: Boolean(root),
hasGlobalBar: Boolean(root?.getElementById('impeccable-live-global-bar')),
documentHasGlobalBar: Boolean(document.querySelector('#impeccable-live-global-bar')),
};
});
assert.equal(result.hasHost, true);
assert.equal(result.hasShadowRoot, true);
assert.equal(result.hasGlobalBar, true);
assert.equal(result.documentHasGlobalBar, false, 'live chrome is not mounted in app DOM');
}
async function assertHostileCssIsolation(page) {
await waitForGlobalBarStable(page);
const before = await globalBarSnapshot(page);
const hostileStyle = await page.addStyleTag({
content: `
button, div, input, svg, #impeccable-live-root, impeccable-live-root * {
display: block !important;
color: rgb(255, 0, 255) !important;
background: rgb(0, 255, 255) !important;
font-size: 44px !important;
opacity: 0.2 !important;
z-index: 1 !important;
pointer-events: none !important;
}
`,
});
try {
const after = await globalBarSnapshot(page);
assert.equal(after.display, before.display, 'hostile CSS cannot change display');
assert.equal(after.pointerEvents, before.pointerEvents, 'hostile CSS cannot disable pointer events');
assert.ok(
Math.abs(Number.parseFloat(after.opacity) - Number.parseFloat(before.opacity)) < 0.01,
'hostile CSS cannot change opacity',
);
assert.equal(after.fontSize, before.fontSize, 'hostile CSS cannot change font size');
assert.equal(after.color, before.color, 'hostile CSS cannot change color');
assert.equal(after.backgroundColor, before.backgroundColor, 'hostile CSS cannot change background color');
assert.ok(Math.abs(after.rect.width - before.rect.width) < 1, 'hostile CSS cannot change bar width');
assert.ok(Math.abs(after.rect.height - before.rect.height) < 1, 'hostile CSS cannot change bar height');
} finally {
await hostileStyle.evaluate((el) => el.remove()).catch(() => {});
}
}
async function waitForGlobalBarStable(page) {
await installLiveQueryHelpers(page);
await page.waitForFunction(
() => {
const bar = window.__impeccableLiveQuery?.('#impeccable-live-global-bar');
if (!bar) return false;
const opacity = Number.parseFloat(getComputedStyle(bar).opacity || '0');
return opacity >= 0.999;
},
null,
{ timeout: 5_000 },
);
}
async function assertSelectedElementChrome(page) {
await installLiveQueryHelpers(page);
const result = await page.evaluate(() => {
const q = window.__impeccableLiveQuery;
const bar = q('#impeccable-live-bar');
const picker = q('#impeccable-live-picker');
const badge = q('#impeccable-live-edit-badge');
const highlight = q('#impeccable-live-highlight');
const input = q('#impeccable-live-input');
return {
hasBar: Boolean(bar),
barText: bar?.textContent || '',
hasPicker: Boolean(picker),
hasBadge: Boolean(badge),
badgeText: badge?.textContent || '',
badgeLabel: badge?.querySelector('button')?.getAttribute('aria-label') || badge?.title || '',
hasHighlight: Boolean(highlight),
highlightVisible: highlight?.style.display !== 'none',
hasInput: Boolean(input),
hasCount: /×\d+/.test(bar?.textContent || ''),
hasGo: /Go/.test(bar?.textContent || ''),
};
});
assert.equal(result.hasBar, true);
assert.equal(result.hasPicker, true);
assert.equal(result.hasBadge, true);
assert.match(result.badgeLabel, /Edit copy/);
assert.equal(result.hasHighlight, true);
assert.equal(result.highlightVisible, true);
assert.equal(result.hasInput, true);
assert.equal(result.hasCount, true);
assert.equal(result.hasGo, true);
}
async function selectAction(page, label) {
await installLiveQueryHelpers(page);
const opened = await page.evaluate(() => {
const bar = window.__impeccableLiveQuery('#impeccable-live-bar');
const btn = [...(bar?.querySelectorAll('button') || [])].find((candidate) => /|/.test(candidate.textContent || ''));
if (!btn) return false;
btn.click();
return true;
});
assert.equal(opened, true, 'action picker button opens');
await page.waitForFunction(
(expected) => {
const picker = window.__impeccableLiveQuery('#impeccable-live-picker');
return picker?.style.display !== 'none'
&& [...picker.querySelectorAll('button')].some((btn) => (btn.textContent || '').includes(expected));
},
label,
{ timeout: 5_000 },
);
const clicked = await page.evaluate((expected) => {
const picker = window.__impeccableLiveQuery('#impeccable-live-picker');
const btn = [...(picker?.querySelectorAll('button') || [])].find((candidate) => (candidate.textContent || '').includes(expected));
if (!btn) return false;
btn.click();
return true;
}, label);
assert.equal(clicked, true, `${label} action clicked`);
}
async function clickEditBadgeAction(page, label) {
const clicked = await page.evaluate((expected) => {
const badge = window.__impeccableLiveQuery('#impeccable-live-edit-badge');
const btn = [...(badge?.querySelectorAll('button') || [])].find((candidate) => (candidate.textContent || '').includes(expected));
if (!btn) return false;
btn.click();
return true;
}, label);
assert.equal(clicked, true, `${label} edit action clicked`);
}
async function clickPendingTrash(page) {
const dialog = page.waitForEvent('dialog', { timeout: 5_000 })
.then((d) => d.accept())
.catch(() => {});
const clicked = await page.evaluate(() => {
const dock = window.__impeccableLiveQuery('#impeccable-live-pending-dock');
const btn = [...(dock?.querySelectorAll('button') || [])].find((candidate) => /Discard copy edits/i.test(candidate.getAttribute('aria-label') || candidate.title || ''));
if (!btn) return false;
btn.click();
return true;
});
await dialog;
assert.equal(clicked, true, 'pending dock discard clicked');
}
async function cycleTo(page, target) {
for (let i = 0; i < 6; i++) {
const visible = await getVisibleVariant(page);
if (visible === target) return;
if (visible == null) await page.waitForTimeout(250);
else if (visible < target) await clickNext(page);
else await clickPrev(page);
}
assert.equal(await getVisibleVariant(page), target, `variant ${target} visible`);
}
async function waitForVisibleCycling(page, count, { timeout }) {
await waitForCycling(page, count, { timeout });
await page.waitForFunction(
(expectedCount) => {
const bar = window.__impeccableLiveQuery?.('#impeccable-live-bar');
if (!bar) return false;
const style = getComputedStyle(bar);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number.parseFloat(style.opacity || '0') > 0.5
&& (bar.textContent || '').includes(`/${expectedCount}`);
},
count,
{ timeout: 10_000 },
);
}
async function assertVariantCounter(page, variant, count) {
await page.waitForFunction(
({ variant, count }) => {
const bar = window.__impeccableLiveQuery('#impeccable-live-bar');
if (!bar || !(bar.textContent || '').includes(`${variant}/${count}`)) return false;
const style = getComputedStyle(bar);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number.parseFloat(style.opacity || '0') > 0.5;
},
{ variant, count },
{ timeout: 10_000 },
);
assert.equal(await getVisibleVariant(page), variant);
}
async function revealExpenseRow(page) {
const row = page.locator("[data-testid='expense-row']").first();
if (await row.isVisible().catch(() => false)) return;
await page.locator("[data-testid='add-expense']").click();
await row.waitFor({ state: 'visible', timeout: 10_000 });
}
async function assertRecoverableMessage(page, variant, count) {
await page.waitForFunction(
({ variant, count }) => {
const bar = window.__impeccableLiveQuery('#impeccable-live-bar');
const raw = localStorage.getItem('impeccable-live-session');
let saved = null;
try { saved = raw ? JSON.parse(raw) : null; } catch {}
return Boolean(
bar
&& /Variants ready\. Reveal the selected element to resume\./.test(bar.textContent || '')
&& saved?.visible === variant
&& saved?.expected === count
&& saved?.previewMode === 'svelte-component'
);
},
{ variant, count },
{ timeout: 30_000 },
);
}
async function currentSveltePreviewSession(page, tmp) {
const saved = await page.evaluate(() => {
const raw = localStorage.getItem('impeccable-live-session');
return raw ? JSON.parse(raw) : null;
});
assert.ok(saved?.id, 'live session is stored locally');
assert.equal(saved.previewMode, 'svelte-component');
assert.ok(saved.previewFile, 'Svelte preview manifest path is stored');
const manifest = JSON.parse(readFileSync(join(tmp, saved.previewFile), 'utf-8'));
return { ...saved, manifest };
}
async function waitForSvelteAcceptComplete(tmp, session, { timeout }) {
const manifestPath = join(tmp, session.previewFile);
const sourcePath = join(tmp, ROUTE_FILE);
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const snapshot = readSessionSnapshot(tmp, session.id);
const source = readFileSync(sourcePath, 'utf-8');
if (
snapshot?.phase === 'completed'
&& snapshot.sourceFile === ROUTE_FILE
&& !existsSync(manifestPath)
&& !/data-impeccable-variants|impeccable-variants-start|data-impeccable-variant=/.test(source)
) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(`Svelte accept did not complete for ${session.id}; snapshot=${JSON.stringify(readSessionSnapshot(tmp, session.id))}`);
}
function assertCompletedRealSource(tmp, id) {
const snapshot = readSessionSnapshot(tmp, id);
assert.equal(snapshot?.phase, 'completed');
assert.equal(snapshot.sourceFile, ROUTE_FILE);
assert.doesNotMatch(snapshot.sourceFile || '', /node_modules\/\.impeccable-live/);
assert.match(snapshot.previewFile || '', /node_modules\/\.impeccable-live\/[^/]+\/manifest\.json/);
}
function assertDiscardedSession(tmp, id) {
const snapshot = readSessionSnapshot(tmp, id);
assert.equal(snapshot?.phase, 'discarded');
}
function readSessionSnapshot(tmp, id) {
const file = join(tmp, '.impeccable/live/sessions', `${id}.snapshot.json`);
if (!existsSync(file)) return null;
return JSON.parse(readFileSync(file, 'utf-8'));
}
async function assertPendingDockCount(live, expected) {
const deadline = Date.now() + 20_000;
let last = null;
while (Date.now() < deadline) {
last = await getPendingDockCount(live);
if (last === expected) return;
await new Promise((resolve) => setTimeout(resolve, 250));
}
assert.equal(last, expected, 'pending copy edit count');
}
async function waitForPendingDockCleared(live, { timeout }) {
const deadline = Date.now() + timeout;
let last = null;
while (Date.now() < deadline) {
last = await getPendingDockCount(live);
if (last === 0) return;
await new Promise((resolve) => setTimeout(resolve, 500));
}
assert.equal(last, 0, 'pending copy edits clear after Apply');
}
async function getPendingDockCount(live) {
const res = await fetch(`http://127.0.0.1:${live.port}/manual-edit-stash?token=${encodeURIComponent(live.token)}&pageUrl=%2F`);
if (!res.ok) throw new Error(`manual-edit-stash failed: ${res.status}`);
const json = await res.json();
return json.count || 0;
}
async function assertNoCyclingZero(page) {
const text = await page.evaluate(() => window.__impeccableLiveQuery('#impeccable-live-bar')?.textContent || '');
assert.doesNotMatch(text, /0\s*\/\s*0/);
}
async function assertNoRawSvelteExpressions(page) {
const body = await page.evaluate(() => document.body.textContent || '');
assert.doesNotMatch(body, /\{expenses\[0\]\.(name|amount)\}/);
}
async function readStateProbe(page) {
return page.evaluate(() => ({
openCount: document.querySelector('[data-testid="open-count"]')?.textContent?.trim(),
mounts: window.__impeccableStatefulMounts,
}));
}
async function assertOpenCount(page) {
const state = await readStateProbe(page);
assert.equal(state.openCount, '1 offen');
}
async function assertStateProbeUnchanged(page, expected, message) {
const state = await readStateProbe(page);
assert.equal(state.openCount, expected.openCount);
assert.equal(state.mounts, expected.mounts, message);
}
function assertFinalSourceClean(tmp) {
const source = readFileSync(join(tmp, ROUTE_FILE), 'utf-8');
assert.doesNotMatch(source, /node_modules\/\.impeccable-live|data-impeccable-variants|impeccable-variants-start|data-impeccable-variant=/);
}
function latestJournalEvent(tmp, predicate) {
const events = readJournalEvents(tmp);
for (let i = events.length - 1; i >= 0; i--) {
if (predicate(events[i])) return events[i];
}
return null;
}
async function waitForLatestJournalEvent(tmp, predicate, { timeout = 20_000 } = {}) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const event = latestJournalEvent(tmp, predicate);
if (event) return event;
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error('timed out waiting for journal event');
}
function readJournalEvents(tmp) {
const dir = join(tmp, '.impeccable/live/sessions');
if (!existsSync(dir)) return [];
const events = [];
for (const file of readdirSync(dir).filter((name) => name.endsWith('.jsonl'))) {
const body = readFileSync(join(dir, file), 'utf-8');
for (const line of body.split('\n')) {
if (!line.trim()) continue;
try {
const parsed = JSON.parse(line);
events.push(parsed.event || parsed);
} catch {}
}
}
return events;
}
async function globalBarSnapshot(page) {
await installLiveQueryHelpers(page);
return page.evaluate(() => {
const bar = window.__impeccableLiveQuery('#impeccable-live-global-bar');
if (!bar) return { hasBar: false };
const rect = bar.getBoundingClientRect();
const style = getComputedStyle(bar);
return {
hasBar: true,
display: style.display,
pointerEvents: style.pointerEvents,
opacity: style.opacity,
fontSize: style.fontSize,
color: style.color,
backgroundColor: style.backgroundColor,
transition: style.transition,
transform: style.transform,
rect: { width: rect.width, height: rect.height, left: rect.left, top: rect.top },
};
});
}
function wrapTargetFromPickedElement(event) {
const element = event.element || {};
const tag = typeof element.tagName === 'string'
? element.tagName.trim().toLowerCase()
: '';
const classes = Array.isArray(element.classes)
? element.classes.filter(Boolean).join(' ')
: typeof element.className === 'string'
? element.className.trim().split(/\s+/).filter(Boolean).join(' ')
: extractClassAttr(element.outerHTML);
const elementId = typeof element.id === 'string' ? element.id.trim() : '';
return {
tag: tag || 'h1',
...(classes ? { classes } : {}),
...(elementId ? { elementId } : {}),
...(element.textContent ? { text: String(element.textContent).trim() } : {}),
};
}
function extractClassAttr(outerHTML) {
if (typeof outerHTML !== 'string') return '';
const match = outerHTML.match(/\sclass=(["'])(.*?)\1/);
return match ? match[2].trim().split(/\s+/).filter(Boolean).join(' ') : '';
}
function createEvidenceWriter({ page, tmp, live, consoleErrors }) {
mkdirSync(artifactRoot, { recursive: true });
writeFileSync(join(artifactRoot, 'fixture-tmp.txt'), tmp + '\n');
return {
async capture(name) {
const safe = name.replace(/[^a-z0-9_.-]+/gi, '-');
const dir = join(artifactRoot, safe);
mkdirSync(dir, { recursive: true });
try { await page.screenshot({ path: join(dir, 'page.png'), fullPage: true }); } catch {}
try {
const dom = await page.evaluate(() => {
const host = document.getElementById('impeccable-live-root');
const shadow = host?.shadowRoot;
return {
url: location.href,
bodyText: document.body.textContent?.replace(/\s+/g, ' ').trim().slice(0, 4000),
appHtml: document.body.innerHTML.slice(0, 12000),
shadowHtml: shadow?.innerHTML.slice(0, 12000) || null,
liveState: window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.() || null,
localSession: localStorage.getItem('impeccable-live-session'),
};
});
writeFileSync(join(dir, 'dom.json'), JSON.stringify(dom, null, 2) + '\n');
} catch (err) {
writeFileSync(join(dir, 'dom-error.txt'), err.stack || err.message || String(err));
}
writeFileSync(join(dir, 'console-errors.json'), JSON.stringify(consoleErrors, null, 2) + '\n');
writeFileSync(join(dir, 'source.diff'), git(tmp, ['diff', '--', APP_HTML, LAYOUT_FILE, ROUTE_FILE]));
copyIfExists(join(tmp, APP_HTML), join(dir, 'app.html'));
copyIfExists(join(tmp, LAYOUT_FILE), join(dir, 'layout.svelte'));
copyIfExists(join(tmp, ROUTE_FILE), join(dir, 'page.svelte'));
copyDirIfExists(join(tmp, '.impeccable/live/sessions'), join(dir, 'sessions'));
copyDirIfExists(join(tmp, 'node_modules/.impeccable-live'), join(dir, 'impeccable-live-preview'));
writeFileSync(join(dir, 'preview-files.json'), JSON.stringify(listPreviewFiles(tmp), null, 2) + '\n');
try {
const status = await fetch(`http://127.0.0.1:${live.port}/status?token=${encodeURIComponent(live.token)}`).then((res) => res.json());
writeFileSync(join(dir, 'status.json'), JSON.stringify(status, null, 2) + '\n');
} catch {}
},
};
}
function listPreviewFiles(tmp) {
const root = join(tmp, 'node_modules/.impeccable-live');
if (!existsSync(root)) return [];
const out = [];
walk(root, (file) => out.push(relative(tmp, file).split('\\').join('/')));
return out.sort();
}
function walk(dir, visit) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const abs = join(dir, entry.name);
if (entry.isDirectory()) walk(abs, visit);
else visit(abs);
}
}
function copyIfExists(from, to) {
if (existsSync(from)) cpSync(from, to);
}
function copyDirIfExists(from, to) {
if (!existsSync(from)) return;
rmSync(to, { recursive: true, force: true });
cpSync(from, to, { recursive: true });
}
function git(cwd, args) {
try {
return execFileSync('git', args, { cwd, encoding: 'utf-8' });
} catch (err) {
return err.stdout?.toString?.() || err.message || String(err);
}
}
function createArtifactRoot() {
const explicit = process.env.IMPECCABLE_SVELTE_DEEPSEEK_ARTIFACT_DIR;
if (explicit) return explicit;
const base = join(REPO_ROOT, 'tmp/svelte-live-adapter-deepseek');
for (const round of ['round-1', 'round-2']) {
const candidate = join(base, round);
if (!existsSync(candidate)) return candidate;
}
return join(base, `run-${new Date().toISOString().replace(/[:.]/g, '-')}`);
}
function assertNoConsoleErrors(consoleErrors) {
const ignored = consoleErrors.filter((line) =>
!/favicon|ResizeObserver loop limit exceeded/i.test(line)
);
assert.deepEqual(ignored, []);
}