From cf3350e931c0b99b35fa508ea1653922b7bbaf5e Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Wed, 7 Oct 2026 19:32:33 +0200 Subject: [PATCH 1/2] [Bugfix] Keep the editor's menu bar when SwiftUI installs its own Since #153 the editor is a SwiftUI App, for the space the Apple Vision Pro preview shows, with its menus still built by the AppKit delegate. SwiftUI installs a menu bar of its own from its default commands; on macOS 26 it does so after the delegate has set the editor's, and the bar then reads Edit, View, Window and Help: no File menu, none of the editor's View items, and the app menu named after the Info.plist. On macOS 27 the editor's menu survived, which is why the change was not seen before the merge. EditorMainMenuKeeper installs the editor's menu and watches the application's main menu; whenever another menu replaces it, the editor's is put back on the next turn of the run loop (logged once). Five tests on the real NSApplication, one of them showing that without the keeper another menu stays. --- .../Editor/Window/EditorMainMenuKeeper.swift | 52 +++++++++++ Sources/UntoldEditor/UntoldEditorApp.swift | 4 +- .../EditorMainMenuKeeperTests.swift | 87 +++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift create mode 100644 Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift diff --git a/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift b/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift new file mode 100644 index 0000000..ac2b4f5 --- /dev/null +++ b/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift @@ -0,0 +1,52 @@ +// +// EditorMainMenuKeeper.swift +// UntoldEditor +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +import AppKit +import UntoldEngine + +/// Keeps the editor's menu bar in place. The editor is a SwiftUI `App`, for +/// the space the Apple Vision Pro preview shows, with its menus built by its +/// AppKit delegate. SwiftUI installs a menu bar of its own, from its default +/// commands, and on macOS 26 it does so after the delegate has set the +/// editor's: the bar then reads Edit, View, Window and Help, without the +/// editor's File menu and View items. The keeper installs the editor's menu +/// and puts it back whenever something else replaces it. Made and used on +/// the main thread, where AppKit sets the menu bar. +final class EditorMainMenuKeeper: @unchecked Sendable { + /// The editor's menu bar. + let menu: NSMenu + /// How many times another menu was put aside; for the tests and the log. + private(set) var restorations = 0 + private let application: NSApplication + private var observation: NSKeyValueObservation? + + init(menu: NSMenu, application: NSApplication = .shared) { + self.menu = menu + self.application = application + application.mainMenu = menu + observation = application.observe(\.mainMenu, options: [.new]) { [weak self] application, _ in + guard let self, application.mainMenu !== menu else { return } + // Outside the setter that is replacing it. + DispatchQueue.main.async { + self.restore() + } + } + } + + private func restore() { + guard application.mainMenu !== menu else { return } + restorations += 1 + if restorations == 1 { + Logger.log(message: "The menu bar was replaced; the editor's is put back.") + } + application.mainMenu = menu + } +} diff --git a/Sources/UntoldEditor/UntoldEditorApp.swift b/Sources/UntoldEditor/UntoldEditorApp.swift index b820dda..c88839a 100644 --- a/Sources/UntoldEditor/UntoldEditorApp.swift +++ b/Sources/UntoldEditor/UntoldEditorApp.swift @@ -54,6 +54,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { private var splatBlendCapItems: [SplatBlendCapOption: NSMenuItem] = [:] private var splatWorkingSetItems: [EditorSplatWorkingSet: NSMenuItem] = [:] private var splatLevelModeItems: [SplatLevelModeOption: NSMenuItem] = [:] + /// Installs the menu bar and keeps it in place (SwiftUI installs its own). + private var mainMenuKeeper: EditorMainMenuKeeper? func applicationDidFinishLaunching(_: Notification) { Logger.log(message: "Launching \(appName) v\(Self.editorVersion) as \(Bundle.main.bundleIdentifier ?? "an executable without an identity")") @@ -344,7 +346,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "") NSApp.windowsMenu = windowMenu - NSApp.mainMenu = mainMenu + mainMenuKeeper = EditorMainMenuKeeper(menu: mainMenu) } /// A menu item that shows or hides a panel of the docking layout; its diff --git a/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift b/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift new file mode 100644 index 0000000..9584094 --- /dev/null +++ b/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift @@ -0,0 +1,87 @@ +// +// EditorMainMenuKeeperTests.swift +// UntoldEditor +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +import AppKit +@testable import UntoldEditor +import XCTest + +/// The editor's menu bar stays in place when SwiftUI, or anything else, +/// installs another one. +@MainActor +final class EditorMainMenuKeeperTests: XCTestCase { + private var menuBefore: NSMenu? + + override func setUp() { + super.setUp() + menuBefore = NSApplication.shared.mainMenu + } + + override func tearDown() { + NSApplication.shared.mainMenu = menuBefore + super.tearDown() + } + + private func turnTheRunLoop() { + RunLoop.main.run(until: Date().addingTimeInterval(0.05)) + } + + func test_theKeeper_installsTheEditorsMenu() { + let editors = NSMenu(title: "Editor") + let keeper = EditorMainMenuKeeper(menu: editors) + + XCTAssertTrue(NSApplication.shared.mainMenu === editors) + XCTAssertEqual(keeper.restorations, 0) + } + + func test_anotherMenuInstalledLater_isPutAside() { + let editors = NSMenu(title: "Editor") + let keeper = EditorMainMenuKeeper(menu: editors) + + NSApplication.shared.mainMenu = NSMenu(title: "SwiftUI") + turnTheRunLoop() + + XCTAssertTrue(NSApplication.shared.mainMenu === editors) + XCTAssertEqual(keeper.restorations, 1) + } + + func test_theEditorsMenuSetAgain_countsAsNothing() { + let editors = NSMenu(title: "Editor") + let keeper = EditorMainMenuKeeper(menu: editors) + + NSApplication.shared.mainMenu = editors + turnTheRunLoop() + + XCTAssertEqual(keeper.restorations, 0) + } + + func test_everyReplacement_isUndone() { + let editors = NSMenu(title: "Editor") + let keeper = EditorMainMenuKeeper(menu: editors) + + for _ in 0 ..< 3 { + NSApplication.shared.mainMenu = NSMenu(title: "SwiftUI") + turnTheRunLoop() + } + + XCTAssertTrue(NSApplication.shared.mainMenu === editors) + XCTAssertEqual(keeper.restorations, 3) + } + + func test_withoutAKeeper_anotherMenuStays() { + let editors = NSMenu(title: "Editor") + NSApplication.shared.mainMenu = editors + let other = NSMenu(title: "SwiftUI") + + NSApplication.shared.mainMenu = other + turnTheRunLoop() + + XCTAssertTrue(NSApplication.shared.mainMenu === other) + } +} From 29671319059446900e40cb1732f7569eaf12adcf Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Wed, 7 Oct 2026 19:54:49 +0200 Subject: [PATCH 2/2] [Bugfix] Menu bar: move the editor's menus into the bar SwiftUI installs Putting the editor's menu back by KVO did not hold on macOS 26: SwiftUI keeps the bar it installed. The keeper no longer replaces that NSMenu; it moves the editor's menus into whatever bar is installed, takes the bar's own items out, drops the delegate that fills it in, and keeps the Window menu the editor's. It looks again when the main menu changes and after every event the application handles (NSApplication.didUpdate, didBecomeActive, NSWindow.didBecomeKey), so it does not depend on the setter being observable. Nine tests, on a bar installed before, one installed later, one refilled in place without a setter, one that fills itself in through its delegate. --- .../Editor/Window/EditorMainMenuKeeper.swift | 100 +++++++--- Sources/UntoldEditor/UntoldEditorApp.swift | 4 +- .../EditorMainMenuKeeperTests.swift | 173 +++++++++++++++--- 3 files changed, 223 insertions(+), 54 deletions(-) diff --git a/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift b/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift index ac2b4f5..d130c62 100644 --- a/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift +++ b/Sources/UntoldEditor/Editor/Window/EditorMainMenuKeeper.swift @@ -12,41 +12,95 @@ import AppKit import UntoldEngine -/// Keeps the editor's menu bar in place. The editor is a SwiftUI `App`, for -/// the space the Apple Vision Pro preview shows, with its menus built by its -/// AppKit delegate. SwiftUI installs a menu bar of its own, from its default -/// commands, and on macOS 26 it does so after the delegate has set the -/// editor's: the bar then reads Edit, View, Window and Help, without the -/// editor's File menu and View items. The keeper installs the editor's menu -/// and puts it back whenever something else replaces it. Made and used on +/// Keeps the editor's menus in the menu bar. The editor is a SwiftUI `App`, +/// for the space the Apple Vision Pro preview shows, with its menus built by +/// its AppKit delegate. SwiftUI installs a menu bar of its own, from its +/// default commands, and on macOS 26 it does so after the delegate has set +/// the editor's, and again whenever it updates: the bar then reads Edit, +/// View, Window and Help, without the editor's File menu and View items. +/// +/// The keeper does not fight over which `NSMenu` is the main menu: it moves +/// the editor's menus into whatever menu bar is installed and takes that +/// bar's own items out, so SwiftUI keeps the object it installed and the +/// user sees the editor's menus. It looks again whenever the main menu +/// changes and after every event the application handles. Made and used on /// the main thread, where AppKit sets the menu bar. final class EditorMainMenuKeeper: @unchecked Sendable { - /// The editor's menu bar. - let menu: NSMenu - /// How many times another menu was put aside; for the tests and the log. + /// The editor's menus, in order: the app menu first. + let items: [NSMenuItem] + /// The editor's Window menu, which lists the open windows. + let windowsMenu: NSMenu? + /// How many times the editor's menus were put back; for the tests and the log. private(set) var restorations = 0 private let application: NSApplication private var observation: NSKeyValueObservation? + private var observers: [NSObjectProtocol] = [] + private var checkIsScheduled = false - init(menu: NSMenu, application: NSApplication = .shared) { - self.menu = menu + init(menu: NSMenu, windowsMenu: NSMenu? = nil, application: NSApplication = .shared) { + items = menu.items + self.windowsMenu = windowsMenu self.application = application - application.mainMenu = menu - observation = application.observe(\.mainMenu, options: [.new]) { [weak self] application, _ in - guard let self, application.mainMenu !== menu else { return } - // Outside the setter that is replacing it. - DispatchQueue.main.async { - self.restore() - } + install(into: application.mainMenu ?? menu) + observation = application.observe(\.mainMenu, options: [.new]) { [weak self] _, _ in + self?.scheduleCheck() + } + let center = NotificationCenter.default + for name in [NSApplication.didUpdateNotification, NSApplication.didBecomeActiveNotification, NSWindow.didBecomeKeyNotification] { + observers.append(center.addObserver(forName: name, object: nil, queue: nil) { [weak self] _ in + self?.checkNow() + }) + } + } + + deinit { + for observer in observers { + NotificationCenter.default.removeObserver(observer) } } - private func restore() { - guard application.mainMenu !== menu else { return } + /// True while the menu bar shows the editor's menus and nothing else. + var isInPlace: Bool { + guard let bar = application.mainMenu else { return false } + return bar.items.count == items.count && zip(bar.items, items).allSatisfy { $0 === $1 } + } + + /// Outside the setter that is replacing the menu bar. + private func scheduleCheck() { + guard checkIsScheduled == false else { return } + checkIsScheduled = true + DispatchQueue.main.async { [weak self] in + self?.checkIsScheduled = false + self?.checkNow() + } + } + + /// Puts the editor's menus back when the menu bar shows something else. + func checkNow() { + guard isInPlace == false else { return } restorations += 1 if restorations == 1 { - Logger.log(message: "The menu bar was replaced; the editor's is put back.") + Logger.log(message: "The menu bar was replaced; the editor's menus are put back.") + } + install(into: application.mainMenu ?? NSMenu()) + } + + /// Makes `bar` the menu bar, with the editor's menus and nothing else. + private func install(into bar: NSMenu) { + // A menu bar of SwiftUI's fills itself in through its delegate. + bar.delegate = nil + for item in items { + item.menu?.removeItem(item) + } + bar.removeAllItems() + for item in items { + bar.addItem(item) + } + if application.mainMenu !== bar { + application.mainMenu = bar + } + if let windowsMenu, application.windowsMenu !== windowsMenu { + application.windowsMenu = windowsMenu } - application.mainMenu = menu } } diff --git a/Sources/UntoldEditor/UntoldEditorApp.swift b/Sources/UntoldEditor/UntoldEditorApp.swift index c88839a..0ad5d81 100644 --- a/Sources/UntoldEditor/UntoldEditorApp.swift +++ b/Sources/UntoldEditor/UntoldEditorApp.swift @@ -54,7 +54,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { private var splatBlendCapItems: [SplatBlendCapOption: NSMenuItem] = [:] private var splatWorkingSetItems: [EditorSplatWorkingSet: NSMenuItem] = [:] private var splatLevelModeItems: [SplatLevelModeOption: NSMenuItem] = [:] - /// Installs the menu bar and keeps it in place (SwiftUI installs its own). + /// Puts the editor's menus in the menu bar and keeps them there (SwiftUI installs its own bar). private var mainMenuKeeper: EditorMainMenuKeeper? func applicationDidFinishLaunching(_: Notification) { @@ -346,7 +346,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "") NSApp.windowsMenu = windowMenu - mainMenuKeeper = EditorMainMenuKeeper(menu: mainMenu) + mainMenuKeeper = EditorMainMenuKeeper(menu: mainMenu, windowsMenu: windowMenu) } /// A menu item that shows or hides a panel of the docking layout; its diff --git a/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift b/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift index 9584094..bbb4843 100644 --- a/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift +++ b/Tests/UntoldEditorTests/EditorMainMenuKeeperTests.swift @@ -12,76 +12,191 @@ import AppKit @testable import UntoldEditor import XCTest -/// The editor's menu bar stays in place when SwiftUI, or anything else, -/// installs another one. +/// The editor's menus stay in the menu bar when SwiftUI, or anything else, +/// installs another one or fills the installed one with its own items. @MainActor final class EditorMainMenuKeeperTests: XCTestCase { private var menuBefore: NSMenu? + private var windowsMenuBefore: NSMenu? override func setUp() { super.setUp() menuBefore = NSApplication.shared.mainMenu + windowsMenuBefore = NSApplication.shared.windowsMenu } override func tearDown() { NSApplication.shared.mainMenu = menuBefore + NSApplication.shared.windowsMenu = windowsMenuBefore super.tearDown() } - private func turnTheRunLoop() { - RunLoop.main.run(until: Date().addingTimeInterval(0.05)) + /// Turns the run loop until the keeper has looked (two seconds at most, for a + /// busy machine), or for a moment when nothing is expected to happen. + private func turnTheRunLoop(until done: (() -> Bool)? = nil) { + guard let done else { + RunLoop.main.run(until: Date().addingTimeInterval(0.2)) + return + } + let deadline = Date().addingTimeInterval(2) + repeat { + RunLoop.main.run(until: Date().addingTimeInterval(0.02)) + } while done() == false && Date() < deadline + } + + /// A menu bar as the editor builds it: three menus, the last the Window menu. + private func editorsMenu() -> (menu: NSMenu, items: [NSMenuItem], windows: NSMenu) { + let menu = NSMenu(title: "Editor") + let windows = NSMenu(title: "Window") + for title in ["Untold Engine Editor", "File", "View"] { + let item = NSMenuItem() + item.submenu = NSMenu(title: title) + menu.addItem(item) + } + let windowItem = NSMenuItem() + windowItem.submenu = windows + menu.addItem(windowItem) + return (menu, menu.items, windows) + } + + /// A menu bar as SwiftUI installs it: its own menus, filled in by a delegate. + private final class FillingDelegate: NSObject, NSMenuDelegate { + func menuNeedsUpdate(_ menu: NSMenu) { + menu.addItem(withTitle: "Help", action: nil, keyEquivalent: "") + } + } + + private func swiftUIsMenu() -> NSMenu { + let menu = NSMenu(title: "SwiftUI") + for title in ["Untold Engine Studio", "Edit", "View", "Window", "Help"] { + let item = NSMenuItem() + item.submenu = NSMenu(title: title) + menu.addItem(item) + } + return menu + } + + private func titles(_ menu: NSMenu?) -> [String] { + menu?.items.map { $0.submenu?.title ?? $0.title } ?? [] } - func test_theKeeper_installsTheEditorsMenu() { - let editors = NSMenu(title: "Editor") - let keeper = EditorMainMenuKeeper(menu: editors) + func test_withNoMenuBar_theEditorsMenuIsInstalled() { + NSApplication.shared.mainMenu = nil + let editors = editorsMenu() - XCTAssertTrue(NSApplication.shared.mainMenu === editors) + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) + + XCTAssertTrue(NSApplication.shared.mainMenu === editors.menu) + XCTAssertTrue(keeper.isInPlace) + XCTAssertTrue(NSApplication.shared.windowsMenu === editors.windows) XCTAssertEqual(keeper.restorations, 0) } - func test_anotherMenuInstalledLater_isPutAside() { - let editors = NSMenu(title: "Editor") - let keeper = EditorMainMenuKeeper(menu: editors) + func test_aMenuBarInstalledBefore_getsTheEditorsMenusAndKeepsItsIdentity() { + let swiftUIs = swiftUIsMenu() + NSApplication.shared.mainMenu = swiftUIs + let editors = editorsMenu() - NSApplication.shared.mainMenu = NSMenu(title: "SwiftUI") - turnTheRunLoop() + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) + + XCTAssertTrue(NSApplication.shared.mainMenu === swiftUIs) + XCTAssertEqual(titles(NSApplication.shared.mainMenu), ["Untold Engine Editor", "File", "View", "Window"]) + XCTAssertTrue(keeper.isInPlace) + XCTAssertEqual(editors.menu.items.count, 0, "the editor's items moved into the installed bar") + } - XCTAssertTrue(NSApplication.shared.mainMenu === editors) + func test_aMenuBarInstalledLater_getsTheEditorsMenus() { + NSApplication.shared.mainMenu = nil + let editors = editorsMenu() + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) + + let swiftUIs = swiftUIsMenu() + NSApplication.shared.mainMenu = swiftUIs + turnTheRunLoop { keeper.isInPlace } + + XCTAssertTrue(NSApplication.shared.mainMenu === swiftUIs) + XCTAssertEqual(titles(NSApplication.shared.mainMenu), ["Untold Engine Editor", "File", "View", "Window"]) + XCTAssertEqual(keeper.restorations, 1) + } + + func test_aMenuBarRefilledWithoutBeingReplaced_isPutBackAfterTheNextEvent() throws { + NSApplication.shared.mainMenu = nil + let editors = editorsMenu() + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) + let bar = try XCTUnwrap(NSApplication.shared.mainMenu) + + // What a menu bar's owner may do on an update of its own: no setter runs. + bar.removeAllItems() + for title in ["Edit", "Help"] { + bar.addItem(withTitle: title, action: nil, keyEquivalent: "") + } + XCTAssertFalse(keeper.isInPlace) + NotificationCenter.default.post(name: NSApplication.didUpdateNotification, object: NSApplication.shared) + + XCTAssertEqual(titles(NSApplication.shared.mainMenu), ["Untold Engine Editor", "File", "View", "Window"]) XCTAssertEqual(keeper.restorations, 1) } - func test_theEditorsMenuSetAgain_countsAsNothing() { - let editors = NSMenu(title: "Editor") - let keeper = EditorMainMenuKeeper(menu: editors) + func test_aMenuBarThatFillsItselfInThroughItsDelegate_isStopped() { + let swiftUIs = swiftUIsMenu() + let filling = FillingDelegate() + swiftUIs.delegate = filling + NSApplication.shared.mainMenu = swiftUIs + let editors = editorsMenu() + + _ = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) + + XCTAssertNil(swiftUIs.delegate) + } + + func test_theWindowsMenu_staysTheEditors() { + NSApplication.shared.mainMenu = nil + let editors = editorsMenu() + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) + + let swiftUIs = swiftUIsMenu() + NSApplication.shared.windowsMenu = swiftUIs.items[3].submenu + NSApplication.shared.mainMenu = swiftUIs + turnTheRunLoop { keeper.isInPlace } + + XCTAssertTrue(NSApplication.shared.windowsMenu === editors.windows) + } + + func test_theEditorsMenusInPlace_countAsNothing() { + NSApplication.shared.mainMenu = nil + let editors = editorsMenu() + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) - NSApplication.shared.mainMenu = editors + NotificationCenter.default.post(name: NSApplication.didUpdateNotification, object: NSApplication.shared) + NotificationCenter.default.post(name: NSApplication.didBecomeActiveNotification, object: NSApplication.shared) + NSApplication.shared.mainMenu = editors.menu turnTheRunLoop() XCTAssertEqual(keeper.restorations, 0) } func test_everyReplacement_isUndone() { - let editors = NSMenu(title: "Editor") - let keeper = EditorMainMenuKeeper(menu: editors) + NSApplication.shared.mainMenu = nil + let editors = editorsMenu() + let keeper = EditorMainMenuKeeper(menu: editors.menu, windowsMenu: editors.windows) for _ in 0 ..< 3 { - NSApplication.shared.mainMenu = NSMenu(title: "SwiftUI") - turnTheRunLoop() + NSApplication.shared.mainMenu = swiftUIsMenu() + turnTheRunLoop { keeper.isInPlace } } - XCTAssertTrue(NSApplication.shared.mainMenu === editors) + XCTAssertEqual(titles(NSApplication.shared.mainMenu), ["Untold Engine Editor", "File", "View", "Window"]) XCTAssertEqual(keeper.restorations, 3) } - func test_withoutAKeeper_anotherMenuStays() { - let editors = NSMenu(title: "Editor") - NSApplication.shared.mainMenu = editors - let other = NSMenu(title: "SwiftUI") + func test_withoutAKeeper_anotherMenuBarStays() { + let editors = editorsMenu() + NSApplication.shared.mainMenu = editors.menu + let swiftUIs = swiftUIsMenu() - NSApplication.shared.mainMenu = other + NSApplication.shared.mainMenu = swiftUIs turnTheRunLoop() - XCTAssertTrue(NSApplication.shared.mainMenu === other) + XCTAssertEqual(titles(NSApplication.shared.mainMenu), ["Untold Engine Studio", "Edit", "View", "Window", "Help"]) } }