Update screenshots

This commit is contained in:
Kaede Akino committed 2026-09-19 10:16:26 +08:00
1 parent d1afa1d744
commit 637e87761c
5 files changed
+194 -30

No files matched your search

+4
View File
@@ -21,6 +21,10 @@ dist/
*.tsbuildinfo
*.tgz
# Python bytecode caches
__pycache__/
*.py[cod]
# Local environment files
.env
.env.*
+80
View File
@@ -72,6 +72,8 @@ The runner defaults to the iOS/iPadOS 26.5 simulator runtime so installing a new
runtime cannot silently change the marketing UI or select another same-name
device. Set `TERMIND_MARKETING_RUNTIME` to a full CoreSimulator runtime identifier
only when intentionally updating the runtime used for a capture set.
An iPad-only selection builds on the selected iPad and does not require an iPhone
simulator to be installed.
Each locale launches a separate test process with matching Apple language, region,
and locale arguments. `TERMIND_MARKETING_LOCALE` selects that process's fixture
@@ -119,10 +121,32 @@ simulated sidebar or back buttons to the top-level Metrics and Assistant pages.
Regenerate only the matching nine iPad device frames and App Store boards 04-06;
first boards use iPad SFTP and remain unchanged.
The iPad SSH and SFTP fixtures mount the production `MobileRootView` and workspace
pages, with deterministic in-memory data supplied through the existing
`MobileWorkspaceConnectionServices`. They select Workspaces, collapse the native
sidebar, and show the current top tabs, workspace switcher and action menu. Keep
the iPhone-only legacy scene wrappers out of this iPad path. Terminal output stays
English in every locale. The approved topbar capture set uses iPadOS 27.0:
```sh
TERMIND_MARKETING_SCENES=ssh,sftp \
TERMIND_MARKETING_DEVICES=ipad \
TERMIND_MARKETING_RUNTIME=com.apple.CoreSimulator.SimRuntime.iOS-27-0 \
Scripts/run-mobile-marketing-screenshots.sh
```
Refresh those six device frames and iPad boards 02–03, plus the iPad SFTP image in
all nine three-device first boards. Other frames and boards are outside that set.
The runner sets a deterministic `9:41` status bar, charged battery, Wi-Fi, and
cellular state. It sets the iPad orientation and captures its landscape display
with `devicectl`, verifying `2752x2064` before acknowledging each capture. The
iPhone uses `simctl` screenshots. No post-capture rotation is required.
Each locale process receives a fresh `TERMIND_MARKETING_CAPTURE_ID`; the runner
ignores ready markers from earlier processes. Captures are staged under the
derived-data directory and copied to the source tree only after the tests and
expected capture count pass. Back up raw images and a SHA-256 manifest before
running a refresh.
Whenever SFTP, Metrics, Assistant navigation, or system chrome changes, recapture
the affected device and every locale. A marketing fixture may be updated to expose
@@ -136,6 +160,8 @@ mounts a second RootView or duplicates a terminal/transcript surface. Before it
publishes the ready marker, it requires both `NSApp.isActive` and
`window.isKeyWindow`, allows the focused appearance to render, and requires native
2× backing scale. Focus is checked again when the capture is acknowledged.
The focus retry loop also requires activation to survive the 300 ms appearance
render interval. Do not run another Mac app activation alongside a capture.
Generate all three localized Workspace sources:
@@ -459,3 +485,57 @@ The following failures persisted in unchanged tests:
Logs and `.xcresult` bundles remain in `.build/marketing-forwarding-refresh/`.
No production behavior or unrelated tests were changed to address these failures.
The later 2026-09-19 navigation topbar refresh replaced Mac Metrics overview and
Vault in all three languages (`2360x1480`) and iPad SSH/SFTP in all three languages
(`2752x2064`, iPadOS 27.0, iPad Pro 13-inch M5 landscape, 9:41). All six Mac sources
passed active/key checks before capture and after acknowledgement; visual and
pixel checks confirmed colored traffic lights and the current `plus` icons.
The iPad captures use the actual root/workspace views, with Workspaces selected,
the sidebar collapsed and the native topbar and bottom controls fully visible.
Only the six matching iPad device frames (`2960x2290`) were regenerated. In the
verified `Termind` Figma file, the refresh replaced Mac boards 03/05, iPad boards
02/03 and the iPad SFTP image in every platform's board 01, across all locales.
English was reviewed first. Readback confirmed all 21 image fills match their
local source PNG bytes, and every other reported node property stayed unchanged,
including copy, fonts, geometry, FIT, shadows and stacking. The 21 JPG exports use
`save_screenshots` at 1×, retain the original dimensions and have opaque RGB pixels
with embedded sRGB profiles. Profile metadata was added without re-encoding or
changing decoded pixels.
The complete pre-capture image backup, SHA-256 manifests, focus evidence, Figma
readbacks, contact sheets and validation logs are in
`.build/marketing-topbar-refresh/`. Exact-path auditing found 39 changed and 93
unchanged images, with no additions or removals: 45 captures, 33 device frames and
54 App Store JPGs. A profile audit also found 23 untouched baseline JPGs without an
embedded ICC profile; this corrects the earlier blanket statement about all 54
files being tagged. Those 23 originals remain byte-identical to the backup.
The website rebuilt successfully (17 pages, no errors or warnings), using the
existing image references. The English, Simplified Chinese and Traditional Chinese
homepages were checked visually, including their hero and SFTP images; `/zh/`
redirects to `/zh-hans/`. Full Swift formatting, Chinese localization, screenshot
shell syntax, generator Python syntax and whitespace checks passed. The Xcode
project and resolved package versions remain unchanged. Python bytecode caches
are now ignored by Git.
All nine opt-in capture runs passed (six Mac runs and three iPad locale runs,
producing 12 sources). The complete TFM run finished with 1,719 passed tests,
one failed test and 23 skipped tests. Its only failure is the previously recorded
`MetricsDetailToolbarTests.legacyCompactSearchOpensTheNativeFieldAndKeepsItsQueryWhenWidened`
in all three language cases. `ScreenshotDemoTests`, `AppLanguageTests` and all
352 shared-framework tests passed.
The TFI run completed 328 app tests and all 352 shared-framework tests: 679 passed
tests and one failed test. The unchanged
`MobileAssistantInteractionRenderingTests.interactionTextRemainsVisibleOnMaterial`
reported 12 OCR mismatches on iPadOS 27.0, including `Deployment` read as
`Deplovment` and `prod.example` read as `oroo.examole`. The existing capture build
excluded `MobileNavigationParityTests`; a normal pinned build and separate run of
all four tests passed, and the obsolete exclusion was removed from the runner.
After both main suites had finished, Xcode stalled collecting simulator diagnostics;
only its `simctl diagnose` child was stopped so the complete `.xcresult` could
finalize. No tests were interrupted. Both full-run result bundles and the
navigation supplement are retained in the evidence directory. Product code and
unrelated test assertions were not changed to address the failures.
+30 -12
View File
@@ -54,10 +54,12 @@ run_capture() {
zh-Hans) region=CN; apple_locale=zh_CN ;;
zh-Hant) region=TW; apple_locale=zh_TW ;;
esac
local capture_id="$(uuidgen)"
local localized_xctestrun="$derived_data/Build/Products/Marketing-TFI-$language.xctestrun"
cp "$marketing_xctestrun" "$localized_xctestrun"
/usr/libexec/PlistBuddy \
-c "Add :TFITests:EnvironmentVariables:TERMIND_MARKETING_LOCALE string $language" \
-c "Add :TFITests:EnvironmentVariables:TERMIND_MARKETING_CAPTURE_ID string $capture_id" \
-c "Set :TFITests:TestLanguage $language" \
-c "Set :TFITests:TestRegion $region" \
-c 'Add :TFITests:CommandLineArguments:0 string -AppleLanguages' \
@@ -74,25 +76,28 @@ run_capture() {
local test_pid=$!
local captures=0
local last_token=""
local capture_directory="$derived_data/captures/$device_id-$language"
local -a captured_paths destinations
mkdir -p "$capture_directory"
while kill -0 "$test_pid" 2>/dev/null; do
local app_container
app_container="$(xcrun simctl get_app_container "$device_id" com.akinokaede.termind data 2>/dev/null || true)"
local ready="$app_container/Documents/marketing-capture-control/ready"
if [[ -n "$app_container" && -f "$ready" ]]; then
local feature device locale token
IFS=$'\t' read -r feature device locale token < "$ready"
if [[ -n "$token" && "$token" != "$last_token" ]]; then
local feature device locale token request_capture_id
IFS=$'\t' read -r feature device locale token request_capture_id < "$ready"
if [[ "$request_capture_id" == "$capture_id" && -n "$token" && "$token" != "$last_token" ]]; then
local destination="$output_directory/$feature/$device-$locale.png"
mkdir -p "${destination:h}"
local captured="$capture_directory/$feature.png"
if [[ "$device" == "ipad" ]]; then
rm -f "$destination"
rm -f "$captured"
xcrun devicectl device capture screenshot \
--device "$device_id" \
--destination "$destination" >/dev/null
--destination "$captured" >/dev/null
local width height
width="$(sips -g pixelWidth "$destination" | awk '/pixelWidth/ { print $2 }')"
height="$(sips -g pixelHeight "$destination" | awk '/pixelHeight/ { print $2 }')"
width="$(sips -g pixelWidth "$captured" | awk '/pixelWidth/ { print $2 }')"
height="$(sips -g pixelHeight "$captured" | awk '/pixelHeight/ { print $2 }')"
if [[ "$width" != "2752" || "$height" != "2064" ]]; then
print -u2 "Unexpected iPad screenshot dimensions: ${width}x${height}"
kill "$test_pid" 2>/dev/null || true
@@ -101,8 +106,10 @@ run_capture() {
exit 70
fi
else
xcrun simctl io "$device_id" screenshot --type=png "$destination" >/dev/null
xcrun simctl io "$device_id" screenshot --type=png "$captured" >/dev/null
fi
captured_paths+=("$captured")
destinations+=("$destination")
touch "$app_container/Documents/marketing-capture-control/ack-$token"
last_token="$token"
(( captures += 1 ))
@@ -123,12 +130,24 @@ run_capture() {
print -u2 "Expected $expected captures for $name ($language), received $captures. See $test_log"
exit 70
fi
for (( index = 1; index <= ${#captured_paths}; index++ )); do
mkdir -p "${destinations[$index]:h}"
cp "$captured_paths[$index]" "$destinations[$index]"
done
}
mkdir -p "$output_directory"
build_device_id="$(find_device "iPhone 17 Pro Max")"
if [[ ",$marketing_devices," == *,iphone,* ]]; then
build_device_name="iPhone 17 Pro Max"
elif [[ ",$marketing_devices," == *,ipad,* ]]; then
build_device_name="iPad Pro 13-inch (M5)"
else
print -u2 "Select at least one marketing device: iphone,ipad"
exit 64
fi
build_device_id="$(find_device "$build_device_name")"
if [[ -z "$build_device_id" ]]; then
print -u2 "Required simulator is unavailable: iPhone 17 Pro Max ($marketing_runtime)"
print -u2 "Required simulator is unavailable: $build_device_name ($marketing_runtime)"
exit 69
fi
xcodebuild \
@@ -137,7 +156,6 @@ xcodebuild \
-destination "id=$build_device_id" \
-onlyUsePackageVersionsFromResolvedFile \
-derivedDataPath "$derived_data" \
EXCLUDED_SOURCE_FILE_NAMES=MobileNavigationParityTests.swift \
build-for-testing
generated_xctestrun="$(find "$derived_data/Build/Products" -maxdepth 1 -name 'TFI_*.xctestrun' -print -quit)"
+70 -11
View File
@@ -4,6 +4,7 @@ import SwiftUI
import TermindApplication
import TermindDomain
import TermindPresentation
import TermindRemoteInfrastructure
import Testing
import UIKit
@@ -34,7 +35,9 @@ struct MobileMarketingScreenshotTests {
}
}
if includes("sftp") {
if includes("sftp"), UIDevice.current.userInterfaceIdiom == .pad {
try await captureWorkspace(.sftp, language: language)
} else if includes("sftp") {
let sftp = MobileSFTPSessionModel()
try await sftp.attach(
fileSystem: MarketingFileSystem(), path: "/srv/termind/releases/2026.09",
@@ -63,8 +66,10 @@ struct MobileMarketingScreenshotTests {
}
}
if includes("ssh") {
let terminal = makeTerminal(language: language)
if includes("ssh"), UIDevice.current.userInterfaceIdiom == .pad {
try await captureWorkspace(.ssh, language: language)
} else if includes("ssh") {
let terminal = makeTerminal()
try await capture("ssh", language: language) {
NavigationStack { MarketingTerminalScene(terminal: terminal, language: language) }
}
@@ -96,8 +101,10 @@ struct MobileMarketingScreenshotTests {
}
}
private func makeApp(language: String) throws -> MobileAppModel {
let app = try MobileAppModel(inMemory: true)
private func makeApp(
language: String, connectionServices: MobileWorkspaceConnectionServices = .live
) throws -> MobileAppModel {
let app = try MobileAppModel(inMemory: true, workspaceConnectionServices: connectionServices)
let production = HostGroup(name: text(language, "Production", "生产环境", "正式環境"))
let edge = HostGroup(name: text(language, "Edge", "边缘节点", "邊緣節點"))
app.library.upsert(production)
@@ -122,12 +129,52 @@ struct MobileMarketingScreenshotTests {
return app
}
private func makeTerminal(language: String) -> TerminalViewState {
private func captureWorkspace(_ kind: ConnectionHistoryEntry.Kind, language: String) async throws {
let output = terminalOutput
let services = MobileWorkspaceConnectionServices(
makeSSHSession: { receive, exit, shellIntegration in
receive(output)
return SSHSessionActor(output: receive, exit: exit, shellIntegration: shellIntegration)
},
connectSSH: { _, _, _, _, _, _ in },
connectSFTP: { _, _, _ in MarketingFileSystem() },
writeSSH: { _, _ in }
)
let app = try makeApp(language: language, connectionServices: services)
defer { app.closeAll() }
let identity = Identity(name: "Deploy", username: "deploy")
try app.library.upsert(identity, passwordUpdate: .replace("marketing-fixture-only"))
var host = try #require(app.library.hosts.first(where: { $0.name == "web-01" }))
host.identityID = identity.id
app.library.upsert(host)
let id = try #require(app.open(hostID: host.id, kind: kind))
let workspace = try #require(app.workspaces.first(where: { $0.id == id }))
for _ in 0..<200 where !workspace.connected {
try await Task.sleep(for: .milliseconds(25))
}
try #require(workspace.connected)
if kind == .sftp {
#expect(app.renameWorkspace(id, to: text(language, "Production files", "生产文件", "正式環境檔案")))
try #require(workspace.sftpSession.files.count == 6)
}
let scene = MobileSceneModel(app: app)
scene.selectWorkspace(id)
try await capture(kind == .ssh ? "ssh" : "sftp", language: language) {
MobileRootView(app: app, scene: scene)
}
}
private func makeTerminal() -> TerminalViewState {
let backend = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
let terminal = TerminalViewState(configSource: .none)
terminal.configuration = TerminalSurfaceOptions(backend: .inMemory(backend), resizeThrottleMilliseconds: 0)
let surface = MobileTerminalSurface()
terminal.makePlatformView = { surface.view }
backend.receive(terminalOutput)
return terminal
}
private var terminalOutput: Data {
let prompt = "\u{001B}[36mdeploy@web-01\u{001B}[0m \u{001B}[34m/var/log/nginx\u{001B}[0m %"
let lines = [
"Last login: Sun Sep 13 14:20:06 from 10.0.0.12",
@@ -141,8 +188,7 @@ struct MobileMarketingScreenshotTests {
"10.0.4.31 GET /assets/app 304 2ms",
prompt,
]
backend.receive(Data(lines.joined(separator: "\r\n").utf8))
return terminal
return Data(lines.joined(separator: "\r\n").utf8)
}
private func makeMetrics(language: String) -> [ServerMetric] {
@@ -203,7 +249,7 @@ struct MobileMarketingScreenshotTests {
if isPad, let tabs = tabController(in: controller) {
// Keep each scene's navigation presentation independent of restored simulator state.
tabs.sidebar.isHidden = name == "metrics"
tabs.sidebar.isHidden = ["metrics", "ssh", "sftp"].contains(name)
try await Task.sleep(for: .milliseconds(900))
}
@@ -247,7 +293,9 @@ struct MobileMarketingScreenshotTests {
let locale = language == "zh-Hans" ? "zh-hans" : language == "zh-Hant" ? "zh-hant" : "en"
let ready = control.appending(path: "ready")
let ack = control.appending(path: "ack-\(token)")
try "\(name)\t\(device)\t\(locale)\t\(token)\n".write(to: ready, atomically: true, encoding: .utf8)
let captureID = try #require(ProcessInfo.processInfo.environment["TERMIND_MARKETING_CAPTURE_ID"])
try "\(name)\t\(device)\t\(locale)\t\(token)\t\(captureID)\n".write(
to: ready, atomically: true, encoding: .utf8)
for _ in 0..<300 {
if FileManager.default.fileExists(atPath: ack.path) {
@@ -381,7 +429,7 @@ private struct MarketingMetricsScene: View {
}
}
private actor MarketingFileSystem: RemoteFileSystem {
private actor MarketingFileSystem: SFTPClientProtocol {
private let files: [RemoteFile] = [
RemoteFile(
path: "/srv/termind/releases/2026.09/bin", name: "bin", kind: .directory, size: 0,
@@ -404,6 +452,9 @@ private actor MarketingFileSystem: RemoteFileSystem {
modifiedAt: Date(timeIntervalSince1970: 1_788_942_000), permissions: 0o755, ownerUID: 501, ownerGID: 20),
]
func connect(using _: SSHConnectionConfiguration, progress _: SSHRouteProgressHandler?) async throws {}
func disconnect() async {}
func homeDirectory() async throws -> String { "/srv/termind/releases/2026.09" }
func listDirectory(at _: String) async throws -> [RemoteFile] { files }
func attributes(at _: String) async throws -> RemoteFile? { nil }
func readFile(at _: String, maximumBytes _: Int) async throws -> Data { Data() }
@@ -413,4 +464,12 @@ private actor MarketingFileSystem: RemoteFileSystem {
func rename(from _: String, to _: String, overwrite _: Bool) async throws {}
func removeFile(at _: String) async throws {}
func removeDirectory(at _: String) async throws {}
func uploadFile(
from _: URL, to _: String, overwrite _: Bool, cancellation _: TransferCancellationToken,
progress _: @escaping @Sendable (SFTPProgress) -> Void
) async throws {}
func downloadFile(
from _: String, to _: URL, overwrite _: Bool, cancellation _: TransferCancellationToken,
progress _: @escaping @Sendable (SFTPProgress) -> Void
) async throws {}
}
+10 -7
View File
@@ -27,6 +27,10 @@ import Testing
window.contentView?.layoutSubtreeIfNeeded()
try await Task.sleep(for: .seconds(1))
window.contentView?.layoutSubtreeIfNeeded()
if configuration.scene == .hosts {
window.initialFirstResponder = nil
window.makeFirstResponder(nil)
}
for _ in 0..<100 {
NSRunningApplication.current.activate(options: [.activateAllWindows])
@@ -34,19 +38,18 @@ import Testing
window.orderFrontRegardless()
window.makeMain()
window.makeKey()
if NSApp.isActive, window.isKeyWindow { break }
if NSApp.isActive, window.isKeyWindow {
// 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))
if NSApp.isActive, window.isKeyWindow { break }
}
try await Task.sleep(for: .milliseconds(50))
}
guard NSApp.isActive, window.isKeyWindow else {
Issue.record("Marketing window could not become the focused key window")
return
}
if configuration.scene == .hosts {
window.initialFirstResponder = nil
window.makeFirstResponder(nil)
}
// Allow the active appearance to render before the external capture.
try await Task.sleep(for: .milliseconds(300))
try #require(NSApp.isActive && window.isKeyWindow)
try #require(window.backingScaleFactor == 2)