From 11416bd617961290b942b00f00d894554c5784b8 Mon Sep 17 00:00:00 2001 From: cmux Date: Fri, 26 Jun 2026 01:01:58 -0700 Subject: [PATCH 1/3] Add failing regression test for unreachable native fullscreen (#5933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmux creates its main window programmatically and never declares .fullScreenPrimary, relying on AppKit's implicit grant of fullscreen capability to a resizable, titled window. On macOS 26 (Tahoe) a freshly-created CmuxMainWindow reports an empty collection behavior (rawValue == 0) and AppKit does not treat it as fullscreen-capable, so Toggle Full Screen / ⌃⌘F / the green traffic-light button all fail to enter a native fullscreen Space (the green button only zooms). This test asserts the main window declares .fullScreenPrimary. It fails on current code (no fix yet) and will pass once the window declares the capability explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmux.xcodeproj/project.pbxproj | 4 ++ ...xMainWindowFullScreenCapabilityTests.swift | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 8da4291ed351..e5db319fe1c5 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -302,6 +302,7 @@ E7E00000000000000000000B /* CmuxLifecycleEventPublishing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E00000000000000000000C /* CmuxLifecycleEventPublishing.swift */; }; 2F0C07000000000000000002 /* CmuxMainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0C07000000000000000001 /* CmuxMainWindow.swift */; }; D36090010000000000000005 /* CmuxMainWindowConstrainFrameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36090010000000000000006 /* CmuxMainWindowConstrainFrameTests.swift */; }; + D36090020000000000000005 /* CmuxMainWindowFullScreenCapabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36090020000000000000006 /* CmuxMainWindowFullScreenCapabilityTests.swift */; }; 799F1BEF876006EEB8CD1035 /* CMUXMobileCore in Frameworks */ = {isa = PBXBuildFile; productRef = EFB18E3B3099DFE2ECA3C263 /* CMUXMobileCore */; }; EFB18E3C0000000000000001 /* CMUXMobileCore in Frameworks */ = {isa = PBXBuildFile; productRef = EFB18E3B3099DFE2ECA3C263 /* CMUXMobileCore */; }; CA1F0A01CA1F0A01CA1F0A01 /* CmuxModalAlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1F0A02CA1F0A02CA1F0A02 /* CmuxModalAlertPresentation.swift */; }; @@ -1450,6 +1451,7 @@ E7E00000000000000000000C /* CmuxLifecycleEventPublishing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxLifecycleEventPublishing.swift; sourceTree = ""; }; 2F0C07000000000000000001 /* CmuxMainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CmuxMainWindow.swift; sourceTree = ""; }; D36090010000000000000006 /* CmuxMainWindowConstrainFrameTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxMainWindowConstrainFrameTests.swift; sourceTree = ""; }; + D36090020000000000000006 /* CmuxMainWindowFullScreenCapabilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxMainWindowFullScreenCapabilityTests.swift; sourceTree = ""; }; CA1F0A02CA1F0A02CA1F0A02 /* CmuxModalAlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxModalAlertPresentation.swift; sourceTree = ""; }; C0DE31390000000000000102 /* CMUXOpenCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXOpenCommandTests.swift; sourceTree = ""; }; C0DE31390000000000000112 /* CMUXOpenHTMLFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXOpenHTMLFocusTests.swift; sourceTree = ""; }; @@ -3235,6 +3237,7 @@ A50019B3 /* SettingsSearchIndexTests.swift */, D36090010000000000000004 /* SettingsWindowPresenterTests.swift */, D36090010000000000000006 /* CmuxMainWindowConstrainFrameTests.swift */, + D36090020000000000000006 /* CmuxMainWindowFullScreenCapabilityTests.swift */, 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */, BABA25000000000000000006 /* BrowserHTTPBasicAuthPromptCoordinatorTests.swift */, BABA25000000000000000004 /* BrowserHTTPBasicAuthPromptTests.swift */, @@ -4693,6 +4696,7 @@ C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */, E7E000000000000000000003 /* CmuxEventBusTests.swift in Sources */, D36090010000000000000005 /* CmuxMainWindowConstrainFrameTests.swift in Sources */, + D36090020000000000000005 /* CmuxMainWindowFullScreenCapabilityTests.swift in Sources */, C0DE31390000000000000101 /* CMUXOpenCommandTests.swift in Sources */, C0DE31390000000000000111 /* CMUXOpenHTMLFocusTests.swift in Sources */, C3677001000000000000001 /* CmuxSSHURLRequestTests.swift in Sources */, diff --git a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift new file mode 100644 index 000000000000..b566efd25fc1 --- /dev/null +++ b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift @@ -0,0 +1,48 @@ +import AppKit +import XCTest + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +#if DEBUG +@MainActor +final class CmuxMainWindowFullScreenCapabilityTests: XCTestCase { + // cmux creates its main window programmatically and never loaded fullscreen + // capability from a nib, so it historically relied on AppKit *implicitly* + // granting `.fullScreenPrimary` to a resizable, titled window. That implicit + // grant is not reliable across macOS versions / display arrangements: on + // macOS 26 (Tahoe) a freshly-created CmuxMainWindow reports an empty + // collection behavior (`rawValue == 0`) and AppKit does NOT treat it as + // fullscreen-capable — so `toggleFullScreen(_:)`, ⌃⌘F, and the green + // traffic-light button all fail to enter a native fullscreen Space (the + // green button only zooms). See issue #5933. + // + // A CmuxMainWindow must therefore *declare* `.fullScreenPrimary` itself so + // native fullscreen is reachable regardless of the OS's implicit default. + func testMainWindowDeclaresFullScreenPrimaryCapability() { + let window = CmuxMainWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.isReleasedWhenClosed = false + defer { + window.orderOut(nil) + window.close() + } + + XCTAssertTrue( + window.collectionBehavior.contains(.fullScreenPrimary), + "Main window must declare .fullScreenPrimary so native fullscreen is reachable" + ) + XCTAssertFalse( + window.collectionBehavior.contains(.fullScreenNone), + "Main window must never carry .fullScreenNone, which suppresses native fullscreen" + ) + } +} +#endif From 28cb9ee15b3e3e3f62ce387cdd92cf31281f2ee2 Mon Sep 17 00:00:00 2001 From: cmux Date: Fri, 26 Jun 2026 01:20:32 -0700 Subject: [PATCH 2/3] Declare native fullscreen capability on the main window (#5933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #5933. CmuxMainWindow is created programmatically and never declared `.fullScreenPrimary`, so it relied on AppKit implicitly granting fullscreen capability to a resizable, titled window. On macOS 26 (Tahoe) that implicit grant does not happen — a freshly-created window reports an empty collection behavior (`rawValue == 0`) and AppKit does not treat it as fullscreen-capable. As a result Toggle Full Screen, ⌃⌘F, and the green traffic-light button all fail to enter a native fullscreen Space (the green button only zooms), which is what multi-monitor Tahoe users hit. Declare `.fullScreenPrimary` explicitly in the window initializer via a pure, unit-testable `canonicalCollectionBehavior(_:)` helper so native fullscreen is reachable regardless of the OS's implicit default. The helper also strips any stale `.fullScreenNone` and preserves unrelated bits, so it composes with the temporary `.fullScreenDisallowsTiling` opt-out the window factory applies when spawning a window out of an existing fullscreen Space. This makes the regression test added in the previous commit pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- Sources/App/CmuxMainWindow.swift | 52 +++++++++++++++++++ ...xMainWindowFullScreenCapabilityTests.swift | 32 ++++++++++++ 2 files changed, 84 insertions(+) diff --git a/Sources/App/CmuxMainWindow.swift b/Sources/App/CmuxMainWindow.swift index 74f3a4396bb3..c60e7150f62b 100644 --- a/Sources/App/CmuxMainWindow.swift +++ b/Sources/App/CmuxMainWindow.swift @@ -69,6 +69,58 @@ final class CmuxMainWindow: NSWindow { return frame } + /// cmux creates its main window programmatically (never from a nib), so it + /// cannot inherit fullscreen capability from Interface Builder and instead + /// relied on AppKit *implicitly* granting `.fullScreenPrimary` to a + /// resizable, titled window. That implicit grant is not reliable across + /// macOS versions / display arrangements: on macOS 26 (Tahoe) a + /// freshly-created window reports an empty collection behavior + /// (`rawValue == 0`) and AppKit does not treat it as fullscreen-capable, so + /// Toggle Full Screen / ⌃⌘F / the green traffic-light button all fail to + /// enter a native fullscreen Space — the green button only zooms (#5933). + /// + /// Declaring `.fullScreenPrimary` here makes native fullscreen reachable + /// regardless of the OS's implicit default. It is idempotent where AppKit + /// would have granted it anyway, and composes with the temporary + /// `.fullScreenDisallowsTiling` opt-out the window factory applies when + /// spawning a window out of an existing fullscreen Space. + override init( + contentRect: NSRect, + styleMask: NSWindow.StyleMask, + backing: NSWindow.BackingStoreType, + defer flag: Bool + ) { + super.init( + contentRect: contentRect, + styleMask: styleMask, + backing: backing, + defer: flag + ) + collectionBehavior = Self.canonicalCollectionBehavior(collectionBehavior) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Returns `base` guaranteed to carry `.fullScreenPrimary` (and never + /// `.fullScreenNone`) so a cmux main window can always enter a native + /// fullscreen Space. Pure and `nonisolated` so it can be unit-tested + /// without constructing a window; see ``init(contentRect:styleMask:backing:defer:)`` + /// for why declaring the capability explicitly is required. + nonisolated static func canonicalCollectionBehavior( + _ base: NSWindow.CollectionBehavior + ) -> NSWindow.CollectionBehavior { + var behavior = base + // `.fullScreenNone` and `.fullScreenPrimary` are mutually exclusive; + // drop any stale "none" before declaring primary so fullscreen is not + // suppressed. + behavior.remove(.fullScreenNone) + behavior.insert(.fullScreenPrimary) + return behavior + } + private var isSoftHiddenForVisibilityController = false func setSoftHiddenForVisibilityController(_ isSoftHidden: Bool) { diff --git a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift index b566efd25fc1..2e56df28abe5 100644 --- a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift +++ b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift @@ -44,5 +44,37 @@ final class CmuxMainWindowFullScreenCapabilityTests: XCTestCase { "Main window must never carry .fullScreenNone, which suppresses native fullscreen" ) } + + // The capability decision is a pure, screen-agnostic transform so it runs + // deterministically on CI regardless of the test host's display setup. + + func testCanonicalBehaviorAddsFullScreenPrimaryToEmptyBehavior() { + let result = CmuxMainWindow.canonicalCollectionBehavior([]) + XCTAssertTrue(result.contains(.fullScreenPrimary)) + XCTAssertFalse(result.contains(.fullScreenNone)) + } + + func testCanonicalBehaviorDropsStaleFullScreenNone() { + let result = CmuxMainWindow.canonicalCollectionBehavior([.fullScreenNone]) + XCTAssertTrue(result.contains(.fullScreenPrimary)) + XCTAssertFalse(result.contains(.fullScreenNone)) + } + + func testCanonicalBehaviorPreservesUnrelatedBehaviorBits() { + // The window factory may layer `.fullScreenDisallowsTiling` on top when + // spawning out of an existing fullscreen Space; canonicalization must + // not clobber that (or any other unrelated bit). + let base: NSWindow.CollectionBehavior = [.fullScreenDisallowsTiling, .moveToActiveSpace] + let result = CmuxMainWindow.canonicalCollectionBehavior(base) + XCTAssertTrue(result.contains(.fullScreenPrimary)) + XCTAssertTrue(result.contains(.fullScreenDisallowsTiling)) + XCTAssertTrue(result.contains(.moveToActiveSpace)) + } + + func testCanonicalBehaviorIsIdempotent() { + let once = CmuxMainWindow.canonicalCollectionBehavior([]) + let twice = CmuxMainWindow.canonicalCollectionBehavior(once) + XCTAssertEqual(once, twice) + } } #endif From 4199e20add70325ca599971afc01dff3d53fc190 Mon Sep 17 00:00:00 2001 From: cmux Date: Fri, 26 Jun 2026 01:35:52 -0700 Subject: [PATCH 3/3] Use Swift Testing for the fullscreen capability tests (#5933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aziz test-framework policy: new, non-UI test files should use Swift Testing (XCTest stays for cmuxUITests only). Convert the new CmuxMainWindowFullScreenCapabilityTests from XCTestCase/XCTAssert to a @MainActor @Suite with @Test/#expect. No behavior change — same window-instantiation assertion plus the four pure-helper contracts. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...xMainWindowFullScreenCapabilityTests.swift | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift index 2e56df28abe5..c962b4ca2ca2 100644 --- a/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift +++ b/cmuxTests/CmuxMainWindowFullScreenCapabilityTests.swift @@ -1,5 +1,5 @@ import AppKit -import XCTest +import Testing #if canImport(cmux_DEV) @testable import cmux_DEV @@ -7,9 +7,9 @@ import XCTest @testable import cmux #endif -#if DEBUG @MainActor -final class CmuxMainWindowFullScreenCapabilityTests: XCTestCase { +@Suite("CmuxMainWindow native fullscreen capability") +struct CmuxMainWindowFullScreenCapabilityTests { // cmux creates its main window programmatically and never loaded fullscreen // capability from a nib, so it historically relied on AppKit *implicitly* // granting `.fullScreenPrimary` to a resizable, titled window. That implicit @@ -22,7 +22,7 @@ final class CmuxMainWindowFullScreenCapabilityTests: XCTestCase { // // A CmuxMainWindow must therefore *declare* `.fullScreenPrimary` itself so // native fullscreen is reachable regardless of the OS's implicit default. - func testMainWindowDeclaresFullScreenPrimaryCapability() { + @Test func mainWindowDeclaresFullScreenPrimaryCapability() { let window = CmuxMainWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], @@ -35,12 +35,12 @@ final class CmuxMainWindowFullScreenCapabilityTests: XCTestCase { window.close() } - XCTAssertTrue( + #expect( window.collectionBehavior.contains(.fullScreenPrimary), "Main window must declare .fullScreenPrimary so native fullscreen is reachable" ) - XCTAssertFalse( - window.collectionBehavior.contains(.fullScreenNone), + #expect( + !window.collectionBehavior.contains(.fullScreenNone), "Main window must never carry .fullScreenNone, which suppresses native fullscreen" ) } @@ -48,33 +48,32 @@ final class CmuxMainWindowFullScreenCapabilityTests: XCTestCase { // The capability decision is a pure, screen-agnostic transform so it runs // deterministically on CI regardless of the test host's display setup. - func testCanonicalBehaviorAddsFullScreenPrimaryToEmptyBehavior() { + @Test func canonicalBehaviorAddsFullScreenPrimaryToEmptyBehavior() { let result = CmuxMainWindow.canonicalCollectionBehavior([]) - XCTAssertTrue(result.contains(.fullScreenPrimary)) - XCTAssertFalse(result.contains(.fullScreenNone)) + #expect(result.contains(.fullScreenPrimary)) + #expect(!result.contains(.fullScreenNone)) } - func testCanonicalBehaviorDropsStaleFullScreenNone() { + @Test func canonicalBehaviorDropsStaleFullScreenNone() { let result = CmuxMainWindow.canonicalCollectionBehavior([.fullScreenNone]) - XCTAssertTrue(result.contains(.fullScreenPrimary)) - XCTAssertFalse(result.contains(.fullScreenNone)) + #expect(result.contains(.fullScreenPrimary)) + #expect(!result.contains(.fullScreenNone)) } - func testCanonicalBehaviorPreservesUnrelatedBehaviorBits() { + @Test func canonicalBehaviorPreservesUnrelatedBehaviorBits() { // The window factory may layer `.fullScreenDisallowsTiling` on top when // spawning out of an existing fullscreen Space; canonicalization must // not clobber that (or any other unrelated bit). let base: NSWindow.CollectionBehavior = [.fullScreenDisallowsTiling, .moveToActiveSpace] let result = CmuxMainWindow.canonicalCollectionBehavior(base) - XCTAssertTrue(result.contains(.fullScreenPrimary)) - XCTAssertTrue(result.contains(.fullScreenDisallowsTiling)) - XCTAssertTrue(result.contains(.moveToActiveSpace)) + #expect(result.contains(.fullScreenPrimary)) + #expect(result.contains(.fullScreenDisallowsTiling)) + #expect(result.contains(.moveToActiveSpace)) } - func testCanonicalBehaviorIsIdempotent() { + @Test func canonicalBehaviorIsIdempotent() { let once = CmuxMainWindow.canonicalCollectionBehavior([]) let twice = CmuxMainWindow.canonicalCollectionBehavior(once) - XCTAssertEqual(once, twice) + #expect(once == twice) } } -#endif