Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/runtime-core/src/browser-interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 7 additions & 3 deletions packages/runtime-playground/src/browser-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<void, AnnotationSpec>((spec) => {
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions tests/browser-annotations.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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("<div></div>")
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() }
})
Loading