| 1 | import XCTest |
| 2 | |
| 3 | @testable import CallsKit |
| 4 | |
| 5 | /// "Reconnecting…" only for a problem that lasts (spec §15.23). Paul, |
| 6 | /// 2026-09-30: "I saw 'Reconnecting' appear occasionally over the video, |
| 7 | /// but only for a second … I didn't notice any interruption." |
| 8 | final class ReconnectWatchTests: XCTestCase { |
| 9 | private typealias Peer = ReconnectWatch.Peer |
| 10 | private let fine = Peer() |
| 11 | |
| 12 | /// Runs the watch at 5 Hz from `from` to `to` seconds, and returns the |
| 13 | /// times at which the whole call's words were showing. |
| 14 | private func run( |
| 15 | _ watch: inout ReconnectWatch, from: Double, to: Double, |
| 16 | stream: (Double) -> Bool, people: (Double) -> [String: Peer] |
| 17 | ) -> (call: [Double], peers: [Double: Set<String>]) { |
| 18 | var call: [Double] = [] |
| 19 | var peers: [Double: Set<String>] = [:] |
| 20 | var t = from |
| 21 | while t < to { |
| 22 | watch.observe(at: t, streamDown: stream(t), people: people(t)) |
| 23 | if watch.call { call.append(t) } |
| 24 | if !watch.peers.isEmpty { peers[t] = watch.peers } |
| 25 | t = (t * 10 + 2).rounded() / 10 |
| 26 | } |
| 27 | return (call, peers) |
| 28 | } |
| 29 | |
| 30 | func testARoutineStreamRenewalIsNeverShown() { |
| 31 | // The stream ends on purpose and is back half a second later, over and |
| 32 | // over, and a slower resume of three and a half seconds as well. |
| 33 | var watch = ReconnectWatch() |
| 34 | let seen = run( |
| 35 | &watch, from: 0, to: 60, |
| 36 | stream: { t in (10..<10.6).contains(t) || (30..<33.5).contains(t) || (50..<50.4).contains(t) }, |
| 37 | people: { _ in ["sam": self.fine] }) |
| 38 | XCTAssertEqual(seen.call, []) |
| 39 | XCTAssertTrue(seen.peers.isEmpty) |
| 40 | } |
| 41 | |
| 42 | func testAStreamDownFourSecondsIsShownAndHeldASecondPastRecovery() { |
| 43 | var watch = ReconnectWatch() |
| 44 | let seen = run(&watch, from: 0, to: 20, stream: { (5..<12).contains($0) }, people: { _ in ["sam": self.fine] }) |
| 45 | XCTAssertEqual(seen.call.first, 9, "down at 5, said at 9") |
| 46 | XCTAssertEqual(seen.call.last, 12.8, "back at 12, said until a second after") |
| 47 | } |
| 48 | |
| 49 | func testShortBlipsOnOnePersonAreNeverShown() { |
| 50 | var watch = ReconnectWatch() |
| 51 | let seen = run( |
| 52 | &watch, from: 0, to: 30, stream: { _ in false }, |
| 53 | people: { t in |
| 54 | ["sam": Peer(connectionBad: (5..<7.5).contains(t), audioMissing: (15..<16.5).contains(t))] |
| 55 | }) |
| 56 | XCTAssertEqual(seen.call, []) |
| 57 | XCTAssertTrue(seen.peers.isEmpty, "\(seen.peers)") |
| 58 | } |
| 59 | |
| 60 | func testOnePersonsLastingTroubleIsSaidOnTheirTileAlone() { |
| 61 | var watch = ReconnectWatch() |
| 62 | let seen = run( |
| 63 | &watch, from: 0, to: 30, stream: { _ in false }, |
| 64 | people: { t in ["sam": Peer(connectionBad: (5..<15).contains(t)), "ana": self.fine] }) |
| 65 | XCTAssertEqual(seen.call, [], "not the whole call") |
| 66 | let times = seen.peers.keys.sorted() |
| 67 | XCTAssertEqual(times.first, 8, "disconnected at 5, said at 8") |
| 68 | XCTAssertEqual(times.last, 15.8, "held a second past recovery") |
| 69 | XCTAssertTrue(seen.peers.values.allSatisfy { $0 == ["sam"] }) |
| 70 | } |
| 71 | |
| 72 | func testAudioThatStopsFromSomeoneUnmutedIsSaidAfterTwoSeconds() { |
| 73 | var watch = ReconnectWatch() |
| 74 | let seen = run( |
| 75 | &watch, from: 0, to: 20, stream: { _ in false }, |
| 76 | people: { t in ["sam": Peer(audioMissing: (5..<10).contains(t)), "ana": self.fine] }) |
| 77 | XCTAssertEqual(seen.peers.keys.sorted().first, 7) |
| 78 | } |
| 79 | |
| 80 | func testEveryonesTroubleIsTheWholeCall() { |
| 81 | var watch = ReconnectWatch() |
| 82 | let seen = run( |
| 83 | &watch, from: 0, to: 20, stream: { _ in false }, |
| 84 | people: { t in |
| 85 | let bad = (5..<12).contains(t) |
| 86 | return ["sam": Peer(connectionBad: bad), "ana": Peer(connectionBad: bad)] |
| 87 | }) |
| 88 | XCTAssertEqual(seen.call.first, 8) |
| 89 | XCTAssertTrue(seen.peers.isEmpty, "not two tiles as well") |
| 90 | } |
| 91 | |
| 92 | func testTheWords() { |
| 93 | XCTAssertEqual(ReconnectWords.call, "Reconnecting…") |
| 94 | XCTAssertEqual(ReconnectWords.peer("Samuel"), "Reconnecting to Samuel…") |
| 95 | } |
| 96 | |
| 97 | func testTheNoticeFollowsTheWatchNotThePhase() { |
| 98 | let notices = CallNotices.notices( |
| 99 | phase: "admitted", status: "In the meeting", sharingScreen: false, screenNote: "", floor: false) |
| 100 | XCTAssertFalse(notices.contains { $0.kind == .reconnecting }) |
| 101 | } |
| 102 | } |
| 103 | |