From 8c9bc8f8263f3172505a9adb4fbd832b0496d78c Mon Sep 17 00:00:00 2001 From: Iris Agi Date: Mon, 5 Oct 2026 12:15:10 -0700 Subject: [PATCH 1/3] Make the render tests independent of the screen's pixel scale CI has been red since the first run: 81 failures on a 1x runner, none on a 2x Mac. Cause. CodeBlockRenderTests measured the drawn pixels of a bitmap made with bitmapImageRepForCachingDisplay, which takes the host screen's scale. Its ink and gap thresholds were tuned on a 2x display, so a 1x host drew a quarter of the pixels: 30 and 21 against "greater than 40", and a box gap of 7.5 against 8.0. ShellViewTests made its fixture PNG with NSImage.lockFocus, which is also screen scaled: 64 by 32 pixels on 1x, and the test expects the 128 by 64 it gets on 2x. Fix. The code-block capture renders into an explicit 2x bitmap. The fixture PNG is an explicit 128 by 64 pixel bitmap. Neither depends on the host screen now. Evidence. Forcing the old capture to a 1x bitmap on a 2x Mac reproduces the CI numbers exactly: 80 failures (the 7.5 against 8.0 gap and the 30 and 21 ink counts) plus the image test make the 81 seen on CI. With the fix the same tests pass at native scale (10 tests, 1362 checks) and the image test passes (16 checks). Co-Authored-By: Claude Sonnet 5.5 --- Tests/FloKitTests/CodeBlockRenderTests.swift | 7 ++++++- Tests/FloStateNativeTests/ShellViewTests.swift | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Tests/FloKitTests/CodeBlockRenderTests.swift b/Tests/FloKitTests/CodeBlockRenderTests.swift index 3e7d32b..ada4ea7 100644 --- a/Tests/FloKitTests/CodeBlockRenderTests.swift +++ b/Tests/FloKitTests/CodeBlockRenderTests.swift @@ -67,7 +67,12 @@ final class CodeBlockRenderTests: XCTestCase { let tlm = tv.textLayoutManager! tlm.ensureLayout(for: tlm.documentRange) let rect = tv.visibleRect - let rep = tv.bitmapImageRepForCachingDisplay(in: rect)! + // Always 2x: the ink and gap thresholds were tuned on a Retina display, and a 1x host (a CI runner) + // would draw a quarter of the pixels with bitmapImageRepForCachingDisplay. + let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(rect.width * 2), pixelsHigh: Int(rect.height * 2), + bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, + colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)! + rep.size = rect.size tv.cacheDisplay(in: rect, to: rep) if let dir = ProcessInfo.processInfo.environment["CODEBLOCK_PNG_DIR"] { let file = "\(dir)/\(name)-\(reading ? "reading" : "writing")-\(dark ? "dark" : "light").png" diff --git a/Tests/FloStateNativeTests/ShellViewTests.swift b/Tests/FloStateNativeTests/ShellViewTests.swift index ceca1c0..4b22d5f 100644 --- a/Tests/FloStateNativeTests/ShellViewTests.swift +++ b/Tests/FloStateNativeTests/ShellViewTests.swift @@ -54,8 +54,13 @@ final class ShellViewTests: XCTestCase { var box = CGRect(x: 0, y: 0, width: 300, height: 400) let ctx = CGContext(URL(fileURLWithPath: pdf) as CFURL, mediaBox: &box, nil)! ctx.beginPDFPage(nil); ctx.setFillColor(NSColor.systemBlue.cgColor); ctx.fill(CGRect(x: 40, y: 40, width: 220, height: 320)); ctx.endPDFPage(); ctx.beginPDFPage(nil); ctx.endPDFPage(); ctx.closePDF() - let img = NSImage(size: NSSize(width: 64, height: 32)); img.lockFocus(); NSColor.red.setFill(); NSRect(x: 0, y: 0, width: 64, height: 32).fill(); img.unlockFocus() - try NSBitmapImageRep(data: img.tiffRepresentation!)!.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: png)) + let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 128, pixelsHigh: 64, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, + isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)! + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep) + NSColor.red.setFill(); NSRect(x: 0, y: 0, width: 128, height: 64).fill() + NSGraphicsContext.restoreGraphicsState() + try rep.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: png)) let before = try Data(contentsOf: URL(fileURLWithPath: png)) for (path, kind) in [(pdf, WorkspaceFS.ViewerKind.pdf), (png, .image)] { try await f.model.editor.openFileInTabOrFocus(path) From 09c537cb26f657ac1f8cdae10a19021af9396908 Mon Sep 17 00:00:00 2001 From: Iris Agi Date: Mon, 5 Oct 2026 12:27:32 -0700 Subject: [PATCH 2/3] Skip the substitution test on a host that substitutes nothing After the scale fix CI had one failure left: testCodeGetsNoSubstitutions. It types text and expects macOS text substitution to rewrite prose and leave code alone. The CI runner has no text checking service, so nothing is substituted anywhere. Prose failed its own control, and the code cases would only pass because nothing changes. Prose is now a precondition: when it is not substituted the test skips with the typed text in the reason. On a Mac with the service the test runs as before (10 tests, 1361 checks pass). A host with substitution off skips instead of failing. Co-Authored-By: Claude Sonnet 5.5 --- Tests/FloKitTests/CodeBlockPolishTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/FloKitTests/CodeBlockPolishTests.swift b/Tests/FloKitTests/CodeBlockPolishTests.swift index 555a6ef..b92738d 100644 --- a/Tests/FloKitTests/CodeBlockPolishTests.swift +++ b/Tests/FloKitTests/CodeBlockPolishTests.swift @@ -185,7 +185,7 @@ extension CodeBlockRenderTests { } /// With the user's substitutions turned on (Edit > Substitutions), prose gets them and code does not. - func testCodeGetsNoSubstitutions() { + func testCodeGetsNoSubstitutions() throws { for (name, doc, isCode) in [("prose", "text", false), ("fenced", "```\ncode", true), ("fenced-closed", "```\ncode\n```", true)] { let r = KeyReplayer(width: 1000, height: 600) let end = name == "fenced-closed" ? 8 : (doc as NSString).length @@ -201,7 +201,9 @@ extension CodeBlockRenderTests { if isCode { XCTAssertTrue(r.doc.contains(typed), "\(name): typed as is: \(r.doc.debugDescription)") } else { - XCTAssertFalse(r.doc.contains(typed), "\(name): prose still gets substitutions: \(r.doc.debugDescription)") + // Prose is the control. A host with no text checking service (a CI runner) substitutes nothing, + // so "code gets none" would pass for no reason. Skip there instead of passing or failing. + try XCTSkipIf(r.doc.contains(typed), "this host does not substitute text in prose: \(r.doc.debugDescription)") } XCTAssertEqual(r.viewText, r.doc, "\(name): view and state agree") } From 4793bb11dc099deae684e8fdb8f313783e3428cf Mon Sep 17 00:00:00 2001 From: Iris Agi Date: Mon, 5 Oct 2026 12:33:58 -0700 Subject: [PATCH 3/3] Stop the cursor test from double releasing its window testViewsInAWindowInvalidateTheirRectsWhenTheyMoveOrHide closes a window it created in code. NSWindow releases itself on close by default, and ARC releases it again, so the process dies. Every other test that closes a window sets isReleasedWhenClosed = false. The crash is intermittent: the test passes on a Mac most runs, and the FloStateNativeTests process died inside it on the CI runner (a bare "TESTS FAIL: exit 1", the log ends at "started"). It was hidden in the first PR run behind the earlier failure. Evidence. The old test body run 400 times in one process crashes at once (exit 139). With isReleasedWhenClosed = false it runs 400 times without a crash. FindTests is the only other test that makes a window without the flag, and it never closes it. Co-Authored-By: Claude Sonnet 5.5 --- Tests/FloStateNativeTests/PointerCursorTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/FloStateNativeTests/PointerCursorTests.swift b/Tests/FloStateNativeTests/PointerCursorTests.swift index b600cd6..5b3caa7 100644 --- a/Tests/FloStateNativeTests/PointerCursorTests.swift +++ b/Tests/FloStateNativeTests/PointerCursorTests.swift @@ -89,6 +89,7 @@ final class PointerCursorTests: XCTestCase { func testViewsInAWindowInvalidateTheirRectsWhenTheyMoveOrHide() { // not hover: only that the hooks run without a window and with one let w = NSWindow(contentRect: NSRect(x: -10000, y: -10000, width: 200, height: 100), styleMask: [.titled], backing: .buffered, defer: true) + w.isReleasedWhenClosed = false // close() would release a window ARC also owns: a double release let host = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 100)) w.contentView = host let b = OverflowToggleButton()