import SwiftUI import Testing import UIKit import WireProtocol @testable import WebTerm /// UX-A · Proves the FROZEN design system: the status mapping is complete and /// "color + shape" (never color alone), the token scales are well-formed, and /// the adaptive accent resolves distinctly in light vs dark. Pure logic — runs /// on both iPhone and iPad sims with no rendering. @Suite("DesignSystem") struct DesignSystemTests { // MARK: - StatusStyle: complete, distinct, color + shape @Test("every status maps to a non-empty symbol + Chinese label") func everyStatusHasSymbolAndLabel() { for status in DisplayStatus.allCases { let style = StatusStyle.style(for: status) #expect(!style.symbolName.isEmpty, "\(status) has no symbol") #expect(!style.label.isEmpty, "\(status) has no label") } } @Test("status is never color-alone — all seven symbols are distinct") func symbolsAreDistinctAcrossStatuses() { let symbols = DisplayStatus.allCases.map { StatusStyle.style(for: $0).symbolName } #expect(Set(symbols).count == DisplayStatus.allCases.count) } @Test("Chinese labels are all distinct") func labelsAreDistinct() { let labels = DisplayStatus.allCases.map { StatusStyle.style(for: $0).label } #expect(Set(labels).count == DisplayStatus.allCases.count) } @Test("the critical trio working/waiting/stuck use distinct colors") func criticalColorsAreDistinct() { let working = rgba(StatusStyle.style(for: DisplayStatus.working).color) let waiting = rgba(StatusStyle.style(for: DisplayStatus.waiting).color) let stuck = rgba(StatusStyle.style(for: DisplayStatus.stuck).color) #expect(!approxEqual(working, waiting)) #expect(!approxEqual(working, stuck)) #expect(!approxEqual(waiting, stuck)) } /// T-iOS-34 **有意改写**:这三个 token 从"单值"变成了 scheme-adaptive /// (深色 = web 原值不变,浅色 = 新增的可读变体,见 `Tokens.swift`)。原用例 /// 用 `UIColor(color).getRed(...)` 隐式按**测试进程当前 trait**(浅色)解析, /// 于是量到了新的浅色值。断言的意图没变 —— "深色档必须逐字节等于桌面 web 的 /// 状态色" —— 只是现在必须**显式指定深色 trait** 才能表达这个意图。 /// 浅色档的取值与对比度门槛由 `AppThemeTests` 覆盖。 @Test("semantic status colors match the desktop/web status palette (dark scheme)") func statusColorsMatchDirection() { let dark = UITraitCollection(userInterfaceStyle: .dark) assertColor(StatusStyle.style(for: DisplayStatus.working).color, in: dark, r: 70, g: 208, b: 127) // #46D07F web --green assertColor(StatusStyle.style(for: DisplayStatus.waiting).color, in: dark, r: 245, g: 177, b: 76) // #F5B14C web --amber assertColor(StatusStyle.style(for: DisplayStatus.stuck).color, in: dark, r: 255, g: 107, b: 107) // #FF6B6B web --red } @Test("the wire ClaudeStatus bridge covers all five cases") func wireBridgeCoversEveryCase() { let pairs: [(ClaudeStatus, DisplayStatus)] = [ (.working, .working), (.waiting, .waiting), (.idle, .idle), (.stuck, .stuck), (.unknown, .unknown), ] for (wire, expected) in pairs { #expect(DisplayStatus(wire) == expected) #expect(StatusStyle.style(for: wire) == StatusStyle.style(for: expected)) } } // MARK: - Token scales are well-formed @Test("spacing scale is exactly 2·4·8·12·16·20·24 and strictly increasing") func spacingScaleIsMonotonic() { let scale: [CGFloat] = [ DS.Space.xs2, DS.Space.xs4, DS.Space.sm8, DS.Space.md12, DS.Space.lg16, DS.Space.xl20, DS.Space.xxl24, ] #expect(scale == [2, 4, 8, 12, 16, 20, 24]) #expect(isStrictlyIncreasing(scale)) } @Test("radius scale is strictly increasing sm8 < md12 < lg16 < pill") func radiusScaleIsMonotonic() { let scale: [CGFloat] = [DS.Radius.sm8, DS.Radius.md12, DS.Radius.lg16, DS.Radius.pill] #expect(scale == [8, 12, 16, 999]) #expect(isStrictlyIncreasing(scale)) } @Test("opacity dimming tokens are within (0, 1)") func opacityTokensAreFractions() { for value in [DS.Opacity.stale, DS.Opacity.exited, DS.Opacity.pressed] { #expect(value > 0 && value < 1) } } @Test("minimum hit target meets the 44pt HIG floor") func hitTargetMeetsHIG() { #expect(DS.Layout.minHitTarget >= 44) } // MARK: - Motion honors Reduce Motion @Test("motion collapses to nil under Reduce Motion, animates otherwise") func motionGating() { #expect(DS.Motion.gated(DS.Motion.base, reduceMotion: true) == nil) #expect(DS.Motion.gated(DS.Motion.base, reduceMotion: false) != nil) #expect(DS.Motion.fastDuration < DS.Motion.baseDuration) } // MARK: - Adaptive accent resolves for both schemes @Test("accent is defined and distinct in light vs dark") func accentIsAdaptive() { let accent = DS.Palette.accentUIColor() let dark = accent.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark)) let light = accent.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light)) let darkRGBA = rgba(dark) let lightRGBA = rgba(light) // Both fully opaque, and visibly different between schemes. #expect(darkRGBA.a == 1) #expect(lightRGBA.a == 1) #expect(!approxEqual(darkRGBA, lightRGBA)) // Desktop amber gold: dark ≈ #E3A64A (--accent), light ≈ #C9892F (--accent-2). assertRGBA(darkRGBA, r: 0xE3, g: 0xA6, b: 0x4A) assertRGBA(lightRGBA, r: 0xC9, g: 0x89, b: 0x2F) } // MARK: - Helpers private typealias RGBA = (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) private func isStrictlyIncreasing(_ values: [CGFloat]) -> Bool { zip(values, values.dropFirst()).allSatisfy { $0 < $1 } } private func rgba(_ color: Color) -> RGBA { rgba(UIColor(color)) } private func rgba(_ color: UIColor) -> RGBA { var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 color.getRed(&r, green: &g, blue: &b, alpha: &a) return (r, g, b, a) } private func approxEqual(_ lhs: RGBA, _ rhs: RGBA, tolerance: CGFloat = 0.02) -> Bool { abs(lhs.r - rhs.r) < tolerance && abs(lhs.g - rhs.g) < tolerance && abs(lhs.b - rhs.b) < tolerance } private func assertColor(_ color: Color, r: Int, g: Int, b: Int) { assertRGBA(rgba(color), r: r, g: g, b: b) } /// Same, but resolving an adaptive token in an explicit scheme. private func assertColor( _ color: Color, in traits: UITraitCollection, r: Int, g: Int, b: Int ) { assertRGBA(rgba(UIColor(color).resolvedColor(with: traits)), r: r, g: g, b: b) } private func assertRGBA(_ value: RGBA, r: Int, g: Int, b: Int, tolerance: CGFloat = 0.02) { #expect(abs(value.r - CGFloat(r) / 255) < tolerance) #expect(abs(value.g - CGFloat(g) / 255) < tolerance) #expect(abs(value.b - CGFloat(b) / 255) < tolerance) } }