fix(ios,android): close the acceptance gaps the review found, add Android CI
- T-iOS-34's stated acceptance is "最大字号不破版" and it was failing: the key bar froze its height at 52pt while an AX5 keycap needs 108.24pt, so caps clipped — recorded as a withKnownIssue rather than fixed. Height now derives from the content size category (and tracks live changes via registerForTraitChanges); the known-issue marker is gone, replaced by positive assertions including a 12-category no-clip sweep and a guard that stays red if anyone writes the constant back. Honest tradeoff: the keycap font is clamped at .accessibility2, the same policy the design system already applies to dense content, because an unclamped AX5 bar would eat the terminal. A test pins the clamp so the two cannot drift. - Thumbnails silently 401'd on a token-gated host: the pipeline built its own transport with no token source, and by design never throws, so every preview degraded to a placeholder with no signal. Assembled from AppEnvironment now. - Android had zero CI while the token wave shipped 24 files of secret-handling code. The instrumented leg is workflow_dispatch-only and says why in the file: no one has ever seen it green on a runner, and a required leg nobody trusts just produces a false green. - Android persisted a validated token before the host probe succeeded, stranding a secret for a host that never paired. App bundle 534 -> 550 on both simulators, zero known issues. Android 687 -> 691.
This commit is contained in:
@@ -75,6 +75,102 @@ struct KeyBarTests {
|
||||
#expect(recorder.keys == KeyBarLayout.buttons.map(\.key))
|
||||
}
|
||||
|
||||
// MARK: - Dynamic Type (T-iOS-34 · the bar tracks the keycap it draws)
|
||||
|
||||
/// Measured height of every keycap, in layout order (the mic key last when
|
||||
/// present) — the only honest way to ask "did the keycaps really re-render".
|
||||
private func keycapHeights(_ bar: KeyBarView) -> [CGFloat] {
|
||||
(bar.keyButtons + [bar.voiceButton].compactMap { $0 })
|
||||
.map { $0.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height }
|
||||
}
|
||||
|
||||
@Test("changing the text size re-renders every keycap and re-measures the bar")
|
||||
func contentSizeCategoryChangeRebuildsTheKeycaps() {
|
||||
// Arrange: a bar born at the standard size, mic key included.
|
||||
let bar = KeyBarView(
|
||||
voice: KeyBarVoiceHandlers(onDown: {}, onUp: {}),
|
||||
contentSizeCategory: .large
|
||||
)
|
||||
let standardHeight = bar.intrinsicContentSize.height
|
||||
let standardKeycaps = keycapHeights(bar)
|
||||
|
||||
// Act: the user drags the text-size slider to the maximum.
|
||||
bar.apply(contentSizeCategory: .accessibilityExtraExtraExtraLarge)
|
||||
|
||||
// Assert: EVERY keycap grew (mic included) and the bar grew to hold them.
|
||||
let largestKeycaps = keycapHeights(bar)
|
||||
#expect(bar.contentSizeCategory == .accessibilityExtraExtraExtraLarge)
|
||||
#expect(bar.intrinsicContentSize.height > standardHeight)
|
||||
#expect(bar.frame.height == bar.intrinsicContentSize.height)
|
||||
#expect(standardKeycaps.count == KeyBarLayout.buttons.count + 1)
|
||||
#expect(zip(standardKeycaps, largestKeycaps).allSatisfy { $1 > $0 })
|
||||
#expect(largestKeycaps.allSatisfy { $0 <= bar.intrinsicContentSize.height })
|
||||
}
|
||||
|
||||
/// The runtime path, not just the direct call: the user changes the text size
|
||||
/// in Settings while a terminal is open, UIKit pushes a new trait collection,
|
||||
/// and the bar must re-measure itself — otherwise it keeps whatever height it
|
||||
/// was born with until the terminal is reopened.
|
||||
@Test("a trait-collection change re-lays-out the bar (no reopen needed)")
|
||||
func traitChangePropagatesToTheBar() {
|
||||
// Arrange: a bar inside a real window (trait propagation needs one).
|
||||
let bar = KeyBarView(contentSizeCategory: .large)
|
||||
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
||||
window.addSubview(bar)
|
||||
window.layoutIfNeeded()
|
||||
let before = bar.intrinsicContentSize.height
|
||||
|
||||
// Act: what UIKit does when the system text size changes.
|
||||
window.traitOverrides.preferredContentSizeCategory =
|
||||
.accessibilityExtraExtraExtraLarge
|
||||
window.layoutIfNeeded()
|
||||
|
||||
// Assert
|
||||
#expect(bar.contentSizeCategory == .accessibilityExtraExtraExtraLarge)
|
||||
#expect(bar.intrinsicContentSize.height > before)
|
||||
}
|
||||
|
||||
@Test("re-applying the same text size is a no-op (trait callbacks fire for anything)")
|
||||
func reapplyingTheSameCategoryChangesNothing() {
|
||||
// Arrange
|
||||
let bar = KeyBarView(contentSizeCategory: .large)
|
||||
let before = keycapHeights(bar)
|
||||
let beforeHeight = bar.intrinsicContentSize.height
|
||||
|
||||
// Act
|
||||
bar.apply(contentSizeCategory: .large)
|
||||
|
||||
// Assert
|
||||
#expect(bar.contentSizeCategory == .large)
|
||||
#expect(keycapHeights(bar) == before)
|
||||
#expect(bar.intrinsicContentSize.height == beforeHeight)
|
||||
}
|
||||
|
||||
/// The HIG floor is enforced by a constraint, not by the glyph: at the small
|
||||
/// text sizes a keycap's own content is only ~37pt tall, so without this the
|
||||
/// bar would shrink below a tappable target. Asserted structurally because
|
||||
/// `systemLayoutSizeFitting` reports the CONTENT size (it is what the
|
||||
/// clipping test needs) and would not show the constraint at all.
|
||||
@Test("every keycap carries the 44pt HIG hit-target floor, at every text size")
|
||||
func keycapsCarryTheHitTargetFloor() {
|
||||
for category in [UIContentSizeCategory.extraSmall, .large,
|
||||
.accessibilityExtraExtraExtraLarge] {
|
||||
let bar = KeyBarView(
|
||||
voice: KeyBarVoiceHandlers(onDown: {}, onUp: {}),
|
||||
contentSizeCategory: category
|
||||
)
|
||||
|
||||
for button in bar.keyButtons + [bar.voiceButton].compactMap({ $0 }) {
|
||||
#expect(button.constraints.contains {
|
||||
$0.firstAttribute == .height
|
||||
&& $0.relation == .greaterThanOrEqual
|
||||
&& $0.constant == DS.Layout.minHitTarget
|
||||
})
|
||||
}
|
||||
#expect(bar.intrinsicContentSize.height >= DS.Layout.minHitTarget)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hardware keyboard (UIKeyCommand, same KeyByteMap mapping)
|
||||
|
||||
@Test("hardware chords: Esc, Shift+Tab and the 7 Ctrl chords map through KeyByteMap")
|
||||
|
||||
Reference in New Issue
Block a user