mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-12 20:07:31 +00:00
190 lines
8.4 KiB
Swift
190 lines
8.4 KiB
Swift
import Observation
|
|
import SwiftUI
|
|
import UIKit
|
|
import XCTest
|
|
@testable import HapiUI
|
|
|
|
@MainActor
|
|
final class TranscriptTypographyTests: XCTestCase {
|
|
struct Row: Identifiable, Equatable {
|
|
let id: String
|
|
let text: String
|
|
}
|
|
|
|
@Observable @MainActor
|
|
final class Driver {
|
|
let rows = (0..<80).map {
|
|
Row(id: String($0), text: "Message \($0) · 中英文阅读。" + String(repeating: "Dynamic Type keeps long answers readable. ", count: 3))
|
|
}
|
|
var size = DynamicTypeSize.large
|
|
var weight: LegibilityWeight? = .regular
|
|
var width: CGFloat = 390
|
|
var version = 0
|
|
@ObservationIgnored var layouts: [Int] = []
|
|
@ObservationIgnored var typography: HapiTypography?
|
|
}
|
|
|
|
private struct Probe: UIViewRepresentable {
|
|
let driver: Driver
|
|
func makeUIView(context: Context) -> UIView { UIView() }
|
|
func updateUIView(_ view: UIView, context: Context) {
|
|
driver.typography = context.environment.hapiTypography
|
|
}
|
|
}
|
|
|
|
private struct TextRow: View {
|
|
let row: Row
|
|
@Environment(\.hapiTypography) private var typography
|
|
var body: some View {
|
|
Text(row.text)
|
|
.font(typography.bodyFont)
|
|
.lineSpacing(typography.bodyLineSpacing)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
private struct Harness: View {
|
|
let driver: Driver
|
|
var body: some View {
|
|
AnchoredTranscriptList(items: driver.rows, historyVersion: driver.version, jumpToken: 0,
|
|
historyControlID: "history", onViewport: { _ in },
|
|
onLayout: { version, _ in driver.layouts.append(version) }) { row in
|
|
AnyView(TextRow(row: row))
|
|
}
|
|
.overlay { Probe(driver: driver).frame(width: 0, height: 0) }
|
|
.frame(width: driver.width, height: 700)
|
|
.hapiTypography()
|
|
.environment(\.dynamicTypeSize, driver.size)
|
|
.environment(\.legibilityWeight, driver.weight)
|
|
}
|
|
}
|
|
|
|
private func host(_ driver: Driver) throws -> (UIWindow, UICollectionView) {
|
|
let scene = try XCTUnwrap(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
|
|
let window = UIWindow(windowScene: scene)
|
|
let host = UIHostingController(rootView: Harness(driver: driver))
|
|
window.rootViewController = host
|
|
window.makeKeyAndVisible()
|
|
host.view.layoutIfNeeded()
|
|
func find(_ view: UIView) -> UICollectionView? {
|
|
if let collection = view as? UICollectionView { return collection }
|
|
return view.subviews.lazy.compactMap(find).first
|
|
}
|
|
return (window, try XCTUnwrap(find(host.view)))
|
|
}
|
|
|
|
private func settle(_ condition: () -> Bool = { true }, file: StaticString = #filePath, line: UInt = #line) async {
|
|
for _ in 0..<100 {
|
|
try? await Task.sleep(for: .milliseconds(20))
|
|
if condition() {
|
|
try? await Task.sleep(for: .milliseconds(160))
|
|
return
|
|
}
|
|
}
|
|
XCTFail("Typography layout did not settle", file: file, line: line)
|
|
}
|
|
|
|
private func browse(_ view: UICollectionView, index: Int) async throws -> UICollectionViewCell {
|
|
view.delegate?.scrollViewWillBeginDragging?(view)
|
|
view.scrollToItem(at: IndexPath(item: index, section: 0), at: .top, animated: false)
|
|
await settle()
|
|
view.contentOffset.y += 31
|
|
view.layoutIfNeeded()
|
|
await settle()
|
|
return try XCTUnwrap(view.cellForItem(at: IndexPath(item: index, section: 0)))
|
|
}
|
|
|
|
func testEqualItemsResizeAtLargeTextAndKeepTheHistoryAnchor() async throws {
|
|
let driver = Driver()
|
|
let (window, view) = try host(driver)
|
|
defer { window.isHidden = true }
|
|
await settle { driver.layouts.contains(0) }
|
|
XCTAssertEqual(try XCTUnwrap(driver.typography).bodySize, 16, accuracy: 0.01)
|
|
let farHeight = try await browse(view, index: 60).frame.height
|
|
let cell = try await browse(view, index: 25)
|
|
let oldHeight = cell.frame.height
|
|
let offset = cell.frame.minY - view.contentOffset.y
|
|
driver.size = .xxxLarge
|
|
await settle { (driver.typography?.bodySize ?? 0) > 16 && cell.frame.height > oldHeight }
|
|
XCTAssertEqual(cell.frame.minY - view.contentOffset.y, offset, accuracy: 1)
|
|
let typography = try XCTUnwrap(driver.typography)
|
|
XCTAssertEqual(typography.inlineCodeSize / typography.bodySize, 15 / 16, accuracy: 0.001)
|
|
let recycled = try await browse(view, index: 60)
|
|
XCTAssertGreaterThan(recycled.frame.height, farHeight)
|
|
}
|
|
|
|
func testMaximumAccessibilitySizeAndBoldTextPreserveBottomFollowing() async throws {
|
|
let driver = Driver()
|
|
let (window, view) = try host(driver)
|
|
defer { window.isHidden = true }
|
|
await settle { driver.layouts.contains(0) }
|
|
driver.size = .accessibility5
|
|
driver.weight = .bold
|
|
await settle { (driver.typography?.bodySize ?? 0) > 40 && driver.typography?.boldText == true }
|
|
XCTAssertEqual(view.contentOffset.y, view.contentSize.height - view.bounds.height, accuracy: 1)
|
|
let cell = try XCTUnwrap(view.cellForItem(at: IndexPath(item: 79, section: 0)))
|
|
XCTAssertGreaterThan(cell.frame.height, 500)
|
|
driver.size = .large
|
|
await settle { driver.typography?.bodySize == 16 }
|
|
XCTAssertEqual(view.contentOffset.y, view.contentSize.height - view.bounds.height, accuracy: 1)
|
|
}
|
|
|
|
func testShrinkingTextClampsAnOffsetBeyondTheNewRowHeight() async throws {
|
|
let driver = Driver()
|
|
driver.size = .accessibility5
|
|
let (window, view) = try host(driver)
|
|
defer { window.isHidden = true }
|
|
await settle { driver.layouts.contains(0) }
|
|
let cell = try await browse(view, index: 25)
|
|
view.contentOffset.y = cell.frame.minY + 400
|
|
view.layoutIfNeeded()
|
|
await settle()
|
|
driver.size = .large
|
|
await settle { driver.typography?.bodySize == 16 }
|
|
let resized = try XCTUnwrap(view.cellForItem(at: IndexPath(item: 25, section: 0)))
|
|
XCTAssertEqual(resized.frame.maxY - view.contentOffset.y, 1, accuracy: 1)
|
|
}
|
|
|
|
func testGrowingTextDoesNotClampADeepOffsetToAnUnmeasuredEstimate() async throws {
|
|
let driver = Driver()
|
|
driver.size = .accessibility3
|
|
let (window, view) = try host(driver)
|
|
defer { window.isHidden = true }
|
|
await settle { driver.layouts.contains(0) }
|
|
let cell = try await browse(view, index: 25)
|
|
// Keep the anchor inside this measured row on iPad as well as iPhone.
|
|
// A fixed 350pt can already be in the next row with tablet metrics.
|
|
let deepOffset = min(350, cell.frame.height / 2)
|
|
XCTAssertGreaterThan(deepOffset, 100, "Exercise an offset beyond the unmeasured height estimate")
|
|
view.contentOffset.y = cell.frame.minY + deepOffset
|
|
view.layoutIfNeeded()
|
|
await settle()
|
|
let previousSize = try XCTUnwrap(driver.typography).bodySize
|
|
driver.size = .accessibility5
|
|
await settle { (driver.typography?.bodySize ?? 0) > previousSize }
|
|
let grown = try XCTUnwrap(view.cellForItem(at: IndexPath(item: 25, section: 0)))
|
|
XCTAssertEqual(view.contentOffset.y - grown.frame.minY, deepOffset, accuracy: 1)
|
|
}
|
|
|
|
func testReadingWidthAndBoldTextChangesKeepHistoryInPlace() async throws {
|
|
let driver = Driver()
|
|
let (window, view) = try host(driver)
|
|
defer { window.isHidden = true }
|
|
await settle { driver.layouts.contains(0) }
|
|
let cell = try await browse(view, index: 25)
|
|
let offset = cell.frame.minY - view.contentOffset.y
|
|
for width: CGFloat in [1024, 768, 507, 320] {
|
|
driver.width = width
|
|
await settle { abs(view.bounds.width - width) < 1 }
|
|
let frame = try XCTUnwrap(view.cellForItem(at: IndexPath(item: 25, section: 0))).frame
|
|
XCTAssertEqual(frame.width, HapiReadingLayout.contentWidth(in: width), accuracy: 1)
|
|
XCTAssertEqual(frame.midX, width / 2, accuracy: 1)
|
|
XCTAssertEqual(frame.minY - view.contentOffset.y, offset, accuracy: 1)
|
|
}
|
|
driver.weight = .bold
|
|
await settle { driver.typography?.boldText == true }
|
|
let frame = try XCTUnwrap(view.cellForItem(at: IndexPath(item: 25, section: 0))).frame
|
|
XCTAssertEqual(frame.minY - view.contentOffset.y, offset, accuracy: 1)
|
|
}
|
|
}
|