fix: stabilize native window layout transitions
This commit is contained in:
+19
-1
@@ -36,6 +36,22 @@ class StartupStreamingServer(StreamingServer):
|
||||
super()._get(request)
|
||||
|
||||
|
||||
def check_window_layout(root, app, fixtures):
|
||||
result_path = root / "window-layout.json"
|
||||
diagnostics = root / "window-layout.log"
|
||||
result = subprocess.run(
|
||||
["open", "-g", "-n", "-W", "--stderr", str(diagnostics), "-a", str(app),
|
||||
str(fixtures / "cache.mp4"), "--args", "--no-config", "--check-window-layout", str(result_path)],
|
||||
capture_output=True, text=True, timeout=40,
|
||||
)
|
||||
log = diagnostics.read_text(errors="replace") if diagnostics.exists() else ""
|
||||
report = json.loads(result_path.read_text()) if result_path.exists() else {}
|
||||
if result.returncode != 0 or not report.get("passed") or report.get("steps") != 32:
|
||||
raise RuntimeError(f"native window layout failed: {report}; {result.stdout}{result.stderr}{log}")
|
||||
print("PASS native SwiftUI window layout: repeated compact/normal transitions with queue open/closed, "
|
||||
"rapid intent changes, original window and exact normal geometry restored")
|
||||
|
||||
|
||||
def check_cli_instances(root, app, fixtures):
|
||||
cli = root / "terebi"
|
||||
cli.symlink_to(app / "Contents/MacOS/terebi-cli")
|
||||
@@ -635,7 +651,9 @@ with tempfile.TemporaryDirectory(prefix="terebi-startup-") as directory:
|
||||
if launch.returncode != 0 or not result["passed"] or result["bundleIdentifier"] != identifier:
|
||||
raise RuntimeError(f"packaged startup failed: {result}; {stdout}{stderr}")
|
||||
print("PASS packaged bootstrap: registered app PID/identity, in-process player load, actual playback and Now Playing")
|
||||
check_cli_instances(root, app, fixtures)
|
||||
check_window_layout(root, app, fixtures)
|
||||
if os.environ.get("TEREBI_CHECK_LAYOUT_ONLY") != "1":
|
||||
check_cli_instances(root, app, fixtures)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
subprocess.run(
|
||||
|
||||
@@ -5,6 +5,10 @@ import MediaPlayer
|
||||
@_cdecl("terebi_app_main")
|
||||
@MainActor
|
||||
public func checkAppStartup() {
|
||||
if let index = CommandLine.arguments.firstIndex(of: "--check-window-layout") {
|
||||
WindowLayoutCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
|
||||
return
|
||||
}
|
||||
if let configuration = Bundle.main.url(forResource: "cli-check", withExtension: "json") {
|
||||
CLIInstanceCheck.run(configuration)
|
||||
return
|
||||
@@ -12,6 +16,80 @@ public func checkAppStartup() {
|
||||
AppStartupCheck.run()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private enum WindowLayoutCheck {
|
||||
static var timer: Timer?
|
||||
static var resultURL: URL!
|
||||
static var step = 0
|
||||
static var windowIdentity: ObjectIdentifier?
|
||||
static var normalFrame = NSRect.zero
|
||||
// Infrastructure timeout only; assertions depend on native transition completion.
|
||||
static let deadline = ContinuousClock.now.advanced(by: .seconds(30))
|
||||
|
||||
static func run(result: URL) {
|
||||
resultURL = result
|
||||
_ = NSApplication.shared
|
||||
_ = NotificationCenter.default.addObserver(forName: NSApplication.willFinishLaunchingNotification,
|
||||
object: nil, queue: .main) { _ in
|
||||
MainActor.assumeIsolated { PlayerModel.shared.setVolume(0) }
|
||||
}
|
||||
_ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification,
|
||||
object: nil, queue: .main) { _ in
|
||||
MainActor.assumeIsolated {
|
||||
for window in NSApp.windows { window.alphaValue = 0; window.ignoresMouseEvents = true }
|
||||
}
|
||||
}
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
|
||||
MainActor.assumeIsolated { advance() }
|
||||
}
|
||||
terebiAppMain()
|
||||
}
|
||||
|
||||
static func finish(_ error: String? = nil) {
|
||||
timer?.invalidate()
|
||||
let result: [String: Any] = ["passed": error == nil, "error": error ?? "", "steps": step]
|
||||
try! JSONSerialization.data(withJSONObject: result).write(to: resultURL, options: .atomic)
|
||||
PlayerModel.shared.shutdown()
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
|
||||
static func advance() {
|
||||
let model = PlayerModel.shared
|
||||
guard ContinuousClock.now < deadline else { finish("Window layout did not settle at step \(step)"); return }
|
||||
guard model.canTogglePlayback, model.state.has_video, !model.compactWindow.isTransitioning,
|
||||
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
|
||||
if windowIdentity == nil { windowIdentity = ObjectIdentifier(window) }
|
||||
guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return }
|
||||
window.layoutIfNeeded()
|
||||
switch step % 8 {
|
||||
case 0, 4:
|
||||
if step > 0 {
|
||||
let actual = window.frame
|
||||
guard abs(actual.minX - normalFrame.minX) < 1, abs(actual.minY - normalFrame.minY) < 1,
|
||||
abs(actual.width - normalFrame.width) < 1, abs(actual.height - normalFrame.height) < 1,
|
||||
!model.isCompact else { finish("Normal geometry was not restored at step \(step): \(actual)"); return }
|
||||
}
|
||||
if step == 32 { finish(); return }
|
||||
model.showsQueue = step % 8 == 4
|
||||
case 1, 5:
|
||||
window.setFrame(NSRect(x: 100, y: 200, width: 763, height: 368), display: true)
|
||||
normalFrame = window.frame
|
||||
case 2, 6:
|
||||
// Repeated intent changes must keep the original restoration frame.
|
||||
model.toggleCompact()
|
||||
model.toggleCompact()
|
||||
model.toggleCompact()
|
||||
default:
|
||||
guard model.isCompact, window.frame.width >= 160, window.frame.height >= 90,
|
||||
abs(window.frame.width / window.frame.height - model.state.video_aspect_ratio) < 0.01 else {
|
||||
finish("Compact geometry did not settle at step \(step): \(window.frame)"); return
|
||||
}
|
||||
model.toggleCompact()
|
||||
}
|
||||
step += 1
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private enum AppStartupCheck {
|
||||
static var model: PlayerModel?
|
||||
|
||||
Reference in New Issue
Block a user