268 lines
11 KiB
Swift
268 lines
11 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AppKit
|
|
import SwiftUI
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite(.requiresVisibleWindow, .serialized)
|
|
@MainActor
|
|
struct MetricsMenuNavigationTests {
|
|
@Test
|
|
func detailsReuseTheMostRecentWindowAndPreserveOtherWindows() throws {
|
|
let fixture = MetricsMenuFixture()
|
|
defer { fixture.close() }
|
|
let first = WindowModel()
|
|
let second = WindowModel()
|
|
let firstNative = fixture.register(first)
|
|
_ = fixture.register(second)
|
|
let servers = fixture.app.serverMetrics.servers
|
|
let server = try #require(servers.first)
|
|
let other = try #require(servers.dropFirst().first)
|
|
first.metricsHostID = other.id
|
|
first.metricsDetailPage = .processes
|
|
second.selectedDestination = .assistant
|
|
second.metricsHostID = other.id
|
|
NotificationCenter.default.post(name: NSWindow.didBecomeKeyNotification, object: firstNative)
|
|
|
|
fixture.app.mainWindows.openMetrics(hostID: server.id)
|
|
#expect(first.selectedDestination == .metrics)
|
|
#expect(first.metricsHostID == server.id)
|
|
#expect(first.metricsDetailPage == .status)
|
|
#expect(second.selectedDestination == .assistant)
|
|
#expect(second.metricsHostID == other.id)
|
|
#expect(fixture.requests.isEmpty)
|
|
|
|
first.metricsDetailPage = .processes
|
|
fixture.app.mainWindows.openMetrics(hostID: server.id)
|
|
#expect(first.metricsDetailPage == .processes)
|
|
first.selectedDestination = .vault
|
|
fixture.app.mainWindows.openMetrics()
|
|
#expect(first.selectedDestination == .metrics)
|
|
#expect(first.metricsHostID == server.id)
|
|
#expect(first.metricsDetailPage == .processes)
|
|
|
|
fixture.app.serverMetrics.remove(server.id)
|
|
fixture.app.mainWindows.openMetrics(hostID: server.id)
|
|
#expect(first.selectedDestination == .metrics)
|
|
#expect(first.metricsHostID == nil)
|
|
}
|
|
|
|
@Test
|
|
func detailsRestoreAMinimizedWindowFromTheBackground() async throws {
|
|
let fixture = MetricsMenuFixture()
|
|
defer { fixture.close() }
|
|
let model = WindowModel()
|
|
let window = fixture.register(model)
|
|
let server = try #require(fixture.app.serverMetrics.servers.first)
|
|
try await activate(window)
|
|
window.miniaturize(nil)
|
|
try await waitUntil { window.isMiniaturized }
|
|
NSApp.hide(nil)
|
|
try await waitUntil { !NSApp.isActive }
|
|
fixture.app.mainWindows.openMetrics(hostID: server.id)
|
|
try await waitUntil { !window.isMiniaturized && window.isKeyWindow && NSApp.isActive }
|
|
#expect(model.metricsHostID == server.id)
|
|
#expect(fixture.requests.isEmpty)
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func aNewWindowReceivesItsHostAndRevalidatesItAtRegistration(removeBeforeRegistration: Bool) throws {
|
|
let fixture = MetricsMenuFixture()
|
|
defer { fixture.close() }
|
|
let server = try #require(fixture.app.serverMetrics.servers.first)
|
|
fixture.app.mainWindows.openMetrics(hostID: server.id)
|
|
#expect(fixture.requests.count == 1)
|
|
let request = try #require(fixture.requests.first)
|
|
let welcomeRequest = try #require(fixture.app.mainWindows.newWindow())
|
|
if removeBeforeRegistration { fixture.app.serverMetrics.remove(server.id) }
|
|
|
|
let welcome = WindowModel()
|
|
_ = fixture.register(welcome, requestID: welcomeRequest)
|
|
let metrics = WindowModel()
|
|
_ = fixture.register(metrics, requestID: request)
|
|
#expect(welcome.selectedDestination == .vault)
|
|
#expect(welcome.metricsHostID == nil)
|
|
#expect(metrics.selectedDestination == .metrics)
|
|
#expect(metrics.metricsHostID == (removeBeforeRegistration ? nil : server.id))
|
|
#expect(metrics.metricsDetailPage == .status)
|
|
}
|
|
|
|
@Test(arguments: [AppLanguage.english, .simplifiedChinese, .traditionalChinese])
|
|
func theWholeRowOpensTheCorrectHostInEveryConnectionState(language: AppLanguage) async throws {
|
|
let fixture = MetricsMenuFixture(language: language)
|
|
defer { fixture.close() }
|
|
var opened: [UUID] = []
|
|
var openedMainWindow = false
|
|
let menu = MetricsMenuView(
|
|
model: fixture.app.serverMetrics, openMetrics: { openedMainWindow = true },
|
|
openDetails: { opened.append($0) }
|
|
)
|
|
.environment(\.locale, language.locale)
|
|
.environment(\.colorScheme, .light)
|
|
.background(Color(nsColor: .windowBackgroundColor))
|
|
let window = fixture.makeWindow()
|
|
window.setContentSize(MetricsMenuView.preferredSize(serverCount: fixture.app.serverMetrics.servers.count))
|
|
window.contentViewController = NSHostingController(rootView: menu)
|
|
window.makeKeyAndOrderFront(nil)
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
let server = try #require(fixture.app.serverMetrics.servers.dropFirst().first)
|
|
let view = try #require(window.contentView)
|
|
view.layoutSubtreeIfNeeded()
|
|
#expect(view.bounds.size == MetricsMenuView.preferredSize(serverCount: fixture.app.serverMetrics.servers.count))
|
|
try capture(window, name: "metrics-menu-\(language.rawValue)")
|
|
|
|
// Name, instruments and empty padding must all belong to the button.
|
|
let top = 49.0 + 119
|
|
for (index, point) in [
|
|
CGPoint(x: 30, y: top + 20),
|
|
CGPoint(x: 190, y: top + 59),
|
|
CGPoint(x: 3, y: top + 115),
|
|
].enumerated() {
|
|
try clickFromTop(point, in: view, window: window)
|
|
try await Task.sleep(for: .milliseconds(30))
|
|
#expect(opened.count == index + 1, "Row click at \(point)")
|
|
}
|
|
#expect(opened == Array(repeating: server.id, count: 3))
|
|
|
|
server.point = nil
|
|
server.isConnecting = true
|
|
try await Task.sleep(for: .milliseconds(30))
|
|
try clickFromTop(CGPoint(x: 190, y: top + 59), in: view, window: window)
|
|
server.isConnecting = false
|
|
server.needsAttention = true
|
|
server.status = "Fixture connection failure"
|
|
server.journey.fail("Fixture connection failure")
|
|
try await Task.sleep(for: .milliseconds(30))
|
|
try clickFromTop(CGPoint(x: 190, y: top + 59), in: view, window: window)
|
|
server.selection.enabled = false
|
|
server.needsAttention = false
|
|
server.status = "Paused"
|
|
try await Task.sleep(for: .milliseconds(30))
|
|
try clickFromTop(CGPoint(x: 190, y: top + 59), in: view, window: window)
|
|
#expect(opened == Array(repeating: server.id, count: 6))
|
|
#expect(!openedMainWindow)
|
|
}
|
|
|
|
@Test
|
|
func choosingAHostClosesTheMenuAndRoutesItsIdentifier() async throws {
|
|
let fixture = MetricsMenuFixture()
|
|
defer { fixture.close() }
|
|
let suite = "termind.metrics-menu.tests.\(UUID())"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.set(true, forKey: DesktopSettingsKey.showMetricsInMenuBar)
|
|
let popover = NSPopover()
|
|
popover.animates = false
|
|
let controller = MetricsMenuBarController(defaults: defaults, popover: popover)
|
|
defer {
|
|
controller.stop()
|
|
defaults.removePersistentDomain(forName: suite)
|
|
}
|
|
var opened: UUID?
|
|
controller.openDetails = {
|
|
#expect(!popover.isShown)
|
|
opened = $0
|
|
}
|
|
let anchor = fixture.makeWindow()
|
|
try await activate(anchor)
|
|
controller.start(fixture.app.serverMetrics)
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
controller.togglePopover()
|
|
let server = try #require(fixture.app.serverMetrics.servers.first)
|
|
try await waitUntil { popover.isShown }
|
|
let view = try #require(popover.contentViewController?.view)
|
|
let window = try #require(view.window)
|
|
view.layoutSubtreeIfNeeded()
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
try clickFromTop(CGPoint(x: 190, y: 49 + 59), in: view, window: window)
|
|
#expect(opened == server.id)
|
|
try await waitUntil { !window.isVisible }
|
|
}
|
|
|
|
private func activate(_ window: NSWindow) async throws {
|
|
try await waitUntil {
|
|
NSRunningApplication.current.activate(options: [.activateAllWindows])
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
window.orderFrontRegardless()
|
|
window.makeMain()
|
|
window.makeKey()
|
|
return window.isKeyWindow && NSApp.isActive
|
|
}
|
|
}
|
|
|
|
private func clickFromTop(_ point: CGPoint, in view: NSView, window: NSWindow) throws {
|
|
let local = CGPoint(x: point.x, y: view.isFlipped ? point.y : view.bounds.height - point.y)
|
|
try click(view.convert(local, to: nil), in: window)
|
|
}
|
|
|
|
private func click(_ point: CGPoint, in window: NSWindow) throws {
|
|
for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] {
|
|
let event = try #require(
|
|
NSEvent.mouseEvent(
|
|
with: type, location: point, modifierFlags: [],
|
|
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
|
|
context: nil, eventNumber: 0, clickCount: 1, pressure: type == .leftMouseDown ? 1 : 0))
|
|
window.sendEvent(event)
|
|
}
|
|
}
|
|
|
|
private func waitUntil(sourceLocation: SourceLocation = #_sourceLocation, _ condition: () -> Bool) async throws {
|
|
for _ in 0..<200 {
|
|
if condition() { return }
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
try #require(condition(), sourceLocation: sourceLocation)
|
|
}
|
|
|
|
private func capture(_ window: NSWindow, name: String) throws {
|
|
let view = try #require(window.contentView)
|
|
let bitmap = try #require(view.bitmapImageRepForCachingDisplay(in: view.bounds))
|
|
view.cacheDisplay(in: view.bounds, to: bitmap)
|
|
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
|
Attachment.record(data, named: "\(name).png")
|
|
try data.write(to: FileManager.default.temporaryDirectory.appending(path: "\(name).png"))
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class MetricsMenuFixture {
|
|
let app: AppModel
|
|
var requests: [UUID] = []
|
|
private var windows: [NSWindow] = []
|
|
|
|
init(language: AppLanguage = .english) {
|
|
let hosts = HostLibraryModel.demo(language: language)
|
|
let metrics = ScreenshotDemoConfiguration(scene: .metrics, language: language).makeMetricsModel(hosts: hosts)
|
|
app = AppModel(hosts: hosts, serverMetrics: metrics)
|
|
app.mainWindows.createScene = { [weak self] in self?.requests.append($0) }
|
|
}
|
|
|
|
func makeWindow() -> NSWindow {
|
|
let window = NSWindow(
|
|
contentRect: NSRect(x: 100, y: 150, width: 800, height: 600),
|
|
styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
|
|
window.isReleasedWhenClosed = false
|
|
windows.append(window)
|
|
return window
|
|
}
|
|
|
|
func register(_ model: WindowModel, requestID: UUID? = nil) -> NSWindow {
|
|
let window = makeWindow()
|
|
app.mainWindows.register(window, model: model, requestID: requestID)
|
|
return window
|
|
}
|
|
|
|
func close() {
|
|
for window in windows {
|
|
window.orderOut(nil)
|
|
window.contentViewController = nil
|
|
window.close()
|
|
}
|
|
app.shutdownForTermination()
|
|
}
|
|
}
|