From 80b4641af219a32bf53b38d136df489ddf4be979 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Sun, 27 Sep 2026 23:51:30 +0800 Subject: [PATCH] docs: update screenshots --- Scripts/run-mac-marketing-screenshots.sh | 2 +- TFM/App/ScreenshotDemo.swift | 25 ++++++++++-- TFM/App/TermindApp.swift | 10 +++-- .../FeatureModels/HostLibraryModel+Demo.swift | 6 ++- TFMTests/MacMarketingScreenshotTests.swift | 33 +++++++++++++++- TFMTests/ScreenshotDemoTests.swift | 39 ++++++++++++++++++- 6 files changed, 103 insertions(+), 12 deletions(-) diff --git a/Scripts/run-mac-marketing-screenshots.sh b/Scripts/run-mac-marketing-screenshots.sh index 02e84a79..5f894192 100755 --- a/Scripts/run-mac-marketing-screenshots.sh +++ b/Scripts/run-mac-marketing-screenshots.sh @@ -100,7 +100,7 @@ capture() { local_test_pid=$! local ready_payload="" - for _ in {1..600}; do + for _ in {1..2400}; do ready_payload="$( rg -o 'TERMIND_MAC_MARKETING_READY .*' "$test_log" \ | tail -n 1 \ diff --git a/TFM/App/ScreenshotDemo.swift b/TFM/App/ScreenshotDemo.swift index 6bc06e73..8bc33d25 100644 --- a/TFM/App/ScreenshotDemo.swift +++ b/TFM/App/ScreenshotDemo.swift @@ -97,6 +97,18 @@ import TermindApplication } } + func makeHostLibrary() -> HostLibraryModel { + let hosts = HostLibraryModel.demo(language: language, includesConnectionActivity: scene != .hosts) + if scene == .hosts { + // Seed once at app launch, matching the two workspaces below. Window construction + // runs inside SwiftUI rendering and must not mutate the observed host library. + for host in hosts.hosts.prefix(2) { + hosts.setStatus(.connected, for: host.id) + } + } + return hosts + } + func makeWindow(hosts: HostLibraryModel) -> WindowModel { switch scene { case .workspace: @@ -106,7 +118,7 @@ import TermindApplication case .agent: return agentWindow(hosts: hosts) case .hosts: - return hostsWindow() + return hostsWindow(hosts: hosts) case .appSettings: let window = WindowModel() window.selectedDestination = .settings @@ -122,8 +134,15 @@ import TermindApplication } } - private func hostsWindow() -> WindowModel { - let window = WindowModel(vaultDestination: .hosts) + private func hostsWindow(hosts: HostLibraryModel) -> WindowModel { + let workspaces = hosts.hosts.prefix(2).map { host in + let session = SSHSessionModel.demo(host: host) + let workspace = Workspace(pane: Pane(.sshSession(session))) + workspace.customTitle = host.name + return workspace + } + let window = WindowModel(workspaces: workspaces, vaultDestination: .hosts) + window.selectedDestination = .vault window.showsSidebar = true window.sidebarMode = .navigation return window diff --git a/TFM/App/TermindApp.swift b/TFM/App/TermindApp.swift index 67196f34..2b18a387 100644 --- a/TFM/App/TermindApp.swift +++ b/TFM/App/TermindApp.swift @@ -37,6 +37,9 @@ struct TermindApp: App { private let modelContainer: ModelContainer @State private var app: AppModel private let launchLanguage: AppLanguage + #if DEBUG + @State private var screenshotWindow: WindowModel? + #endif init() { ContextAttachmentOpener.cleanPreviousSession() @@ -58,13 +61,14 @@ struct TermindApp: App { modelContainer = container #if DEBUG if let screenshotDemo { - let hosts = HostLibraryModel.demo(language: screenshotDemo.language) + let hosts = screenshotDemo.makeHostLibrary() let app = AppModel( hosts: hosts, ai: .demo(), proAccess: .unrestrictedForTesting(), onboarding: OnboardingController(automaticPresentationEnabled: false), serverMetrics: screenshotDemo.makeMetricsModel(hosts: hosts) ) _app = State(initialValue: app) + _screenshotWindow = State(initialValue: screenshotDemo.makeWindow(hosts: hosts)) return } #endif @@ -205,8 +209,8 @@ struct TermindApp: App { private func makeWindow(requestID: UUID?) -> WindowModel { #if DEBUG - if requestID == nil, let demo = ScreenshotDemoConfiguration.active { - return demo.makeWindow(hosts: app.hostLibrary) + if requestID == nil, let screenshotWindow { + return screenshotWindow } #endif let window = WindowModel() diff --git a/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift b/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift index 1193fb9e..3390d282 100644 --- a/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift +++ b/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift @@ -9,7 +9,7 @@ import struct TermindDomain.Host extension HostLibraryModel { /// Seed data for P0, so the sidebar and workspace list can be exercised before any /// persistence or networking exists. - static func demo(language: AppLanguage = .english) -> HostLibraryModel { + static func demo(language: AppLanguage = .english, includesConnectionActivity: Bool = true) -> HostLibraryModel { func text(_ en: String, _ zhHans: String, _ zhHant: String) -> String { switch language { case .simplifiedChinese: zhHans @@ -233,10 +233,12 @@ extension HostLibraryModel { snippets: snippets, snippetPackages: [diagnostics, webOperations], portForwardings: portForwardings, - sftpTransfers: sftpTransfers, + sftpTransfers: includesConnectionActivity ? sftpTransfers : [], knownHosts: knownHosts ) + guard includesConnectionActivity else { return store } + // A spread of connection states so Vault and Welcome Host cards can be // checked against every dot color without a server. store.setStatus(.connected, for: web01.id) diff --git a/TFMTests/MacMarketingScreenshotTests.swift b/TFMTests/MacMarketingScreenshotTests.swift index a134a561..a4bba9f4 100644 --- a/TFMTests/MacMarketingScreenshotTests.swift +++ b/TFMTests/MacMarketingScreenshotTests.swift @@ -32,13 +32,27 @@ import Testing window.makeFirstResponder(nil) } - for _ in 0..<100 { + // Keep real pointer hover out of every capture without changing production row styling. + let originalPointer = NSEvent.mouseLocation + let desktopTop = try #require(NSScreen.screens.first).frame.maxY + func movePointer(to point: NSPoint) { + let location = CGPoint(x: point.x, y: desktopTop - point.y) + CGWarpMouseCursorPosition(location) + CGEvent( + mouseEventSource: nil, mouseType: .mouseMoved, + mouseCursorPosition: location, mouseButton: .left + )?.post(tap: .cghidEventTap) + } + defer { movePointer(to: originalPointer) } + + for _ in 0..<3600 { NSRunningApplication.current.activate(options: [.activateAllWindows]) NSApp.activate(ignoringOtherApps: true) window.orderFrontRegardless() window.makeMain() window.makeKey() if NSApp.isActive, window.isKeyWindow { + movePointer(to: NSPoint(x: window.frame.midX, y: window.frame.maxY + 16)) // Activation can be transient while test hosts launch and exit. // Require the focused appearance to stay active through rendering. try await Task.sleep(for: .milliseconds(300)) @@ -47,11 +61,25 @@ import Testing try await Task.sleep(for: .milliseconds(50)) } guard NSApp.isActive, window.isKeyWindow else { - Issue.record("Marketing window could not become the focused key window") + let frontmost = NSWorkspace.shared.frontmostApplication + let diagnostic = + "Marketing window could not become the focused key window: " + + "active=\(NSApp.isActive), key=\(window.isKeyWindow), " + + "frontmost=\(frontmost?.localizedName ?? "none") " + + "pid=\(frontmost?.processIdentifier ?? -1)" + Issue.record("\(diagnostic)") return } try #require(NSApp.isActive && window.isKeyWindow) try #require(window.backingScaleFactor == 2) + let pointer = NSEvent.mouseLocation + let contentFrame = window.convertToScreen(window.contentLayoutRect) + try #require(!contentFrame.contains(pointer), "Pointer must be outside the content before capture") + // Once hover has cleared, prevent pointer movement during the capture interval + // from highlighting a card or file row. Restore normal input after the test. + let ignoredMouseEvents = window.ignoresMouseEvents + window.ignoresMouseEvents = true + defer { window.ignoresMouseEvents = ignoredMouseEvents } let token = UUID().uuidString let scene = configuration.scene.rawValue @@ -67,6 +95,7 @@ import Testing // acknowledging files inside another app's container. try await Task.sleep(for: .seconds(5)) #expect(NSApp.isActive && window.isKeyWindow, "Capture window lost focus before the capture interval ended") + #expect(window.ignoresMouseEvents, "Capture window must not accept hover events") } } #endif diff --git a/TFMTests/ScreenshotDemoTests.swift b/TFMTests/ScreenshotDemoTests.swift index 1674a6c7..ec251d99 100644 --- a/TFMTests/ScreenshotDemoTests.swift +++ b/TFMTests/ScreenshotDemoTests.swift @@ -1,3 +1,4 @@ +import Foundation import TermindApplication import TermindDomain import TermindInfrastructure @@ -86,7 +87,7 @@ import TermindRemoteInfrastructure let configuration = ScreenshotDemoConfiguration( scene: .hosts, language: .english ) - let window = configuration.makeWindow(hosts: .demo()) + let window = configuration.makeWindow(hosts: configuration.makeHostLibrary()) #expect(window.selectedDestination == .vault) #expect(window.vaultDestination == .hosts) @@ -94,6 +95,42 @@ import TermindRemoteInfrastructure #expect(window.sidebarMode == .navigation) } + @Test + func vaultCaptureStatusesMatchItsTwoWorkspacesInEveryLanguage() throws { + for language in [AppLanguage.english, .simplifiedChinese, .traditionalChinese] { + let configuration = ScreenshotDemoConfiguration(scene: .hosts, language: language) + let hosts = configuration.makeHostLibrary() + let window = configuration.makeWindow(hosts: hosts) + + #expect(window.workspaces.count == 2) + #expect(hosts.sftpTransfers.isEmpty) + #expect(!hosts.hosts.isEmpty) + var connectedHostIDs: Set = [] + for workspace in window.workspaces { + #expect(workspace.panes.count == 1) + let session = try #require(workspace.panes.first?.sshSession) + #expect(session.status == .connected) + connectedHostIDs.insert(session.hostID) + } + #expect(connectedHostIDs.count == 2) + for host in hosts.hosts { + let expectedStatus: ConnectionStatus = + connectedHostIDs.contains(host.id) ? .connected : .disconnected + #expect(hosts.status(of: host.id) == expectedStatus) + } + } + } + + @Test + func otherCaptureScenesKeepTheirConnectionActivity() { + for scene in ScreenshotDemoConfiguration.Scene.allCases where scene != .hosts { + let hosts = ScreenshotDemoConfiguration(scene: scene, language: .english).makeHostLibrary() + #expect(!hosts.sftpTransfers.isEmpty) + #expect(hosts.hosts.contains { hosts.status(of: $0.id) == .connected }) + #expect(hosts.hosts.contains { hosts.status(of: $0.id) == .connecting }) + } + } + @Test func appSettingsSceneUsesTheProductionSettingsRouteInEveryLanguage() { for language in [AppLanguage.english, .simplifiedChinese, .traditionalChinese] {