Fix Retina scaling in design mode annotation crops (#8831)

* test(browser): reproduce Retina-scaled screenshot crops

* fix(browser): crop screenshots at native pixel scale

* test: update Ghostty surface config ABI lock

* fix: remove redundant diagnostic log await

* test(browser): require transparent annotation outlines

* fix(browser): keep drawn annotations over live page

* test(browser): verify cropped annotation pixels

---------

Co-authored-by: cmux reload-cloud <[email protected]>
This commit is contained in:
Austin Wang
2026-07-24 01:10:19 -07:00
committed by GitHub
co-authored by cmux reload-cloud
parent 22e4173725
commit caf70dfe38
7 changed files with 252 additions and 69 deletions
@@ -95,7 +95,7 @@
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.62 ? "rgba(0, 0, 0, 0.88)" : "white";
};
// Captured annotation cards: one immutable context artifact per stroke.
// Captured annotation regions: one immutable context artifact per stroke.
const regionReferences = [];
const marqueeThresholdPixels = 5;
let pendingPointer = null;
@@ -629,7 +629,7 @@
};
};
// Annotation snapshots translate page-anchored cards to current viewport
// Annotation snapshots translate page-anchored regions to current viewport
// coordinates while keeping a stable identity across scrolling.
const regionSnapshotFor = (region) => {
const x = region.pageX - (globalThis.scrollX || 0);
@@ -1012,8 +1012,8 @@
background: "rgba(10, 132, 255, 0.07)",
});
// Freehand ink is the only visible feedback until native capture returns
// the context-rich composited card.
// Freehand ink is the only visible feedback while native captures context;
// completion replaces it with a transparent region outline.
const svgNS = "http://www.w3.org/2000/svg";
const strokeSvg = document.createElementNS(svgNS, "svg");
Object.assign(strokeSvg.style, {
@@ -1072,7 +1072,7 @@
return element;
};
const annotationCard = () => {
const annotationOutline = () => {
const element = document.createElement("div");
Object.assign(element.style, {
display: "none",
@@ -1081,12 +1081,8 @@
boxSizing: "border-box",
border: "1.5px dashed rgb(10, 132, 255)",
borderRadius: "14px",
backgroundColor: "white",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "100% 100%",
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.18)",
overflow: "hidden",
backgroundColor: "transparent",
boxShadow: "none",
});
return element;
};
@@ -1120,7 +1116,7 @@
const refreshRegionOutlines = () => {
if (!overlay) return;
while (overlay.regionOutlines.length < regionReferences.length) {
const outline = annotationCard();
const outline = annotationOutline();
overlay.regionOutlines.push(outline);
overlay.selectionLayer.append(outline);
}
@@ -1135,9 +1131,8 @@
const isHovered = hoveredSelectionIndex === selectedReferences.length + index;
outline.style.borderColor = tint;
outline.style.boxShadow = isHovered
? `0 0 0 4px ${colorWithAlpha(tint, 0.55)}, 0 8px 24px rgba(0, 0, 0, 0.18)`
: "0 8px 24px rgba(0, 0, 0, 0.18)";
outline.style.backgroundImage = `url("${region.imageURL}")`;
? `0 0 0 4px ${colorWithAlpha(tint, 0.55)}`
: "none";
place(outline, {
x: region.pageX - (globalThis.scrollX || 0),
y: region.pageY - (globalThis.scrollY || 0),
@@ -2060,7 +2055,6 @@
y,
width,
height,
imageURL,
expectedScrollX,
expectedScrollY,
expectedViewportWidth,
@@ -2074,8 +2068,7 @@
|| descriptor.scroll_x !== expectedScrollX
|| descriptor.scroll_y !== expectedScrollY
|| descriptor.viewport.width !== expectedViewportWidth
|| descriptor.viewport.height !== expectedViewportHeight
|| !String(imageURL || "").startsWith("data:image/png;base64,")) {
|| descriptor.viewport.height !== expectedViewportHeight) {
return null;
}
regionReferences.push({
@@ -2084,12 +2077,9 @@
pageY: y + expectedScrollY,
width,
height,
imageURL: String(imageURL),
colorIndex: pendingAnnotation.colorIndex,
});
// Each card retains screenshot-sized encoded and decoded image data.
// Keep a useful multi-stroke stack while evicting the oldest card so a
// long drawing session has a fixed memory ceiling.
// Keep the prompt and overlay bounded during a long drawing session.
if (regionReferences.length > maxAnnotationReferences) {
regionReferences.splice(0, regionReferences.length - maxAnnotationReferences);
hoveredSelectionIndex = null;
@@ -2104,7 +2094,7 @@
revision += 1;
// Native capture completion is the authoritative phase transition.
// Reconcile it directly so a frame request paused during WebKit's
// snapshot cannot strand the card behind an outstanding frame token.
// snapshot cannot strand the outline behind an outstanding frame token.
refreshOverlay();
return emit();
},
@@ -4,24 +4,26 @@ import Testing
@Suite
struct GhosttySurfaceConfigABITests {
@Test func publicConfigLayoutRemainsStable() {
#expect(MemoryLayout<ghostty_surface_config_s>.size == 120)
#expect(MemoryLayout<ghostty_surface_config_s>.stride == 120)
#expect(MemoryLayout<ghostty_surface_config_s>.size == 168)
#expect(MemoryLayout<ghostty_surface_config_s>.stride == 168)
#expect(MemoryLayout<ghostty_surface_config_s>.alignment == 8)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.platform_tag) == 0)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.platform) == 8)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.userdata) == 16)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.scale_factor) == 24)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.font_size) == 32)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.working_directory) == 40)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.command) == 48)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.env_vars) == 56)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.env_var_count) == 64)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.initial_input) == 72)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.wait_after_command) == 80)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.context) == 84)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.io_mode) == 88)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.io_write_cb) == 96)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.io_write_userdata) == 104)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.renderer_event_cb) == 112)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.userdata) == 48)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.scale_factor) == 56)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.font_size) == 64)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.working_directory) == 72)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.command) == 80)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.env_vars) == 88)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.env_var_count) == 96)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.initial_input) == 104)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.wait_after_command) == 112)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.context) == 116)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.io_mode) == 120)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.io_write_cb) == 128)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.io_write_userdata) == 136)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.renderer_event_cb) == 144)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.pty_tee_cb) == 152)
#expect(MemoryLayout<ghostty_surface_config_s>.offset(of: \.pty_tee_userdata) == 160)
}
}
@@ -123,7 +123,7 @@ extension BrowserDesignModeController {
let value = try await evaluate(
"""
return globalThis.__cmuxDesignMode?.completeAnnotationCapture(
id, x, y, width, height, imageURL,
id, x, y, width, height,
scrollX, scrollY, viewportWidth, viewportHeight
);
""",
@@ -133,7 +133,6 @@ extension BrowserDesignModeController {
"y": capture.contextRect.y,
"width": capture.contextRect.width,
"height": capture.contextRect.height,
"imageURL": "data:image/png;base64,\(pngData.base64EncodedString())",
"scrollX": capture.descriptor.scrollX,
"scrollY": capture.descriptor.scrollY,
"viewportWidth": capture.descriptor.viewport.width,
+27 -4
View File
@@ -113,6 +113,7 @@ enum BrowserScreenshotCrop {
return clamp(imageRect, to: NSRect(origin: .zero, size: imageSize))
}
@MainActor
static func croppedImage(
from image: NSImage,
selectionInView selection: NSRect,
@@ -127,15 +128,37 @@ enum BrowserScreenshotCrop {
throw BrowserScreenshotError.invalidSelection
}
let cropped = NSImage(size: cropRect.size)
cropped.lockFocus()
guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(cropRect.width),
pixelsHigh: Int(cropRect.height),
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .calibratedRGB,
bytesPerRow: 0,
bitsPerPixel: 0
), let context = NSGraphicsContext(bitmapImageRep: bitmap) else {
throw BrowserScreenshotError.invalidImageRepresentation
}
bitmap.size = cropRect.size
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
context.imageInterpolation = .high
image.draw(
in: NSRect(origin: .zero, size: cropRect.size),
from: cropRect,
operation: .copy,
fraction: 1.0
fraction: 1.0,
respectFlipped: false,
hints: nil
)
cropped.unlockFocus()
NSGraphicsContext.restoreGraphicsState()
let cropped = NSImage(size: cropRect.size)
cropped.addRepresentation(bitmap)
return cropped
}
+4
View File
@@ -347,6 +347,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources
BFBAC1A77CEE7A2DF91DA02C /* BrowserRemoteWorkspaceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109D0E38E29A8779FA0BAE82 /* BrowserRemoteWorkspaceStatus.swift */; };
8478A0010000000000000001 /* BrowserSameDocumentNavigationMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8478A0010000000000000002 /* BrowserSameDocumentNavigationMessageHandler.swift */; };
4472A0014472A0014472A001 /* BrowserScreenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4472B0014472B0014472B001 /* BrowserScreenshot.swift */; };
8827C0010000000000000001 /* BrowserScreenshotCropTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8827D0010000000000000001 /* BrowserScreenshotCropTests.swift */; };
4472A0024472A0024472A002 /* BrowserScreenshotPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4472B0024472B0024472B002 /* BrowserScreenshotPipeline.swift */; };
4472A0034472A0034472A003 /* BrowserScreenshotSnapshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4472B0034472B0034472B003 /* BrowserScreenshotSnapshotter.swift */; };
80560000000000000000000B /* BrowserScreenshotTilePlacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80560000000000000000000C /* BrowserScreenshotTilePlacement.swift */; };
@@ -2610,6 +2611,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
109D0E38E29A8779FA0BAE82 /* BrowserRemoteWorkspaceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserRemoteWorkspaceStatus.swift; sourceTree = "<group>"; };
8478A0010000000000000002 /* BrowserSameDocumentNavigationMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserSameDocumentNavigationMessageHandler.swift; sourceTree = "<group>"; };
4472B0014472B0014472B001 /* BrowserScreenshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshot.swift; sourceTree = "<group>"; };
8827D0010000000000000001 /* BrowserScreenshotCropTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserScreenshotCropTests.swift; sourceTree = "<group>"; };
4472B0024472B0024472B002 /* BrowserScreenshotPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshotPipeline.swift; sourceTree = "<group>"; };
4472B0034472B0034472B003 /* BrowserScreenshotSnapshotter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshotSnapshotter.swift; sourceTree = "<group>"; };
80560000000000000000000C /* BrowserScreenshotTilePlacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshotTilePlacement.swift; sourceTree = "<group>"; };
@@ -6311,6 +6313,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
B6585002B6585002B6585002 /* BrowserHiddenWebViewDiscardMemoryPressureTests.swift */,
D35D00000000000000000002 /* BrowserDesignModeComposerHostingViewTests.swift */,
D35D00000000000000000001 /* BrowserDesignModeScreenshotEvaluatorTests.swift */,
8827D0010000000000000001 /* BrowserScreenshotCropTests.swift */,
B75040020000000000000002 /* BrowserDiscardedWebViewRestoreRetryTests.swift */,
B75040030000000000000002 /* BrowserDiscardRestoreHealPredicateTests.swift */,
8054B0040000000000000004 /* BrowserAutomationRecoveryLifecycleTests.swift */,
@@ -8801,6 +8804,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
C42660050000000000000001 /* BrowserPDFPreviewActionUnitTests.swift in Sources */,
C0DE78950000000000000001 /* BrowserPortalFirstRevealScrollTests.swift in Sources */,
B6585001B6585001B658PW01 /* BrowserPrewarmedWebViewPoolTests.swift in Sources */,
8827C0010000000000000001 /* BrowserScreenshotCropTests.swift in Sources */,
C2035A060000000000000001 /* BrowserSSLTrustBypassStateTests.swift in Sources */,
C7B800062D4202A13C962D2D /* BrowserSystemProxyMirrorTests.swift in Sources */,
805500000000000000000005 /* BrowserValueTextFormatter.swift in Sources */,
+169
View File
@@ -0,0 +1,169 @@
import AppKit
import ObjectiveC.runtime
import Testing
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
@MainActor
@Suite(.serialized)
struct BrowserScreenshotCropTests {
private typealias FocusImplementation = @convention(c) (AnyObject, Selector) -> Void
private static let imageFocusBackingScaleKey =
"cmux.browserScreenshotCropTests.imageFocusBackingScale"
private static let imageFocusOverrideInstalled: Bool = {
let lockSelector = #selector(NSImage.lockFocus)
let unlockSelector = #selector(NSImage.unlockFocus)
guard let lockMethod = class_getInstanceMethod(NSImage.self, lockSelector),
let unlockMethod = class_getInstanceMethod(NSImage.self, unlockSelector) else {
return false
}
let originalLock = unsafeBitCast(
method_getImplementation(lockMethod),
to: FocusImplementation.self
)
let originalUnlock = unsafeBitCast(
method_getImplementation(unlockMethod),
to: FocusImplementation.self
)
let lockBlock: @convention(block) (NSImage) -> Void = { image in
guard let scale = Thread.current.threadDictionary[imageFocusBackingScaleKey] as? Int else {
originalLock(image, lockSelector)
return
}
let size = image.size
guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(size.width) * scale,
pixelsHigh: Int(size.height) * scale,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0,
bitsPerPixel: 0
), let context = NSGraphicsContext(bitmapImageRep: bitmap) else {
Issue.record("Could not create the controlled image-focus context")
originalLock(image, lockSelector)
return
}
bitmap.size = size
image.addRepresentation(bitmap)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
}
let unlockBlock: @convention(block) (NSImage) -> Void = { image in
guard Thread.current.threadDictionary[imageFocusBackingScaleKey] is Int else {
originalUnlock(image, unlockSelector)
return
}
NSGraphicsContext.restoreGraphicsState()
}
// Keep the replacement IMPs alive for the test process: parallel test
// threads may already be executing the forwarding path.
method_setImplementation(lockMethod, imp_implementationWithBlock(lockBlock))
method_setImplementation(unlockMethod, imp_implementationWithBlock(unlockBlock))
return true
}()
@Test
func encodedCropUsesOnePixelPerSnapshotCoordinate() throws {
let source = try makePatternedBitmapImage()
let cropped = try withImageFocusBackingScale(2) {
try BrowserScreenshotCrop.croppedImage(
from: source,
selectionInView: NSRect(x: 50, y: 25, width: 100, height: 50),
viewBounds: NSRect(x: 0, y: 0, width: 200, height: 150)
)
}
let pngData = try BrowserScreenshotPasteboardWriter.pngData(for: cropped)
let bitmap = try #require(NSBitmapImageRep(data: pngData))
#expect(bitmap.pixelsWide == 200)
#expect(bitmap.pixelsHigh == 100)
try expectColor(.red, atX: 25, y: 25, in: bitmap)
try expectColor(.green, atX: 175, y: 25, in: bitmap)
try expectColor(.blue, atX: 25, y: 75, in: bitmap)
try expectColor(.yellow, atX: 175, y: 75, in: bitmap)
}
/// Makes the legacy `NSImage.lockFocus()` path deterministically rasterize
/// at Retina scale while forwarding unrelated threads to AppKit unchanged.
private func withImageFocusBackingScale<T>(
_ scale: Int,
operation: () throws -> T
) throws -> T {
guard Self.imageFocusOverrideInstalled else {
Issue.record("Could not install the controlled image-focus context")
return try operation()
}
Thread.current.threadDictionary[Self.imageFocusBackingScaleKey] = scale
defer {
Thread.current.threadDictionary.removeObject(
forKey: Self.imageFocusBackingScaleKey
)
}
return try operation()
}
private func makePatternedBitmapImage() throws -> NSImage {
let width = 400
let height = 300
let bitmap = try #require(NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: width,
pixelsHigh: height,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0,
bitsPerPixel: 0
))
let context = try #require(NSGraphicsContext(bitmapImageRep: bitmap))
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
NSColor.magenta.setFill()
NSRect(x: 0, y: 0, width: width, height: height).fill()
NSColor.red.setFill()
NSRect(x: 100, y: 50, width: 100, height: 50).fill()
NSColor.green.setFill()
NSRect(x: 200, y: 50, width: 100, height: 50).fill()
NSColor.blue.setFill()
NSRect(x: 100, y: 100, width: 100, height: 50).fill()
NSColor.yellow.setFill()
NSRect(x: 200, y: 100, width: 100, height: 50).fill()
NSGraphicsContext.restoreGraphicsState()
let size = NSSize(width: width, height: height)
bitmap.size = size
let image = NSImage(size: size)
image.addRepresentation(bitmap)
return image
}
private func expectColor(
_ expected: NSColor,
atX x: Int,
y: Int,
in bitmap: NSBitmapImageRep
) throws {
let actualRGB = try #require(
bitmap.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB)
)
let expectedRGB = try #require(expected.usingColorSpace(.deviceRGB))
let tolerance = 0.02
#expect(abs(actualRGB.redComponent - expectedRGB.redComponent) < tolerance)
#expect(abs(actualRGB.greenComponent - expectedRGB.greenComponent) < tolerance)
#expect(abs(actualRGB.blueComponent - expectedRGB.blueComponent) < tolerance)
#expect(abs(actualRGB.alphaComponent - expectedRGB.alphaComponent) < tolerance)
}
}
@@ -91,7 +91,6 @@ type DesignRuntime = {
y: number,
width: number,
height: number,
imageURL: string,
expectedScrollX: number,
expectedScrollY: number,
expectedViewportWidth: number,
@@ -709,7 +708,7 @@ describe("browser design-mode runtime", () => {
expect(dom.window.document.querySelector(selector)).toBe(second);
});
test("freehand ink becomes a captured context card only after native capture completes", () => {
test("freehand ink becomes a captured context outline only after native capture completes", () => {
const { dom, messages, runtime } = fixture(`<main><button id="b">B</button></main>`);
const doc = dom.window.document;
const at = (name: string, x: number, y: number) => doc.dispatchEvent(
@@ -727,8 +726,8 @@ describe("browser design-mode runtime", () => {
at("pointermove", 210, 340);
at("pointerup", 110, 140);
// Completion is ink-only: no region token/card exists until the native
// screenshot callback returns the exact composited artifact.
// Completion is ink-only: no region token/outline exists until the native
// screenshot callback returns the captured context artifact.
const inkOnly = runtime.composerState();
expect(inkOnly.annotation_phase).toBe("ink_only");
expect(inkOnly.selection_count).toBe(0);
@@ -748,7 +747,6 @@ describe("browser design-mode runtime", () => {
0,
258,
408,
"data:image/png;base64,Y2FyZA==",
descriptor?.scroll_x ?? 0,
descriptor?.scroll_y ?? 0,
descriptor?.viewport.width ?? 0,
@@ -768,7 +766,7 @@ describe("browser design-mode runtime", () => {
expect(runtime.composerState().selection_count).toBe(1);
// One stroke is one immutable context artifact. A later stroke stacks a
// new request/card rather than merging into or replacing the first.
// new request/outline rather than merging into or replacing the first.
// A deliberate one-axis stroke is still an annotation; it does not need
// circle-like width and height to become a context artifact.
at("pointerdown", 300, 300);
@@ -786,7 +784,6 @@ describe("browser design-mode runtime", () => {
252,
156,
96,
"data:image/png;base64,Y2FyZDI=",
secondDescriptor?.scroll_x ?? 0,
secondDescriptor?.scroll_y ?? 0,
secondDescriptor?.viewport.width ?? 0,
@@ -804,7 +801,7 @@ describe("browser design-mode runtime", () => {
expect(runtime.composerState().selection_count).toBe(0);
});
test("native annotation completion presents the card when an earlier animation frame is stalled", () => {
test("native annotation completion presents only an outline over the live page", () => {
const { dom, messages, overlayShadowRoot, runtime } = fixture(
`<main><button id="b">B</button></main>`,
{ stallAnimationFrames: true },
@@ -828,32 +825,33 @@ describe("browser design-mode runtime", () => {
12,
196,
196,
"data:image/png;base64,Y2FyZA==",
descriptor?.scroll_x ?? 0,
descriptor?.scroll_y ?? 0,
descriptor?.viewport.width ?? 0,
descriptor?.viewport.height ?? 0,
);
const card = Array.from(overlayShadowRoot()?.querySelectorAll("div") ?? []).find(
(element) => element.style.backgroundImage.includes("Y2FyZA=="),
const outline = Array.from(overlayShadowRoot()?.querySelectorAll("div") ?? []).find(
(element) => element.style.left === "2px"
&& element.style.top === "12px"
&& element.style.width === "196px"
&& element.style.height === "196px",
);
expect(card).toBeDefined();
expect(card?.style.display).toBe("block");
expect(card?.style.left).toBe("2px");
expect(card?.style.top).toBe("12px");
expect(card?.style.width).toBe("196px");
expect(card?.style.height).toBe("196px");
expect(card?.style.borderStyle).toBe("dashed");
expect(card?.style.borderColor).toBe("rgb(10, 132, 255)");
expect(outline).toBeDefined();
expect(outline?.style.display).toBe("block");
expect(outline?.style.borderStyle).toBe("dashed");
expect(outline?.style.borderColor).toBe("rgb(10, 132, 255)");
expect(outline?.style.backgroundColor).toBe("transparent");
expect(outline?.style.backgroundImage).toBe("");
expect(outline?.style.boxShadow).toBe("none");
runtime.setSelectionHover(completed.selections?.[0]?.selector ?? "");
expect(card?.style.boxShadow).toContain("rgba(10, 132, 255, 0.55)");
expect(outline?.style.boxShadow).toContain("rgba(10, 132, 255, 0.55)");
runtime.setSelectionHover(null);
expect(card?.style.boxShadow).not.toContain("rgba(10, 132, 255, 0.55)");
expect(outline?.style.boxShadow).toBe("none");
});
test("annotation cards evict the oldest retained image after the bounded stack fills", () => {
test("annotation outlines evict the oldest region after the bounded stack fills", () => {
const { dom, messages, runtime } = fixture(`<main><button>B</button></main>`);
const doc = dom.window.document;
const at = (name: string, x: number, y: number) => doc.dispatchEvent(
@@ -878,7 +876,6 @@ describe("browser design-mode runtime", () => {
0,
128,
128,
`data:image/png;base64,Y2FyZC0${index}`,
descriptor?.scroll_x ?? 0,
descriptor?.scroll_y ?? 0,
descriptor?.viewport.width ?? 0,
@@ -942,7 +939,6 @@ describe("browser design-mode runtime", () => {
0,
188,
188,
"data:image/png;base64,Y2FyZA==",
descriptor?.scroll_x ?? 0,
descriptor?.scroll_y ?? 0,
descriptor?.viewport.width ?? 0,