Skip to content

Commit 890a634

Browse files
authored
test(tabs): drive the tab reorder tests with a gesture the strip can receive (#2546)
Claude-Session: https://claude.ai/code/session_011y4GK3oSBTYUXv5ZW1reE2
1 parent 44d4e0e commit 890a634

2 files changed

Lines changed: 59 additions & 35 deletions

File tree

LocalPackages/CodeEditTextView/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ extension TextLayoutManager {
8585
var yContentAdjustment: CGFloat = 0
8686
var maxFoundLineWidth = maxLineWidth
8787

88-
// The layout view draws its own decorations into a backing store nothing else invalidates when the
89-
// viewport moves, so a band drawn before its lines were laid out would stay blank forever.
90-
var relaidOutRect: CGRect = .null
88+
// The vertical span this pass laid out. The layout view draws its own decorations into a backing store
89+
// nothing else invalidates when the viewport moves, so a band drawn before its lines were laid out would
90+
// stay blank forever. Tracked as a span rather than a rect because a rect union silently drops an operand
91+
// of zero width, which is what an unparented layout view reports.
92+
var relaidOutMinY: CGFloat = .greatestFiniteMagnitude
93+
var relaidOutMaxY: CGFloat = -.greatestFiniteMagnitude
9194

9295
#if DEBUG
9396
var laidOutLines: Set<TextLine.ID> = []
@@ -111,13 +114,10 @@ extension TextLayoutManager {
111114
maxFoundLineWidth: &maxFoundLineWidth
112115
)
113116
yContentAdjustment += yAdjustment
114-
relaidOutRect = relaidOutRect.union(
115-
CGRect(
116-
x: 0,
117-
y: linePosition.yPos,
118-
width: layoutView?.frame.width ?? 0,
119-
height: max(linePosition.height, linePosition.data.lineFragments.height)
120-
)
117+
relaidOutMinY = min(relaidOutMinY, linePosition.yPos)
118+
relaidOutMaxY = max(
119+
relaidOutMaxY,
120+
linePosition.yPos + max(linePosition.height, linePosition.data.lineFragments.height)
121121
)
122122
#if DEBUG
123123
laidOutLines.insert(linePosition.data.id)
@@ -175,17 +175,18 @@ extension TextLayoutManager {
175175
delegate?.layoutManagerHeightDidUpdate(newHeight: lineStorage.height)
176176
}
177177

178-
if !relaidOutRect.isNull {
178+
if let layoutView, relaidOutMinY <= relaidOutMaxY {
179+
// A height change or a y adjustment moves every line below the first one this pass touched.
179180
let movedEverythingBelow = didLayoutChange || yContentAdjustment != 0
180-
let invalidRect = movedEverythingBelow
181-
? CGRect(
182-
x: relaidOutRect.minX,
183-
y: relaidOutRect.minY,
184-
width: relaidOutRect.width,
185-
height: max(maxY - relaidOutRect.minY, relaidOutRect.height)
181+
let bottom = movedEverythingBelow ? max(maxY, relaidOutMaxY) : relaidOutMaxY
182+
layoutView.setNeedsDisplay(
183+
CGRect(
184+
x: 0,
185+
y: relaidOutMinY,
186+
width: layoutView.frame.width,
187+
height: bottom - relaidOutMinY
186188
)
187-
: relaidOutRect
188-
layoutView?.setNeedsDisplay(invalidRect)
189+
)
189190
}
190191

191192
#if DEBUG

TableProUITests/EditorTabReorderUITests.swift

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@ final class EditorTabReorderUITests: UITestCase {
2323

2424
drag(tab(named: before[0], in: window), onto: tab(named: before[2], in: window))
2525

26-
let after = tabLabels(in: window)
27-
28-
XCTAssertNotEqual(
29-
before,
30-
after,
26+
XCTAssertTrue(
27+
waitForTabOrder(toChangeFrom: before, in: window),
3128
"Dragging the first tab across the strip must change the tab order, was \(before)"
3229
)
3330
}
@@ -53,9 +50,8 @@ final class EditorTabReorderUITests: UITestCase {
5350

5451
drag(tab(named: unselected, in: window), onto: tab(named: before[before.count - 1], in: window))
5552

56-
XCTAssertNotEqual(
57-
before,
58-
tabLabels(in: window),
53+
XCTAssertTrue(
54+
waitForTabOrder(toChangeFrom: before, in: window),
5955
"Dragging an unselected tab must change the tab order, was \(before)"
6056
)
6157
}
@@ -81,9 +77,8 @@ final class EditorTabReorderUITests: UITestCase {
8177

8278
drag(tab(named: selected, in: window), onto: tab(named: target, in: window))
8379

84-
XCTAssertNotEqual(
85-
before,
86-
tabLabels(in: window),
80+
XCTAssertTrue(
81+
waitForTabOrder(toChangeFrom: before, in: window),
8782
"Dragging the selected tab must change the tab order, was \(before)"
8883
)
8984
}
@@ -107,11 +102,22 @@ final class EditorTabReorderUITests: UITestCase {
107102

108103
let before = tabLabels(in: window)
109104

110-
drag(tab(named: before[0], in: window), onto: tab(named: before[2], in: window))
105+
// Not `before[0]`: once the track scrolls, the leading tabs stay in the accessibility tree
106+
// at frames outside the viewport, and the pointer cannot land on one.
107+
let reachable = onScreenTabs(in: window).map { $0.label }
108+
XCTAssertGreaterThanOrEqual(
109+
reachable.count,
110+
4,
111+
"The viewport must hold four tabs to drag between, showed \(reachable) of \(before)"
112+
)
111113

112-
XCTAssertNotEqual(
113-
before,
114-
tabLabels(in: window),
114+
// Interior tabs only. A tab at either edge of a scrolled track is half outside the
115+
// viewport, and a click at its centre lands on the chrome beside the track rather than on
116+
// the tab, so the drag never reaches the strip at all.
117+
drag(tab(named: reachable[1], in: window), onto: tab(named: reachable[reachable.count - 2], in: window))
118+
119+
XCTAssertTrue(
120+
waitForTabOrder(toChangeFrom: before, in: window),
115121
"Dragging a tab in an overflowing strip must change the tab order, was \(before)"
116122
)
117123
}
@@ -171,6 +177,23 @@ final class EditorTabReorderUITests: UITestCase {
171177
forDuration: 0.6,
172178
thenDragTo: destination.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
173179
)
174-
Thread.sleep(forTimeInterval: 1.0)
180+
}
181+
182+
/// The strip animates the move and commits it on release, so the new order arrives some time
183+
/// after the gesture returns. Waiting for it rather than sleeping a fixed amount is what keeps
184+
/// this readable on a loaded machine: a sleep long enough for CI is dead time on every local
185+
/// run, and one short enough for a local run reads the pre-drag order on CI and reports the
186+
/// reorder as broken.
187+
private func waitForTabOrder(toChangeFrom before: [String], in window: XCUIElement) -> Bool {
188+
waitForPredicate(timeout: 15) { self.tabLabels(in: window) != before }
189+
}
190+
191+
/// The tabs the pointer can actually reach.
192+
///
193+
/// Once the strip overflows, the track scrolls and the tabs outside the viewport stay in the
194+
/// accessibility tree with frames the pointer cannot land on. Dragging one of those is not a
195+
/// weaker version of the gesture, it is no gesture at all.
196+
private func onScreenTabs(in window: XCUIElement) -> [XCUIElement] {
197+
tabElements(in: window).filter { $0.exists && $0.isHittable }
175198
}
176199
}

0 commit comments

Comments
 (0)