diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index c7708c7c..a50f275c 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -8944,6 +8944,7 @@ private final class FeedbackComposerMessageEditorView: NSView { } private enum SidebarHelpMenuAction { + case importBrowserData case keyboardShortcuts case docs case changelog @@ -9514,6 +9515,12 @@ private struct SidebarHelpMenuButton: View { accessibilityIdentifier: "SidebarHelpMenuOptionKeyboardShortcuts", isExternalLink: false ) + helpOptionButton( + title: String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…"), + action: .importBrowserData, + accessibilityIdentifier: "SidebarHelpMenuOptionImportBrowserData", + isExternalLink: false + ) if docsURL != nil { helpOptionButton( title: String(localized: "about.docs", defaultValue: "Docs"), @@ -9618,6 +9625,11 @@ private struct SidebarHelpMenuButton: View { private func perform(_ action: SidebarHelpMenuAction) { switch action { + case .importBrowserData: + isPopoverPresented = false + DispatchQueue.main.async { + BrowserDataImportCoordinator.shared.presentImportDialog() + } case .keyboardShortcuts: isPopoverPresented = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index dc943d31..dd65fad9 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -259,7 +259,7 @@ enum BrowserImportHintSettings { static let variantKey = "browserImportHintVariant" static let showOnBlankTabsKey = "browserImportHintShowOnBlankTabs" static let dismissedKey = "browserImportHintDismissed" - static let defaultVariant: BrowserImportHintVariant = .inlineStrip + static let defaultVariant: BrowserImportHintVariant = .toolbarChip static let defaultShowOnBlankTabs = true static let defaultDismissed = false diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 7108d183..0fc8446b 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -873,6 +873,14 @@ struct BrowserPanelView: View { } .buttonStyle(.plain) + Button { + presentImportDialogFromProfileMenu() + } label: { + Text(String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…")) + .font(.system(size: 12)) + } + .buttonStyle(.plain) + if browserProfileStore.canRenameProfile(id: panel.profileID) { Button { isBrowserProfileMenuPresented = false @@ -1470,6 +1478,16 @@ struct BrowserPanelView: View { private func presentImportDialogFromHint() { isBrowserImportHintPopoverPresented = false + // Let the popover fully dismiss before entering the modal import flow. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { + BrowserDataImportCoordinator.shared.presentImportDialog( + defaultDestinationProfileID: panel.profileID + ) + } + } + + private func presentImportDialogFromProfileMenu() { + isBrowserProfileMenuPresented = false DispatchQueue.main.async { BrowserDataImportCoordinator.shared.presentImportDialog( defaultDestinationProfileID: panel.profileID @@ -1479,7 +1497,7 @@ struct BrowserPanelView: View { private func openBrowserImportSettings() { isBrowserImportHintPopoverPresented = false - AppDelegate.presentPreferencesWindow(navigationTarget: .browser) + AppDelegate.presentPreferencesWindow(navigationTarget: .browserImport) } private func dismissBrowserImportHint() { diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 5c5dd445..4140cbd6 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -2252,6 +2252,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate { enum SettingsNavigationTarget: String { case browser + case browserImport case keyboardShortcuts } @@ -4659,6 +4660,7 @@ struct SettingsView: View { } .buttonStyle(.bordered) .controlSize(.small) + .accessibilityIdentifier("SettingsBrowserImportChooseButton") Button(String(localized: "settings.browser.import.refresh", defaultValue: "Refresh")) { refreshDetectedImportBrowsers() @@ -4680,6 +4682,8 @@ struct SettingsView: View { .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } + .id(SettingsNavigationTarget.browserImport) + .accessibilityIdentifier("SettingsBrowserImportSection") .padding(.horizontal, 14) .padding(.vertical, 10) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 820cdb0b..8b569de5 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -2550,6 +2550,24 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertEqual(activateApplicationCallCount, 1) } + func testPresentPreferencesWindowForwardsBrowserImportNavigationTarget() { + var receivedNavigationTarget: SettingsNavigationTarget? + var activateApplicationCallCount = 0 + + AppDelegate.presentPreferencesWindow( + navigationTarget: .browserImport, + showFallbackSettingsWindow: { navigationTarget in + receivedNavigationTarget = navigationTarget + }, + activateApplication: { + activateApplicationCallCount += 1 + } + ) + + XCTAssertEqual(receivedNavigationTarget, .browserImport) + XCTAssertEqual(activateApplicationCallCount, 1) + } + private func makeKeyDownEvent( key: String, modifiers: NSEvent.ModifierFlags, diff --git a/cmuxTests/BrowserImportMappingTests.swift b/cmuxTests/BrowserImportMappingTests.swift index 6eed3932..e4d5f54f 100644 --- a/cmuxTests/BrowserImportMappingTests.swift +++ b/cmuxTests/BrowserImportMappingTests.swift @@ -144,14 +144,15 @@ final class BrowserImportMappingTests: XCTestCase { XCTAssertTrue(manyProfilesPresentation.showsHelpText) } - func testBrowserImportHintPresentationDefaultsToInlineStrip() { - let presentation = BrowserImportHintPresentation( - variant: .inlineStrip, - showOnBlankTabs: true, - isDismissed: false - ) + func testBrowserImportHintSettingsDefaultToToolbarChip() throws { + let suiteName = "BrowserImportHintDefaults-\(UUID().uuidString)" + let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) + defaults.removePersistentDomain(forName: suiteName) + defer { defaults.removePersistentDomain(forName: suiteName) } - XCTAssertEqual(presentation.blankTabPlacement, .inlineStrip) + let presentation = BrowserImportHintSettings.presentation(defaults: defaults) + + XCTAssertEqual(presentation.blankTabPlacement, .toolbarChip) XCTAssertEqual(presentation.settingsStatus, .visible) } diff --git a/cmuxUITests/BrowserImportProfilesUITests.swift b/cmuxUITests/BrowserImportProfilesUITests.swift index 62f85537..d959de30 100644 --- a/cmuxUITests/BrowserImportProfilesUITests.swift +++ b/cmuxUITests/BrowserImportProfilesUITests.swift @@ -119,9 +119,22 @@ final class BrowserImportProfilesUITests: XCTestCase { XCTAssertTrue(settingsButton.waitForExistence(timeout: 5.0)) settingsButton.click() + let importSection = app.otherElements["SettingsBrowserImportSection"] XCTAssertTrue( - app.switches["SettingsBrowserImportHintToggle"].waitForExistence(timeout: 5.0), - "Expected Browser Settings to open from the blank-tab import hint" + importSection.waitForExistence(timeout: 5.0), + "Expected Browser Settings to scroll to the import section" + ) + + let chooseButton = app.buttons["SettingsBrowserImportChooseButton"] + XCTAssertTrue( + chooseButton.waitForExistence(timeout: 5.0), + "Expected Browser Settings to expose the import actions" + ) + XCTAssertTrue( + browserImportPollUntil(timeout: 5.0) { + importSection.isHittable && chooseButton.isHittable + }, + "Expected Browser Settings to scroll directly to the import controls" ) }