diff --git a/scripts/benchmark-live.mjs b/scripts/benchmark-live.mjs index 025e75cea..dcbb99eaf 100644 --- a/scripts/benchmark-live.mjs +++ b/scripts/benchmark-live.mjs @@ -192,7 +192,7 @@ try { renderedArtifacts.variants.push(await captureRenderedElement(session.page, { filePath: join(runArtifactDir, `variant-${acceptVariant}.png`), variantId: acceptVariant, - selector: renderedContext.captureSelector, + selector: renderedContext.captureMode === 'target' ? null : renderedContext.captureSelector, })); } const browserTiming = await readBrowserTimingProbe(session.page); @@ -206,7 +206,7 @@ try { renderedArtifacts.variants.push(await captureRenderedElement(session.page, { filePath: join(runArtifactDir, `variant-${variantId}.png`), variantId, - selector: renderedContext.captureSelector, + selector: renderedContext.captureMode === 'target' ? null : renderedContext.captureSelector, })); } await ensureBenchmarkVariant(session.page, 1); @@ -310,7 +310,8 @@ try { root: evidenceRoot ? '.' : artifactRoot.startsWith(`${ROOT}${sep}`) ? relative(ROOT, artifactRoot) : null, externalRoot: evidenceRoot ? false : !artifactRoot.startsWith(`${ROOT}${sep}`), report: evidenceRoot ? 'report.json' : null, - screenshotScope: renderedContext.captureSelector, + screenshotScope: renderedContext.captureMode === 'target' ? 'selected-target' : renderedContext.captureSelector, + sourceSelector: renderedContext.captureSelector, }; if (judgeRendered) { report.renderedQuality = { @@ -348,7 +349,7 @@ async function readGenerationSnapshot(tmp, eventId) { } async function captureRenderedElement(page, { filePath, selector = null, variantId = null }) { - const geometry = await page.evaluate(({ targetSelector, targetVariantId }) => { + const geometry = await page.evaluate(async ({ targetSelector, targetVariantId }) => { const wrapper = targetVariantId == null ? null : document.querySelector('[data-impeccable-variants]'); const variant = targetVariantId == null ? null @@ -358,7 +359,8 @@ async function captureRenderedElement(page, { filePath, selector = null, variant ? document.querySelector(targetSelector) : variant?.firstElementChild; if (!element) return null; - element.scrollIntoView({ block: 'center', inline: 'nearest' }); + element.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); + await new Promise((resolveFrame) => requestAnimationFrame(() => requestAnimationFrame(resolveFrame))); const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); const core = window.__IMPECCABLE_LIVE_CHROME_CORE__; @@ -371,10 +373,10 @@ async function captureRenderedElement(page, { filePath, selector = null, variant const padding = 40; const pageWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth || 0); const pageHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0); - const x = Math.max(0, rect.left + window.scrollX - padding); - const y = Math.max(0, rect.top + window.scrollY - padding); - const width = Math.max(1, Math.min(pageWidth - x, rect.width + padding * 2)); - const height = Math.max(1, Math.min(pageHeight - y, rect.height + padding * 2)); + const x = Math.floor(Math.max(0, rect.left + window.scrollX - padding)); + const y = Math.floor(Math.max(0, rect.top + window.scrollY - padding)); + const width = Math.max(1, Math.min(Math.floor(pageWidth - x), Math.ceil(rect.width + padding * 2))); + const height = Math.max(1, Math.min(Math.floor(pageHeight - y), Math.ceil(rect.height + padding * 2))); return { x, y, width, height, elementWidth: rect.width, @@ -394,9 +396,15 @@ async function captureRenderedElement(page, { filePath, selector = null, variant await document.fonts?.ready; await new Promise((resolveFrame) => requestAnimationFrame(() => requestAnimationFrame(resolveFrame))); }); - await page.screenshot({ + const screenshotSelector = selector + || `[data-impeccable-variant="${variantId}"] > :first-child`; + const screenshotTarget = page.locator(screenshotSelector); + const screenshotTargetCount = await screenshotTarget.count(); + if (screenshotTargetCount !== 1) { + throw new Error(`rendered capture selector ${screenshotSelector} matched ${screenshotTargetCount} elements`); + } + await screenshotTarget.screenshot({ path: filePath, - clip: { x: geometry.x, y: geometry.y, width: geometry.width, height: geometry.height }, animations: 'disabled', caret: 'hide', }); diff --git a/scripts/lib/live-rendered-quality.mjs b/scripts/lib/live-rendered-quality.mjs index c68bde3c7..a0c246ebd 100644 --- a/scripts/lib/live-rendered-quality.mjs +++ b/scripts/lib/live-rendered-quality.mjs @@ -38,6 +38,7 @@ export function buildRenderedReviewContext({ fixture, fixtureConfig, action, bri action: selectedAction, brief: String(brief || configured.brief || `Apply /${selectedAction} to the selected element while preserving its project identity and functional contract.`), captureSelector: String(configured.captureSelector || fixtureConfig?.runtime?.pickSelector || 'body'), + captureMode: configured.mode === 'target' ? 'target' : 'selector', safeContext: { fixture: String(fixture || ''), reviewFocus: String(configured.reviewFocus || ''), diff --git a/tests/framework-fixtures/README.md b/tests/framework-fixtures/README.md index 662aa842e..fb0fc6a72 100644 --- a/tests/framework-fixtures/README.md +++ b/tests/framework-fixtures/README.md @@ -143,12 +143,18 @@ An external fixture has the same shape as a directory in this folder: { "evidenceCapture": { "captureSelector": "section.case-study", + "mode": "target", "viewport": { "width": 1440, "height": 1080 }, "action": "bolder" } } ``` +Use `"mode": "target"` when `captureSelector` is the picked element itself; +the original resolves through that selector and each variant resolves through +its exact Live wrapper. Omit it when the selector is a stable ancestor used as +shared page context for every capture. + The bundle contains `report.json`, the original capture, each progressively delivered variant capture, geometry/overflow facts, hashes, and timing data. It deliberately cannot run `--judge-rendered`; comparative rubrics, private diff --git a/tests/live-rendered-quality.test.mjs b/tests/live-rendered-quality.test.mjs index a1f34fbad..f2780b96c 100644 --- a/tests/live-rendered-quality.test.mjs +++ b/tests/live-rendered-quality.test.mjs @@ -54,6 +54,7 @@ describe('Live rendered quality judge', () => { runtime: { pickSelector: '.picked' }, evidenceCapture: { captureSelector: '.selected-section', + mode: 'target', action: 'bolder', }, renderedQuality: { @@ -63,6 +64,7 @@ describe('Live rendered quality judge', () => { }, }); assert.equal(context.captureSelector, '.selected-section'); + assert.equal(context.captureMode, 'target'); assert.equal(context.action, 'bolder'); assert.equal(context.safeContext.reviewFocus, ''); });