import AppKit import CallsKit import SwiftUI /// Offline visual fixtures of the shipping lobby. Only built into the test executable. /// /// Every model reads its server from a throwaway defaults suite, so a /// fixture neither sees nor changes the setting of whoever runs it. @MainActor enum LobbySnapshots { static func save(to directory: String) async throws { NSApplication.shared.setActivationPolicy(.prohibited) let folder = URL(fileURLWithPath: directory) try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) let suite = "CallsInterop.LobbySnapshots" guard let defaults = UserDefaults(suiteName: suite) else { return } defaults.removePersistentDomain(forName: suite) defer { defaults.removePersistentDomain(forName: suite) } for scheme in [ColorScheme.light, .dark] { for state in [ "welcome", "signin", "home", "ready", "error", "expired", "link-other", "visiting", ] { let model = CallsModel(defaults: defaults) if state == "signin" { model.authenticating = true model.confirmationCode = "A1B2-C3D4" model.signin = URL(string: "https://example.invalid") } if state == "home" { model.signedIn = true model.canStart = true model.origin = "https://calls.oficina.works" } if state == "ready" { model.signedIn = true model.canStart = true model.origin = "https://calls.oficina.works" model.room = "team-catchup" } if state == "error" { model.error = "Could not reach calls.oficina.works. Choose Sign in to try again." } if state == "expired" { model.error = "Your sign-in to calls.oficina.works has ended. Choose Sign in to continue." } if state == "link-other" { model.pendingLink = "https://calls.example.org/j/invitation" } if state == "visiting" { model.signedIn = true model.visiting = true model.origin = "https://calls.example.org" model.room = "abc123" } try await render( CallsLobby(model: model), scheme: scheme, size: CGSize(width: 900, height: 720), to: folder.appendingPathComponent("\(state)-\(name(scheme)).png")) } // The same signed-out home at iPhone width, with the Change button // the phone's settings sheet is reached by. try await render( CallsLobby(model: CallsModel(defaults: defaults), openSettings: {}), scheme: scheme, size: CGSize(width: 390, height: 780), to: folder.appendingPathComponent("phone-welcome-\(name(scheme)).png")) try await render( GuestJoinView(model: CallsModel(defaults: defaults)) {}, scheme: scheme, size: nil, to: folder.appendingPathComponent("guest-\(name(scheme)).png")) try await render( GuestJoinView( model: CallsModel(defaults: defaults), link: "calls.example.org/settings", problem: true ) {}, scheme: scheme, size: nil, to: folder.appendingPathComponent("guest-invalid-\(name(scheme)).png")) try await render( ServerSettingsSheet(model: CallsModel(defaults: defaults)) {}, scheme: scheme, size: CGSize(width: 390, height: 520), to: folder.appendingPathComponent("phone-settings-\(name(scheme)).png")) } try await SettingsSnapshots.save(to: folder) try await MiniWindowSnapshots.save(to: folder) try await CallSnapshots.save(to: folder) } static func name(_ scheme: ColorScheme) -> String { scheme == .dark ? "dark" : "light" } /// Draws a view off screen into a PNG. A nil size takes the view's own. static func render(_ content: some View, scheme: ColorScheme, size: CGSize?, to file: URL) async throws { let view = NSHostingView( rootView: content.background(scheme == .dark ? Color(white: 0.12) : Color(white: 0.98)) .environment(\.colorScheme, scheme)) let frame = size ?? view.fittingSize view.frame.size = frame let window = NSWindow( contentRect: NSRect(origin: CGPoint(x: -2000, y: -2000), size: frame), styleMask: [.borderless], backing: .buffered, defer: false) window.isReleasedWhenClosed = false window.appearance = NSAppearance(named: scheme == .dark ? .darkAqua : .aqua) window.contentView = view window.orderFront(nil) try await Task.sleep(for: .milliseconds(300)) view.layoutSubtreeIfNeeded() guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { throw CocoaError(.fileWriteUnknown) } view.cacheDisplay(in: view.bounds, to: bitmap) guard let data = bitmap.representation(using: .png, properties: [:]) else { throw CocoaError(.fileWriteUnknown) } try data.write(to: file) window.close() } }