Add browser import hint debug variants
This commit is contained in:
parent
f97716939a
commit
b9de0f0446
7 changed files with 892 additions and 22 deletions
|
|
@ -5669,6 +5669,125 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.dismiss": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hide Hint"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ヒントを隠す"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.import": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import…"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポート…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.settings": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Browser Settings"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザー設定"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.settingsFootnote": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "You can always find this in Settings > Browser."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "あとでいつでも「設定 > ブラウザー」で見つけられます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.title": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import browser data"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザーデータをインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.toolbar": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.toolbar.help": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import browser data"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザーデータをインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.validation.scope": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
@ -50827,6 +50946,74 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.note.hidden": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The blank-tab import hint is hidden. Turn it back on here any time."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "空タブのインポート案内は非表示です。ここでいつでも再表示できます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.note.settingsOnly": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Blank tabs are currently using Settings only mode from the debug window."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "現在、空タブはデバッグウィンドウの「設定のみ」モードになっています。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.note.visible": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Blank browser tabs can show this import suggestion. Hide or re-enable it here."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "空のブラウザータブにこのインポート案内を表示できます。ここで非表示や再表示を切り替えられます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.show": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show import hint on blank browser tabs"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "空のブラウザータブにインポート案内を表示"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.history.clearButton": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -2306,6 +2306,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
// In UI tests, `WindowGroup` occasionally fails to materialize a window quickly on the VM.
|
||||
// If there are no windows shortly after launch, force-create one so XCUITest can proceed.
|
||||
if isRunningUnderXCTest {
|
||||
if let rawVariant = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_VARIANT"] {
|
||||
UserDefaults.standard.set(
|
||||
BrowserImportHintSettings.variant(for: rawVariant).rawValue,
|
||||
forKey: BrowserImportHintSettings.variantKey
|
||||
)
|
||||
}
|
||||
if let rawShow = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_SHOW"] {
|
||||
UserDefaults.standard.set(
|
||||
rawShow == "1",
|
||||
forKey: BrowserImportHintSettings.showOnBlankTabsKey
|
||||
)
|
||||
}
|
||||
if let rawDismissed = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_DISMISSED"] {
|
||||
UserDefaults.standard.set(
|
||||
rawDismissed == "1",
|
||||
forKey: BrowserImportHintSettings.dismissedKey
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
||||
guard let self else { return }
|
||||
if NSApp.windows.isEmpty {
|
||||
|
|
@ -2314,6 +2332,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
|
||||
self.writeUITestDiagnosticsIfNeeded(stage: "afterForceWindow")
|
||||
}
|
||||
if env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_BLANK_BROWSER"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { [weak self] in
|
||||
guard let self else { return }
|
||||
_ = self.openBrowserAndFocusAddressBar(insertAtEnd: true)
|
||||
}
|
||||
}
|
||||
if env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_SETTINGS"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) { [weak self] in
|
||||
self?.openPreferencesWindow(
|
||||
debugSource: "uiTest.browserImportHint",
|
||||
navigationTarget: .browser
|
||||
)
|
||||
}
|
||||
}
|
||||
if env["CMUX_UI_TEST_BROWSER_IMPORT_AUTO_OPEN"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
|
|
|
|||
|
|
@ -198,6 +198,111 @@ enum BrowserThemeSettings {
|
|||
}
|
||||
}
|
||||
|
||||
enum BrowserImportHintVariant: String, CaseIterable, Identifiable {
|
||||
case inlineStrip
|
||||
case floatingCard
|
||||
case toolbarChip
|
||||
case settingsOnly
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
enum BrowserImportHintBlankTabPlacement: Equatable {
|
||||
case hidden
|
||||
case inlineStrip
|
||||
case floatingCard
|
||||
case toolbarChip
|
||||
}
|
||||
|
||||
enum BrowserImportHintSettingsStatus: Equatable {
|
||||
case visible
|
||||
case hidden
|
||||
case settingsOnly
|
||||
}
|
||||
|
||||
struct BrowserImportHintPresentation: Equatable {
|
||||
let blankTabPlacement: BrowserImportHintBlankTabPlacement
|
||||
let settingsStatus: BrowserImportHintSettingsStatus
|
||||
|
||||
init(
|
||||
variant: BrowserImportHintVariant,
|
||||
showOnBlankTabs: Bool,
|
||||
isDismissed: Bool
|
||||
) {
|
||||
if variant == .settingsOnly {
|
||||
blankTabPlacement = .hidden
|
||||
settingsStatus = .settingsOnly
|
||||
return
|
||||
}
|
||||
|
||||
if !showOnBlankTabs || isDismissed {
|
||||
blankTabPlacement = .hidden
|
||||
settingsStatus = .hidden
|
||||
return
|
||||
}
|
||||
|
||||
switch variant {
|
||||
case .inlineStrip:
|
||||
blankTabPlacement = .inlineStrip
|
||||
case .floatingCard:
|
||||
blankTabPlacement = .floatingCard
|
||||
case .toolbarChip:
|
||||
blankTabPlacement = .toolbarChip
|
||||
case .settingsOnly:
|
||||
blankTabPlacement = .hidden
|
||||
}
|
||||
settingsStatus = .visible
|
||||
}
|
||||
}
|
||||
|
||||
enum BrowserImportHintSettings {
|
||||
static let variantKey = "browserImportHintVariant"
|
||||
static let showOnBlankTabsKey = "browserImportHintShowOnBlankTabs"
|
||||
static let dismissedKey = "browserImportHintDismissed"
|
||||
static let defaultVariant: BrowserImportHintVariant = .inlineStrip
|
||||
static let defaultShowOnBlankTabs = true
|
||||
static let defaultDismissed = false
|
||||
|
||||
static func variant(for rawValue: String?) -> BrowserImportHintVariant {
|
||||
guard let rawValue, let variant = BrowserImportHintVariant(rawValue: rawValue) else {
|
||||
return defaultVariant
|
||||
}
|
||||
return variant
|
||||
}
|
||||
|
||||
static func variant(defaults: UserDefaults = .standard) -> BrowserImportHintVariant {
|
||||
variant(for: defaults.string(forKey: variantKey))
|
||||
}
|
||||
|
||||
static func showOnBlankTabs(defaults: UserDefaults = .standard) -> Bool {
|
||||
if defaults.object(forKey: showOnBlankTabsKey) == nil {
|
||||
return defaultShowOnBlankTabs
|
||||
}
|
||||
return defaults.bool(forKey: showOnBlankTabsKey)
|
||||
}
|
||||
|
||||
static func isDismissed(defaults: UserDefaults = .standard) -> Bool {
|
||||
if defaults.object(forKey: dismissedKey) == nil {
|
||||
return defaultDismissed
|
||||
}
|
||||
return defaults.bool(forKey: dismissedKey)
|
||||
}
|
||||
|
||||
static func presentation(defaults: UserDefaults = .standard) -> BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: variant(defaults: defaults),
|
||||
showOnBlankTabs: showOnBlankTabs(defaults: defaults),
|
||||
isDismissed: isDismissed(defaults: defaults)
|
||||
)
|
||||
}
|
||||
|
||||
static func reset(defaults: UserDefaults = .standard) {
|
||||
defaults.set(defaultVariant.rawValue, forKey: variantKey)
|
||||
defaults.set(defaultShowOnBlankTabs, forKey: showOnBlankTabsKey)
|
||||
defaults.set(defaultDismissed, forKey: dismissedKey)
|
||||
}
|
||||
}
|
||||
|
||||
struct BrowserProfileDefinition: Codable, Hashable, Identifiable, Sendable {
|
||||
let id: UUID
|
||||
var displayName: String
|
||||
|
|
|
|||
|
|
@ -250,6 +250,9 @@ struct BrowserPanelView: View {
|
|||
@AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var devToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue
|
||||
@AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var devToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue
|
||||
@AppStorage(BrowserThemeSettings.modeKey) private var browserThemeModeRaw = BrowserThemeSettings.defaultMode.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.variantKey) private var browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.showOnBlankTabsKey) private var showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
@AppStorage(BrowserImportHintSettings.dismissedKey) private var isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
@AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey)
|
||||
private var toggleBrowserDeveloperToolsShortcutData = Data()
|
||||
@State private var suggestionTask: Task<Void, Never>?
|
||||
|
|
@ -267,6 +270,7 @@ struct BrowserPanelView: View {
|
|||
@State private var focusFlashAnimationGeneration: Int = 0
|
||||
@State private var omnibarPillFrame: CGRect = .zero
|
||||
@State private var addressBarHeight: CGFloat = 0
|
||||
@State private var isBrowserImportHintPopoverPresented = false
|
||||
@State private var lastHandledAddressBarFocusRequestId: UUID?
|
||||
@State private var pendingAddressBarFocusRetryRequestId: UUID?
|
||||
@State private var pendingAddressBarFocusRetryGeneration: UInt64 = 0
|
||||
|
|
@ -321,6 +325,18 @@ struct BrowserPanelView: View {
|
|||
BrowserThemeSettings.mode(for: browserThemeModeRaw)
|
||||
}
|
||||
|
||||
private var browserImportHintVariant: BrowserImportHintVariant {
|
||||
BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
|
||||
}
|
||||
|
||||
private var browserImportHintPresentation: BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: browserImportHintVariant,
|
||||
showOnBlankTabs: showBrowserImportHintOnBlankTabs,
|
||||
isDismissed: isBrowserImportHintDismissed
|
||||
)
|
||||
}
|
||||
|
||||
private var browserChromeBackground: Color {
|
||||
Color(nsColor: browserChromeStyle.backgroundColor)
|
||||
}
|
||||
|
|
@ -346,6 +362,14 @@ struct BrowserPanelView: View {
|
|||
return "\(base) (\(toggleBrowserDeveloperToolsShortcut.displayString))"
|
||||
}
|
||||
|
||||
private var browserImportHintSummary: String {
|
||||
InstalledBrowserDetector.summaryText(for: emptyStateImportBrowsers)
|
||||
}
|
||||
|
||||
private var shouldShowToolbarImportHintChip: Bool {
|
||||
shouldShowEmptyStateImportOverlay && browserImportHintPresentation.blankTabPlacement == .toolbarChip
|
||||
}
|
||||
|
||||
private var owningWorkspace: Workspace? {
|
||||
guard let app = AppDelegate.shared,
|
||||
let manager = app.tabManagerFor(tabId: panel.workspaceId) else {
|
||||
|
|
@ -459,6 +483,10 @@ struct BrowserPanelView: View {
|
|||
if browserThemeModeRaw != resolvedThemeMode.rawValue {
|
||||
browserThemeModeRaw = resolvedThemeMode.rawValue
|
||||
}
|
||||
let resolvedHintVariant = BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
|
||||
if browserImportHintVariantRaw != resolvedHintVariant.rawValue {
|
||||
browserImportHintVariantRaw = resolvedHintVariant.rawValue
|
||||
}
|
||||
panel.refreshAppearanceDrivenColors()
|
||||
panel.setBrowserThemeMode(browserThemeMode)
|
||||
applyPendingAddressBarFocusRequestIfNeeded()
|
||||
|
|
@ -613,6 +641,9 @@ struct BrowserPanelView: View {
|
|||
.accessibilityIdentifier("BrowserOmnibarPill")
|
||||
.accessibilityLabel("Browser omnibar")
|
||||
|
||||
if shouldShowToolbarImportHintChip {
|
||||
browserImportHintToolbarChip
|
||||
}
|
||||
browserProfileButton
|
||||
browserThemeModeButton
|
||||
developerToolsButton
|
||||
|
|
@ -776,6 +807,29 @@ struct BrowserPanelView: View {
|
|||
.accessibilityIdentifier("BrowserThemeModeButton")
|
||||
}
|
||||
|
||||
private var browserImportHintToolbarChip: some View {
|
||||
Button(action: {
|
||||
isBrowserImportHintPopoverPresented.toggle()
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "square.and.arrow.down.on.square")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
Text(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundStyle(devToolsColorOption.color)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.popover(isPresented: $isBrowserImportHintPopoverPresented, arrowEdge: .bottom) {
|
||||
browserImportHintPopover
|
||||
}
|
||||
.safeHelp(String(localized: "browser.import.hint.toolbar.help", defaultValue: "Import browser data"))
|
||||
.accessibilityIdentifier("BrowserImportHintToolbarChip")
|
||||
}
|
||||
|
||||
private var browserProfilePopover: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "browser.profile.menu.title", defaultValue: "Profiles"))
|
||||
|
|
@ -1018,9 +1072,16 @@ struct BrowserPanelView: View {
|
|||
setAddressBarFocused(false, reason: "placeholderContent.tapBlur")
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
if shouldShowEmptyStateImportOverlay,
|
||||
browserImportHintPresentation.blankTabPlacement == .inlineStrip {
|
||||
emptyBrowserStateInlineStrip
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if shouldShowEmptyStateImportOverlay {
|
||||
emptyBrowserStateOverlay
|
||||
if shouldShowEmptyStateImportOverlay,
|
||||
browserImportHintPresentation.blankTabPlacement == .floatingCard {
|
||||
emptyBrowserStateCardOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1288,28 +1349,11 @@ struct BrowserPanelView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
private var emptyBrowserStateOverlay: some View {
|
||||
private var emptyBrowserStateCardOverlay: some View {
|
||||
VStack {
|
||||
Spacer(minLength: 22)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "settings.browser.emptyImport.title", defaultValue: "Import browser data"))
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(InstalledBrowserDetector.summaryText(for: emptyStateImportBrowsers))
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Button(String(localized: "settings.browser.emptyImport.choose", defaultValue: "Choose What to Import…")) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog(
|
||||
defaultDestinationProfileID: panel.profileID
|
||||
)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
browserImportHintBody
|
||||
.padding(12)
|
||||
.frame(maxWidth: 360, alignment: .leading)
|
||||
.background(
|
||||
|
|
@ -1329,10 +1373,118 @@ struct BrowserPanelView: View {
|
|||
.padding(.horizontal, 18)
|
||||
}
|
||||
|
||||
private var emptyBrowserStateInlineStrip: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
browserImportHintBody
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: 520, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.84))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous).stroke(
|
||||
Color(nsColor: .separatorColor).opacity(0.35),
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 6, y: 2)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 14)
|
||||
}
|
||||
|
||||
private var browserImportHintPopover: some View {
|
||||
browserImportHintBody
|
||||
.padding(12)
|
||||
.frame(width: 300, alignment: .leading)
|
||||
}
|
||||
|
||||
private var browserImportHintBody: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "browser.import.hint.title", defaultValue: "Import browser data"))
|
||||
.font(.system(size: 12.5, weight: .semibold))
|
||||
|
||||
Text(browserImportHintSummary)
|
||||
.font(.system(size: 11.5))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(String(localized: "browser.import.hint.settingsFootnote", defaultValue: "You can always find this in Settings > Browser."))
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundStyle(.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 10) {
|
||||
browserImportHintPrimaryButton
|
||||
browserImportHintSettingsButton
|
||||
browserImportHintDismissButton
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
browserImportHintPrimaryButton
|
||||
HStack(spacing: 10) {
|
||||
browserImportHintSettingsButton
|
||||
browserImportHintDismissButton
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
}
|
||||
|
||||
private var browserImportHintPrimaryButton: some View {
|
||||
Button(String(localized: "browser.import.hint.import", defaultValue: "Import…")) {
|
||||
presentImportDialogFromHint()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
private var browserImportHintSettingsButton: some View {
|
||||
Button(String(localized: "browser.import.hint.settings", defaultValue: "Browser Settings")) {
|
||||
openBrowserImportSettings()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("BrowserImportHintSettingsButton")
|
||||
}
|
||||
|
||||
private var browserImportHintDismissButton: some View {
|
||||
Button(String(localized: "browser.import.hint.dismiss", defaultValue: "Hide Hint")) {
|
||||
dismissBrowserImportHint()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("BrowserImportHintDismissButton")
|
||||
}
|
||||
|
||||
private var shouldShowEmptyStateImportOverlay: Bool {
|
||||
!panel.shouldRenderWebView && isWebViewBlank()
|
||||
}
|
||||
|
||||
private func presentImportDialogFromHint() {
|
||||
isBrowserImportHintPopoverPresented = false
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog(
|
||||
defaultDestinationProfileID: panel.profileID
|
||||
)
|
||||
}
|
||||
|
||||
private func openBrowserImportSettings() {
|
||||
isBrowserImportHintPopoverPresented = false
|
||||
AppDelegate.presentPreferencesWindow(navigationTarget: .browser)
|
||||
}
|
||||
|
||||
private func dismissBrowserImportHint() {
|
||||
showBrowserImportHintOnBlankTabs = false
|
||||
isBrowserImportHintDismissed = true
|
||||
isBrowserImportHintPopoverPresented = false
|
||||
}
|
||||
|
||||
/// Treat a WebView with no URL (or about:blank) as "blank" for UX purposes.
|
||||
private func isWebViewBlank() -> Bool {
|
||||
guard let url = panel.webView.url else { return true }
|
||||
|
|
|
|||
|
|
@ -337,6 +337,10 @@ struct cmuxApp: App {
|
|||
DebugWindowControlsWindowController.shared.show()
|
||||
}
|
||||
|
||||
Button("Browser Import Hint Debug…") {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
}
|
||||
|
||||
Button("Settings/About Titlebar Debug…") {
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
}
|
||||
|
|
@ -1060,6 +1064,7 @@ struct cmuxApp: App {
|
|||
}
|
||||
|
||||
private func openAllDebugWindows() {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
SidebarDebugWindowController.shared.show()
|
||||
BackgroundDebugWindowController.shared.show()
|
||||
|
|
@ -1074,6 +1079,7 @@ private let cmuxAuxiliaryWindowIdentifiers: Set<String> = [
|
|||
"cmux.browser-popup",
|
||||
"cmux.settingsAboutTitlebarDebug",
|
||||
"cmux.debugWindowControls",
|
||||
"cmux.browserImportHintDebug",
|
||||
"cmux.sidebarDebug",
|
||||
"cmux.menubarDebug",
|
||||
"cmux.backgroundDebug",
|
||||
|
|
@ -1689,6 +1695,9 @@ private struct DebugWindowControlsView: View {
|
|||
|
||||
GroupBox("Open") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button("Browser Import Hint Debug…") {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
}
|
||||
Button("Settings/About Titlebar Debug…") {
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
}
|
||||
|
|
@ -1702,6 +1711,7 @@ private struct DebugWindowControlsView: View {
|
|||
MenuBarExtraDebugWindowController.shared.show()
|
||||
}
|
||||
Button("Open All Debug Windows") {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
SidebarDebugWindowController.shared.show()
|
||||
BackgroundDebugWindowController.shared.show()
|
||||
|
|
@ -1905,6 +1915,210 @@ private struct DebugWindowControlsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private final class BrowserImportHintDebugWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = BrowserImportHintDebugWindowController()
|
||||
|
||||
private init() {
|
||||
let window = NSPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 380, height: 420),
|
||||
styleMask: [.titled, .closable, .utilityWindow],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "Browser Import Hint Debug"
|
||||
window.titleVisibility = .visible
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.isMovableByWindowBackground = true
|
||||
window.isReleasedWhenClosed = false
|
||||
window.identifier = NSUserInterfaceItemIdentifier("cmux.browserImportHintDebug")
|
||||
window.center()
|
||||
window.contentView = NSHostingView(rootView: BrowserImportHintDebugView())
|
||||
AppDelegate.shared?.applyWindowDecorations(to: window)
|
||||
super.init(window: window)
|
||||
window.delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func show() {
|
||||
window?.center()
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private struct BrowserImportHintDebugView: View {
|
||||
@AppStorage(BrowserImportHintSettings.variantKey)
|
||||
private var variantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.showOnBlankTabsKey)
|
||||
private var showOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
@AppStorage(BrowserImportHintSettings.dismissedKey)
|
||||
private var isDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
|
||||
private var selectedVariant: BrowserImportHintVariant {
|
||||
BrowserImportHintSettings.variant(for: variantRaw)
|
||||
}
|
||||
|
||||
private var variantSelection: Binding<String> {
|
||||
Binding(
|
||||
get: { selectedVariant.rawValue },
|
||||
set: { variantRaw = BrowserImportHintSettings.variant(for: $0).rawValue }
|
||||
)
|
||||
}
|
||||
|
||||
private var showOnBlankTabsBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { showOnBlankTabs },
|
||||
set: { newValue in
|
||||
showOnBlankTabs = newValue
|
||||
if newValue {
|
||||
isDismissed = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var presentation: BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: selectedVariant,
|
||||
showOnBlankTabs: showOnBlankTabs,
|
||||
isDismissed: isDismissed
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Browser Import Hint")
|
||||
.font(.headline)
|
||||
|
||||
Text("Try lighter blank-tab import surfaces and dismissal states without touching the permanent Browser settings home.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
GroupBox("Variant") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Picker("Blank Tab Style", selection: variantSelection) {
|
||||
ForEach(BrowserImportHintVariant.allCases) { variant in
|
||||
Text(title(for: variant)).tag(variant.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
Text(description(for: selectedVariant))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox("State") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Toggle("Show on blank browser tabs", isOn: showOnBlankTabsBinding)
|
||||
Toggle("Pretend the user dismissed it", isOn: $isDismissed)
|
||||
|
||||
Text("Current blank-tab placement: \(placementTitle(presentation.blankTabPlacement))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Settings status: \(settingsStatusTitle(presentation.settingsStatus))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox("Quick Actions") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 10) {
|
||||
Button("Open Browser Settings") {
|
||||
AppDelegate.presentPreferencesWindow(navigationTarget: .browser)
|
||||
}
|
||||
Button("Open Import Dialog") {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
}
|
||||
}
|
||||
|
||||
Button("Reset Hint Debug State") {
|
||||
BrowserImportHintSettings.reset()
|
||||
}
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox("Ideas") {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Inline strip: default candidate, visible but quieter than the old floating card.")
|
||||
Text("Floating card: strongest nudge, useful when we want more explanation.")
|
||||
Text("Toolbar chip: most subtle, best when the hint should stay out of the content area.")
|
||||
Text("Settings only: no in-browser nudge, Browser settings becomes the only permanent home.")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
private func title(for variant: BrowserImportHintVariant) -> String {
|
||||
switch variant {
|
||||
case .inlineStrip:
|
||||
return "Inline Strip"
|
||||
case .floatingCard:
|
||||
return "Floating Card"
|
||||
case .toolbarChip:
|
||||
return "Toolbar Chip"
|
||||
case .settingsOnly:
|
||||
return "Settings Only"
|
||||
}
|
||||
}
|
||||
|
||||
private func description(for variant: BrowserImportHintVariant) -> String {
|
||||
switch variant {
|
||||
case .inlineStrip:
|
||||
return "Shows a thin hint bar at the top of blank browser tabs."
|
||||
case .floatingCard:
|
||||
return "Shows the fuller callout card inside blank browser tabs."
|
||||
case .toolbarChip:
|
||||
return "Moves the hint into a small toolbar chip beside the browser controls."
|
||||
case .settingsOnly:
|
||||
return "Hides the blank-tab hint and leaves Browser settings as the only home."
|
||||
}
|
||||
}
|
||||
|
||||
private func placementTitle(_ placement: BrowserImportHintBlankTabPlacement) -> String {
|
||||
switch placement {
|
||||
case .hidden:
|
||||
return "Hidden"
|
||||
case .inlineStrip:
|
||||
return "Inline Strip"
|
||||
case .floatingCard:
|
||||
return "Floating Card"
|
||||
case .toolbarChip:
|
||||
return "Toolbar Chip"
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsStatusTitle(_ status: BrowserImportHintSettingsStatus) -> String {
|
||||
switch status {
|
||||
case .visible:
|
||||
return "Visible"
|
||||
case .hidden:
|
||||
return "Hidden"
|
||||
case .settingsOnly:
|
||||
return "Settings Only"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AboutWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = AboutWindowController()
|
||||
|
||||
|
|
@ -2035,6 +2249,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
|||
}
|
||||
|
||||
enum SettingsNavigationTarget: String {
|
||||
case browser
|
||||
case keyboardShortcuts
|
||||
}
|
||||
|
||||
|
|
@ -3103,6 +3318,9 @@ struct SettingsView: View {
|
|||
@AppStorage(BrowserSearchSettings.searchEngineKey) private var browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
|
||||
@AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
@AppStorage(BrowserThemeSettings.modeKey) private var browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.variantKey) private var browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.showOnBlankTabsKey) private var showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
@AppStorage(BrowserImportHintSettings.dismissedKey) private var isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
@AppStorage(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) private var openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser
|
||||
@AppStorage(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey)
|
||||
private var interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.initialInterceptTerminalOpenCommandInCmuxBrowserValue()
|
||||
|
|
@ -3204,6 +3422,30 @@ struct SettingsView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var browserImportHintVariant: BrowserImportHintVariant {
|
||||
BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
|
||||
}
|
||||
|
||||
private var browserImportHintPresentation: BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: browserImportHintVariant,
|
||||
showOnBlankTabs: showBrowserImportHintOnBlankTabs,
|
||||
isDismissed: isBrowserImportHintDismissed
|
||||
)
|
||||
}
|
||||
|
||||
private var browserImportHintVisibilityBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { showBrowserImportHintOnBlankTabs },
|
||||
set: { newValue in
|
||||
showBrowserImportHintOnBlankTabs = newValue
|
||||
if newValue {
|
||||
isBrowserImportHintDismissed = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var socketModeSelection: Binding<String> {
|
||||
Binding(
|
||||
get: { socketControlMode },
|
||||
|
|
@ -3266,6 +3508,17 @@ struct SettingsView: View {
|
|||
InstalledBrowserDetector.summaryText(for: detectedImportBrowsers)
|
||||
}
|
||||
|
||||
private var browserImportHintSettingsNote: String {
|
||||
switch browserImportHintPresentation.settingsStatus {
|
||||
case .visible:
|
||||
return String(localized: "settings.browser.import.hint.note.visible", defaultValue: "Blank browser tabs can show this import suggestion. Hide or re-enable it here.")
|
||||
case .hidden:
|
||||
return String(localized: "settings.browser.import.hint.note.hidden", defaultValue: "The blank-tab import hint is hidden. Turn it back on here any time.")
|
||||
case .settingsOnly:
|
||||
return String(localized: "settings.browser.import.hint.note.settingsOnly", defaultValue: "Blank tabs are currently using Settings only mode from the debug window.")
|
||||
}
|
||||
}
|
||||
|
||||
private var browserInsecureHTTPAllowlistHasUnsavedChanges: Bool {
|
||||
browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist
|
||||
}
|
||||
|
|
@ -4187,6 +4440,8 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
SettingsSectionHeader(title: String(localized: "settings.section.browser", defaultValue: "Browser"))
|
||||
.id(SettingsNavigationTarget.browser)
|
||||
.accessibilityIdentifier("SettingsBrowserSection")
|
||||
SettingsCard {
|
||||
SettingsPickerRow(
|
||||
String(localized: "settings.browser.searchEngine", defaultValue: "Default Search Engine"),
|
||||
|
|
@ -4361,7 +4616,38 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(String(localized: "settings.browser.import", defaultValue: "Import From Browser"), subtitle: browserImportSubtitle) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "settings.browser.import", defaultValue: "Import From Browser"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(String(localized: "browser.import.hint.title", defaultValue: "Import browser data"))
|
||||
.font(.system(size: 12.5, weight: .semibold))
|
||||
|
||||
Text(browserImportSubtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(String(localized: "browser.import.hint.settingsFootnote", defaultValue: "You can always find this in Settings > Browser."))
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundStyle(.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(nsColor: .controlBackgroundColor))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.stroke(Color(nsColor: .separatorColor).opacity(0.4), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button(String(localized: "settings.browser.import.choose", defaultValue: "Choose…")) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
|
|
@ -4376,7 +4662,22 @@ struct SettingsView: View {
|
|||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
.accessibilityIdentifier("SettingsBrowserImportActions")
|
||||
|
||||
Toggle(
|
||||
String(localized: "settings.browser.import.hint.show", defaultValue: "Show import hint on blank browser tabs"),
|
||||
isOn: browserImportHintVisibilityBinding
|
||||
)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("SettingsBrowserImportHintToggle")
|
||||
|
||||
Text(browserImportHintSettingsNote)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
|
|
@ -4520,6 +4821,7 @@ struct SettingsView: View {
|
|||
BrowserHistoryStore.shared.loadIfNeeded()
|
||||
notificationStore.refreshAuthorizationStatus()
|
||||
browserThemeMode = BrowserThemeSettings.mode(defaults: .standard).rawValue
|
||||
browserImportHintVariantRaw = BrowserImportHintSettings.variant(for: browserImportHintVariantRaw).rawValue
|
||||
browserHistoryEntryCount = BrowserHistoryStore.shared.entries.count
|
||||
browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist
|
||||
refreshDetectedImportBrowsers()
|
||||
|
|
@ -4633,6 +4935,9 @@ struct SettingsView: View {
|
|||
browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
|
||||
browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
|
||||
browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser
|
||||
interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser
|
||||
browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist
|
||||
|
|
|
|||
|
|
@ -144,6 +144,50 @@ final class BrowserImportMappingTests: XCTestCase {
|
|||
XCTAssertTrue(manyProfilesPresentation.showsHelpText)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationDefaultsToInlineStrip() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .inlineStrip,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .inlineStrip)
|
||||
XCTAssertEqual(presentation.settingsStatus, .visible)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationHidesBlankTabHintWhenDismissed() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .floatingCard,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: true
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .hidden)
|
||||
XCTAssertEqual(presentation.settingsStatus, .hidden)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationUsesToolbarChipWhenEnabled() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .toolbarChip,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .toolbarChip)
|
||||
XCTAssertEqual(presentation.settingsStatus, .visible)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationSettingsOnlyVariantStaysInSettings() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .settingsOnly,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .hidden)
|
||||
XCTAssertEqual(presentation.settingsStatus, .settingsOnly)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testRealizePlanCreatesMissingDestinationProfilesOnlyWhenRequested() throws {
|
||||
let suiteName = "BrowserImportMappingTests-\(UUID().uuidString)"
|
||||
|
|
|
|||
|
|
@ -112,6 +112,32 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
XCTAssertEqual(capture["scope"] as? String, "everything")
|
||||
}
|
||||
|
||||
func testBlankBrowserImportHintCanOpenBrowserSettings() {
|
||||
let app = launchAppForBlankImportHint()
|
||||
|
||||
let settingsButton = app.buttons["BrowserImportHintSettingsButton"]
|
||||
XCTAssertTrue(settingsButton.waitForExistence(timeout: 5.0))
|
||||
settingsButton.click()
|
||||
|
||||
XCTAssertTrue(
|
||||
app.otherElements["SettingsBrowserSection"].waitForExistence(timeout: 5.0),
|
||||
"Expected Browser Settings to open from the blank-tab import hint"
|
||||
)
|
||||
}
|
||||
|
||||
func testBlankBrowserImportHintCanBeDismissed() {
|
||||
let app = launchAppForBlankImportHint()
|
||||
|
||||
let dismissButton = app.buttons["BrowserImportHintDismissButton"]
|
||||
XCTAssertTrue(dismissButton.waitForExistence(timeout: 5.0))
|
||||
dismissButton.click()
|
||||
|
||||
XCTAssertTrue(
|
||||
browserImportPollUntil(timeout: 2.0) { !dismissButton.exists },
|
||||
"Expected the blank-tab import hint to disappear after dismissal"
|
||||
)
|
||||
}
|
||||
|
||||
private func launchApp() -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||
|
|
@ -125,6 +151,18 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
return app
|
||||
}
|
||||
|
||||
private func launchAppForBlankImportHint() -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_VARIANT"] = "inlineStrip"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_SHOW"] = "1"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_DISMISSED"] = "0"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_BLANK_BROWSER"] = "1"
|
||||
launchAndActivate(app)
|
||||
waitForBlankImportHint(app)
|
||||
return app
|
||||
}
|
||||
|
||||
private func waitForImportWizard(_ app: XCUIApplication) {
|
||||
let wizardOpened = browserImportPollUntil(timeout: 5.0) {
|
||||
app.buttons["Next"].exists || app.windows["Import Browser Data"].exists
|
||||
|
|
@ -132,6 +170,13 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
XCTAssertTrue(wizardOpened, "Expected the import wizard to open")
|
||||
}
|
||||
|
||||
private func waitForBlankImportHint(_ app: XCUIApplication) {
|
||||
let hintOpened = browserImportPollUntil(timeout: 5.0) {
|
||||
app.buttons["BrowserImportHintDismissButton"].exists
|
||||
}
|
||||
XCTAssertTrue(hintOpened, "Expected the blank browser import hint to appear")
|
||||
}
|
||||
|
||||
private func waitForCapturedSelection(timeout: TimeInterval) -> [String: Any]? {
|
||||
let url = URL(fileURLWithPath: capturePath)
|
||||
let foundCapture = browserImportPollUntil(timeout: timeout) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue