import XCTest @_spi(Fixtures) @testable import CallsKit /// The warm rim (0.4.3, "T5, warm soft breath"): the four layers' /// opacity and blur for a level, exactly the mock-up's box-shadow recipe; /// its colours; when it is on; and Reduce Motion. final class WarmRimTests: XCTestCase { private func level(db: Float) -> Float { pow(10, db / 20) } func testTheFourLayersFollowTheLevel() { for lvl in stride(from: 0.0, through: 1, by: 0.05) { let rim = WarmRim(on: true, level: lvl) XCTAssertEqual(rim.rimOpacity, 0.10 + lvl * 0.45, accuracy: 1e-12, "rim at \(lvl)") XCTAssertEqual(rim.topOpacity, 0, "T5 has no top highlight") XCTAssertEqual(rim.innerOpacity, 0.06 + lvl * 0.30, accuracy: 1e-12, "inner at \(lvl)") XCTAssertEqual(rim.innerBlur, 3 + lvl * 6, accuracy: 1e-12) XCTAssertEqual(rim.outerOpacity, lvl * 0.25, accuracy: 1e-12, "outer at \(lvl)") XCTAssertEqual(rim.outerBlur, 1 + lvl * 7, accuracy: 1e-12) } // The ends, spelled out. let quiet = WarmRim(on: true, level: 0) XCTAssertEqual(quiet.rimOpacity, 0.10, accuracy: 1e-12) XCTAssertEqual(quiet.innerOpacity, 0.06, accuracy: 1e-12) XCTAssertEqual(quiet.innerBlur, 3) XCTAssertEqual(quiet.outerOpacity, 0, "no breath until the voice rises") XCTAssertEqual(quiet.outerBlur, 1) let loud = WarmRim(on: true, level: 1) XCTAssertEqual(loud.rimOpacity, 0.55, accuracy: 1e-12) XCTAssertEqual(loud.innerOpacity, 0.36, accuracy: 1e-12) XCTAssertEqual(loud.innerBlur, 9) XCTAssertEqual(loud.outerOpacity, 0.25, accuracy: 1e-12) XCTAssertEqual(loud.outerBlur, 8) XCTAssertEqual(WarmRim.rimWidth, 1) } func testOffIsNothingAtAnyLevel() { for lvl in [0, 0.3, 1.0] { let rim = WarmRim(on: false, level: lvl) XCTAssertEqual(rim.rimOpacity, 0) XCTAssertEqual(rim.innerOpacity, 0) XCTAssertEqual(rim.outerOpacity, 0) } XCTAssertEqual(WarmRim.off, WarmRim(on: false, level: 0)) } func testALevelOutOfRangeIsHeldToIt() { XCTAssertEqual(WarmRim(on: true, level: 2), WarmRim(on: true, level: 1)) XCTAssertEqual(WarmRim(on: true, level: -1), WarmRim(on: true, level: 0)) XCTAssertEqual(WarmRim(on: true, level: .nan), WarmRim(on: true, level: 0)) } func testTheColoursAreWarmWhite() { // hsl(38°, 80 %, 82 / 72 / 66 %), as Chromium computes them. func check(_ l: Double, _ r: Int, _ g: Int, _ b: Int) { let c = WarmRim.rgb(hue: WarmRim.hue, saturation: WarmRim.saturation, lightness: l) XCTAssertEqual(Int((c.red * 255).rounded()), r, "red at \(l)") XCTAssertEqual(Int((c.green * 255).rounded()), g, "green at \(l)") XCTAssertEqual(Int((c.blue * 255).rounded()), b, "blue at \(l)") } check(WarmRim.rimLightness, 246, 219, 172) check(WarmRim.innerLightness, 241, 199, 126) check(WarmRim.outerLightness, 238, 187, 99) XCTAssertEqual(WarmRim.hue, 38) XCTAssertEqual(WarmRim.saturation, 0.80) } func testSpeechTurnsItOnAndItFadesOut() { var glow = SpeakingGlow() XCTAssertEqual(glow.rim(at: 0), .off, "silence: nothing") let loud = level(db: -12) glow.update(level: loud, speaking: true, now: 0) XCTAssertFalse(glow.on(at: 0), "not before the onset") glow.update(level: loud, speaking: true, now: 0.2) XCTAssertTrue(glow.on(at: 0.2)) // Lit, the level rises over the attack from nothing. XCTAssertEqual(glow.rim(at: 0.2), WarmRim(on: true, level: 0)) XCTAssertEqual(glow.rim(at: 0.2 + SpeakingGlow.attack).level, 1 - exp(-1), accuracy: 1e-6) XCTAssertEqual(glow.rim(at: 5).level, 1, accuracy: 1e-6) // Silence: still on while the level falls over the release, and off // once it is under `outBelow` — not at once. glow.update(level: 0, speaking: false, now: 5) XCTAssertFalse(glow.lit) XCTAssertTrue(glow.on(at: 5 + SpeakingGlow.release), "fading") let out = 5 + SpeakingGlow.release * log(1 / SpeakingGlow.outBelow) XCTAssertTrue(glow.on(at: out - 0.01)) XCTAssertFalse(glow.on(at: out + 0.01)) let gone = glow.rim(at: out + 0.01) XCTAssertFalse(gone.on) XCTAssertEqual([gone.rimOpacity, gone.innerOpacity, gone.outerOpacity], [0, 0, 0], "nothing drawn") XCTAssertLessThan(out, glow.settles, "and the view is still redrawing when it goes off") } func testReduceMotionIsASteadyRimAtPointSix() { var glow = SpeakingGlow() XCTAssertEqual(glow.rim(at: 0, reduceMotion: true), .off) glow.update(level: level(db: -10), speaking: true, now: 0) glow.update(level: level(db: -10), speaking: true, now: 0.2) for (db, t) in [(-10, 0.3), (-35, 0.4), (-20, 0.5)] as [(Float, Double)] { glow.update(level: level(db: db), speaking: true, now: t) for dt in [0, 0.01, 0.1, 1.0] { XCTAssertEqual(glow.rim(at: t + dt, reduceMotion: true), WarmRim(on: true, level: 0.6)) } } XCTAssertEqual(SpeakingGlow.steady, 0.6) // Out at once, with no fade. glow.update(level: 0, speaking: false, now: 1) XCTAssertEqual(glow.rim(at: 1, reduceMotion: true), .off) } func testTheFixturesLevels() { XCTAssertEqual(SpeakingGlow.fixture(level: level(db: FixtureSpeaking.silent.decibels)).rim(at: 0), .off) let expected: [(FixtureSpeaking, Double)] = [(.low, 13.0 / 30), (.mid, 21.0 / 30), (.high, 1)] for (fixture, lvl) in expected { let rim = SpeakingGlow.fixture(level: level(db: fixture.decibels)).rim(at: 0) XCTAssertTrue(rim.on, "\(fixture)") XCTAssertEqual(rim.level, lvl, accuracy: 1e-5, "\(fixture)") } } }