Skip to content

Commit af38e3b

Browse files
authored
fix(tabs): answer accessibility hit tests from the strip's own subviews (#2577)
* fix(tabs): answer accessibility hit tests from the strip's own subviews Claude-Session: https://claude.ai/code/session_01L7uaHbJBPV1LaWL5QXzxyp * test(tabs): assert the detach flow against every window rather than a re-resolving query Claude-Session: https://claude.ai/code/session_01L7uaHbJBPV1LaWL5QXzxyp
1 parent 474271b commit af38e3b

2 files changed

Lines changed: 30 additions & 7 deletions

File tree

TablePro/Views/Main/EditorTabInteractionView.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,18 @@ internal final class EditorTabInteractionView: NSView {
124124
return self
125125
}
126126

127-
/// Answers from the SwiftUI tree, which is where the tabs publish themselves. The pointer's
128-
/// claim is lifted for the length of the question.
127+
/// Answers from the SwiftUI tree, which is where the tabs publish themselves.
128+
///
129+
/// The subviews are asked directly rather than through `super`, which will not walk into them
130+
/// from a receiver that is not itself an accessibility element: lifting the pointer's claim and
131+
/// deferring to `super` left every tab unreachable exactly as before. The point arrives in
132+
/// screen coordinates and is passed on unchanged, because that is what the children expect too.
129133
override internal func accessibilityHitTest(_ point: NSPoint) -> Any? {
130134
isResolvingAccessibilityHit = true
131135
defer { isResolvingAccessibilityHit = false }
136+
for subview in subviews.reversed() {
137+
if let hit = subview.accessibilityHitTest(point) { return hit }
138+
}
132139
return super.accessibilityHitTest(point)
133140
}
134141

TableProUITests/EditorTabDetachUITests.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ final class EditorTabDetachUITests: UITestCase {
3030
waitForPredicate(timeout: 20) { app.windows.count >= 2 },
3131
"Move Tab to New Window must open a second window"
3232
)
33+
/// Asked of every window rather than of `window`, which is a query and re-resolves: once
34+
/// the move opens a second window, `app.windows.firstMatch` can be that one, and the
35+
/// assertion then reads the tab it was looking for in the window it was moved into.
3336
XCTAssertTrue(
34-
waitForPredicate(timeout: 20) { !self.tabLabels(in: window).contains(moved) },
35-
"The tab must leave the window it was moved out of, still shows \(tabLabels(in: window))"
37+
waitForPredicate(timeout: 20) { self.someWindow(in: app, holds: before.filter { $0 != moved }) },
38+
"A window must hold the tabs that stayed behind, windows show \(self.tabsPerWindow(in: app))"
3639
)
3740
XCTAssertTrue(
3841
waitForPredicate(timeout: 20) {
@@ -49,7 +52,8 @@ final class EditorTabDetachUITests: UITestCase {
4952
openTables(Self.tables, in: window)
5053
XCTAssertTrue(waitForPredicate(timeout: 25) { self.tabLabels(in: window).count >= 3 })
5154

52-
let moved = try XCTUnwrap(tabLabels(in: window).last)
55+
let before = tabLabels(in: window)
56+
let moved = try XCTUnwrap(before.last)
5357
detach(tabNamed: moved, in: window, of: app)
5458
XCTAssertTrue(waitForPredicate(timeout: 20) { app.windows.count >= 2 })
5559

@@ -63,9 +67,12 @@ final class EditorTabDetachUITests: UITestCase {
6367
waitForPredicate(timeout: 20) { app.windows.count == 1 },
6468
"Closing the detached window must close it rather than empty it"
6569
)
70+
/// Compared against what the strip held before the move rather than against a number: the
71+
/// sample database opens a tab of its own, so the total is not the count of tables opened
72+
/// here.
6673
XCTAssertTrue(
67-
waitForPredicate(timeout: 20) { self.tabLabels(in: window).count == 2 },
68-
"The original window keeps its remaining tabs, shows \(tabLabels(in: window))"
74+
waitForPredicate(timeout: 20) { self.someWindow(in: app, holds: before.filter { $0 != moved }) },
75+
"The remaining window keeps the other tabs, windows show \(self.tabsPerWindow(in: app)) of \(before)"
6976
)
7077
}
7178

@@ -124,6 +131,15 @@ final class EditorTabDetachUITests: UITestCase {
124131
.map { $0.label }
125132
}
126133

134+
/// Whether any window's strip holds exactly these tabs, in this order.
135+
private func someWindow(in app: XCUIApplication, holds tabs: [String]) -> Bool {
136+
app.windows.allElementsBoundByIndex.contains { tabLabels(in: $0) == tabs }
137+
}
138+
139+
private func tabsPerWindow(in app: XCUIApplication) -> [[String]] {
140+
app.windows.allElementsBoundByIndex.map { tabLabels(in: $0) }
141+
}
142+
127143
private func windowTitles(in app: XCUIApplication) -> [String] {
128144
app.windows.allElementsBoundByIndex.map { $0.title }
129145
}

0 commit comments

Comments
 (0)