oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2
3@testable @_spi(Fixtures) import CallsKit
4
5/// 0.4.0: the viewer's order, the active speaker, Speaker view's stage and
6/// filmstrip, and Focus becoming Speaker.
7final class ParticipantOrderTests: XCTestCase {
8 func testNewArrivalsGoOnTheEndAndLeaversAreRemoved() {
9 var order = ParticipantOrder()
10 order.merge(present: ["self", "name:Ana"])
11 XCTAssertEqual(order.shown, ["self", "name:Ana"])
12 order.merge(present: ["self", "name:Ana", "name:Tom"])
13 XCTAssertEqual(order.shown, ["self", "name:Ana", "name:Tom"])
14 order.move("name:Tom", to: 0)
15 order.merge(present: ["name:Li", "self", "name:Tom", "name:Ana"])
16 XCTAssertEqual(order.shown, ["name:Tom", "self", "name:Ana", "name:Li"], "roster order does not undo the viewer's")
17 order.merge(present: ["self", "name:Tom", "name:Li"])
18 XCTAssertEqual(order.shown, ["name:Tom", "self", "name:Li"])
19 XCTAssertFalse(order.keys.contains("name:Ana"), "a leaver is gone, not remembered")
20 order.merge(present: ["self", "name:Tom", "name:Li", "name:Ana"])
21 XCTAssertEqual(order.shown.last, "name:Ana", "and comes back at the end")
22 }
23
24 func testMovingTo() {
25 var order = ParticipantOrder()
26 order.merge(present: ["a", "b", "c", "d"])
27 XCTAssertTrue(order.move("d", to: 1))
28 XCTAssertEqual(order.shown, ["a", "d", "b", "c"])
29 XCTAssertTrue(order.move("a", to: 99))
30 XCTAssertEqual(order.shown, ["d", "b", "c", "a"], "past the end is last")
31 XCTAssertTrue(order.move("c", to: -3))
32 XCTAssertEqual(order.shown, ["c", "d", "b", "a"], "before the start is first")
33 XCTAssertFalse(order.move("c", to: 0), "already there")
34 XCTAssertFalse(order.move("zz", to: 0), "not here")
35 }
36
37 func testEarlierLaterFirst() {
38 var order = ParticipantOrder()
39 order.merge(present: ["a", "b", "c"])
40 XCTAssertFalse(order.moveEarlier("a"))
41 XCTAssertFalse(order.moveLater("c"))
42 XCTAssertTrue(order.moveLater("a"))
43 XCTAssertEqual(order.shown, ["b", "a", "c"])
44 XCTAssertTrue(order.moveEarlier("c"))
45 XCTAssertEqual(order.shown, ["b", "c", "a"])
46 XCTAssertTrue(order.moveToFirst("a"))
47 XCTAssertEqual(order.shown, ["a", "b", "c"])
48 XCTAssertFalse(order.moveToFirst("a"))
49 XCTAssertEqual(order.canMove("a").earlier, false)
50 XCTAssertEqual(order.canMove("a").later, true)
51 XCTAssertEqual(order.canMove("c").later, false)
52 }
53
54 /// Put back for a rejoin: the people remembered keep their places as
55 /// they arrive, one by one, and nobody is lost for not being in the
56 /// first roster.
57 func testARestoredOrderKeepsItsPlacesAsPeopleArrive() throws {
58 var kept = ParticipantOrder()
59 kept.merge(present: ["self", "name:Ana", "name:Tom", "name:Li"])
60 kept.move("name:Li", to: 0)
61 kept.move("self", to: 3)
62 let data = try JSONEncoder().encode(kept)
63 var order = try JSONDecoder().decode(ParticipantOrder.self, from: data)
64 XCTAssertEqual(order.shown, [], "nobody is here yet")
65 order.merge(present: ["self"])
66 XCTAssertEqual(order.shown, ["self"])
67 order.merge(present: ["self", "name:Tom"])
68 order.merge(present: ["self", "name:Tom", "name:Li", "name:Ana"])
69 XCTAssertEqual(order.shown, ["name:Li", "name:Ana", "name:Tom", "self"])
70 order.merge(present: ["self", "name:Tom", "name:Li", "name:Ana", "name:Sam"])
71 XCTAssertEqual(order.shown.last, "name:Sam")
72 }
73
74 /// A move among the people here leaves remembered absentees where they
75 /// were.
76 func testMovingSkipsTheAbsent() {
77 var order = ParticipantOrder(keys: ["a", "gone", "b", "c"])
78 order.merge(present: ["a", "b", "c"])
79 // "gone" was never seen, so it is remembered, not a leaver.
80 XCTAssertEqual(order.keys, ["a", "gone", "b", "c"])
81 order.move("c", to: 0)
82 XCTAssertEqual(order.shown, ["c", "a", "b"])
83 XCTAssertEqual(order.keys, ["c", "gone", "a", "b"])
84 }
85
86 func testThePersistenceKey() {
87 XCTAssertEqual(
88 ParticipantOrder.storageKey(meeting: "https://calls.oficina.works/abc"),
89 "calls.order.https://calls.oficina.works/abc")
90 }
91
92 @MainActor func testOrdersAreKeptPerMeetingForTheSession() {
93 ParticipantOrders.forgetAll()
94 defer { ParticipantOrders.forgetAll() }
95 var order = ParticipantOrder()
96 order.merge(present: ["self", "name:Ana"])
97 order.move("name:Ana", to: 0)
98 ParticipantOrders.save(order, meeting: "s/one")
99 XCTAssertEqual(ParticipantOrders.load(meeting: "s/one").keys, ["name:Ana", "self"])
100 XCTAssertEqual(ParticipantOrders.load(meeting: "s/two").keys, [])
101 ParticipantOrders.save(order, meeting: "")
102 XCTAssertEqual(ParticipantOrders.load(meeting: "").keys, [], "a call with no meeting keeps nothing")
103 }
104
105 func testKeysAreNamesSoARejoinKeepsThem() {
106 let keys = ParticipantKeys.keys(
107 roster: [("p1", "Paul"), ("p2", "Ana"), ("p3", "Ana"), ("p4", "Tom")], me: "p1")
108 XCTAssertEqual(keys.map(\.key), ["self", "name:Ana", "name:Ana#2", "name:Tom"])
109 let again = ParticipantKeys.keys(roster: [("q9", "Ana"), ("q1", "Paul")], me: "q1")
110 XCTAssertEqual(again.map(\.key), ["name:Ana", "self"], "new peer ids, the same keys")
111 }
112}
113
114final class ActiveSpeakerTests: XCTestCase {
115 typealias Voice = ActiveSpeaker.Voice
116 let everyone: Set<String> = ["ana", "tom", "li"]
117
118 /// Feeds readings at 5 Hz, as the app takes them, from `from` to `to`
119 /// seconds, with `speaking` speaking at their levels.
120 private func run(
121 _ rule: inout ActiveSpeaker, from: Double, to: Double, _ speaking: [String: Float],
122 present: Set<String>? = nil, me: String? = "me"
123 ) {
124 var step = 0
125 while from + Double(step) * 0.2 < to - 1e-9 {
126 let t = from + Double(step) * 0.2
127 step += 1
128 let voices = (present ?? everyone).union(me.map { [$0] } ?? []).sorted().map { peer in
129 Voice(peer: peer, speaking: speaking[peer] != nil, level: speaking[peer] ?? 0.001)
130 }
131 rule.update(voices, present: present ?? everyone, me: me, now: t)
132 }
133 }
134
135 func testTheThresholdsAreInTheRangeAsked() {
136 XCTAssertGreaterThanOrEqual(ActiveSpeaker.dominance, 1.2)
137 XCTAssertLessThanOrEqual(ActiveSpeaker.dominance, 1.5)
138 XCTAssertGreaterThanOrEqual(ActiveSpeaker.minimumHold, 3)
139 }
140
141 func testTheStageGoesToSomeoneOnlyAfterTheDominanceDelay() {
142 var rule = ActiveSpeaker()
143 run(&rule, from: 0, to: 1.2, ["ana": 0.2])
144 XCTAssertNil(rule.current, "1.2 s is not yet a turn")
145 run(&rule, from: 1.2, to: 1.5, ["ana": 0.2])
146 XCTAssertEqual(rule.current, "ana")
147 XCTAssertEqual(rule.since, 1.4, accuracy: 0.001)
148 }
149
150 func testTheLoudestSpeakerIsTheDominantOne() {
151 var rule = ActiveSpeaker()
152 run(&rule, from: 0, to: 2, ["ana": 0.05, "tom": 0.3])
153 XCTAssertEqual(rule.current, "tom")
154 }
155
156 func testAnInterjectionDoesNotTakeTheStage() {
157 var rule = ActiveSpeaker()
158 run(&rule, from: 0, to: 5, ["ana": 0.2])
159 XCTAssertEqual(rule.current, "ana")
160 // Tom: "right, yes" for a second, louder than Ana.
161 run(&rule, from: 5, to: 6, ["ana": 0.1, "tom": 0.4])
162 run(&rule, from: 6, to: 10, ["ana": 0.2])
163 XCTAssertEqual(rule.current, "ana")
164 }
165
166 func testPausesHoldTheCurrentSpeaker() {
167 var rule = ActiveSpeaker()
168 run(&rule, from: 0, to: 2, ["tom": 0.2])
169 XCTAssertEqual(rule.current, "tom")
170 run(&rule, from: 2, to: 30, [:])
171 XCTAssertEqual(rule.current, "tom", "silence changes nothing")
172 }
173
174 func testTheMinimumHoldHoldsThroughATakeover() {
175 var rule = ActiveSpeaker()
176 run(&rule, from: 0, to: 1.5, ["ana": 0.2])
177 XCTAssertEqual(rule.current, "ana")
178 let took = rule.since
179 // Tom starts at once and keeps going: dominant long enough after
180 // 1.3 s, but Ana has not had her 3 s yet.
181 run(&rule, from: 1.6, to: took + 2.9, ["tom": 0.3])
182 XCTAssertEqual(rule.current, "ana")
183 run(&rule, from: took + 3, to: took + 3.1, ["tom": 0.3])
184 XCTAssertEqual(rule.current, "tom")
185 }
186
187 func testTwoPeopleTalkingOverEachOtherDoNotFlap() {
188 var rule = ActiveSpeaker()
189 var switches = 0
190 var last: String?
191 var t = 0.0
192 // Alternating every 0.8 s for a minute: each is loudest for less than
193 // the dominance delay.
194 while t < 60 {
195 let who = Int(t / 0.8) % 2 == 0 ? "ana" : "tom"
196 let other = who == "ana" ? "tom" : "ana"
197 rule.update(
198 [Voice(peer: who, speaking: true, level: 0.3), Voice(peer: other, speaking: true, level: 0.1)],
199 present: everyone, me: "me", now: t)
200 if rule.current != last {
201 switches += 1
202 last = rule.current
203 }
204 t += 0.2
205 }
206 XCTAssertEqual(switches, 0, "nobody held the floor for 1.3 s")
207 }
208
209 func testNeverYou() {
210 var rule = ActiveSpeaker()
211 let present: Set<String> = ["ana", "me"]
212 for step in 0..<50 {
213 rule.update(
214 [Voice(peer: "me", speaking: true, level: 0.9), Voice(peer: "ana", speaking: false, level: 0)],
215 present: present, me: "me", now: Double(step) * 0.2)
216 }
217 XCTAssertNil(rule.current, "your own voice never puts you on the stage")
218 }
219
220 func testALeaverLeavesTheStageAtOnce() {
221 var rule = ActiveSpeaker()
222 run(&rule, from: 0, to: 2, ["li": 0.2])
223 XCTAssertEqual(rule.current, "li")
224 XCTAssertTrue(rule.update([], present: ["ana", "tom"], me: "me", now: 2.2))
225 XCTAssertNil(rule.current)
226 // And the next speaker needs only the dominance delay, not a hold.
227 run(&rule, from: 2.4, to: 3.9, ["ana": 0.2], present: ["ana", "tom"])
228 XCTAssertEqual(rule.current, "ana")
229 }
230}
231
232final class SpeakerStageTests: XCTestCase {
233 func testPinBeatsScreenBeatsSpeakerBeatsOrder() {
234 let others = ["ana", "tom", "li"]
235 XCTAssertEqual(Stage.speaker(pinned: nil, active: nil, others: others, screens: []), .camera("ana"))
236 XCTAssertEqual(Stage.speaker(pinned: nil, active: "li", others: others, screens: []), .camera("li"))
237 XCTAssertEqual(Stage.speaker(pinned: nil, active: "li", others: others, screens: ["tom"]), .screen("tom"))
238 XCTAssertEqual(
239 Stage.speaker(pinned: .camera("ana"), active: "li", others: others, screens: ["tom"]), .camera("ana"),
240 "a new screen does not take a pinned stage")
241 XCTAssertEqual(
242 Stage.speaker(pinned: .camera("me"), active: "li", others: others, screens: [], me: "me"), .camera("me"),
243 "you only by your own pin")
244 }
245
246 func testTheSpeakerWhoLeftFallsBackToTheOrder() {
247 XCTAssertEqual(Stage.speaker(pinned: nil, active: "gone", others: ["tom", "ana"], screens: []), .camera("tom"))
248 }
249
250 func testTheFilmstripIsEveryoneElseInTheViewersOrder() {
251 let order = ["tom", "me", "ana", "li"]
252 XCTAssertEqual(
253 Stage.filmstrip(stage: .camera("ana"), order: order, screens: []),
254 [.camera("tom"), .camera("me"), .camera("li")])
255 // A screen on the stage: the sharer's camera is in the filmstrip.
256 XCTAssertEqual(
257 Stage.filmstrip(stage: .screen("li"), order: order, screens: ["li"]),
258 [.camera("tom"), .camera("me"), .camera("ana"), .camera("li")])
259 // Two screens: the other one leads the filmstrip.
260 XCTAssertEqual(
261 Stage.filmstrip(stage: .screen("li"), order: order, screens: ["ana", "li"]).first, .screen("ana"))
262 }
263
264 func testYouFloatOnlyWhenTheFilmstripWouldBeYouAlone() {
265 XCTAssertTrue(Stage.selfFloats(filmstrip: [.camera("me")], me: "me"), "a call of two")
266 XCTAssertTrue(Stage.selfFloats(filmstrip: [], me: "me"))
267 XCTAssertFalse(Stage.selfFloats(filmstrip: [.camera("me"), .camera("tom")], me: "me"))
268 XCTAssertFalse(Stage.selfFloats(filmstrip: [.camera("me"), .screen("tom")], me: "me"))
269 }
270
271 func testStageLabels() {
272 XCTAssertEqual(stageLabel("Tony", onStage: true, pinned: false), "On stage: Tony")
273 XCTAssertEqual(stageLabel("Tony, muted", onStage: true, pinned: true), "On stage: Tony, muted, pinned")
274 XCTAssertEqual(stageLabel("Tony", onStage: false, pinned: true), "Tony")
275 }
276}
277
278final class VideoLayoutTests: XCTestCase {
279 private var defaults: UserDefaults!
280 private let suite = "calls.tests.videoLayout"
281
282 override func setUp() {
283 defaults = UserDefaults(suiteName: suite)
284 defaults.removePersistentDomain(forName: suite)
285 }
286 override func tearDown() { defaults.removePersistentDomain(forName: suite) }
287
288 func testFocusBecomesSpeaker() {
289 XCTAssertEqual(VideoLayout.stored("focus"), .speaker)
290 XCTAssertEqual(VideoLayout.stored("grid"), .grid)
291 XCTAssertEqual(VideoLayout.stored("speaker"), .speaker)
292 XCTAssertEqual(VideoLayout.stored(nil), .speaker)
293 XCTAssertEqual(VideoLayout.stored("mosaic"), .speaker)
294 }
295
296 func testASavedFocusIsRewrittenOnce() {
297 defaults.set("focus", forKey: VideoLayout.key)
298 XCTAssertEqual(VideoLayout.load(defaults), .speaker)
299 XCTAssertEqual(defaults.string(forKey: VideoLayout.key), "speaker")
300 defaults.set("grid", forKey: VideoLayout.key)
301 XCTAssertEqual(VideoLayout.load(defaults), .grid)
302 defaults.removeObject(forKey: VideoLayout.key)
303 XCTAssertEqual(VideoLayout.load(defaults), .speaker)
304 XCTAssertNil(defaults.string(forKey: VideoLayout.key), "a default is not written down")
305 }
306
307 @MainActor func testChoosingIsKeptAndPinningChoosesSpeaker() {
308 let arrangement = CallArrangement(defaults: defaults)
309 XCTAssertEqual(arrangement.layout, .speaker)
310 arrangement.choose(.grid)
311 XCTAssertEqual(defaults.string(forKey: VideoLayout.key), "grid")
312 XCTAssertEqual(CallArrangement(defaults: defaults).layout, .grid, "the next call starts in Grid")
313 arrangement.pin(.camera("ana"))
314 XCTAssertEqual(arrangement.layout, .speaker)
315 arrangement.show(.grid)
316 XCTAssertEqual(defaults.string(forKey: VideoLayout.key), "speaker", "a fixture's layout is not kept")
317 }
318}
319
320/// The arrangement over a real roster: peer ids in, stable keys kept.
321@MainActor final class CallArrangementTests: XCTestCase {
322 private let suite = "calls.tests.arrangement"
323
324 private func roster(_ phase: String, me: String, _ people: [(String, String)]) -> RoomState {
325 let peers = people.map { #"{"peer":"\#($0.0)","name":"\#($0.1)","kind":"member"}"# }
326 let json = #"{"phase":"\#(phase)","peer":"\#(me)","boot":"t","peers":[\#(peers.joined(separator: ","))],"waiting":[]}"#
327 return try! JSONDecoder().decode(RoomState.self, from: Data(json.utf8))
328 }
329
330 func testARejoinPutsTheOrderBack() {
331 ParticipantOrders.forgetAll()
332 defer { ParticipantOrders.forgetAll() }
333 let defaults = UserDefaults(suiteName: suite)!
334 let arrangement = CallArrangement(defaults: defaults)
335 arrangement.begin(meeting: "https://x/m1")
336 arrangement.roster(roster("admitted", me: "p1", [("p1", "Paul"), ("p2", "Ana"), ("p3", "Tom")]))
337 XCTAssertEqual(arrangement.people, ["p1", "p2", "p3"])
338 XCTAssertTrue(arrangement.move("p3", to: 0))
339 XCTAssertTrue(arrangement.moveLater("p1"))
340 XCTAssertEqual(arrangement.people, ["p3", "p2", "p1"])
341 XCTAssertEqual(arrangement.others, ["p3", "p2"])
342 // Leaving: the roster goes, the order is kept.
343 arrangement.roster(nil)
344 // Rejoining: new peer ids, and the roster arriving in pieces.
345 let again = CallArrangement(defaults: defaults)
346 again.begin(meeting: "https://x/m1")
347 again.roster(roster("admitted", me: "q1", [("q1", "Paul")]))
348 again.roster(roster("admitted", me: "q1", [("q1", "Paul"), ("q7", "Ana"), ("q8", "Tom")]))
349 XCTAssertEqual(again.people, ["q8", "q7", "q1"])
350 // A roster while reconnecting says nothing about who left.
351 again.roster(roster("reconnecting", me: "q1", [("q1", "Paul")]))
352 XCTAssertEqual(again.people, ["q8", "q7", "q1"])
353 defaults.removePersistentDomain(forName: suite)
354 }
355
356 func testThePinAndTheStage() {
357 let defaults = UserDefaults(suiteName: suite)!
358 defer { defaults.removePersistentDomain(forName: suite) }
359 let arrangement = CallArrangement(defaults: defaults)
360 arrangement.roster(roster("admitted", me: "p1", [("p1", "Paul"), ("p2", "Ana"), ("p3", "Tom")]))
361 XCTAssertEqual(arrangement.stage(active: "p3", screens: []), .camera("p3"))
362 arrangement.pin(.camera("p2"))
363 XCTAssertEqual(arrangement.stage(active: "p3", screens: ["p3"]), .camera("p2"))
364 arrangement.togglePin(.camera("p2"))
365 XCTAssertNil(arrangement.pinned)
366 XCTAssertEqual(arrangement.stage(active: "p3", screens: ["p3"]), .screen("p3"))
367 XCTAssertEqual(
368 arrangement.filmstrip(stage: .camera("p3"), screens: []), [.camera("p1"), .camera("p2")])
369 }
370}
371