From a8395bdba2eeff92238c98a70bfc3b95792dce1a Mon Sep 17 00:00:00 2001 From: "homeboy-ci[bot]" <266378653+homeboy-ci[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:04:54 +0000 Subject: [PATCH] fix: cook wp-codebox --- .../runtime-core/src/browser-interaction.ts | 3 ++ .../src/browser-annotations.ts | 10 +++++-- tests/browser-annotations.browser.test.ts | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/browser-interaction.ts b/packages/runtime-core/src/browser-interaction.ts index 5a14e70f..ce3a4239 100644 --- a/packages/runtime-core/src/browser-interaction.ts +++ b/packages/runtime-core/src/browser-interaction.ts @@ -101,6 +101,8 @@ export interface BrowserInteractionStep { /** JSON input passed to the caller-provided host tool. */ input?: JsonValue position?: "top" | "center" | "bottom" + /** Additional vertical inset in pixels for captions. */ + offset?: number by?: { x: number; y: number } behavior?: "smooth" | "instant" block?: "start" | "center" | "end" @@ -389,6 +391,7 @@ export function validateBrowserInteractionScript(input: unknown): BrowserInterac if (["label", "caption"].includes(String(step.shape)) && !hasText) issues.push({ index, message: `annotate ${step.shape} requires text` }) if (step.shape === "label" && (!step.anchor || typeof step.anchor.selector !== "string" || !step.anchor.selector)) issues.push({ index, message: "annotate label requires anchor.selector" }) if (step.shape === "caption" && step.position !== undefined && !["top", "center", "bottom"].includes(step.position)) issues.push({ index, message: "annotate caption position must be top, center, or bottom" }) + if (step.offset !== undefined && (step.shape !== "caption" || !Number.isFinite(step.offset) || step.offset < 0)) issues.push({ index, message: "annotate offset must be a non-negative number for captions" }) if (step.shape === "arrow" && step.direction !== undefined && !["top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"].includes(step.direction)) issues.push({ index, message: "annotate arrow direction is invalid" }) break } diff --git a/packages/runtime-playground/src/browser-annotations.ts b/packages/runtime-playground/src/browser-annotations.ts index 282d3d25..effa449a 100644 --- a/packages/runtime-playground/src/browser-annotations.ts +++ b/packages/runtime-playground/src/browser-annotations.ts @@ -11,6 +11,7 @@ type AnnotationSpec = { animate?: string direction?: string position?: string + offset?: number style?: { variant?: string; padding?: number } anchor?: { selector?: string; placement?: string } theme?: BrowserAnnotationTheme @@ -37,7 +38,7 @@ export async function executeBrowserAnnotation(page: Page, step: BrowserInteract const spec: AnnotationSpec = { id: step.id, shape: step.shape, selector: step.selector, text: step.text, durationMs: step.durationMs, animate: step.animate, direction: step.direction, position: step.position, - style: step.style, anchor: step.anchor, theme: annotationTheme, + style: step.style, anchor: step.anchor, theme: annotationTheme, offset: step.offset, } await page.evaluate("globalThis.__name ??= (value) => value") await page.evaluate((spec) => { @@ -115,8 +116,11 @@ export async function executeBrowserAnnotation(page: Page, step: BrowserInteract if (spec.shape === "arrow") { const svg=document.createElementNS("http://www.w3.org/2000/svg","svg"); svg.setAttribute("width","80");svg.setAttribute("height","80");svg.setAttribute("viewBox","0 0 80 80");const path=document.createElementNS(svg.namespaceURI,"path");path.setAttribute("fill","none");path.setAttribute("stroke",color);path.setAttribute("stroke-width",String(theme.strokeWidth??3));path.setAttribute("stroke-linecap","round");path.setAttribute("stroke-linejoin","round");if(spec.animate==="draw"){path.setAttribute("stroke-dasharray","120");path.setAttribute("stroke-dashoffset","120");path.animate([{strokeDashoffset:"120"},{strokeDashoffset:"0"}],{duration:600,fill:"forwards"})}svg.append(path);node.append(svg) } if (spec.shape === "label" || spec.shape === "caption") { node.textContent = spec.text || "" - Object.assign(node.style, { padding: "8px 12px", borderRadius: `${Number(theme.radius ?? 6)}px`, color: textColor, background, fontFamily: theme.fontFamily ?? "system-ui, sans-serif", fontSize: `${Number(theme.fontSize ?? 16)}px`, whiteSpace: "nowrap" }) - if (spec.shape === "caption") Object.assign(node.style, { left: "10%", right: "10%", top: spec.position === "top" ? "8%" : spec.position === "center" ? "45%" : "auto", bottom: spec.position === "bottom" || !spec.position ? "8%" : "auto", textAlign: "center" }) + Object.assign(node.style, { padding: "8px 12px", borderRadius: `${Number(theme.radius ?? 6)}px`, color: textColor, background, fontFamily: theme.fontFamily ?? "system-ui, sans-serif", fontSize: `${Number(theme.fontSize ?? 16)}px`, whiteSpace: spec.shape === "caption" ? "normal" : "nowrap" }) + if (spec.shape === "caption") { + const inset = `${8 + Number(spec.offset ?? 0)}px` + Object.assign(node.style, { left: "10%", right: "10%", top: spec.position === "top" ? `calc(8% + ${inset})` : spec.position === "center" ? `calc(45% + ${inset})` : "auto", bottom: spec.position === "bottom" || !spec.position ? `calc(8% + ${inset})` : "auto", textAlign: "center", overflowWrap: "anywhere" }) + } } node.dataset.annotationId = id layer.append(node) diff --git a/tests/browser-annotations.browser.test.ts b/tests/browser-annotations.browser.test.ts index 60704676..a7c8ded2 100644 --- a/tests/browser-annotations.browser.test.ts +++ b/tests/browser-annotations.browser.test.ts @@ -22,6 +22,9 @@ test("annotation shapes and clear validate", () => { ]) assert.equal(validateBrowserInteractionScript([step]).valid, true) assert.equal(validateBrowserInteractionScript([{ kind: "annotate", shape: "label", text: "x" }]).valid, false) assert.equal(validateBrowserInteractionScript([{ kind: "annotate", shape: "arrow", selector: "button", direction: "nearby" }]).valid, false) + assert.equal(validateBrowserInteractionScript([{ kind: "annotate", shape: "caption", text: "Next", offset: 120 }]).valid, true) + assert.equal(validateBrowserInteractionScript([{ kind: "annotate", shape: "caption", text: "Next", offset: -1 }]).valid, false) + assert.equal(validateBrowserInteractionScript([{ kind: "annotate", shape: "label", text: "Next", offset: 10, anchor: { selector: "button" } }]).valid, false) }) test("annotation renders in isolated overlay, follows scrolling, passes clicks, and clears by id", async () => { @@ -73,3 +76,28 @@ test("arrows point to the target edge and labels avoid adjacent controls", async assert.equal(overlap, false) } finally { await browser.close() } }) + +test("captions wrap inside their pill and apply a validated vertical offset", async () => { + const browser = await chromium.launch({ headless: true }) + const page = await browser.newPage({ viewport: { width: 320, height: 600 } }) + try { + await page.setContent("
") + await executeBrowserAnnotation(page, { kind: "annotate", id: "caption", shape: "caption", position: "top", offset: 120, text: "WordPress Meetup · Wed Oct 21 · 6:30pm" } as never) + const dimensions = await page.evaluate(() => { + const root = document.querySelector("#__wp_codebox_annotations")!.shadowRoot! + const caption = root.querySelector('[data-annotation-id="caption"]')! + const pill = caption.getBoundingClientRect() + const range = document.createRange() + range.selectNodeContents(caption) + const text = [...range.getClientRects()] + return { textRects: text.map(({ left, right, top, bottom }) => ({ left, right, top, bottom })), pill: { left: pill.left, right: pill.right, top: pill.top, bottom: pill.bottom }, viewportWidth: innerWidth, height: pill.height, whiteSpace: getComputedStyle(caption).whiteSpace, overflowWrap: getComputedStyle(caption).overflowWrap } + }) + assert.ok(dimensions.textRects.length > 1, "long caption should produce multiple text lines") + assert.ok(dimensions.textRects.every(({ left, right, top, bottom }) => left >= dimensions.pill.left && right <= dimensions.pill.right && top >= dimensions.pill.top && bottom <= dimensions.pill.bottom)) + assert.ok(dimensions.textRects.every(({ left, right }) => left >= 0 && right <= dimensions.viewportWidth)) + assert.equal(dimensions.pill.top, 176) + assert.ok(dimensions.height > 32, "long caption should wrap into multiple lines") + assert.equal(dimensions.whiteSpace, "normal") + assert.equal(dimensions.overflowWrap, "anywhere") + } finally { await browser.close() } +})