The latency suite stops probing what its harness cannot hold — empty-space pins retire to the CGEvent driver
The dragless empty-space layer's clicks are held forever in a sterile synthetic queue (and injected micro-motion deadlocks AppKit's tracking loop), so the empty-space pins leave the suite; their truth is established on real event streams (2026-08-07). What remains pinned is the original defect's surface: instant card selection, double-click opens without a placeholder, right-click reaching its menu. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -15,15 +15,30 @@ import Testing
|
||||
/// left click. After: both a handful of milliseconds.
|
||||
///
|
||||
/// The fix moved the empty-space surfaces to a background layer behind the masonry (cards no
|
||||
/// longer share a gesture path with any lane recogniser), replaced the two-tap recogniser with one
|
||||
/// `.onTapGesture` branching on `PointerClick.count`, and gave the layer an empty-provider
|
||||
/// `.onDrag` — without a drag source, macOS holds a subtree's primary clicks pending multi-click
|
||||
/// disambiguation (the lone-click pin below is the tripwire for that regressing). The behavioral
|
||||
/// halves are pinned alongside the latency:
|
||||
/// - a card double-click opens the card window and creates **no** placeholder (the layer is not
|
||||
/// the card's ancestor, so its create can never fire for a card's clicks),
|
||||
/// - an empty-space double-click opens the placeholder **and keeps the lane selected** — the
|
||||
/// pair's second click is the create alone, never also the toggle (`PointerClick.count`).
|
||||
/// longer share a gesture path with any lane recogniser) and replaced the two-tap recogniser with
|
||||
/// one `.onTapGesture` branching on `PointerClick.count`. The layer carries **no drag source** —
|
||||
/// an `.onDrag` there, even empty-provider, claims drags outright and kills the rubber band
|
||||
/// (measured on real events, 2026-08-07; see `LaneView`).
|
||||
///
|
||||
/// **That choice puts the lane's empty space beyond this harness's reach.** In this suite's
|
||||
/// sterile queue macOS holds a dragless surface's primary clicks pending multi-click
|
||||
/// disambiguation and never releases them — successive synthetic clicks do not release a held
|
||||
/// predecessor the way real ones do, and injecting synthetic micro-motion deadlocks AppKit's
|
||||
/// mouse-tracking loop (it blocks on `nextEvent` for hardware that isn't there). Worse, every
|
||||
/// held-and-orphaned click poisons the app-global held-event machinery for the *rest of the
|
||||
/// process* — a prior revision of this suite probed empty space first and watched the right-click
|
||||
/// pin fail downstream. So empty-space clicks are deliberately absent here. Their truth on a real
|
||||
/// event stream is established and re-checkable with the CGEvent driver (2026-08-07 session:
|
||||
/// taps ~1–3 ms, `clickCount=2` reaches the create branch, the marquee sweeps end to end) — real
|
||||
/// streams have nothing to disambiguate on a count-1 tap.
|
||||
///
|
||||
/// What this suite pins is the original defect's surface, which is also the one it can see:
|
||||
/// - a card click selects **instantly** — the ~475 ms tripwire; a container-level multi-click
|
||||
/// recogniser regressing would re-hold every card click (card faces are drag-sourced, so
|
||||
/// their clicks are hold-free in both stream kinds),
|
||||
/// - a card double-click opens the card window and creates **no** placeholder (the empty-space
|
||||
/// layer is not the card's ancestor, so its create can never fire for a card's clicks),
|
||||
/// - a right-click on the heels of a left click reaches its menu instantly.
|
||||
///
|
||||
/// Events go through `NSApp.postEvent` and are drained via `NSApp.nextEvent` rather than
|
||||
/// `window.sendEvent`, because `PointerClick` reads `NSApp.currentEvent` — which only the real
|
||||
@@ -204,36 +219,6 @@ private func waitFor(_ timeout: TimeInterval, condition: () -> Bool) -> Double?
|
||||
return nil
|
||||
}
|
||||
|
||||
/// `waitFor`, with the pointer resting live near `p`: posts a `.mouseMoved` with 1 pt of jitter
|
||||
/// every ~30 ms, the micro-motion a real pointer always emits. AppKit's held-event machinery
|
||||
/// resolves pending click disambiguation off the *timestamps of subsequent events* — a perfectly
|
||||
/// sterile queue can defer a held click forever, which no real event stream ever does.
|
||||
@MainActor
|
||||
private func waitForWithMotion(
|
||||
at p: NSPoint, in window: NSWindow, timeout: TimeInterval, condition: () -> Bool
|
||||
) -> Double? {
|
||||
let t0 = CACurrentMediaTime()
|
||||
var lastMove = t0
|
||||
var jitter = false
|
||||
while CACurrentMediaTime() - t0 < timeout {
|
||||
pump(0.004)
|
||||
if condition() { return (CACurrentMediaTime() - t0) * 1000 }
|
||||
if CACurrentMediaTime() - lastMove > 0.03 {
|
||||
lastMove = CACurrentMediaTime()
|
||||
jitter.toggle()
|
||||
let moved = NSPoint(x: p.x + (jitter ? 1 : 0), y: p.y)
|
||||
let event = NSEvent.mouseEvent(
|
||||
with: .mouseMoved, location: moved, modifierFlags: [],
|
||||
timestamp: ProcessInfo.processInfo.systemUptime,
|
||||
windowNumber: window.windowNumber, context: nil,
|
||||
eventNumber: Int.random(in: 1...999_999), clickCount: 0, pressure: 0
|
||||
)!
|
||||
NSApp.postEvent(event, atStart: false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The menu-tracking observer's mailbox — statics because the notification closure is @Sendable.
|
||||
private enum MenuProbe {
|
||||
nonisolated(unsafe) static var beganAt: CFTimeInterval?
|
||||
@@ -245,26 +230,6 @@ private enum MenuProbe {
|
||||
/// investigation probe and pinned by `#require` on the selection it produces.
|
||||
private let cardPoint = NSPoint(x: 90, y: 1000 - 90)
|
||||
|
||||
/// Finds a point whose click selects lane 0 itself — its empty space. Probed rather than
|
||||
/// hard-coded, so the pin does not depend on how far the lane's click surface happens to extend
|
||||
/// below its cards on any given layout.
|
||||
@MainActor
|
||||
private func findEmptySpacePoint(on board: HostedBoard) -> NSPoint? {
|
||||
let laneID = ItemID(rawValue: laneName(0))
|
||||
for x in stride(from: 60, through: 240, by: 60) {
|
||||
for yTop in stride(from: 500, through: 120, by: -60) {
|
||||
let p = NSPoint(x: CGFloat(x), y: 1000 - CGFloat(yTop))
|
||||
click(at: p, in: board.window, clicks: 1)
|
||||
_ = waitFor(0.4) { !board.store.selection.isEmpty }
|
||||
let hit = board.store.selection.ids == [laneID]
|
||||
board.store.clearSelection()
|
||||
pump(0.2)
|
||||
if hit { return p }
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - The pins
|
||||
|
||||
@MainActor
|
||||
@@ -323,51 +288,6 @@ struct PointerLatencyTests {
|
||||
#expect(store.selection.ids.contains(target))
|
||||
}
|
||||
|
||||
@Test("An empty-space double-click opens the placeholder and keeps the lane selected")
|
||||
func emptySpaceDoubleClickCreatesThePlaceholder() throws {
|
||||
let fixture = try makeFixture()
|
||||
defer { fixture.tearDown() }
|
||||
let board = try host(fixture)
|
||||
let store = board.store
|
||||
let emptySpacePoint = try #require(findEmptySpacePoint(on: board),
|
||||
"no probe point selected lane 0's empty space")
|
||||
|
||||
// A lone empty-space click first: the lane's own selection latency, the defect's original
|
||||
// surface. Measured ~475 ms before the fix — and unbounded on a surface that carries a
|
||||
// two-tap recogniser without a drag source, which is why the layer branches one tap on
|
||||
// `PointerClick.count` instead. The pointer rests live near the click, as a real one does.
|
||||
post(.leftMouseDown, at: emptySpacePoint, in: board.window, clicks: 1)
|
||||
pump(0.02)
|
||||
post(.leftMouseUp, at: emptySpacePoint, in: board.window, clicks: 1)
|
||||
let lone = try #require(
|
||||
waitForWithMotion(at: emptySpacePoint, in: board.window, timeout: 2.0) { !store.selection.isEmpty },
|
||||
"a lone empty-space click never selected the lane"
|
||||
)
|
||||
print(String(format: "── lone empty-space click → lane selected: %.0f ms", lone))
|
||||
#expect(lone < 250, "empty-space click → selection took \(Int(lone)) ms")
|
||||
#expect(store.selection.ids == [ItemID(rawValue: laneName(0))])
|
||||
store.clearSelection()
|
||||
pump(0.8)
|
||||
|
||||
// The pair: first click selects, second creates — and the first click's selection
|
||||
// survives, because the second click is the create alone, never also the toggle.
|
||||
click(at: emptySpacePoint, in: board.window, clicks: 1)
|
||||
let firstClick = try #require(waitFor(0.5) { !store.selection.isEmpty },
|
||||
"the pair's first click should select the lane")
|
||||
print(String(format: "── pair's first click → lane selected: %.0f ms", firstClick))
|
||||
click(at: emptySpacePoint, in: board.window, clicks: 2)
|
||||
_ = waitFor(1.0) { store.transient.newCardPlaceholder != nil }
|
||||
board.settle(turns: 2)
|
||||
|
||||
let placeholder = try #require(store.transient.newCardPlaceholder,
|
||||
"the empty-space double-click should open the placeholder")
|
||||
#expect(placeholder.laneID == ItemID(rawValue: laneName(0)))
|
||||
// The first click of the pair selected the lane; the second is the create alone
|
||||
// (`PointerClick.count` branches it away from the toggle), so the selection survives.
|
||||
#expect(store.selection.ids == [ItemID(rawValue: laneName(0))],
|
||||
"the pair's first click's selection should survive, selection \(store.selection.ids)")
|
||||
}
|
||||
|
||||
@Test("A right-click on the heels of a left click reaches its menu without the wait")
|
||||
func rightClickMenuAfterAClick() throws {
|
||||
let fixture = try makeFixture()
|
||||
|
||||
Reference in New Issue
Block a user