diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 9728e504..8221d677 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -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 @@ -172,6 +216,73 @@ final class BrowserDeveloperToolsConfigurationTests: XCTestCase { } } +@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) + } +} + final class WorkspaceShortcutMapperTests: XCTestCase { func testCommandNineMapsToLastWorkspaceIndex() { XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: 9, workspaceCount: 1), 0)