oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2 import AppKit
3 import SwiftUI
4 import LiveKitWebRTC
5 import XCTest
6
7 @testable import CallsKit
8
9 /// The mini window's rules, held without a real call or a person
10 /// switching apps: the defaults, where each corner lands, and when the
11 /// window comes and goes.
12 final class MiniWindowPreferencesTests: XCTestCase {
13 private func defaults() -> UserDefaults {
14 let name = "MiniWindowTests.\(UUID().uuidString)"
15 let defaults = UserDefaults(suiteName: name)!
16 addTeardownBlock { defaults.removePersistentDomain(forName: name) }
17 return defaults
18 }
19
20 func testOnAndBottomRightUntilSomeoneSaysOtherwise() {
21 let preferences = MiniWindowPreferences(defaults: defaults())
22 XCTAssertTrue(preferences.enabled)
23 XCTAssertEqual(preferences.corner, .bottomRight)
24 }
25
26 func testChoicesPersist() {
27 let store = defaults()
28 MiniWindowPreferences(defaults: store).enabled = false
29 MiniWindowPreferences(defaults: store).corner = .topLeft
30 XCTAssertFalse(MiniWindowPreferences(defaults: store).enabled)
31 XCTAssertEqual(MiniWindowPreferences(defaults: store).corner, .topLeft)
32 }
33
34 func testAnUnknownCornerFallsBackToTheDefault() {
35 let store = defaults()
36 store.set("middle", forKey: MiniWindowPreferences.cornerKey)
37 XCTAssertEqual(MiniWindowPreferences(defaults: store).corner, .bottomRight)
38 }
39
40 func testTheCornersAreOfferedInTheOrderTheSettingShowsThem() {
41 XCTAssertEqual(
42 MiniWindowCorner.allCases.map(\.title),
43 ["Bottom right", "Bottom left", "Top right", "Top left"])
44 }
45 }
46
47 final class MiniWindowCornerTests: XCTestCase {
48 // A 1440×900 screen with a 25pt menu bar and a 70pt Dock along the
49 // bottom: the visible frame AppKit reports for it.
50 private let visible = CGRect(x: 0, y: 70, width: 1440, height: 805)
51 private let size = MiniWindowRules.size
52
53 func testEachCornerSitsInsetFromTheVisibleFrame() {
54 XCTAssertEqual(
55 MiniWindowCorner.bottomRight.frame(size: size, in: visible),
56 CGRect(x: 1100, y: 90, width: 320, height: 180))
57 XCTAssertEqual(
58 MiniWindowCorner.bottomLeft.frame(size: size, in: visible),
59 CGRect(x: 20, y: 90, width: 320, height: 180))
60 XCTAssertEqual(
61 MiniWindowCorner.topRight.frame(size: size, in: visible),
62 CGRect(x: 1100, y: 675, width: 320, height: 180))
63 XCTAssertEqual(
64 MiniWindowCorner.topLeft.frame(size: size, in: visible),
65 CGRect(x: 20, y: 675, width: 320, height: 180))
66 }
67
68 func testASecondScreenLeftOfTheMainOneHasANegativeOrigin() {
69 let left = CGRect(x: -1920, y: 0, width: 1920, height: 1055)
70 XCTAssertEqual(
71 MiniWindowCorner.bottomRight.frame(size: size, in: left),
72 CGRect(x: -340, y: 20, width: 320, height: 180))
73 XCTAssertEqual(
74 MiniWindowCorner.topLeft.frame(size: size, in: left),
75 CGRect(x: -1900, y: 855, width: 320, height: 180))
76 }
77
78 func testTheWindowIs16By9() {
79 XCTAssertEqual(size.width / size.height, 16 / 9, accuracy: 0.001)
80 }
81
82 func testADraggedWindowSnapsToTheCornerItsMiddleIsNearest() {
83 func dropped(_ x: CGFloat, _ y: CGFloat) -> MiniWindowCorner {
84 .nearest(to: CGRect(origin: CGPoint(x: x, y: y), size: size), in: visible)
85 }
86 XCTAssertEqual(dropped(900, 120), .bottomRight)
87 XCTAssertEqual(dropped(300, 120), .bottomLeft)
88 XCTAssertEqual(dropped(900, 600), .topRight)
89 XCTAssertEqual(dropped(100, 600), .topLeft)
90 // Half off the screen still belongs to the corner it is nearest.
91 XCTAssertEqual(dropped(1300, -50), .bottomRight)
92 // Every corner's own frame snaps back to that corner.
93 for corner in MiniWindowCorner.allCases {
94 XCTAssertEqual(.nearest(to: corner.frame(size: size, in: visible), in: visible), corner)
95 }
96 }
97 }
98
99 final class MiniWindowPolicyTests: XCTestCase {
100 private let inCall = MiniWindowPolicy.Conditions(
101 enabled: true, inCall: true, mainWindowVisible: true)
102
103 func testSwitchingAwayDuringACallShowsItAndComingBackRestores() {
104 var policy = MiniWindowPolicy()
105 XCTAssertEqual(policy.resignedActive(inCall), .show(hideMain: true))
106 XCTAssertTrue(policy.showing)
107 XCTAssertEqual(policy.becameActive(), .restore(activate: true))
108 XCTAssertFalse(policy.showing)
109 // Coming back again with nothing up is nothing to do.
110 XCTAssertEqual(policy.becameActive(), .none)
111 }
112
113 func testASecondResignWhileShowingDoesNotOpenAnother() {
114 var policy = MiniWindowPolicy()
115 _ = policy.resignedActive(inCall)
116 XCTAssertEqual(policy.resignedActive(inCall), .none)
117 }
118
119 func testNothingAppearsOutsideACallOrWithTheSettingOff() {
120 var policy = MiniWindowPolicy()
121 var conditions = inCall
122 conditions.inCall = false
123 XCTAssertEqual(policy.resignedActive(conditions), .none)
124 conditions = inCall
125 conditions.enabled = false
126 XCTAssertEqual(policy.resignedActive(conditions), .none)
127 XCTAssertFalse(policy.showing)
128 }
129
130 func testACallWindowPutAwayByThePersonStaysAway() {
131 var policy = MiniWindowPolicy()
132 var conditions = inCall
133 conditions.mainWindowMiniaturized = true
134 XCTAssertEqual(policy.resignedActive(conditions), .none)
135 conditions = inCall
136 conditions.mainWindowVisible = false
137 XCTAssertEqual(policy.resignedActive(conditions), .none)
138 }
139
140 func testAFullScreenCallWindowIsLeftInItsSpace() {
141 var policy = MiniWindowPolicy()
142 var conditions = inCall
143 conditions.mainWindowFullScreen = true
144 XCTAssertEqual(policy.resignedActive(conditions), .show(hideMain: false))
145 XCTAssertFalse(policy.mainHidden)
146 XCTAssertEqual(policy.becameActive(), .restore(activate: true))
147 }
148
149 func testHidingTheAppBeforeItResignsShowsNothing() {
150 var policy = MiniWindowPolicy()
151 XCTAssertEqual(policy.willHide(), .none)
152 XCTAssertEqual(policy.resignedActive(inCall), .none)
153 policy.didUnhide()
154 XCTAssertEqual(policy.becameActive(), .none)
155 // And once it is back, switching away works as before.
156 XCTAssertEqual(policy.resignedActive(inCall), .show(hideMain: true))
157 }
158
159 func testHidingTheAppAfterItResignedPutsTheCallWindowBackFirst() {
160 var policy = MiniWindowPolicy()
161 _ = policy.resignedActive(inCall)
162 XCTAssertEqual(policy.willHide(), .restore(activate: false))
163 XCTAssertEqual(policy.resignedActive(inCall), .none)
164 }
165
166 func testTheCallEndingPutsItAwayWithoutPullingTheAppForward() {
167 var policy = MiniWindowPolicy()
168 _ = policy.resignedActive(inCall)
169 XCTAssertEqual(policy.callEnded(), .restore(activate: false))
170 XCTAssertEqual(policy.becameActive(), .none)
171 XCTAssertEqual(policy.callEnded(), .none)
172 }
173 }
174
175 /// Size: 16:9 always, never under 200 pt wide, never over 40% of the
176 /// screen, and remembered.
177 final class MiniWindowSizeTests: XCTestCase {
178 private let visible = CGRect(x: 0, y: 70, width: 1440, height: 805)
179
180 func testEverySizeIs16By9() {
181 for width in [0, 150, 200, 247, 320, 575, 900, 5000] as [CGFloat] {
182 let size = MiniWindowRules.size(width: width, in: visible)
183 XCTAssertEqual(size.width / size.height, 16 / 9, accuracy: 0.0001, "\(width)")
184 }
185 }
186
187 func testTheWidthIsHeldBetweenTheMinimumAnd40PercentOfTheScreen() {
188 XCTAssertEqual(MiniWindowRules.size(width: 120, in: visible).width, 200)
189 XCTAssertEqual(MiniWindowRules.size(width: 320, in: visible).width, 320)
190 XCTAssertEqual(MiniWindowRules.size(width: 900, in: visible).width, 576) // 1440 × 0.4
191 // A width chosen on a big display, shown on a small one.
192 let small = CGRect(x: 0, y: 0, width: 1024, height: 700)
193 XCTAssertEqual(MiniWindowRules.size(width: 576, in: small).width, 409)
194 // A screen too small for 40% of it to reach the minimum still gets it.
195 let tiny = CGRect(x: 0, y: 0, width: 400, height: 300)
196 XCTAssertEqual(MiniWindowRules.size(width: 900, in: tiny).width, 200)
197 }
198
199 func testResizingFromACornerKeepsTheOppositeCornerAndTheAspect() {
200 let start = CGRect(x: 1100, y: 90, width: 320, height: 180)
201 // The top-left corner, dragged up and left: the bottom right stays.
202 let grown = MiniWindowResize.frame(
203 from: start, dragging: [.top, .left], dx: -80, dy: 20, in: visible)
204 XCTAssertEqual(grown.maxX, start.maxX)
205 XCTAssertEqual(grown.minY, start.minY)
206 XCTAssertEqual(grown.width, 400)
207 XCTAssertEqual(grown.width / grown.height, 16 / 9, accuracy: 0.0001)
208 // Whichever direction asks for more wins.
209 let tall = MiniWindowResize.frame(
210 from: start, dragging: [.top, .left], dx: -10, dy: 90, in: visible)
211 XCTAssertEqual(tall.height, 270, accuracy: 0.001)
212 XCTAssertEqual(tall.width, 480)
213 }
214
215 func testResizingIsClampedAtBothEnds() {
216 let start = CGRect(x: 100, y: 100, width: 320, height: 180)
217 let shrunk = MiniWindowResize.frame(
218 from: start, dragging: .right, dx: -500, dy: 0, in: visible)
219 XCTAssertEqual(shrunk.size, MiniWindowRules.minimumSize())
220 let huge = MiniWindowResize.frame(
221 from: start, dragging: .bottom, dx: 0, dy: -2000, in: visible)
222 XCTAssertEqual(huge.size, MiniWindowRules.maximumSize(in: visible))
223 XCTAssertEqual(huge.maxY, start.maxY, "a bottom edge grows downward")
224 // An edge alone grows evenly either side of the middle.
225 let wide = MiniWindowResize.frame(from: start, dragging: .right, dx: 80, dy: 0, in: visible)
226 XCTAssertEqual(wide.minX, start.minX)
227 XCTAssertEqual(wide.midY, start.midY, accuracy: 0.001)
228 }
229
230 func testThePressTakesHoldOfTheEdgesItIsNear() {
231 let size = CGSize(width: 320, height: 180)
232 XCTAssertEqual(MiniWindowResize.handle(at: CGPoint(x: 160, y: 90), in: size), [])
233 XCTAssertEqual(MiniWindowResize.handle(at: CGPoint(x: 2, y: 90), in: size), .left)
234 XCTAssertEqual(MiniWindowResize.handle(at: CGPoint(x: 318, y: 178), in: size), [.right, .top])
235 XCTAssertEqual(MiniWindowResize.handle(at: CGPoint(x: 3, y: 3), in: size), [.left, .bottom])
236 }
237
238 func testTheSizeIsRemembered() {
239 let name = "MiniWindowSizeTests.\(UUID().uuidString)"
240 let store = UserDefaults(suiteName: name)!
241 defer { store.removePersistentDomain(forName: name) }
242 XCTAssertEqual(MiniWindowPreferences(defaults: store).width, 320)
243 MiniWindowPreferences(defaults: store).width = 480
244 XCTAssertEqual(MiniWindowPreferences(defaults: store).width, 480)
245 }
246
247 func testSnappingKeepsTheChosenSize() {
248 let name = "MiniWindowSizeTests.\(UUID().uuidString)"
249 let store = UserDefaults(suiteName: name)!
250 defer { store.removePersistentDomain(forName: name) }
251 var memory = MiniWindowCallMemory()
252 let dropped = CGRect(x: 800, y: 500, width: 480, height: 270)
253 let settled = memory.dropped(
254 dropped, in: visible, display: "1", option: false,
255 preferences: MiniWindowPreferences(defaults: store))
256 XCTAssertEqual(settled, MiniWindowCorner.topRight.frame(size: dropped.size, in: visible))
257 }
258 }
259
260 /// The tab: against the nearest edge, off it by dragging, and saying who
261 /// the call is with.
262 final class MiniWindowTabTests: XCTestCase {
263 private let visible = CGRect(x: 0, y: 70, width: 1440, height: 805)
264 private let size = MiniWindowTab.size
265
266 func testEachCornerWindowDocksToItsSide() {
267 let window = CGSize(width: 320, height: 180)
268 let (right, tab) = MiniWindowTab.dock(
269 MiniWindowCorner.bottomRight.frame(size: window, in: visible), in: visible)
270 XCTAssertEqual(right, .right)
271 XCTAssertEqual(tab.maxX, visible.maxX, "flush against the edge")
272 XCTAssertEqual(tab.midY, 90 + 90, accuracy: 0.001, "level with the window's middle")
273 XCTAssertEqual(tab.size, size)
274 XCTAssertEqual(
275 MiniWindowTab.dock(MiniWindowCorner.topLeft.frame(size: window, in: visible), in: visible).0,
276 .left)
277 }
278
279 func testAWindowNearerTheTopOrBottomDocksThere() {
280 let nearBottom = CGRect(x: 600, y: 75, width: 320, height: 180)
281 let (edge, tab) = MiniWindowTab.dock(nearBottom, in: visible)
282 XCTAssertEqual(edge, .bottom)
283 XCTAssertEqual(tab.minY, visible.minY)
284 XCTAssertEqual(tab.midX, nearBottom.midX, accuracy: 0.001)
285 let nearTop = CGRect(x: 600, y: 690, width: 320, height: 180)
286 XCTAssertEqual(MiniWindowTab.dock(nearTop, in: visible).0, .top)
287 XCTAssertEqual(MiniWindowTab.dock(nearTop, in: visible).1.maxY, visible.maxY)
288 }
289
290 func testTheTabStaysOnScreen() {
291 // A window half off the bottom right: the tab is still all on screen.
292 let off = CGRect(x: 1400, y: 20, width: 320, height: 180)
293 let tab = MiniWindowTab.dock(off, in: visible).1
294 XCTAssertTrue(visible.contains(tab), "\(tab)")
295 }
296
297 func testDraggingAlongAnEdgeRedocksAndPullingItOutExpands() {
298 let docked = MiniWindowTab.dock(CGRect(x: 1100, y: 90, width: 320, height: 180), in: visible).1
299 // Slid up the right edge, a little off it.
300 let slid = docked.offsetBy(dx: -20, dy: 300)
301 guard case .dock(.right, let frame) = MiniWindowTab.release(slid, in: visible) else {
302 return XCTFail("slid along the edge should dock again")
303 }
304 XCTAssertEqual(frame.maxX, visible.maxX)
305 XCTAssertEqual(frame.minY, slid.minY)
306 // Pulled into the middle of the screen.
307 XCTAssertEqual(MiniWindowTab.release(docked.offsetBy(dx: -500, dy: 200), in: visible), .expand)
308 }
309
310 func testAPulledOutWindowOpensWhereTheTabWasAndOnScreen() {
311 let tab = CGRect(x: 1284, y: 400, width: size.width, height: size.height)
312 let frame = MiniWindowTab.expanded(
313 from: tab, size: CGSize(width: 320, height: 180), in: visible)
314 XCTAssertEqual(frame.midY, tab.midY, accuracy: 0.001)
315 XCTAssertEqual(frame.maxX, visible.maxX, "clamped rather than hanging off the edge")
316 }
317
318 func testTheCaption() {
319 XCTAssertEqual(MiniWindowTab.caption(others: []), "Just you")
320 XCTAssertEqual(MiniWindowTab.caption(others: ["Bea"]), "Bea")
321 XCTAssertEqual(MiniWindowTab.caption(others: ["Bea", "Cai"]), "3 people")
322 }
323 }
324
325 /// Placement: corners, anywhere, and ⌥ for one call.
326 final class MiniWindowPlacementTests: XCTestCase {
327 private let visible = CGRect(x: 0, y: 70, width: 1440, height: 805)
328 private let size = CGSize(width: 320, height: 180)
329
330 private func preferences() -> MiniWindowPreferences {
331 let name = "MiniWindowPlacementTests.\(UUID().uuidString)"
332 let defaults = UserDefaults(suiteName: name)!
333 addTeardownBlock { defaults.removePersistentDomain(forName: name) }
334 return MiniWindowPreferences(defaults: defaults)
335 }
336
337 func testCornersByDefault() {
338 let preferences = preferences()
339 XCTAssertEqual(preferences.placement, .corners)
340 XCTAssertEqual(
341 MiniWindowPlacement.allCases.map(\.title), ["Snap to corners", "Anywhere I put it"])
342 XCTAssertEqual(
343 MiniWindowCallMemory().frame(size: size, in: visible, display: "1", preferences: preferences),
344 MiniWindowCorner.bottomRight.frame(size: size, in: visible))
345 }
346
347 func testAnywhereKeepsTheDropExactlyAndRemembersItPerDisplay() {
348 let preferences = preferences()
349 preferences.placement = .anywhere
350 var memory = MiniWindowCallMemory()
351 let drop = CGRect(x: 612, y: 333, width: 320, height: 180)
352 XCTAssertEqual(
353 memory.dropped(drop, in: visible, display: "1", option: false, preferences: preferences),
354 drop)
355 XCTAssertEqual(preferences.corner, .bottomRight, "a free drop is not a corner")
356 // Next call, same display: there. Another display: its own default.
357 let next = MiniWindowCallMemory()
358 XCTAssertEqual(next.frame(size: size, in: visible, display: "1", preferences: preferences), drop)
359 let other = CGRect(x: 1440, y: 0, width: 1920, height: 1055)
360 XCTAssertEqual(
361 next.frame(size: size, in: other, display: "2", preferences: preferences),
362 MiniWindowCorner.bottomRight.frame(size: size, in: other))
363 }
364
365 func testAFreePositionFollowsItsDisplayWhenTheArrangementChanges() {
366 let preferences = preferences()
367 preferences.placement = .anywhere
368 var memory = MiniWindowCallMemory()
369 let right = CGRect(x: 1440, y: 0, width: 1920, height: 1055)
370 _ = memory.dropped(
371 CGRect(x: 2000, y: 400, width: 320, height: 180), in: right, display: "2", option: false,
372 preferences: preferences)
373 // The display dragged to the left of the main one in Arrange.
374 let left = CGRect(x: -1920, y: 0, width: 1920, height: 1055)
375 XCTAssertEqual(
376 memory.frame(size: size, in: left, display: "2", preferences: preferences),
377 CGRect(x: -1920 + 560, y: 400, width: 320, height: 180))
378 }
379
380 func testAFreePositionIsClampedBackOnAScreenThatShrank() {
381 let preferences = preferences()
382 preferences.placement = .anywhere
383 var memory = MiniWindowCallMemory()
384 let big = CGRect(x: 0, y: 0, width: 2560, height: 1415)
385 _ = memory.dropped(
386 CGRect(x: 2200, y: 1200, width: 320, height: 180), in: big, display: "1", option: false,
387 preferences: preferences)
388 // Same display at a lower resolution.
389 let smaller = CGRect(x: 0, y: 0, width: 1440, height: 875)
390 let frame = memory.frame(size: size, in: smaller, display: "1", preferences: preferences)
391 XCTAssertTrue(smaller.contains(frame), "\(frame)")
392 XCTAssertEqual(frame.maxX, smaller.maxX)
393 XCTAssertEqual(frame.maxY, smaller.maxY)
394 // And a drop half off the screen lands on it.
395 let half = memory.dropped(
396 CGRect(x: -100, y: -50, width: 320, height: 180), in: smaller, display: "1", option: false,
397 preferences: preferences)
398 XCTAssertEqual(half.origin, CGPoint(x: 0, y: 0))
399 }
400
401 func testOptionDragPlacesFreelyForThisCallOnly() {
402 let preferences = preferences()
403 var memory = MiniWindowCallMemory()
404 let drop = CGRect(x: 612, y: 333, width: 320, height: 180)
405 XCTAssertEqual(
406 memory.dropped(drop, in: visible, display: "1", option: true, preferences: preferences), drop)
407 XCTAssertEqual(preferences.corner, .bottomRight, "the setting is untouched")
408 XCTAssertNil(preferences.position(on: "1"), "and nothing is saved")
409 // Shown again in the same call: where it was dropped.
410 XCTAssertEqual(memory.frame(size: size, in: visible, display: "1", preferences: preferences), drop)
411 // Resizing it keeps it placed rather than snapping it.
412 let bigger = CGRect(x: 612, y: 333, width: 400, height: 225)
413 XCTAssertEqual(
414 memory.resized(bigger, in: visible, display: "1", preferences: preferences), bigger)
415 // The next call starts from the setting again.
416 XCTAssertEqual(
417 MiniWindowCallMemory().frame(size: size, in: visible, display: "1", preferences: preferences),
418 MiniWindowCorner.bottomRight.frame(size: size, in: visible))
419 }
420
421 func testAnOrdinaryDragAfterAnOptionDragSnapsAndForgetsTheOneOff() {
422 let preferences = preferences()
423 var memory = MiniWindowCallMemory()
424 _ = memory.dropped(
425 CGRect(x: 612, y: 333, width: 320, height: 180), in: visible, display: "1", option: true,
426 preferences: preferences)
427 let snapped = memory.dropped(
428 CGRect(x: 100, y: 600, width: 320, height: 180), in: visible, display: "1", option: false,
429 preferences: preferences)
430 XCTAssertEqual(snapped, MiniWindowCorner.topLeft.frame(size: size, in: visible))
431 XCTAssertEqual(preferences.corner, .topLeft)
432 XCTAssertEqual(memory.frame(size: size, in: visible, display: "1", preferences: preferences), snapped)
433 XCTAssertTrue(memory.oneOff.isEmpty)
434 }
435
436 /// Collapsed, and any ⌥ one-off, last for the call and no longer.
437 @MainActor func testEachCallStartsExpandedWithNoOneOffs() {
438 let name = "MiniWindowPlacementTests.\(UUID().uuidString)"
439 let defaults = UserDefaults(suiteName: name)!
440 defer { defaults.removePersistentDomain(forName: name) }
441 let model = CallsModel(defaults: defaults)
442 let controller = MiniWindowController(preferences: MiniWindowPreferences(defaults: defaults))
443 controller.attach(model: model, mainWindow: nil)
444 model.joining = true
445 controller.memory.collapsed = true
446 _ = controller.memory.dropped(
447 CGRect(x: 612, y: 333, width: 320, height: 180), in: visible, display: "1", option: true,
448 preferences: MiniWindowPreferences(defaults: defaults))
449 XCTAssertNotEqual(controller.memory, MiniWindowCallMemory())
450 model.joining = false
451 XCTAssertEqual(controller.memory, MiniWindowCallMemory())
452 XCTAssertFalse(controller.memory.collapsed)
453 }
454
455 func testForgettingPositionsPutsItBackInTheCorner() {
456 let preferences = preferences()
457 preferences.placement = .anywhere
458 preferences.setPosition(CGPoint(x: 10, y: 10), on: "1")
459 preferences.forgetPositions()
460 XCTAssertNil(preferences.position(on: "1"))
461 }
462 }
463
464 /// Space-to-mute's rules, and how AppKit's first responder is read.
465 final class SpaceToMuteTests: XCTestCase {
466 func testBareSpaceWithNothingFocusedToggles() {
467 XCTAssertEqual(
468 SpaceToMute.action(isSpace: true, isRepeat: false, modified: false, focus: .none), .toggle)
469 }
470
471 func testARepeatIsSwallowedNotToggled() {
472 XCTAssertEqual(
473 SpaceToMute.action(isSpace: true, isRepeat: true, modified: false, focus: .none), .swallow)
474 }
475
476 func testTextFieldsAndControlsKeepTheirSpace() {
477 for focus in [KeyboardFocus.text, .control] {
478 for repeating in [false, true] {
479 XCTAssertEqual(
480 SpaceToMute.action(isSpace: true, isRepeat: repeating, modified: false, focus: focus),
481 .pass)
482 }
483 }
484 }
485
486 func testOtherKeysAndModifiedSpaceAreNotOurs() {
487 XCTAssertEqual(
488 SpaceToMute.action(isSpace: false, isRepeat: false, modified: false, focus: .none), .pass)
489 XCTAssertEqual(
490 SpaceToMute.action(isSpace: true, isRepeat: false, modified: true, focus: .none), .pass)
491 }
492
493 @MainActor func testTheFirstResponderIsReadConservatively() {
494 let root = NSView()
495 XCTAssertEqual(SpaceToMute.focus(of: nil, contentView: root), .none)
496 XCTAssertEqual(SpaceToMute.focus(of: NSWindow(), contentView: root), .none)
497 XCTAssertEqual(SpaceToMute.focus(of: root, contentView: root), .none)
498 // The field editor a SwiftUI TextField (the chat composer) edits
499 // through, and an editable field itself.
500 XCTAssertEqual(SpaceToMute.focus(of: NSTextView(), contentView: root), .text)
501 XCTAssertEqual(SpaceToMute.focus(of: NSTextField(string: ""), contentView: root), .text)
502 XCTAssertEqual(SpaceToMute.focus(of: NSButton(), contentView: root), .control)
503 XCTAssertEqual(SpaceToMute.focus(of: NSPopUpButton(), contentView: root), .control)
504 // Anything unrecognised — SwiftUI's focus proxy for a Button among
505 // them — keeps its Space.
506 XCTAssertEqual(SpaceToMute.focus(of: NSView(), contentView: root), .control)
507 }
508
509 @MainActor func testAKeyEventInAWindowIsDecidedByItsFirstResponder() throws {
510 NSApplication.shared.setActivationPolicy(.prohibited)
511 let window = NSWindow(
512 contentRect: NSRect(x: 0, y: 0, width: 200, height: 100), styleMask: [.titled],
513 backing: .buffered, defer: false)
514 window.isReleasedWhenClosed = false
515 let field = NSTextField(frame: NSRect(x: 10, y: 10, width: 100, height: 24))
516 window.contentView?.addSubview(field)
517 func key(_ code: UInt16, _ characters: String, repeating: Bool = false, flags: NSEvent.ModifierFlags = [])
518 throws -> NSEvent
519 {
520 try XCTUnwrap(
521 NSEvent.keyEvent(
522 with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0,
523 windowNumber: window.windowNumber, context: nil, characters: characters,
524 charactersIgnoringModifiers: characters, isARepeat: repeating, keyCode: code))
525 }
526 window.makeFirstResponder(nil)
527 XCTAssertEqual(SpaceToMute.action(for: try key(49, " "), in: window), .toggle)
528 XCTAssertEqual(SpaceToMute.action(for: try key(49, " ", repeating: true), in: window), .swallow)
529 XCTAssertEqual(SpaceToMute.action(for: try key(49, " ", flags: .command), in: window), .pass)
530 XCTAssertEqual(SpaceToMute.action(for: try key(49, " ", flags: .capsLock), in: window), .toggle)
531 XCTAssertEqual(SpaceToMute.action(for: try key(46, "m"), in: window), .pass)
532 window.makeFirstResponder(field)
533 XCTAssertEqual(SpaceToMute.action(for: try key(49, " "), in: window), .pass, "typing a space")
534 window.close()
535 }
536 }
537
538 final class MiniWindowPanelTests: XCTestCase {
539 @MainActor func testThePanelFloatsEverywhereAndStaysOutOfScreenCaptures() throws {
540 NSApplication.shared.setActivationPolicy(.prohibited)
541 let screen = try XCTUnwrap(NSScreen.main)
542 let panel = MiniWindowPanel(call: Call(playAudio: false), actions: MiniWindowActions())
543 XCTAssertEqual(panel.sharingType, .none)
544 XCTAssertEqual(panel.level, .floating)
545 XCTAssertFalse(panel.hidesOnDeactivate)
546 XCTAssertTrue(panel.styleMask.contains(.nonactivatingPanel))
547 XCTAssertTrue(panel.collectionBehavior.contains(.canJoinAllSpaces))
548 XCTAssertTrue(panel.collectionBehavior.contains(.fullScreenAuxiliary))
549 XCTAssertTrue(panel.canBecomeKey)
550 XCTAssertTrue(panel.becomesKeyOnlyIfNeeded, "clicking Mute must not take the keyboard")
551 let visible = screen.visibleFrame
552 let big = MiniWindowRules.size(width: 480, in: visible)
553 let frame = MiniWindowCorner.topLeft.frame(size: big, in: visible)
554 panel.expand(to: frame, animated: false)
555 XCTAssertEqual(panel.frame, frame)
556 XCTAssertFalse(panel.collapsed)
557
558 // The tab is the same window: it keeps every one of those settings.
559 let tab = MiniWindowTab.dock(frame, in: visible).1
560 panel.collapse(to: tab, expanded: frame)
561 XCTAssertTrue(panel.collapsed)
562 XCTAssertEqual(panel.frame, tab)
563 XCTAssertEqual(panel.expandedFrame, frame, "the size to come back to")
564 XCTAssertEqual(panel.sharingType, .none)
565 XCTAssertEqual(panel.level, .floating)
566 XCTAssertTrue(panel.collectionBehavior.contains(.canJoinAllSpaces))
567 panel.expand(to: panel.expandedFrame, animated: false)
568 XCTAssertEqual(panel.frame, frame)
569 panel.dismiss()
570 XCTAssertNil(panel.contentView)
571 XCTAssertFalse(panel.isVisible)
572 }
573
574 /// The mechanism dismiss() relies on to hand renderers back: once the
575 /// content stops naming a track, SwiftUI takes the video view out of
576 /// the hierarchy on the next layout, which is when MacVideoTile
577 /// removes its renderer from the track.
578 @MainActor func testTakingTheVideoOutRemovesItsRendererAtOnce() {
579 NSApplication.shared.setActivationPolicy(.prohibited)
580 let factory = RTCPeerConnectionFactory()
581 let track = factory.videoTrack(with: factory.videoSource(), trackId: "mini-test")
582 let holder = Holder(track: track)
583 let window = NSWindow(
584 contentRect: NSRect(origin: .zero, size: MiniWindowRules.size),
585 styleMask: [.borderless], backing: .buffered, defer: false)
586 window.isReleasedWhenClosed = false
587 window.contentView = NSHostingView(rootView: HolderView(holder: holder))
588 window.orderFrontRegardless()
589 window.contentView?.layoutSubtreeIfNeeded()
590 XCTAssertEqual(renderers(in: window.contentView), 1)
591 holder.track = nil
592 window.contentView?.layoutSubtreeIfNeeded()
593 window.contentView?.displayIfNeeded()
594 XCTAssertEqual(renderers(in: window.contentView), 0)
595 window.close()
596 }
597
598 private func renderers(in view: NSView?) -> Int {
599 guard let view else { return 0 }
600 return (view is RTCMTLNSVideoView ? 1 : 0)
601 + view.subviews.reduce(0) { $0 + renderers(in: $1) }
602 }
603
604 @MainActor private final class Holder: ObservableObject {
605 @Published var track: RTCVideoTrack?
606 init(track: RTCVideoTrack) { self.track = track }
607 }
608
609 private struct HolderView: View {
610 @ObservedObject var holder: Holder
611 var body: some View {
612 if let track = holder.track { MacVideoTile(track: track, label: "Bea") } else { Color.black }
613 }
614 }
615 }
616#endif
617