oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2import XCTest
3
4@_spi(Fixtures) @testable import CallsKit
5
6/// The screen watchdogs (ScreenWatchdog.swift): the sharer's, which puts
7/// its own screen right, and the receiver's, which says when someone
8/// else's has stopped. Written for Paul's report of 2026-09-28.
9final class ScreenWatchdogTests: XCTestCase {
10 private func counts(_ beats: UInt64, _ delivered: UInt64) -> ScreenCaptureCounts {
11 ScreenCaptureCounts(heartbeats: beats, delivered: delivered)
12 }
13 private func sending(_ n: UInt64) -> [String: ScreenSendCounts] {
14 ["vicky": ScreenSendCounts(encoded: n, sent: n, keyframes: 1)]
15 }
16
17 func testAHealthyShareAsksForNothing() {
18 var dog = ScreenWatchdog()
19 for t in 0..<30 {
20 let n = UInt64(t * 30)
21 XCTAssertEqual(dog.observe(at: Double(t), capture: counts(n, n), sending: sending(n), stalledBy: []), [])
22 }
23 XCTAssertEqual(dog.status, "watching")
24 XCTAssertFalse(dog.recovering)
25 }
26
27 func testAStillScreenIsNotAStall() {
28 // ScreenCaptureKit reports a still screen as idle, and the capture
29 // repeats the last frame once a second: heartbeats keep coming and
30 // frames trickle, encoded as they go.
31 var dog = ScreenWatchdog()
32 for t in 0..<30 {
33 let actions = dog.observe(
34 at: Double(t), capture: counts(UInt64(t * 30), UInt64(t)), sending: sending(UInt64(t)), stalledBy: [])
35 XCTAssertEqual(actions, [])
36 }
37 }
38
39 func testACaptureThatStopsReportingIsRestartedWithAGrowingWait() {
40 var dog = ScreenWatchdog()
41 var restarts: [Int] = []
42 for t in 0..<40 {
43 let beats: UInt64 = t < 5 ? UInt64(t * 30) : 120
44 let actions = dog.observe(at: Double(t), capture: counts(beats, beats), sending: sending(beats), stalledBy: [])
45 if actions.contains(.restartCapture) { restarts.append(t) }
46 }
47 // Stalled from t=4 on: first restart three seconds later, then 5, 10, 20 s apart.
48 XCTAssertEqual(restarts, [7, 12, 22])
49 XCTAssertTrue(dog.captureStalled)
50 XCTAssertEqual(dog.captureRestarts, 3)
51 // And it comes back.
52 XCTAssertEqual(dog.observe(at: 40, capture: counts(500, 500), sending: sending(500), stalledBy: []), [])
53 XCTAssertFalse(dog.captureStalled)
54 }
55
56 func testACaptureThatCannotTellIdleFromStalledIsNotWatched() {
57 // A ReplayKit broadcast sends nothing for a still screen.
58 var dog = ScreenWatchdog()
59 for t in 0..<20 {
60 let actions = dog.observe(
61 at: Double(t), capture: ScreenCaptureCounts(heartbeats: nil, delivered: 7), sending: sending(7), stalledBy: [])
62 XCTAssertEqual(actions, [])
63 }
64 }
65
66 func testAnEncoderThatStopsGetsAKeyframeThenAFreshEncoderThenANewTransceiver() {
67 var dog = ScreenWatchdog()
68 var log: [(Int, ScreenRecovery)] = []
69 for t in 0..<47 {
70 let delivered = UInt64(t * 30)
71 let encoded: UInt64 = t < 5 ? delivered : 120
72 for a in dog.observe(at: Double(t), capture: counts(delivered, delivered), sending: sending(encoded), stalledBy: []) {
73 log.append((t, a))
74 }
75 }
76 XCTAssertEqual(log.map(\.1), [.keyframe("vicky"), .recreateEncoder("vicky"), .reAdd("vicky"), .keyframe("vicky")])
77 // Five seconds after it stopped (t=4), three seconds apart, then a
78 // rest of thirty seconds before starting over.
79 // (not before the connection's first ten seconds are up)
80 XCTAssertEqual(log.map(\.0), [10, 13, 16, 46])
81 XCTAssertEqual(dog.keyframes, 2)
82 XCTAssertEqual(dog.encoderRecoveries, 1)
83 XCTAssertEqual(dog.reAdds, 1)
84 }
85
86 func testAnEncoderStoppedForOnePersonIsMendedForThemAlone() {
87 // Each connection has an encoder of its own: the browser's (H.264 at
88 // the level Chromium answers) can refuse frames the app's (High 5.2)
89 // takes. The steps go to the stopped one only.
90 var dog = ScreenWatchdog()
91 var log: [(Int, ScreenRecovery)] = []
92 for t in 0..<20 {
93 let delivered = UInt64(t * 30)
94 let sending = [
95 "browser": ScreenSendCounts(encoded: t < 5 ? delivered : 120, sent: t < 5 ? delivered : 120, keyframes: 1),
96 "app": ScreenSendCounts(encoded: delivered, sent: delivered, keyframes: 1),
97 ]
98 for a in dog.observe(at: Double(t), capture: counts(delivered, delivered), sending: sending, stalledBy: []) {
99 log.append((t, a))
100 }
101 }
102 XCTAssertEqual(log.map(\.1), [.keyframe("browser"), .recreateEncoder("browser"), .reAdd("browser")])
103 }
104
105 func testAnEncoderThatRecoversAfterTheKeyframeIsLeftAlone() {
106 var dog = ScreenWatchdog()
107 var actions: [ScreenRecovery] = []
108 for t in 0..<30 {
109 let delivered = UInt64(t * 30)
110 let encoded: UInt64 = (5..<11).contains(t) ? 120 : delivered
111 actions += dog.observe(at: Double(t), capture: counts(delivered, delivered), sending: sending(encoded), stalledBy: [])
112 }
113 XCTAssertEqual(actions, [.keyframe("vicky")])
114 XCTAssertFalse(dog.recovering)
115 }
116
117 func testNothingEncodedInTheFirstTenSecondsIsAWarmUp() {
118 var dog = ScreenWatchdog()
119 for t in 0..<10 {
120 let delivered = UInt64(t * 30)
121 XCTAssertEqual(dog.observe(at: Double(t), capture: counts(delivered, delivered), sending: sending(1), stalledBy: []), [])
122 }
123 XCTAssertEqual(dog.observe(at: 10, capture: counts(300, 300), sending: sending(1), stalledBy: []), [.keyframe("vicky")])
124 }
125
126 func testALinkThatHoldsTheScreenBackIsNotAnEncoderStall() {
127 // The estimate collapses and WebRTC stops encoding for a while: the
128 // link's business, and a smaller encoder would only stay smaller.
129 var dog = ScreenWatchdog()
130 for t in 0..<20 {
131 let delivered = UInt64(t * 30)
132 let encoded: UInt64 = t < 5 ? delivered : 120
133 let counts = ["vicky": ScreenSendCounts(encoded: encoded, sent: encoded, keyframes: 1, bandwidthLimited: t >= 5)]
134 XCTAssertEqual(dog.observe(at: Double(t), capture: self.counts(delivered, delivered), sending: counts, stalledBy: []), [])
135 }
136 }
137
138 func testNothingGoingInIsNotAnEncoderStall() {
139 // No frames delivered (a broadcast of a still screen): nothing to encode.
140 var dog = ScreenWatchdog()
141 for t in 0..<20 {
142 let actions = dog.observe(
143 at: Double(t), capture: ScreenCaptureCounts(heartbeats: nil, delivered: 50), sending: sending(50), stalledBy: [])
144 XCTAssertEqual(actions, [])
145 }
146 }
147
148 func testAReceiversReportWhileTheScreenLeavesNormallyGetsKeyframesFirstAndNeverAShrink() {
149 var dog = ScreenWatchdog()
150 var log: [(Int, ScreenRecovery)] = []
151 for t in 0..<20 {
152 let n = UInt64(t * 30)
153 let stalledBy: Set<String> = t >= 3 ? ["vicky"] : []
154 for a in dog.observe(at: Double(t), capture: counts(n, n), sending: sending(n), stalledBy: stalledBy) {
155 log.append((t, a))
156 }
157 }
158 let keyframes = log.filter { $0.1 == .keyframe("vicky") }
159 XCTAssertEqual(keyframes.map(\.0), [3, 7, 11])
160 // What comes after the keyframes (spec §15.22) is a rebuild at the
161 // same size, never a smaller encoder.
162 XCTAssertFalse(log.contains { $0.1 == .recreateEncoder("vicky") })
163 XCTAssertEqual(log.first?.1, .keyframe("vicky"))
164 XCTAssertEqual(dog.stallReports, 1, "one report, however long it lasts")
165 XCTAssertTrue(dog.recovering)
166 }
167
168 func testAReceiversReportWhileTheScreenHasStoppedHereTooGoesFurther() {
169 var dog = ScreenWatchdog()
170 var actions: [ScreenRecovery] = []
171 for t in 0..<30 {
172 let delivered = UInt64(t * 30)
173 let encoded: UInt64 = t < 5 ? delivered : 120
174 actions += dog.observe(
175 at: Double(t), capture: counts(delivered, delivered), sending: sending(encoded),
176 stalledBy: t >= 6 ? ["vicky"] : [])
177 }
178 XCTAssertTrue(actions.contains(.recreateEncoder("vicky")))
179 }
180
181 func testAReportThatClearsEndsTheRecovery() {
182 var dog = ScreenWatchdog()
183 var actions: [ScreenRecovery] = []
184 for t in 0..<20 {
185 let n = UInt64(t * 30)
186 let stalledBy: Set<String> = (3..<5).contains(t) ? ["vicky"] : []
187 actions += dog.observe(at: Double(t), capture: counts(n, n), sending: sending(n), stalledBy: stalledBy)
188 }
189 XCTAssertEqual(actions, [.keyframe("vicky")])
190 XCTAssertFalse(dog.recovering)
191 XCTAssertTrue(dog.status.hasPrefix("watching, 1 recovery"))
192 }
193
194 func testAReAddedTransceiverStartingFromZeroIsNotAStall() {
195 var dog = ScreenWatchdog()
196 for t in 0..<10 {
197 let n = UInt64(t * 30)
198 XCTAssertEqual(dog.observe(at: Double(t), capture: counts(n, n), sending: sending(n), stalledBy: []), [])
199 }
200 // The connection's counters start again at zero on a new transceiver.
201 for t in 10..<20 {
202 let n = UInt64(t * 30)
203 let fresh = UInt64((t - 10) * 30)
204 XCTAssertEqual(dog.observe(at: Double(t), capture: counts(n, n), sending: sending(fresh), stalledBy: []), [])
205 }
206 }
207
208 // MARK: - A receiver that keeps reporting (spec §15.22)
209
210 private func escalating(
211 _ range: Range<Int>, stalledBy: (Int) -> Set<String>, sent: (Int) -> [String: ScreenSendCounts]? = { _ in nil },
212 dog: inout ScreenWatchdog
213 ) -> [(Int, ScreenRecovery)] {
214 var log: [(Int, ScreenRecovery)] = []
215 for t in range {
216 let n = UInt64(t * 30)
217 for a in dog.observe(at: Double(t), capture: counts(n, n), sending: sent(t) ?? sending(n), stalledBy: stalledBy(t)) {
218 log.append((t, a))
219 }
220 }
221 return log
222 }
223
224 func testAReportKeyframesDoNotAnswerIsRebuiltThenSharedAgainThenGivenUp() {
225 var dog = ScreenWatchdog()
226 let log = escalating(0..<45, stalledBy: { $0 >= 3 ? ["vicky"] : [] }, dog: &dog)
227 let steps = log.filter { if case .keyframe = $0.1 { return false } else { return true } }
228 // Reported from 3: keyframes at once; six seconds on, still reported,
229 // the connection is rebuilt; ten more, everyone's screen shared again;
230 // ten more, given up.
231 XCTAssertEqual(steps.map(\.1), [.rebuild("vicky"), .reshare])
232 XCTAssertEqual(steps.map(\.0), [9, 19])
233 XCTAssertEqual(dog.escalation(of: "vicky"), .failed)
234 XCTAssertEqual(dog.rebuilds, 1)
235 XCTAssertEqual(dog.reshares, 1)
236 XCTAssertEqual(dog.failures, 1)
237 // A report alone never shrinks the screen or recreates its encoder:
238 // Text and detail stays Text and detail.
239 XCTAssertFalse(log.contains { if case .recreateEncoder = $0.1 { return true } else { return false } })
240 XCTAssertEqual(dog.status, "a receiver's view could not be restored")
241 }
242
243 func testAFailureStaysUntilTheReportClears() {
244 var dog = ScreenWatchdog()
245 _ = escalating(0..<45, stalledBy: { $0 >= 3 ? ["vicky"] : [] }, dog: &dog)
246 let later = escalating(45..<120, stalledBy: { $0 < 100 ? ["vicky"] : [] }, dog: &dog)
247 XCTAssertFalse(later.contains { $0.1 == .reshare || $0.1 == .rebuild("vicky") }, "nothing more is tried")
248 XCTAssertEqual(dog.escalation(of: "vicky"), .none, "and a view that comes back is forgotten")
249 }
250
251 func testARebuildThatWorksEndsIt() {
252 var dog = ScreenWatchdog()
253 let log = escalating(0..<60, stalledBy: { (3..<12).contains($0) ? ["vicky"] : [] }, dog: &dog)
254 let steps = log.filter { if case .keyframe = $0.1 { return false } else { return true } }
255 XCTAssertEqual(steps.map(\.1), [.rebuild("vicky")])
256 XCTAssertEqual(dog.escalation(of: "vicky"), .none)
257 XCTAssertEqual(dog.reshares, 0)
258 }
259
260 func testRebuildsAndSharesAgainAreRationed() {
261 var dog = ScreenWatchdog()
262 // First stall: rebuilt at 9, and that works.
263 _ = escalating(0..<20, stalledBy: { (3..<12).contains($0) ? ["vicky"] : [] }, dog: &dog)
264 // A second, within the minute: no rebuild so soon, so straight to a
265 // share again; that works too.
266 let second = escalating(20..<50, stalledBy: { (25..<34).contains($0) ? ["vicky"] : [] }, dog: &dog)
267 XCTAssertEqual(second.filter { $0.1 == .reshare || $0.1 == .rebuild("vicky") }.map(\.1), [.reshare])
268 // A third, past the minute but inside five: rebuilt, and then there is
269 // no share again to be had, so it is given up.
270 let third = escalating(80..<120, stalledBy: { $0 >= 85 ? ["vicky"] : [] }, dog: &dog)
271 XCTAssertEqual(third.filter { $0.1 == .reshare || $0.1 == .rebuild("vicky") }.map(\.1), [.rebuild("vicky")])
272 XCTAssertEqual(dog.escalation(of: "vicky"), .failed)
273 }
274
275 func testNothingSentIsNothingToEscalate() {
276 // A still screen with nothing new to send, or a link holding it all
277 // back: the receiver cannot be failing to decode frames that never
278 // left, so keyframes are all it gets.
279 var dog = ScreenWatchdog()
280 var log: [(Int, ScreenRecovery)] = []
281 for t in 0..<40 {
282 // Reporting all along (idle reports), nothing new delivered or sent
283 // after t=3.
284 let n = UInt64(min(t, 3) * 30)
285 let still = ["vicky": ScreenSendCounts(encoded: n, sent: n, keyframes: 1)]
286 for a in dog.observe(
287 at: Double(t), capture: counts(UInt64(t * 30), n), sending: still, stalledBy: t >= 3 ? ["vicky"] : [])
288 {
289 log.append((t, a))
290 }
291 }
292 XCTAssertFalse(log.isEmpty)
293 XCTAssertTrue(log.allSatisfy { if case .keyframe = $0.1 { return true } else { return false } }, "\(log)")
294 XCTAssertEqual(dog.escalation(of: "vicky"), .keyframes(since: 3))
295 }
296
297 func testARebuildThatNeverComesBackStillEndsInWords() {
298 // The rebuilt connection never negotiates a screen again: there is
299 // nothing to count, and the steps go on regardless.
300 var dog = ScreenWatchdog()
301 let log = escalating(
302 0..<45, stalledBy: { $0 >= 3 ? ["vicky"] : [] }, sent: { $0 > 9 ? [:] : nil }, dog: &dog)
303 XCTAssertTrue(log.contains { $0.1 == .reshare })
304 XCTAssertEqual(dog.escalation(of: "vicky"), .failed)
305 }
306
307 func testOneShareAgainAnswersEveryReport() {
308 var dog = ScreenWatchdog()
309 var log: [(Int, ScreenRecovery)] = []
310 for t in 0..<30 {
311 let n = UInt64(t * 30)
312 let both = ["vicky": ScreenSendCounts(encoded: n, sent: n, keyframes: 1), "tony": ScreenSendCounts(encoded: n, sent: n, keyframes: 1)]
313 let stalled: Set<String> = t >= 3 ? ["vicky", "tony"] : []
314 for a in dog.observe(at: Double(t), capture: counts(n, n), sending: both, stalledBy: stalled) { log.append((t, a)) }
315 }
316 XCTAssertEqual(log.filter { $0.1 == .reshare }.count, 1)
317 XCTAssertEqual(log.filter { if case .rebuild = $0.1 { return true } else { return false } }.count, 2)
318 }
319
320 func testTheNoticeMovesOnInWords() {
321 XCTAssertEqual(ScreenWords.notice(recovering: [], restarting: [], failed: []), "")
322 XCTAssertEqual(
323 ScreenWords.notice(recovering: ["Samuel"], restarting: [], failed: []),
324 "Samuel\u{2019}s view of your screen stalled \u{2014} recovering")
325 XCTAssertEqual(
326 ScreenWords.notice(recovering: [], restarting: ["Samuel"], failed: []), "Restarting your share for Samuel\u{2026}")
327 XCTAssertEqual(
328 ScreenWords.notice(recovering: [], restarting: [], failed: ["Samuel"]),
329 "Couldn\u{2019}t restore Samuel\u{2019}s view \u{2014} try Stop and Share again")
330 XCTAssertEqual(
331 ScreenWords.notice(recovering: ["Tony"], restarting: [], failed: ["Samuel"]),
332 "Couldn\u{2019}t restore Samuel\u{2019}s view \u{2014} try Stop and Share again. Tony\u{2019}s view of your screen stalled \u{2014} recovering")
333 XCTAssertEqual(
334 ScreenWords.notice(recovering: [], restarting: ["A", "B", "C"], failed: []), "Restarting your share for 3 people\u{2026}")
335 }
336
337 func testTheReceiversNumbersNameTheStall() {
338 var before = ScreenReceiveReport()
339 before.mid = "3"
340 before.packetsReceived = 1000
341 before.framesReceived = 300
342 before.framesDecoded = 300
343 var wedged = before
344 wedged.packetsReceived = 1400
345 wedged.framesReceived = 420
346 XCTAssertEqual(wedged.diagnosis(since: before), "frames arriving, none decoded (the decoder)")
347 XCTAssertTrue(wedged.moved(since: before).hasPrefix("packets +400, frames received +120, decoded +0"))
348 XCTAssertEqual(before.diagnosis(since: before), "no packets arriving (the sender or the path)")
349 var rebuilt = wedged
350 rebuilt.mid = "5"
351 XCTAssertEqual(rebuilt.diagnosis(since: before), "no earlier reading")
352 }
353
354 // MARK: - The receiver
355
356 func testAScreenThatStopsMovingIsReportedAfterFourSecondsAndForThreeMinutesAtMost() {
357 var watch = ScreenReceiveWatch()
358 var reported: [Int] = []
359 for t in 0..<200 {
360 let decoded: UInt64 = t < 10 ? UInt64(t * 10) : 90
361 if !watch.observe(at: Double(t), decoded: ["paul": decoded]).isEmpty { reported.append(t) }
362 }
363 XCTAssertEqual(reported.first, 13, "last moved at 9: stalled from 13")
364 XCTAssertEqual(reported.last, 188, "and no longer than three minutes after it last moved")
365 }
366
367 func testARebuiltScreenStartingFromZeroIsMovingNotStalled() {
368 // 2026-09-30: a screen the sharer rebuilt arrives on a new receiver
369 // whose count starts again. Up to 0.4.4 only a larger count was
370 // movement, so a rebuilt screen that was moving again stayed stalled
371 // here — and the sharer's notice with it — until the new count passed
372 // the old.
373 var watch = ScreenReceiveWatch()
374 for t in 0..<10 { _ = watch.observe(at: Double(t), decoded: ["paul": 900 + UInt64(t * 30)]) }
375 for t in 10..<16 { _ = watch.observe(at: Double(t), decoded: ["paul": 1170]) }
376 XCTAssertEqual(watch.observe(at: 16, decoded: ["paul": 1170]), ["paul"])
377 for t in 17..<40 {
378 XCTAssertEqual(watch.observe(at: Double(t), decoded: ["paul": UInt64((t - 17) * 30)]), [], "t=\(t)")
379 }
380 }
381
382 func testAStalledPictureIsAttachedAgainOnceFourSecondsIn() {
383 var watch = ScreenReceiveWatch()
384 var reattached: [Int] = []
385 for t in 0..<60 {
386 let decoded: UInt64 = t < 10 ? UInt64(t * 10) : 90
387 _ = watch.observe(at: Double(t), decoded: ["paul": decoded])
388 if watch.reattach == ["paul"] { reattached.append(t) }
389 }
390 XCTAssertEqual(reattached, [17], "stalled from 13, attached again four seconds later, once")
391 // Moving again and stalled again: a new stall, a new try.
392 for t in 60..<63 { _ = watch.observe(at: Double(t), decoded: ["paul": UInt64(t)]) }
393 var again: [Int] = []
394 for t in 63..<80 {
395 _ = watch.observe(at: Double(t), decoded: ["paul": 62])
396 if !watch.reattach.isEmpty { again.append(t) }
397 }
398 XCTAssertEqual(again, [70])
399 }
400
401 func testAMovingScreenIsNeverReported() {
402 var watch = ScreenReceiveWatch()
403 for t in 0..<30 {
404 XCTAssertEqual(watch.observe(at: Double(t), decoded: ["paul": UInt64(t)]), [])
405 }
406 }
407
408 func testAScreenThatGoesIsForgotten() {
409 var watch = ScreenReceiveWatch()
410 for t in 0..<10 { _ = watch.observe(at: Double(t), decoded: ["paul": 5]) }
411 XCTAssertEqual(watch.observe(at: 10, decoded: ["paul": 5]), ["paul"])
412 XCTAssertEqual(watch.observe(at: 11, decoded: [:]), [])
413 // Back again, a fresh start.
414 XCTAssertEqual(watch.observe(at: 12, decoded: ["paul": 5]), [])
415 }
416
417 // MARK: - Words and wires
418
419 func testTheSharersNotice() {
420 XCTAssertEqual(ScreenWords.recovering(names: []), "")
421 XCTAssertEqual(ScreenWords.recovering(names: ["Vicky"]), "Vicky\u{2019}s view of your screen stalled \u{2014} recovering")
422 XCTAssertEqual(
423 ScreenWords.recovering(names: ["Tony", "Vicky"]),
424 "Tony\u{2019}s and Vicky\u{2019}s views of your screen stalled \u{2014} recovering")
425 XCTAssertEqual(ScreenWords.recovering(names: ["A", "B", "C"]), "3 people\u{2019}s views of your screen stalled \u{2014} recovering")
426 }
427
428 func testTheStalledListIsReadFromTheRoster() throws {
429 let json = #"{"muted":false,"cameraOff":false,"screenStalled":["0123456789abcdef0123456789abcdef"]}"#
430 let media = try JSONDecoder().decode(PeerMedia.self, from: Data(json.utf8))
431 XCTAssertEqual(media.screenStalled, ["0123456789abcdef0123456789abcdef"])
432 let plain = try JSONDecoder().decode(PeerMedia.self, from: Data(#"{"muted":true,"cameraOff":false}"#.utf8))
433 XCTAssertNil(plain.screenStalled)
434 }
435
436 func testTheDetailLineSaysWhatTheEncoderAndTheWatchdogAreDoing() {
437 var before = ScreenSendReport()
438 before.timestamp = 10
439 before.framesEncoded = 100
440 before.framesSent = 100
441 var now = before
442 now.timestamp = 12
443 now.framesEncoded = 160
444 now.framesSent = 158
445 now.keyFramesEncoded = 3
446 now.keyframeRequests = 2
447 XCTAssertEqual(
448 now.detail(since: before, watchdog: "watching"),
449 "Encoded 30 fps, sent 29 fps, keyframes 3 (2 asked for); watchdog: watching")
450 }
451
452 // MARK: - ICE renewal (§5.3)
453
454 private func ice(_ renewAt: Int64, servers: Int = 1) -> ICEConfig {
455 ICEConfig(
456 iceServers: (0..<servers).map { _ in .init(urls: ["turn:relay"], username: "u", credential: "c") },
457 renewAt: renewAt, expiresAt: renewAt + 1800)
458 }
459
460 func testAFreshListRestartsOnlyWhenARenewalIsDue() {
461 // Every resume brings a fresh list, five minutes apart; 0.4.1
462 // restarted ICE on every one of them.
463 let built = ice(10_000)
464 XCTAssertFalse(ICERenewal.restart(gathered: built, next: ice(10_300), now: 1_300))
465 XCTAssertFalse(ICERenewal.restart(gathered: built, next: ice(10_600), now: 9_999))
466 XCTAssertTrue(ICERenewal.restart(gathered: built, next: ice(19_000), now: 10_000))
467 XCTAssertTrue(ICERenewal.restart(gathered: built, next: ice(19_000), now: 10_290))
468 }
469
470 func testTheFloorHasNothingToRenewAndARelayArrivingIsAdopted() {
471 XCTAssertFalse(ICERenewal.restart(gathered: ice(0, servers: 0), next: ice(0, servers: 0), now: 5))
472 XCTAssertTrue(ICERenewal.restart(gathered: ice(0, servers: 0), next: ice(10_000), now: 5))
473 XCTAssertFalse(ICERenewal.restart(gathered: ice(10_000), next: ice(0, servers: 0), now: 20_000))
474 }
475}
476
477/// The sharing notice's own Stop sharing button (Paul, 2026-09-28: "I
478/// would expect this notification to have a 'Stop sharing' button
479/// inline"), and the recovery notice stacked under it.
480@MainActor final class SharingNoticeTests: XCTestCase {
481 func testTheSharingNoticeCarriesStopSharingAndTheRecoveryNoticeSitsUnderIt() {
482 let notices = CallNotices.notices(
483 phase: "admitted", status: "In the meeting", sharingScreen: true, screenNote: "", floor: false,
484 screenRecovery: ScreenWords.recovering(names: ["Vicky"]))
485 XCTAssertEqual(notices.map(\.kind), [.sharing, .screenRecovering])
486 XCTAssertEqual(notices[0].text, "You are sharing your screen")
487 XCTAssertEqual(notices[0].action, .stopSharing)
488 XCTAssertNil(notices[1].action, "the recovery notice has no button of its own")
489 XCTAssertTrue(notices.allSatisfy(\.important), "both stay while the controls fade")
490 XCTAssertFalse(notices.contains(where: \.dismissible))
491 XCTAssertNotEqual(notices[0].id, notices[1].id, "two rows, never one over the other")
492 }
493
494 func testNotSharingHasNoStopButton() {
495 let notices = CallNotices.notices(
496 phase: "admitted", status: "In the meeting", sharingScreen: false, screenNote: "", floor: false,
497 screenRecovery: "ignored while not sharing")
498 XCTAssertTrue(notices.allSatisfy { $0.action == nil })
499 XCTAssertFalse(notices.contains { $0.kind == .screenRecovering })
500 }
501
502 func testTheButtonStopsTheShare() {
503 let call = Call(playAudio: false)
504 call.showFixtureSelf(muted: false, cameraOff: false, sharingScreen: true)
505 XCTAssertTrue(call.sharingScreen)
506 call.perform(.stopSharing)
507 XCTAssertFalse(call.sharingScreen)
508 }
509}
510