mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-07 19:06:11 +00:00
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention. Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main. Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance.
102 lines
5.1 KiB
Swift
102 lines
5.1 KiB
Swift
import UIKit
|
|
import XCTest
|
|
@testable import HapiUI
|
|
|
|
@MainActor
|
|
final class TranscriptGeometryTests: XCTestCase {
|
|
func testIndexedQueriesMatchLinearGeometryThroughWindowAndWidthChanges() throws {
|
|
let layout = TranscriptLayout()
|
|
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: 390, height: 844), collectionViewLayout: layout)
|
|
var ids = (0..<800).map(String.init)
|
|
var heights: [String: CGFloat] = [:]
|
|
layout.setItems(ids, width: 390)
|
|
layout.prepare()
|
|
for index in stride(from: 0, to: ids.count, by: 7) {
|
|
let height = CGFloat(20 + index % 19 * 87)
|
|
heights[ids[index]] = height
|
|
let original = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: index, section: 0)))
|
|
let preferred = original.copy() as! UICollectionViewLayoutAttributes
|
|
preferred.size.height = height
|
|
_ = layout.invalidationContext(forPreferredLayoutAttributes: preferred, withOriginalAttributes: original)
|
|
}
|
|
layout.prepare()
|
|
|
|
func verify(width: CGFloat) {
|
|
var y: CGFloat = 10
|
|
let expected = ids.map { id -> CGRect in
|
|
let frame = CGRect(x: 12, y: y, width: width - 24, height: heights[id] ?? 100)
|
|
y = frame.maxY + 10
|
|
return frame
|
|
}
|
|
XCTAssertEqual(layout.collectionViewContentSize.height, y, accuracy: 0.01)
|
|
let rects = stride(from: -500, to: Int(y) + 500, by: 377).map {
|
|
CGRect(x: 0, y: $0, width: Int(width), height: 844)
|
|
} + [.zero, .null, .infinite, CGRect(x: 1000, y: 0, width: 100, height: 100000)]
|
|
for rect in rects {
|
|
let indices = expected.indices.filter { expected[$0].intersects(rect) }
|
|
let actual = layout.layoutAttributesForElements(in: rect) ?? []
|
|
XCTAssertEqual(actual.map(\.indexPath.item), indices)
|
|
XCTAssertEqual(actual.map(\.frame), indices.map { expected[$0] })
|
|
}
|
|
}
|
|
verify(width: 390)
|
|
ids = (800..<820).map(String.init) + Array(ids.dropLast(20))
|
|
layout.setItems(ids, width: 390)
|
|
layout.prepare()
|
|
verify(width: 390)
|
|
// Width changes discard stale measurements but retain correct indices.
|
|
heights.removeAll()
|
|
layout.setItems(ids, width: 430)
|
|
layout.prepare()
|
|
verify(width: 430)
|
|
layout.setItems([], width: 430)
|
|
layout.prepare()
|
|
XCTAssertTrue(layout.layoutAttributesForElements(in: .infinite)?.isEmpty == true)
|
|
XCTAssertNil(layout.layoutAttributesForItem(at: IndexPath(item: 0, section: 0)))
|
|
XCTAssertEqual(collection.collectionViewLayout, layout)
|
|
}
|
|
|
|
#if DEBUG
|
|
func testVisibleQueriesVisitOnlyAViewportSizedRange() {
|
|
let layout = TranscriptLayout()
|
|
layout.setItems((0..<800).map(String.init), width: 390)
|
|
layout.prepare()
|
|
for step in 0..<200 {
|
|
let rect = CGRect(x: 0, y: 1000 + step * 350, width: 390, height: 844)
|
|
let visible = layout.layoutAttributesForElements(in: rect) ?? []
|
|
XCTAssertFalse(visible.isEmpty)
|
|
XCTAssertTrue(visible.allSatisfy { $0.frame.intersects(rect) })
|
|
XCTAssertLessThanOrEqual(visible.count, 9)
|
|
}
|
|
print("Transcript geometry: 800 rows / 200 viewport queries / \(layout.visibleQueryProbeCount) probes")
|
|
// log2(800) + visible rows + a small boundary allowance, not 800
|
|
// CGRect intersection tests on every frame.
|
|
XCTAssertLessThanOrEqual(layout.visibleQueryProbeCount, 200 * 32)
|
|
}
|
|
|
|
func testMeasuringRowsDoesNotAllocateLayoutAttributesForTheWholeWindow() throws {
|
|
let layout = TranscriptLayout()
|
|
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: 390, height: 844), collectionViewLayout: layout)
|
|
layout.setItems((0..<800).map(String.init), width: 390)
|
|
layout.prepare()
|
|
let old = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: 300, section: 0)))
|
|
let oldFrame = old.frame
|
|
let allocations = layout.attributeCreationCount
|
|
for index in 0..<20 {
|
|
let original = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: index, section: 0)))
|
|
let preferred = original.copy() as! UICollectionViewLayoutAttributes
|
|
preferred.size.height += 20
|
|
_ = layout.invalidationContext(forPreferredLayoutAttributes: preferred, withOriginalAttributes: original)
|
|
layout.prepare()
|
|
}
|
|
let updated = try XCTUnwrap(layout.layoutAttributesForItem(at: IndexPath(item: 300, section: 0)))
|
|
XCTAssertEqual(updated.frame.minY, oldFrame.minY + 400, accuracy: 0.01)
|
|
XCTAssertEqual(old.frame, oldFrame, "UIKit's previously returned attributes must remain immutable")
|
|
XCTAssertEqual(collection.collectionViewLayout, layout)
|
|
let created = layout.attributeCreationCount - allocations
|
|
print("Transcript geometry: 20 self-sizing updates / \(created) attributes created")
|
|
XCTAssertLessThanOrEqual(created, 40)
|
|
}
|
|
#endif
|
|
}
|