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 XCTest
5
6 import LiveKitWebRTC
7
8 @testable @_spi(Fixtures) import CallsKit
9
10 /// Paul, on 0.3.6: "I should be able to click anywhere on the window to
11 /// resize it." The full-window call has had no title bar since 0.3.5, so
12 /// moving it is the picture's job, and resizing must still work from
13 /// every edge, the top included.
14 final class WindowMoveRuleTests: XCTestCase {
15 func testAShortPressIsAClickAndALongerOneADrag() {
16 XCTAssertEqual(WindowMove.threshold, 4)
17 let start = CGPoint(x: 100, y: 100)
18 XCTAssertFalse(WindowMove.isDrag(from: start, to: start))
19 XCTAssertFalse(WindowMove.isDrag(from: start, to: CGPoint(x: 102, y: 102)), "a shaky click")
20 XCTAssertTrue(WindowMove.isDrag(from: start, to: CGPoint(x: 104, y: 100)))
21 XCTAssertTrue(WindowMove.isDrag(from: start, to: CGPoint(x: 97, y: 97)), "any direction")
22 }
23
24 func testThePressFollowsThePointerOnceItIsADrag() {
25 var press = WindowMove.Press(origin: CGPoint(x: 200, y: 300), grabbed: CGPoint(x: 700, y: 650))
26 XCTAssertNil(press.moved(to: CGPoint(x: 702, y: 651)), "still a click")
27 XCTAssertFalse(press.moving)
28 XCTAssertEqual(press.moved(to: CGPoint(x: 740, y: 625)), CGPoint(x: 240, y: 275))
29 XCTAssertTrue(press.moving)
30 // Once moving, it goes wherever the pointer goes, back to where it
31 // started included: the window is not left 4 pt short.
32 XCTAssertEqual(press.moved(to: CGPoint(x: 701, y: 650)), CGPoint(x: 201, y: 300))
33 XCTAssertEqual(press.moved(to: CGPoint(x: 700, y: 650)), CGPoint(x: 200, y: 300))
34 }
35
36 func testOnlyAWindowThatCanMoveIsMoved() {
37 XCTAssertTrue(WindowMove.canMove(fullScreen: false, movable: true))
38 XCTAssertFalse(WindowMove.canMove(fullScreen: true, movable: true), "full screen is the screen")
39 XCTAssertFalse(WindowMove.canMove(fullScreen: false, movable: false))
40 }
41 }
42
43 /// The real full-window call in a titled, resizable window like the app's,
44 /// driven with mouse events sent to it: what a press on each part of it
45 /// does to the window. The window is never made key or active: a call
46 /// window behind another app's must move by its picture too.
47 @MainActor final class CallWindowMouseTests: XCTestCase {
48 private var window: NSWindow!
49 private var call: Call!
50 private let start = NSRect(x: 200, y: 200, width: 1000, height: 700)
51
52 override func setUp() async throws {
53 NSApplication.shared.setActivationPolicy(.prohibited)
54 UserDefaults.standard.removeObject(forKey: PiPCorner.key)
55 }
56
57 override func tearDown() async throws {
58 window?.close()
59 window = nil
60 UserDefaults.standard.removeObject(forKey: PiPCorner.key)
61 }
62
63 private func open(_ fixture: CallFixture = .focus, grid: Bool = false, controls: Bool = false) {
64 call = Call(playAudio: false)
65 fixture.apply(to: call)
66 window = NSWindow(
67 contentRect: start, styleMask: [.titled, .closable, .miniaturizable, .resizable],
68 backing: .buffered, defer: false)
69 window.isReleasedWhenClosed = false
70 window.contentView = NSHostingView(
71 rootView: MacCallView(
72 call: call, grid: grid, style: .fullWindow, controlsShown: controls, leave: {}, rejoin: {}))
73 window.orderFrontRegardless()
74 pump(0.8)
75 window.setFrame(start, display: true)
76 pump(0.2)
77 }
78
79 private func pump(_ seconds: Double) {
80 let until = Date(timeIntervalSinceNow: seconds)
81 while let event = NSApp.nextEvent(
82 matching: .any, until: until, inMode: .default, dequeue: true)
83 {
84 NSApp.sendEvent(event)
85 }
86 }
87
88 private func event(_ type: NSEvent.EventType, at point: NSPoint, clicks: Int = 1) -> NSEvent {
89 NSEvent.mouseEvent(
90 with: type, location: point, modifierFlags: [],
91 timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
92 context: nil, eventNumber: 0, clickCount: clicks, pressure: type == .leftMouseUp ? 0 : 1)!
93 }
94
95 /// Presses at `point` (window coordinates, y up), drags by `by` in
96 /// steps as a hand would, and lets go; returns how far the window went.
97 private func drag(from point: NSPoint, by: CGVector) -> CGVector {
98 let before = window.frame.origin
99 let from = window.convertPoint(toScreen: point)
100 NSApp.sendEvent(event(.leftMouseDown, at: point))
101 pump(0.05)
102 for step in 1...6 {
103 let part = CGFloat(step) / 6
104 let there = NSPoint(x: from.x + by.dx * part, y: from.y + by.dy * part)
105 NSApp.sendEvent(event(.leftMouseDragged, at: window.convertPoint(fromScreen: there)))
106 pump(0.03)
107 }
108 let end = NSPoint(x: from.x + by.dx, y: from.y + by.dy)
109 NSApp.sendEvent(event(.leftMouseUp, at: window.convertPoint(fromScreen: end)))
110 pump(0.2)
111 return CGVector(dx: window.frame.minX - before.x, dy: window.frame.minY - before.y)
112 }
113
114 private var bounds: NSRect { window.contentView!.bounds }
115
116 func testTheCallLeavesTheWindowResizableWithNoTitleBar() {
117 open()
118 XCTAssertTrue(window.styleMask.contains(.resizable))
119 XCTAssertTrue(window.styleMask.contains(.fullSizeContentView))
120 XCTAssertEqual(window.titleVisibility, .hidden)
121 XCTAssertTrue(window.titlebarAppearsTransparent)
122 }
123
124 /// AppKit keeps a few points inside each edge, and more at the corners,
125 /// for resizing, whatever the content — the drag strip along the top
126 /// included. Only past them do the call's own views get the click.
127 func testTheEdgesAndCornersBelongToTheWindow() {
128 open()
129 let frameView = window.contentView!.superview!
130 let edges: [(String, NSPoint)] = [
131 ("top", NSPoint(x: bounds.midX, y: bounds.maxY - 1)),
132 ("bottom", NSPoint(x: bounds.midX, y: 1)),
133 ("left", NSPoint(x: 1, y: bounds.midY)),
134 ("right", NSPoint(x: bounds.maxX - 1, y: bounds.midY)),
135 ("top left", NSPoint(x: 4, y: bounds.maxY - 4)),
136 ("top right", NSPoint(x: bounds.maxX - 4, y: bounds.maxY - 4)),
137 ("bottom left", NSPoint(x: 4, y: 4)),
138 ("bottom right", NSPoint(x: bounds.maxX - 4, y: 4)),
139 ]
140 for (edge, point) in edges {
141 XCTAssertTrue(frameView.hitTest(point) === frameView, "\(edge) is the window's")
142 }
143 XCTAssertTrue(
144 frameView.hitTest(NSPoint(x: bounds.midX, y: bounds.maxY - 12)) is WindowDragStrip.StripView,
145 "below the edge, the strip")
146 }
147
148 func testEveryEdgeAndCornerResizesTheWindow() {
149 open()
150 // (where, drag, which edges should move: minX, maxX, minY, maxY)
151 let cases: [(String, NSPoint, CGVector, [Bool])] = [
152 ("top", NSPoint(x: bounds.midX, y: bounds.maxY - 1), CGVector(dx: 0, dy: 30), [false, false, false, true]),
153 ("bottom", NSPoint(x: bounds.midX, y: 1), CGVector(dx: 0, dy: -30), [false, false, true, false]),
154 ("left", NSPoint(x: 1, y: bounds.midY), CGVector(dx: -30, dy: 0), [true, false, false, false]),
155 ("right", NSPoint(x: bounds.maxX - 1, y: bounds.midY), CGVector(dx: 30, dy: 0), [false, true, false, false]),
156 ("top left", NSPoint(x: 1, y: bounds.maxY - 1), CGVector(dx: -30, dy: 30), [true, false, false, true]),
157 ("top right", NSPoint(x: bounds.maxX - 1, y: bounds.maxY - 1), CGVector(dx: 30, dy: 30), [false, true, false, true]),
158 ]
159 for (edge, point, by, moves) in cases {
160 window.setFrame(start, display: true)
161 pump(0.1)
162 // AppKit's resize reads the events that follow the press itself,
163 // so they are queued first.
164 let from = window.convertPoint(toScreen: point)
165 let to = window.convertPoint(fromScreen: NSPoint(x: from.x + by.dx, y: from.y + by.dy))
166 NSApp.postEvent(event(.leftMouseDragged, at: to), atStart: false)
167 NSApp.postEvent(event(.leftMouseUp, at: to), atStart: false)
168 NSApp.sendEvent(event(.leftMouseDown, at: point))
169 pump(0.1)
170 let now = window.frame
171 let moved = [now.minX != start.minX, now.maxX != start.maxX, now.minY != start.minY, now.maxY != start.maxY]
172 XCTAssertEqual(moved, moves, "\(edge): \(start) became \(now)")
173 XCTAssertGreaterThan(now.width * now.height, start.width * start.height, "\(edge) grew")
174 }
175 }
176
177 func testDraggingThePictureMovesTheWindow() {
178 open()
179 let moved = drag(from: NSPoint(x: bounds.midX, y: bounds.midY), by: CGVector(dx: 40, dy: -25))
180 XCTAssertEqual(moved, CGVector(dx: 40, dy: -25))
181 XCTAssertEqual(window.frame.size, start.size)
182 let again = drag(from: NSPoint(x: 150, y: 300), by: CGVector(dx: -30, dy: 20))
183 XCTAssertEqual(again, CGVector(dx: -30, dy: 20), "anywhere on it")
184 }
185
186 func testAClickOnThePictureDoesNotMoveTheWindow() {
187 open()
188 let moved = drag(from: NSPoint(x: bounds.midX, y: bounds.midY), by: CGVector(dx: 2, dy: 1))
189 XCTAssertEqual(moved, CGVector(dx: 0, dy: 0))
190 }
191
192 /// A picture arriving is an AppKit view over the tile, which took
193 /// the press for itself before 0.3.7, so the stage's gesture never
194 /// saw it. Behind another app's window too.
195 func testDraggingLiveVideoMovesTheWindow() {
196 let factory = RTCPeerConnectionFactory()
197 let track = factory.videoTrack(with: factory.videoSource(), trackId: "window-move")
198 window = NSWindow(
199 contentRect: start, styleMask: [.titled, .closable, .resizable], backing: .buffered,
200 defer: false)
201 window.isReleasedWhenClosed = false
202 window.contentView = NSHostingView(
203 rootView: MacVideoTile(track: track, label: "Ana", cornerRadius: 0, compact: true)
204 .movesWindow())
205 window.orderFrontRegardless()
206 pump(0.8)
207 let point = NSPoint(x: bounds.midX, y: bounds.midY)
208 XCTAssertFalse(
209 window.contentView!.superview!.hitTest(point) is RTCMTLNSVideoView, "the picture takes no clicks")
210 XCTAssertEqual(drag(from: point, by: CGVector(dx: 40, dy: -25)), CGVector(dx: 40, dy: -25))
211 }
212
213 /// The self view drags itself to a corner (a gesture of its own, which
214 /// the picture's does not take from it), and the window stays put.
215 func testTheSelfViewIsNotAHandle() {
216 open()
217 let moved = drag(from: NSPoint(x: bounds.maxX - 100, y: 80), by: CGVector(dx: -750, dy: 450))
218 XCTAssertEqual(moved, CGVector(dx: 0, dy: 0))
219 }
220
221 /// 0.4.0: a click on a filmstrip tile pins that person to the stage,
222 /// and moves nothing. Ana is on the stage (first in the order, before
223 /// anyone has spoken); you, Tom and Li are the filmstrip along the
224 /// bottom, 117 pt tall and centred.
225 func testAClickOnTheFilmstripPins() {
226 open(.grid)
227 XCTAssertEqual(call.arrangement.layout, .speaker)
228 let before = window.frame.origin
229 NSApp.sendEvent(event(.leftMouseDown, at: NSPoint(x: 716, y: 66)))
230 pump(0.05)
231 NSApp.sendEvent(event(.leftMouseUp, at: NSPoint(x: 716, y: 66)))
232 // A single click is known only once a double click has not come.
233 pump(NSEvent.doubleClickInterval + 0.3)
234 XCTAssertEqual(window.frame.origin, before)
235 XCTAssertEqual(call.arrangement.pinned, .camera("li"))
236 XCTAssertEqual(call.arrangement.layout, .speaker)
237 }
238
239 /// 0.4.0: a drag on a Grid tile takes it to another's place, and the
240 /// window stays where it is.
241 func testDraggingAGridTileReordersAndDoesNotMoveTheWindow() {
242 open(.grid, grid: true)
243 XCTAssertEqual(call.arrangement.people, ["me", "ana", "tom", "li"])
244 let moved = drag(from: NSPoint(x: 750, y: 175), by: CGVector(dx: -500, dy: 350))
245 XCTAssertEqual(moved, CGVector(dx: 0, dy: 0), "a tile is not a handle")
246 XCTAssertEqual(call.arrangement.people, ["li", "me", "ana", "tom"])
247 }
248
249 func testTheBarIsNotAHandle() {
250 open(controls: true)
251 let moved = drag(from: NSPoint(x: bounds.midX, y: 40), by: CGVector(dx: 30, dy: 30))
252 XCTAssertEqual(moved, CGVector(dx: 0, dy: 0))
253 }
254 }
255#endif
256