Merge origin/main into pr-203-browser-whitelist

This commit is contained in:
Lawrence Chen 2026-02-20 19:10:59 -08:00
commit 64f6e34e7b
41 changed files with 6760 additions and 359 deletions

View file

@ -1,6 +1,7 @@
import XCTest
import AppKit
import WebKit
import ObjectiveC.runtime
#if canImport(cmux_DEV)
@testable import cmux_DEV
@ -8,6 +9,49 @@ import WebKit
@testable import cmux
#endif
private var cmuxUnitTestInspectorAssociationKey: UInt8 = 0
private var cmuxUnitTestInspectorOverrideInstalled = false
private extension CmuxWebView {
@objc func cmuxUnitTestInspector() -> NSObject? {
objc_getAssociatedObject(self, &cmuxUnitTestInspectorAssociationKey) as? NSObject
}
}
private extension WKWebView {
func cmuxSetUnitTestInspector(_ inspector: NSObject?) {
objc_setAssociatedObject(
self,
&cmuxUnitTestInspectorAssociationKey,
inspector,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
private func installCmuxUnitTestInspectorOverride() {
guard !cmuxUnitTestInspectorOverrideInstalled else { return }
guard let replacementMethod = class_getInstanceMethod(
CmuxWebView.self,
#selector(CmuxWebView.cmuxUnitTestInspector)
) else {
fatalError("Unable to locate test inspector replacement method")
}
let added = class_addMethod(
CmuxWebView.self,
NSSelectorFromString("_inspector"),
method_getImplementation(replacementMethod),
method_getTypeEncoding(replacementMethod)
)
guard added else {
fatalError("Unable to install CmuxWebView _inspector test override")
}
cmuxUnitTestInspectorOverrideInstalled = true
}
final class CmuxWebViewKeyEquivalentTests: XCTestCase {
private final class ActionSpy: NSObject {
private(set) var invoked: Bool = false
@ -88,6 +132,258 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase {
}
}
final class BrowserDevToolsButtonDebugSettingsTests: XCTestCase {
private func makeIsolatedDefaults() -> UserDefaults {
let suiteName = "BrowserDevToolsButtonDebugSettingsTests.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
fatalError("Failed to create defaults suite")
}
defaults.removePersistentDomain(forName: suiteName)
addTeardownBlock {
defaults.removePersistentDomain(forName: suiteName)
}
return defaults
}
func testIconCatalogIncludesExpandedChoices() {
XCTAssertGreaterThanOrEqual(BrowserDevToolsIconOption.allCases.count, 10)
XCTAssertTrue(BrowserDevToolsIconOption.allCases.contains(.terminal))
XCTAssertTrue(BrowserDevToolsIconOption.allCases.contains(.globe))
XCTAssertTrue(BrowserDevToolsIconOption.allCases.contains(.curlyBracesSquare))
}
func testIconOptionFallsBackToDefaultForUnknownRawValue() {
let defaults = makeIsolatedDefaults()
defaults.set("this.symbol.does.not.exist", forKey: BrowserDevToolsButtonDebugSettings.iconNameKey)
XCTAssertEqual(
BrowserDevToolsButtonDebugSettings.iconOption(defaults: defaults),
BrowserDevToolsButtonDebugSettings.defaultIcon
)
}
func testColorOptionFallsBackToDefaultForUnknownRawValue() {
let defaults = makeIsolatedDefaults()
defaults.set("notAValidColor", forKey: BrowserDevToolsButtonDebugSettings.iconColorKey)
XCTAssertEqual(
BrowserDevToolsButtonDebugSettings.colorOption(defaults: defaults),
BrowserDevToolsButtonDebugSettings.defaultColor
)
}
func testCopyPayloadUsesPersistedValues() {
let defaults = makeIsolatedDefaults()
defaults.set(BrowserDevToolsIconOption.scope.rawValue, forKey: BrowserDevToolsButtonDebugSettings.iconNameKey)
defaults.set(BrowserDevToolsIconColorOption.bonsplitActive.rawValue, forKey: BrowserDevToolsButtonDebugSettings.iconColorKey)
let payload = BrowserDevToolsButtonDebugSettings.copyPayload(defaults: defaults)
XCTAssertTrue(payload.contains("browserDevToolsIconName=scope"))
XCTAssertTrue(payload.contains("browserDevToolsIconColor=bonsplitActive"))
}
}
final class BrowserDeveloperToolsShortcutDefaultsTests: XCTestCase {
func testSafariDefaultShortcutForToggleDeveloperTools() {
let shortcut = KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultShortcut
XCTAssertEqual(shortcut.key, "i")
XCTAssertTrue(shortcut.command)
XCTAssertTrue(shortcut.option)
XCTAssertFalse(shortcut.shift)
XCTAssertFalse(shortcut.control)
}
func testSafariDefaultShortcutForShowJavaScriptConsole() {
let shortcut = KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultShortcut
XCTAssertEqual(shortcut.key, "c")
XCTAssertTrue(shortcut.command)
XCTAssertTrue(shortcut.option)
XCTAssertFalse(shortcut.shift)
XCTAssertFalse(shortcut.control)
}
}
@MainActor
final class BrowserDeveloperToolsConfigurationTests: XCTestCase {
func testBrowserPanelEnablesInspectableWebViewAndDeveloperExtras() {
let panel = BrowserPanel(workspaceId: UUID())
let developerExtras = panel.webView.configuration.preferences.value(forKey: "developerExtrasEnabled") as? Bool
XCTAssertEqual(developerExtras, true)
if #available(macOS 13.3, *) {
XCTAssertTrue(panel.webView.isInspectable)
}
}
}
@MainActor
final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
private final class FakeInspector: NSObject {
private(set) var showCount = 0
private(set) var closeCount = 0
private var visible = false
@objc func isVisible() -> Bool {
visible
}
@objc func show() {
showCount += 1
visible = true
}
@objc func close() {
closeCount += 1
visible = false
}
}
override class func setUp() {
super.setUp()
installCmuxUnitTestInspectorOverride()
}
private func makePanelWithInspector() -> (BrowserPanel, FakeInspector) {
let panel = BrowserPanel(workspaceId: UUID())
let inspector = FakeInspector()
panel.webView.cmuxSetUnitTestInspector(inspector)
return (panel, inspector)
}
func testRestoreReopensInspectorAfterAttachWhenPreferredVisible() {
let (panel, inspector) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
XCTAssertTrue(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.showCount, 1)
// Simulate WebKit closing inspector during detach/reattach churn.
inspector.close()
XCTAssertFalse(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.closeCount, 1)
panel.restoreDeveloperToolsAfterAttachIfNeeded()
XCTAssertTrue(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.showCount, 2)
}
func testSyncRespectsManualCloseAndPreventsUnexpectedRestore() {
let (panel, inspector) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
XCTAssertEqual(inspector.showCount, 1)
// Simulate user closing inspector before detach.
inspector.close()
panel.syncDeveloperToolsPreferenceFromInspector()
panel.restoreDeveloperToolsAfterAttachIfNeeded()
XCTAssertFalse(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.showCount, 1)
}
func testSyncCanPreserveVisibleIntentDuringDetachChurn() {
let (panel, inspector) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
XCTAssertEqual(inspector.showCount, 1)
// Simulate a transient close caused by view detach, not user intent.
inspector.close()
panel.syncDeveloperToolsPreferenceFromInspector(preserveVisibleIntent: true)
panel.restoreDeveloperToolsAfterAttachIfNeeded()
XCTAssertTrue(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.showCount, 2)
}
func testForcedRefreshAfterAttachKeepsVisibleInspectorState() {
let (panel, inspector) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
XCTAssertTrue(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.showCount, 1)
XCTAssertEqual(inspector.closeCount, 0)
panel.requestDeveloperToolsRefreshAfterNextAttach(reason: "unit-test")
panel.restoreDeveloperToolsAfterAttachIfNeeded()
XCTAssertTrue(panel.isDeveloperToolsVisible())
XCTAssertEqual(inspector.closeCount, 0)
XCTAssertEqual(inspector.showCount, 1)
// The force-refresh request should be one-shot.
panel.restoreDeveloperToolsAfterAttachIfNeeded()
XCTAssertEqual(inspector.closeCount, 0)
XCTAssertEqual(inspector.showCount, 1)
}
func testRefreshRequestTracksPendingStateUntilRestoreRuns() {
let (panel, _) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
XCTAssertFalse(panel.hasPendingDeveloperToolsRefreshAfterAttach())
panel.requestDeveloperToolsRefreshAfterNextAttach(reason: "unit-test")
XCTAssertTrue(panel.hasPendingDeveloperToolsRefreshAfterAttach())
panel.restoreDeveloperToolsAfterAttachIfNeeded()
XCTAssertFalse(panel.hasPendingDeveloperToolsRefreshAfterAttach())
}
func testTransientHideAttachmentPreserveFollowsDeveloperToolsIntent() {
let (panel, _) = makePanelWithInspector()
XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide())
XCTAssertTrue(panel.showDeveloperTools())
XCTAssertTrue(panel.shouldPreserveWebViewAttachmentDuringTransientHide())
XCTAssertTrue(panel.hideDeveloperTools())
XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide())
}
func testWebViewDismantleSkipsDetachWhenDeveloperToolsIntentIsVisible() {
let (panel, _) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
let representable = WebViewRepresentable(
panel: panel,
shouldAttachWebView: true,
shouldFocusWebView: false,
isPanelFocused: true,
portalZPriority: 0
)
let coordinator = representable.makeCoordinator()
coordinator.webView = panel.webView
let host = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
host.addSubview(panel.webView)
WebViewRepresentable.dismantleNSView(host, coordinator: coordinator)
XCTAssertTrue(panel.webView.superview === host)
}
func testWebViewDismantleDetachesWhenDeveloperToolsIntentIsHidden() {
let (panel, _) = makePanelWithInspector()
XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide())
let representable = WebViewRepresentable(
panel: panel,
shouldAttachWebView: true,
shouldFocusWebView: false,
isPanelFocused: true,
portalZPriority: 0
)
let coordinator = representable.makeCoordinator()
coordinator.webView = panel.webView
let host = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
host.addSubview(panel.webView)
WebViewRepresentable.dismantleNSView(host, coordinator: coordinator)
XCTAssertNil(panel.webView.superview)
}
}
final class WorkspaceShortcutMapperTests: XCTestCase {
func testCommandNineMapsToLastWorkspaceIndex() {
XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: 9, workspaceCount: 1), 0)
@ -204,6 +500,57 @@ final class SidebarCommandHintPolicyTests: XCTestCase {
func testCommandHintUsesIntentionalHoldDelay() {
XCTAssertGreaterThanOrEqual(SidebarCommandHintPolicy.intentionalHoldDelay, 0.25)
}
func testCurrentWindowRequiresHostWindowToBeKeyAndMatchEventWindow() {
XCTAssertTrue(
SidebarCommandHintPolicy.isCurrentWindow(
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: 42,
keyWindowNumber: 42
)
)
XCTAssertFalse(
SidebarCommandHintPolicy.isCurrentWindow(
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: 7,
keyWindowNumber: 42
)
)
XCTAssertFalse(
SidebarCommandHintPolicy.isCurrentWindow(
hostWindowNumber: 42,
hostWindowIsKey: false,
eventWindowNumber: 42,
keyWindowNumber: 42
)
)
}
func testWindowScopedCommandHintsUseKeyWindowWhenNoEventWindowIsAvailable() {
XCTAssertTrue(
SidebarCommandHintPolicy.shouldShowHints(
for: [.command],
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: nil,
keyWindowNumber: 42
)
)
XCTAssertFalse(
SidebarCommandHintPolicy.shouldShowHints(
for: [.command],
hostWindowNumber: 42,
hostWindowIsKey: true,
eventWindowNumber: nil,
keyWindowNumber: 7
)
)
}
}
final class ShortcutHintDebugSettingsTests: XCTestCase {
@ -339,6 +686,43 @@ final class WorkspacePlacementSettingsTests: XCTestCase {
}
}
final class WorkspaceAutoReorderSettingsTests: XCTestCase {
func testDefaultIsEnabled() {
let suiteName = "WorkspaceAutoReorderSettingsTests.Default.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
XCTAssertTrue(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults))
}
func testDisabledWhenSetToFalse() {
let suiteName = "WorkspaceAutoReorderSettingsTests.Disabled.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(false, forKey: WorkspaceAutoReorderSettings.key)
XCTAssertFalse(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults))
}
func testEnabledWhenSetToTrue() {
let suiteName = "WorkspaceAutoReorderSettingsTests.Enabled.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(true, forKey: WorkspaceAutoReorderSettings.key)
XCTAssertTrue(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults))
}
}
final class AppearanceSettingsTests: XCTestCase {
func testResolvedModeDefaultsToSystemWhenUnset() {
let suiteName = "AppearanceSettingsTests.Default.\(UUID().uuidString)"
@ -354,40 +738,6 @@ final class AppearanceSettingsTests: XCTestCase {
XCTAssertEqual(resolved, .system)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
}
func testResolvedModeMigratesLegacyAndInvalidValuesToSystem() {
let suiteName = "AppearanceSettingsTests.Migrate.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(AppearanceMode.auto.rawValue, forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .system)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
defaults.set("invalid-value", forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .system)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
}
func testResolvedModePreservesExplicitLightAndDark() {
let suiteName = "AppearanceSettingsTests.Preserve.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(AppearanceMode.light.rawValue, forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .light)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.light.rawValue)
defaults.set(AppearanceMode.dark.rawValue, forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .dark)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.dark.rawValue)
}
}
final class UpdateChannelSettingsTests: XCTestCase {
@ -2013,22 +2363,6 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
state = hostedView.debugInactiveOverlayState()
XCTAssertTrue(state.isHidden)
}
func testUnreadNotificationRingVisibilityTracksRequestedState() {
let hostedView = GhosttySurfaceScrollView(
surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 80, height: 50))
)
hostedView.setNotificationRing(visible: true)
var state = hostedView.debugNotificationRingState()
XCTAssertFalse(state.isHidden)
XCTAssertEqual(state.opacity, 1, accuracy: 0.001)
hostedView.setNotificationRing(visible: false)
state = hostedView.debugNotificationRingState()
XCTAssertTrue(state.isHidden)
XCTAssertEqual(state.opacity, 0, accuracy: 0.001)
}
}
@MainActor
@ -2224,6 +2558,222 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase {
}
}
@MainActor
final class BrowserWindowPortalLifecycleTests: XCTestCase {
private func realizeWindowLayout(_ window: NSWindow) {
window.makeKeyAndOrderFront(nil)
window.displayIfNeeded()
window.contentView?.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
window.contentView?.layoutSubtreeIfNeeded()
}
func testPortalHostInstallsAboveContentViewForVisibility() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 320, height: 240),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
let portal = WindowBrowserPortal(window: window)
_ = portal.webViewAtWindowPoint(NSPoint(x: 1, y: 1))
guard let contentView = window.contentView,
let container = contentView.superview else {
XCTFail("Expected content container")
return
}
guard let hostIndex = container.subviews.firstIndex(where: { $0 is WindowBrowserHostView }),
let contentIndex = container.subviews.firstIndex(where: { $0 === contentView }) else {
XCTFail("Expected host/content views in same container")
return
}
XCTAssertGreaterThan(
hostIndex,
contentIndex,
"Browser portal host must remain above content view so portal-hosted web views stay visible"
)
}
func testAnchorRebindKeepsWebViewInStablePortalSuperview() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 300),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
let portal = WindowBrowserPortal(window: window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let anchor1 = NSView(frame: NSRect(x: 20, y: 20, width: 180, height: 120))
let anchor2 = NSView(frame: NSRect(x: 240, y: 40, width: 180, height: 120))
contentView.addSubview(anchor1)
contentView.addSubview(anchor2)
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
portal.bind(webView: webView, to: anchor1, visibleInUI: true)
let firstSuperview = webView.superview
XCTAssertNotNil(firstSuperview)
XCTAssertTrue(firstSuperview is WindowBrowserSlotView)
portal.bind(webView: webView, to: anchor2, visibleInUI: true)
XCTAssertTrue(webView.superview === firstSuperview, "Anchor moves should not reparent the web view")
contentView.layoutSubtreeIfNeeded()
portal.synchronizeWebViewForAnchor(anchor2)
guard let slot = webView.superview as? WindowBrowserSlotView,
let host = slot.superview as? WindowBrowserHostView else {
XCTFail("Expected browser slot + host views")
return
}
let expectedFrame = host.convert(anchor2.bounds, from: anchor2)
XCTAssertEqual(slot.frame.origin.x, expectedFrame.origin.x, accuracy: 0.5)
XCTAssertEqual(slot.frame.origin.y, expectedFrame.origin.y, accuracy: 0.5)
XCTAssertEqual(slot.frame.size.width, expectedFrame.size.width, accuracy: 0.5)
XCTAssertEqual(slot.frame.size.height, expectedFrame.size.height, accuracy: 0.5)
}
func testPortalClampsWebViewFrameToHostBoundsWhenAnchorOverflowsSidebar() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 320, height: 240),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
let portal = WindowBrowserPortal(window: window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
// Simulate a transient oversized anchor rect during split churn.
let anchor = NSView(frame: NSRect(x: 120, y: 20, width: 260, height: 150))
contentView.addSubview(anchor)
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
portal.bind(webView: webView, to: anchor, visibleInUI: true)
contentView.layoutSubtreeIfNeeded()
portal.synchronizeWebViewForAnchor(anchor)
guard let slot = webView.superview as? WindowBrowserSlotView else {
XCTFail("Expected web view slot")
return
}
XCTAssertFalse(slot.isHidden, "Partially visible browser anchor should stay visible")
XCTAssertEqual(slot.frame.origin.x, 120, accuracy: 0.5)
XCTAssertEqual(slot.frame.origin.y, 20, accuracy: 0.5)
XCTAssertEqual(slot.frame.size.width, 200, accuracy: 0.5)
XCTAssertEqual(slot.frame.size.height, 150, accuracy: 0.5)
}
func testPortalSyncNormalizesOutOfBoundsWebFrame() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 300),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
let portal = WindowBrowserPortal(window: window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let anchor = NSView(frame: NSRect(x: 40, y: 20, width: 220, height: 160))
contentView.addSubview(anchor)
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
portal.bind(webView: webView, to: anchor, visibleInUI: true)
contentView.layoutSubtreeIfNeeded()
portal.synchronizeWebViewForAnchor(anchor)
guard let slot = webView.superview as? WindowBrowserSlotView else {
XCTFail("Expected browser slot")
return
}
// Reproduce observed drift from logs where WebKit shifts/expands frame beyond slot bounds.
webView.frame = NSRect(x: 0, y: 250, width: slot.bounds.width, height: slot.bounds.height)
XCTAssertGreaterThan(webView.frame.maxY, slot.bounds.maxY)
portal.synchronizeWebViewForAnchor(anchor)
XCTAssertEqual(webView.frame.origin.x, slot.bounds.origin.x, accuracy: 0.5)
XCTAssertEqual(webView.frame.origin.y, slot.bounds.origin.y, accuracy: 0.5)
XCTAssertEqual(webView.frame.size.width, slot.bounds.size.width, accuracy: 0.5)
XCTAssertEqual(webView.frame.size.height, slot.bounds.size.height, accuracy: 0.5)
}
func testPortalHostBoundsBecomeReadyAfterBindingInFrameDrivenHierarchy() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 320),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
let portal = WindowBrowserPortal(window: window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let anchor = NSView(frame: NSRect(x: 40, y: 24, width: 220, height: 160))
contentView.addSubview(anchor)
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
portal.bind(webView: webView, to: anchor, visibleInUI: true)
portal.synchronizeWebViewForAnchor(anchor)
guard let slot = webView.superview as? WindowBrowserSlotView,
let host = slot.superview as? WindowBrowserHostView else {
XCTFail("Expected portal slot + host views")
return
}
XCTAssertGreaterThan(host.bounds.width, 1, "Portal host width should be ready for clipping/sync")
XCTAssertGreaterThan(host.bounds.height, 1, "Portal host height should be ready for clipping/sync")
}
func testRegistryDetachRemovesPortalHostedWebView() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 320, height: 240),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let anchor = NSView(frame: NSRect(x: 20, y: 20, width: 180, height: 120))
contentView.addSubview(anchor)
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
BrowserWindowPortalRegistry.bind(webView: webView, to: anchor, visibleInUI: true)
XCTAssertNotNil(webView.superview)
BrowserWindowPortalRegistry.detach(webView: webView)
XCTAssertNil(webView.superview)
}
}
final class BrowserLinkOpenSettingsTests: XCTestCase {
private var suiteName: String!
private var defaults: UserDefaults!

View file

@ -126,6 +126,42 @@ final class GhosttyConfigTests: XCTestCase {
XCTAssertEqual(rgb255(config.backgroundColor), RGB(red: 253, green: 246, blue: 227))
}
func testLegacyConfigFallbackUsesLegacyFileWhenConfigGhosttyIsEmpty() {
XCTAssertTrue(
GhosttyApp.shouldLoadLegacyGhosttyConfig(
newConfigFileSize: 0,
legacyConfigFileSize: 42
)
)
}
func testLegacyConfigFallbackSkipsWhenNewFileMissingOrLegacyEmpty() {
XCTAssertFalse(
GhosttyApp.shouldLoadLegacyGhosttyConfig(
newConfigFileSize: nil,
legacyConfigFileSize: 42
)
)
XCTAssertFalse(
GhosttyApp.shouldLoadLegacyGhosttyConfig(
newConfigFileSize: 10,
legacyConfigFileSize: 42
)
)
XCTAssertFalse(
GhosttyApp.shouldLoadLegacyGhosttyConfig(
newConfigFileSize: 0,
legacyConfigFileSize: 0
)
)
XCTAssertFalse(
GhosttyApp.shouldLoadLegacyGhosttyConfig(
newConfigFileSize: 0,
legacyConfigFileSize: nil
)
)
}
private func rgb255(_ color: NSColor) -> RGB {
let srgb = color.usingColorSpace(.sRGB)!
var red: CGFloat = 0

View file

@ -1,5 +1,7 @@
import XCTest
import Foundation
import AppKit
@testable import cmux_DEV
/// Regression test: ensures UpdatePill is never gated behind #if DEBUG in production code paths.
/// This prevents accidentally hiding the update UI in Release builds.
@ -64,3 +66,175 @@ final class UpdatePillReleaseVisibilityTests: XCTestCase {
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
}
/// Regression test: ensure WKWebView can load HTTP development URLs (e.g. *.localtest.me).
final class AppTransportSecurityTests: XCTestCase {
func testInfoPlistAllowsArbitraryLoadsInWebContent() throws {
let projectRoot = findProjectRoot()
let infoPlistURL = projectRoot.appendingPathComponent("Resources/Info.plist")
let data = try Data(contentsOf: infoPlistURL)
var format = PropertyListSerialization.PropertyListFormat.xml
let plist = try XCTUnwrap(
PropertyListSerialization.propertyList(from: data, options: [], format: &format) as? [String: Any]
)
let ats = try XCTUnwrap(plist["NSAppTransportSecurity"] as? [String: Any])
XCTAssertEqual(
ats["NSAllowsArbitraryLoadsInWebContent"] as? Bool,
true,
"Resources/Info.plist must allow HTTP loads in WKWebView for local dev hostnames."
)
}
private func findProjectRoot() -> URL {
var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent()
for _ in 0..<10 {
let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj")
if FileManager.default.fileExists(atPath: marker.path) {
return dir
}
dir = dir.deletingLastPathComponent()
}
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
}
final class BrowserInsecureHTTPSettingsTests: XCTestCase {
func testDefaultAllowlistPatternsArePresent() {
XCTAssertEqual(
BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: nil),
["127.0.0.1", "localhost", "*.localtest.me"]
)
}
func testWildcardAndExactHostMatching() {
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("localhost", rawAllowlist: nil))
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("api.localtest.me", rawAllowlist: nil))
XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("neverssl.com", rawAllowlist: nil))
}
func testCustomAllowlistNormalizesAndDeduplicatesEntries() {
let raw = """
localhost
*.example.com
127.0.0.1
https://dev.internal:8080/path
*.example.com
"""
XCTAssertEqual(
BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: raw),
["localhost", "*.example.com", "127.0.0.1", "dev.internal"]
)
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("foo.example.com", rawAllowlist: raw))
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("dev.internal", rawAllowlist: raw))
XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("example.net", rawAllowlist: raw))
}
func testBlockDecisionUsesAllowlistAndSchemeRules() throws {
let localURL = try XCTUnwrap(URL(string: "http://foo.localtest.me:3000"))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(localURL, rawAllowlist: nil))
let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com"))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil))
let httpsURL = try XCTUnwrap(URL(string: "https://neverssl.com"))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(httpsURL, rawAllowlist: nil))
}
func testOneTimeBypassIsConsumedAfterFirstNavigation() throws {
let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com"))
var bypassHostOnce: String? = "neverssl.com"
XCTAssertTrue(browserShouldConsumeOneTimeInsecureHTTPBypass(
insecureURL,
bypassHostOnce: &bypassHostOnce
))
XCTAssertNil(bypassHostOnce)
// Subsequent visits should prompt again unless host was saved.
XCTAssertFalse(browserShouldConsumeOneTimeInsecureHTTPBypass(
insecureURL,
bypassHostOnce: &bypassHostOnce
))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil))
}
func testAddAllowedHostPersistsToDefaultsAndUnblocksHTTP() throws {
let suiteName = "BrowserInsecureHTTPSettingsTests.Persist.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
let url = try XCTUnwrap(URL(string: "http://persist-me.test"))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(url, defaults: defaults))
BrowserInsecureHTTPSettings.addAllowedHost("persist-me.test", defaults: defaults)
let persisted = defaults.string(forKey: BrowserInsecureHTTPSettings.allowlistKey)
XCTAssertNotNil(persisted)
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("persist-me.test", defaults: defaults))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(url, defaults: defaults))
}
func testAllowlistSelectionPersistsForProceedAndOpenExternal() {
XCTAssertTrue(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertFirstButtonReturn,
suppressionEnabled: true
))
XCTAssertTrue(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertSecondButtonReturn,
suppressionEnabled: true
))
XCTAssertFalse(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertThirdButtonReturn,
suppressionEnabled: true
))
XCTAssertFalse(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertSecondButtonReturn,
suppressionEnabled: false
))
}
}
/// Regression test: ensure new terminal windows are born in full-size content mode so
/// titlebar/content offsets are correct before the first resize.
final class MainWindowLayoutStyleTests: XCTestCase {
func testCreateMainWindowUsesFullSizeContentViewStyleMask() throws {
let projectRoot = findProjectRoot()
let appDelegateURL = projectRoot.appendingPathComponent("Sources/AppDelegate.swift")
let source = try String(contentsOf: appDelegateURL, encoding: .utf8)
guard let start = source.range(of: "func createMainWindow("),
let end = source.range(of: "@objc func checkForUpdates", range: start.upperBound..<source.endIndex) else {
XCTFail("Could not locate createMainWindow block in Sources/AppDelegate.swift")
return
}
let block = String(source[start.lowerBound..<end.lowerBound])
let regex = try NSRegularExpression(
pattern: #"styleMask:\s*\[[^\]]*\.fullSizeContentView"#,
options: [.dotMatchesLineSeparators]
)
let range = NSRange(block.startIndex..<block.endIndex, in: block)
XCTAssertNotNil(
regex.firstMatch(in: block, options: [], range: range),
"""
createMainWindow must include `.fullSizeContentView` in the NSWindow style mask.
Without it, initial titlebar/content offsets can be wrong until a manual resize.
"""
)
}
private func findProjectRoot() -> URL {
var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent()
for _ in 0..<10 {
let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj")
if FileManager.default.fileExists(atPath: marker.path) {
return dir
}
dir = dir.deletingLastPathComponent()
}
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
}