Files
pbakaus_impeccable/tests/live-e2e/preactions.mjs
T
6d5f78eebf Live: the loader now hands off when the resume is the arrival
The overlay could sit in its generating shader over a DOM that already
held all three variants, and only a page refresh cleared it (#719).

The server's generation preflight runs live-wrap with
--defer-source-write, so the wrapper and every variant reach the DOM in a
single HMR batch. The deferred-wrapper scout is constructed at init and
the variant MutationObserver at Go; observer callbacks run in
construction order, so on that batch the scout resumes first and
resumeSession, not the observer, is the transition into CYCLING. It set
the state and the bar but never called hideShaderOverlay(), so the frozen
capture of the original stayed painted over the variants. It also
reported browser_resumed, which does not count as publication progress,
and then disconnected and re-created the observer, dropping the records
that observer had already queued for the same batch, so variants_ready
never fired at all.

resumeSession now finishes the same transition the observer does (shader
down, inline edit off, insert session finalized, params panel rebuilt)
and reports variants_ready when it already holds every variant. The
deferred scout names itself in the journal as
browser_resumed_deferred_wrapper, so the two resume paths are no longer
indistinguishable.

Wrapper resolution goes through findVariantsWrapper, which prefers a
wrapper that actually holds non-original variants. A target inside a
.map() renders one wrapper per item, and an agent that relocates the
wrapper out of the shared primitive live-wrap scaffolded leaves an empty
one behind; first match could pin either and strand the session at 0/N.
With zero or one match this is the querySelector it replaces.

Tests: waitForCycling now asserts the generating shader is gone once the
bar cycles, across every runtime fixture (it failed on vite8-react-plain
before this change and passes after), marked no-retry so the reload
recovery cannot hide it. Source-shape tests pin the transition, the
variants_ready report, and the wrapper preference.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-04 14:31:25 +05:00

143 lines
5.5 KiB
JavaScript

/**
* Pre-pick page setup for live-mode E2E (modals, tabs, routes).
*
* Live mode's picker intercepts page clicks while pickActive is true, so these
* actions temporarily disarm pick mode — same as a user toggling Pick off to
* open a modal, then back on to select an element.
*/
import { installLiveQueryHelpers, waitForCycling } from './ui.mjs';
const PICK_TOGGLE = '#impeccable-live-pick-toggle';
/**
* @param {import('playwright').Page} page
* @param {Array<{ type: string, selector?: string, path?: string }>} actions
*/
export async function runPreActions(page, actions) {
if (!actions?.length) return;
await installLiveQueryHelpers(page);
const wasActive = await page.evaluate((sel) =>
window.__impeccableLiveQuery?.(sel)?.dataset.active === 'true',
PICK_TOGGLE).catch(() => false);
if (wasActive) await clickPickToggle(page, PICK_TOGGLE);
try {
for (let i = 0; i < actions.length; i++) {
const a = actions[i];
if (a.type === 'click') {
const next = actions[i + 1];
if (next?.type === 'wait') {
const alreadyVisible = await page.locator(next.selector).first().isVisible().catch(() => false);
if (alreadyVisible) continue;
}
const loc = page.locator(a.selector);
await loc.first().waitFor({ state: 'visible', timeout: 5_000 });
await loc.first().click();
continue;
}
if (a.type === 'goto') {
const target = new URL(a.path, page.url()).href;
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 10_000 });
continue;
}
if (a.type === 'wait') {
await page.waitForSelector(a.selector, { timeout: 5_000 });
continue;
}
throw new Error(`unknown preAction type: ${a.type}`);
}
} finally {
if (wasActive) {
const isActive = await page.evaluate((sel) =>
window.__impeccableLiveQuery?.(sel)?.dataset.active === 'true',
PICK_TOGGLE).catch(() => false);
if (!isActive) await clickPickToggle(page, PICK_TOGGLE);
}
}
}
async function clickPickToggle(page, selector) {
await installLiveQueryHelpers(page);
try {
await page.locator(selector).click({ timeout: 5_000 });
return;
} catch (err) {
const clicked = await page.evaluate((sel) => {
const btn = window.__impeccableLiveQuery(sel);
if (!btn) return false;
btn.click();
return true;
}, selector);
if (!clicked) throw err;
}
}
/**
* Wait for CYCLING with the same recovery paths live mode expects:
* retrace preActions when conditional UI closed, reload when LLM + HMR lag.
*
* @param {import('playwright').Page} page
* @param {number} expectedCount
* @param {{ agentMode?: string, preActions?: object[], log?: (msg: string) => void }} opts
*/
export async function waitForCyclingRobust(page, expectedCount, opts = {}) {
const agentMode = opts.agentMode || 'fake';
const preActions = opts.preActions;
const log = opts.log || (() => {});
const firstPassTimeoutMs = agentMode === 'llm' ? 90_000 : 5_000;
const finalTimeoutMs = agentMode === 'llm' ? 90_000 : 30_000;
if (preActions?.length) {
try {
await waitForCycling(page, expectedCount, { timeout: firstPassTimeoutMs });
return;
} catch (preErr) {
if (preErr?.impeccableNoRetry) throw preErr;
log(`Cycling not reached in ${firstPassTimeoutMs}ms — retracing preActions`);
await runPreActions(page, preActions);
}
}
try {
await waitForCycling(page, expectedCount, { timeout: finalTimeoutMs });
return;
} catch (firstErr) {
if (process.env.IMPECCABLE_E2E_DEBUG) {
firstErr.message += '\n\n--- live UI snapshot ---\n' + JSON.stringify(await liveUiSnapshot(page), null, 2);
}
if (agentMode !== 'llm' || firstErr?.impeccableNoRetry) throw firstErr;
}
log('Cycling not reached after LLM generate — reloading to pick up HMR');
await page.reload({ waitUntil: 'domcontentloaded', timeout: 30_000 });
await installLiveQueryHelpers(page);
if (preActions?.length) await runPreActions(page, preActions);
await waitForCycling(page, expectedCount, { timeout: 60_000 });
}
async function liveUiSnapshot(page) {
return page.evaluate(() => {
const query = window.__impeccableLiveQuery || ((sel) => document.querySelector(sel));
const root = window.__IMPECCABLE_LIVE_CHROME_CORE__?.root?.() || window.__IMPECCABLE_LIVE_UI_ROOT__ || null;
const bar = query('#impeccable-live-bar');
const toast = query('#impeccable-live-toast');
const wrapper = document.querySelector('[data-impeccable-variants]');
return {
href: location.href,
liveInit: window.__IMPECCABLE_LIVE_INIT__,
adapter: window.__IMPECCABLE_LIVE_ADAPTER__,
hasShadowRoot: Boolean(document.getElementById('impeccable-live-root')?.shadowRoot),
rootText: root?.textContent?.replace(/\s+/g, ' ').trim().slice(0, 500) || null,
bar: bar ? { display: bar.style.display, text: bar.textContent } : null,
toast: toast ? toast.textContent : null,
wrapper: wrapper ? { preview: wrapper.dataset.impeccablePreview, count: wrapper.dataset.impeccableVariantCount, html: wrapper.outerHTML.slice(0, 800) } : null,
debugState: window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.() || null,
storage: localStorage.getItem('impeccable-live-session'),
scripts: document.querySelectorAll('script[data-impeccable-live-script]').length,
consoleHint: 'See page console errors captured by the test session.',
};
}).catch((err) => ({ error: err.message }));
}