* Show update-available banner automatically on launch Probe for updates immediately on launch via Sparkle's checkForUpdateInformation() so the sidebar surfaces a passive update indicator without waiting for the 24h scheduler. When Sparkle detects an available update in the background, the pill now shows "Update Available: X.Y.Z" with accent styling while the updater is idle. Clicking it triggers the full interactive update flow. Also fixes thread safety in delegate callbacks by dispatching @Published mutations to the main queue. Closes #1643 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add periodic background update probe every 15 minutes The launch-only probe wouldn't catch updates published while the app is already running. Add a repeating 15-minute timer that calls checkForUpdateInformation() so the sidebar banner appears within a reasonable window after a new version is published. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Change background update probe interval to 30 minutes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Change update check interval to 1 hour and migrate existing users Reduce Sparkle's scheduled check interval from 24h to 1h so update banners appear sooner. Migrate users stuck on the old 24h default by bumping the migration key to v2. Align background probe interval with the Sparkle check interval instead of hardcoding 30 minutes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
988 lines
35 KiB
Swift
988 lines
35 KiB
Swift
import XCTest
|
|
import AppKit
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
import WebKit
|
|
import ObjectiveC.runtime
|
|
import Bonsplit
|
|
import UserNotifications
|
|
|
|
#if canImport(cmux_DEV)
|
|
@testable import cmux_DEV
|
|
#elseif canImport(cmux)
|
|
@testable import cmux
|
|
#endif
|
|
|
|
final class SplitShortcutTransientFocusGuardTests: XCTestCase {
|
|
func testSuppressesWhenFirstResponderFallsBackAndHostedViewIsTiny() {
|
|
XCTAssertTrue(
|
|
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
|
|
firstResponderIsWindow: true,
|
|
hostedSize: CGSize(width: 79, height: 0),
|
|
hostedHiddenInHierarchy: false,
|
|
hostedAttachedToWindow: true
|
|
)
|
|
)
|
|
}
|
|
|
|
func testSuppressesWhenFirstResponderFallsBackAndHostedViewIsDetached() {
|
|
XCTAssertTrue(
|
|
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
|
|
firstResponderIsWindow: true,
|
|
hostedSize: CGSize(width: 1051.5, height: 1207),
|
|
hostedHiddenInHierarchy: false,
|
|
hostedAttachedToWindow: false
|
|
)
|
|
)
|
|
}
|
|
|
|
func testAllowsWhenFirstResponderFallsBackButGeometryIsHealthy() {
|
|
XCTAssertFalse(
|
|
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
|
|
firstResponderIsWindow: true,
|
|
hostedSize: CGSize(width: 1051.5, height: 1207),
|
|
hostedHiddenInHierarchy: false,
|
|
hostedAttachedToWindow: true
|
|
)
|
|
)
|
|
}
|
|
|
|
func testAllowsWhenFirstResponderIsTerminalEvenIfViewIsTiny() {
|
|
XCTAssertFalse(
|
|
shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
|
|
firstResponderIsWindow: false,
|
|
hostedSize: CGSize(width: 79, height: 0),
|
|
hostedHiddenInHierarchy: false,
|
|
hostedAttachedToWindow: true
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
final class FullScreenShortcutTests: XCTestCase {
|
|
func testMatchesCommandControlF() {
|
|
XCTAssertTrue(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "f",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
}
|
|
|
|
func testMatchesCommandControlFFromKeyCodeWhenCharsAreUnavailable() {
|
|
XCTAssertTrue(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "",
|
|
keyCode: 3,
|
|
layoutCharacterProvider: { _, _ in nil }
|
|
)
|
|
)
|
|
}
|
|
|
|
func testDoesNotFallbackToANSIWhenLayoutTranslationReturnsNonFCharacter() {
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "",
|
|
keyCode: 3,
|
|
layoutCharacterProvider: { _, _ in "u" }
|
|
)
|
|
)
|
|
}
|
|
|
|
func testMatchesCommandControlFWhenCommandAwareLayoutTranslationProvidesF() {
|
|
XCTAssertTrue(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "",
|
|
keyCode: 3,
|
|
layoutCharacterProvider: { _, modifierFlags in
|
|
modifierFlags.contains(.command) ? "f" : "u"
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
func testMatchesCommandControlFWhenCharsAreControlSequence() {
|
|
XCTAssertTrue(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "\u{06}",
|
|
keyCode: 3,
|
|
layoutCharacterProvider: { _, _ in nil }
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRejectsPhysicalFWhenCharacterRepresentsDifferentLayoutKey() {
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "u",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
}
|
|
|
|
func testIgnoresCapsLockForCommandControlF() {
|
|
XCTAssertTrue(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control, .capsLock],
|
|
chars: "f",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRejectsWhenControlIsMissing() {
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command],
|
|
chars: "f",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRejectsAdditionalModifiers() {
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control, .shift],
|
|
chars: "f",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control, .option],
|
|
chars: "f",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRejectsWhenCommandIsMissing() {
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.control],
|
|
chars: "f",
|
|
keyCode: 3
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRejectsNonFKey() {
|
|
XCTAssertFalse(
|
|
shouldToggleMainWindowFullScreenForCommandControlFShortcut(
|
|
flags: [.command, .control],
|
|
chars: "r",
|
|
keyCode: 15
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
final class CommandPaletteKeyboardNavigationTests: XCTestCase {
|
|
func testArrowKeysMoveSelectionWithoutModifiers() {
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [],
|
|
chars: "",
|
|
keyCode: 125
|
|
),
|
|
1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [],
|
|
chars: "",
|
|
keyCode: 126
|
|
),
|
|
-1
|
|
)
|
|
XCTAssertNil(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.shift],
|
|
chars: "",
|
|
keyCode: 125
|
|
)
|
|
)
|
|
}
|
|
|
|
func testControlLetterNavigationSupportsPrintableAndControlChars() {
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "n",
|
|
keyCode: 45
|
|
),
|
|
1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "\u{0e}",
|
|
keyCode: 45
|
|
),
|
|
1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "p",
|
|
keyCode: 35
|
|
),
|
|
-1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "\u{10}",
|
|
keyCode: 35
|
|
),
|
|
-1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "j",
|
|
keyCode: 38
|
|
),
|
|
1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "\u{0a}",
|
|
keyCode: 38
|
|
),
|
|
1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "k",
|
|
keyCode: 40
|
|
),
|
|
-1
|
|
)
|
|
XCTAssertEqual(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "\u{0b}",
|
|
keyCode: 40
|
|
),
|
|
-1
|
|
)
|
|
}
|
|
|
|
func testIgnoresUnsupportedModifiersAndKeys() {
|
|
XCTAssertNil(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.command],
|
|
chars: "n",
|
|
keyCode: 45
|
|
)
|
|
)
|
|
XCTAssertNil(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control, .shift],
|
|
chars: "n",
|
|
keyCode: 45
|
|
)
|
|
)
|
|
XCTAssertNil(
|
|
commandPaletteSelectionDeltaForKeyboardNavigation(
|
|
flags: [.control],
|
|
chars: "x",
|
|
keyCode: 7
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
final class CommandPaletteOpenShortcutConsumptionTests: XCTestCase {
|
|
func testDoesNotConsumeWhenPaletteIsNotVisible() {
|
|
XCTAssertFalse(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: false,
|
|
normalizedFlags: [.command],
|
|
chars: "n",
|
|
keyCode: 45
|
|
)
|
|
)
|
|
}
|
|
|
|
func testConsumesAppCommandShortcutsWhenPaletteIsVisible() {
|
|
XCTAssertTrue(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command],
|
|
chars: "n",
|
|
keyCode: 45
|
|
)
|
|
)
|
|
XCTAssertTrue(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command],
|
|
chars: "t",
|
|
keyCode: 17
|
|
)
|
|
)
|
|
XCTAssertTrue(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command, .shift],
|
|
chars: ",",
|
|
keyCode: 43
|
|
)
|
|
)
|
|
}
|
|
|
|
func testAllowsClipboardAndUndoShortcutsForPaletteTextEditing() {
|
|
XCTAssertFalse(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command],
|
|
chars: "v",
|
|
keyCode: 9
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command],
|
|
chars: "z",
|
|
keyCode: 6
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command, .shift],
|
|
chars: "z",
|
|
keyCode: 6
|
|
)
|
|
)
|
|
}
|
|
|
|
func testAllowsArrowAndDeleteEditingCommandsForPaletteTextEditing() {
|
|
XCTAssertFalse(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command],
|
|
chars: "",
|
|
keyCode: 123
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [.command],
|
|
chars: "",
|
|
keyCode: 51
|
|
)
|
|
)
|
|
}
|
|
|
|
func testConsumesEscapeWhenPaletteIsVisible() {
|
|
XCTAssertTrue(
|
|
shouldConsumeShortcutWhileCommandPaletteVisible(
|
|
isCommandPaletteVisible: true,
|
|
normalizedFlags: [],
|
|
chars: "",
|
|
keyCode: 53
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
final class CommandPaletteRestoreFocusStateMachineTests: XCTestCase {
|
|
func testRestoresBrowserAddressBarWhenPaletteOpenedFromFocusedAddressBar() {
|
|
let panelId = UUID()
|
|
XCTAssertTrue(
|
|
ContentView.shouldRestoreBrowserAddressBarAfterCommandPaletteDismiss(
|
|
focusedPanelIsBrowser: true,
|
|
focusedBrowserAddressBarPanelId: panelId,
|
|
focusedPanelId: panelId
|
|
)
|
|
)
|
|
}
|
|
|
|
func testDoesNotRestoreBrowserAddressBarWhenFocusedPanelIsNotBrowser() {
|
|
let panelId = UUID()
|
|
XCTAssertFalse(
|
|
ContentView.shouldRestoreBrowserAddressBarAfterCommandPaletteDismiss(
|
|
focusedPanelIsBrowser: false,
|
|
focusedBrowserAddressBarPanelId: panelId,
|
|
focusedPanelId: panelId
|
|
)
|
|
)
|
|
}
|
|
|
|
func testDoesNotRestoreBrowserAddressBarWhenAnotherPanelHadAddressBarFocus() {
|
|
XCTAssertFalse(
|
|
ContentView.shouldRestoreBrowserAddressBarAfterCommandPaletteDismiss(
|
|
focusedPanelIsBrowser: true,
|
|
focusedBrowserAddressBarPanelId: UUID(),
|
|
focusedPanelId: UUID()
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
final class CommandPaletteRenameSelectionSettingsTests: XCTestCase {
|
|
private let suiteName = "cmux.tests.commandPaletteRenameSelection.\(UUID().uuidString)"
|
|
|
|
private func makeDefaults() -> UserDefaults {
|
|
let defaults = UserDefaults(suiteName: suiteName)!
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
return defaults
|
|
}
|
|
|
|
func testDefaultsToSelectAllWhenUnset() {
|
|
let defaults = makeDefaults()
|
|
XCTAssertTrue(CommandPaletteRenameSelectionSettings.selectAllOnFocusEnabled(defaults: defaults))
|
|
}
|
|
|
|
func testReturnsFalseWhenStoredFalse() {
|
|
let defaults = makeDefaults()
|
|
defaults.set(false, forKey: CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
|
|
XCTAssertFalse(CommandPaletteRenameSelectionSettings.selectAllOnFocusEnabled(defaults: defaults))
|
|
}
|
|
|
|
func testReturnsTrueWhenStoredTrue() {
|
|
let defaults = makeDefaults()
|
|
defaults.set(true, forKey: CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
|
|
XCTAssertTrue(CommandPaletteRenameSelectionSettings.selectAllOnFocusEnabled(defaults: defaults))
|
|
}
|
|
}
|
|
|
|
|
|
final class CommandPaletteSelectionScrollBehaviorTests: XCTestCase {
|
|
func testFirstEntryPinsToTopAnchor() {
|
|
let anchor = ContentView.commandPaletteScrollPositionAnchor(
|
|
selectedIndex: 0,
|
|
resultCount: 20
|
|
)
|
|
XCTAssertEqual(anchor, UnitPoint.top)
|
|
}
|
|
|
|
func testLastEntryPinsToBottomAnchor() {
|
|
let anchor = ContentView.commandPaletteScrollPositionAnchor(
|
|
selectedIndex: 19,
|
|
resultCount: 20
|
|
)
|
|
XCTAssertEqual(anchor, UnitPoint.bottom)
|
|
}
|
|
|
|
func testMiddleEntryUsesNilAnchorForMinimalScroll() {
|
|
let anchor = ContentView.commandPaletteScrollPositionAnchor(
|
|
selectedIndex: 6,
|
|
resultCount: 20
|
|
)
|
|
XCTAssertNil(anchor)
|
|
}
|
|
|
|
func testEmptyResultsProduceNoAnchor() {
|
|
let anchor = ContentView.commandPaletteScrollPositionAnchor(
|
|
selectedIndex: 0,
|
|
resultCount: 0
|
|
)
|
|
XCTAssertNil(anchor)
|
|
}
|
|
}
|
|
|
|
|
|
final class ShortcutHintModifierPolicyTests: XCTestCase {
|
|
func testShortcutHintRequiresEnabledCommandOnlyModifier() {
|
|
withDefaultsSuite { defaults in
|
|
defaults.set(true, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
|
|
XCTAssertTrue(ShortcutHintModifierPolicy.shouldShowHints(for: [.command], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command, .shift], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control, .shift], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command, .option], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control, .option], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command, .control], defaults: defaults))
|
|
}
|
|
}
|
|
|
|
func testCommandHintCanBeDisabledInSettings() {
|
|
withDefaultsSuite { defaults in
|
|
defaults.set(false, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.command], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control], defaults: defaults))
|
|
}
|
|
}
|
|
|
|
func testCommandHintDefaultsToEnabledWhenSettingMissing() {
|
|
withDefaultsSuite { defaults in
|
|
defaults.removeObject(forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
|
|
XCTAssertTrue(ShortcutHintModifierPolicy.shouldShowHints(for: [.command], defaults: defaults))
|
|
XCTAssertFalse(ShortcutHintModifierPolicy.shouldShowHints(for: [.control], defaults: defaults))
|
|
}
|
|
}
|
|
|
|
func testShortcutHintUsesIntentionalHoldDelay() {
|
|
XCTAssertEqual(ShortcutHintModifierPolicy.intentionalHoldDelay, 0.30, accuracy: 0.001)
|
|
}
|
|
|
|
func testCurrentWindowRequiresHostWindowToBeKeyAndMatchEventWindow() {
|
|
XCTAssertTrue(
|
|
ShortcutHintModifierPolicy.isCurrentWindow(
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: true,
|
|
eventWindowNumber: 42,
|
|
keyWindowNumber: 42
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(
|
|
ShortcutHintModifierPolicy.isCurrentWindow(
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: true,
|
|
eventWindowNumber: 7,
|
|
keyWindowNumber: 42
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(
|
|
ShortcutHintModifierPolicy.isCurrentWindow(
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: false,
|
|
eventWindowNumber: 42,
|
|
keyWindowNumber: 42
|
|
)
|
|
)
|
|
}
|
|
|
|
func testWindowScopedShortcutHintsUseKeyWindowWhenNoEventWindowIsAvailable() {
|
|
withDefaultsSuite { defaults in
|
|
defaults.set(true, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
|
|
XCTAssertTrue(
|
|
ShortcutHintModifierPolicy.shouldShowHints(
|
|
for: [.command],
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: true,
|
|
eventWindowNumber: nil,
|
|
keyWindowNumber: 42,
|
|
defaults: defaults
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(
|
|
ShortcutHintModifierPolicy.shouldShowHints(
|
|
for: [.command],
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: true,
|
|
eventWindowNumber: nil,
|
|
keyWindowNumber: 7,
|
|
defaults: defaults
|
|
)
|
|
)
|
|
|
|
XCTAssertTrue(
|
|
ShortcutHintModifierPolicy.shouldShowHints(
|
|
for: [.command],
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: true,
|
|
eventWindowNumber: nil,
|
|
keyWindowNumber: 42,
|
|
defaults: defaults
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(
|
|
ShortcutHintModifierPolicy.shouldShowHints(
|
|
for: [.control],
|
|
hostWindowNumber: 42,
|
|
hostWindowIsKey: true,
|
|
eventWindowNumber: nil,
|
|
keyWindowNumber: 42,
|
|
defaults: defaults
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private func withDefaultsSuite(_ body: (UserDefaults) -> Void) {
|
|
let suiteName = "ShortcutHintModifierPolicyTests-\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create defaults suite")
|
|
return
|
|
}
|
|
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
body(defaults)
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
}
|
|
}
|
|
|
|
|
|
final class ShortcutHintDebugSettingsTests: XCTestCase {
|
|
func testClampKeepsValuesWithinSupportedRange() {
|
|
XCTAssertEqual(ShortcutHintDebugSettings.clamped(0.0), 0.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.clamped(4.0), 4.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.clamped(-100.0), ShortcutHintDebugSettings.offsetRange.lowerBound)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.clamped(100.0), ShortcutHintDebugSettings.offsetRange.upperBound)
|
|
}
|
|
|
|
func testDefaultOffsetsMatchCurrentBadgePlacements() {
|
|
XCTAssertEqual(ShortcutHintDebugSettings.defaultSidebarHintX, 0.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.defaultSidebarHintY, 0.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.defaultTitlebarHintX, 4.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.defaultTitlebarHintY, 0.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.defaultPaneHintX, 0.0)
|
|
XCTAssertEqual(ShortcutHintDebugSettings.defaultPaneHintY, 0.0)
|
|
XCTAssertFalse(ShortcutHintDebugSettings.defaultAlwaysShowHints)
|
|
XCTAssertTrue(ShortcutHintDebugSettings.defaultShowHintsOnCommandHold)
|
|
}
|
|
|
|
func testShowHintsOnCommandHoldSettingRespectsStoredValue() {
|
|
let suiteName = "ShortcutHintDebugSettingsTests-\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create defaults suite")
|
|
return
|
|
}
|
|
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
|
|
defaults.removeObject(forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
XCTAssertTrue(ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults))
|
|
|
|
defaults.set(false, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
XCTAssertFalse(ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults))
|
|
|
|
defaults.set(true, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
XCTAssertTrue(ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults))
|
|
}
|
|
|
|
func testResetVisibilityDefaultsRestoresAlwaysShowAndCommandHoldFlags() {
|
|
let suiteName = "ShortcutHintDebugSettingsTests-\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create defaults suite")
|
|
return
|
|
}
|
|
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
|
|
defaults.set(true, forKey: ShortcutHintDebugSettings.alwaysShowHintsKey)
|
|
defaults.set(false, forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
|
|
|
ShortcutHintDebugSettings.resetVisibilityDefaults(defaults: defaults)
|
|
|
|
XCTAssertEqual(
|
|
defaults.object(forKey: ShortcutHintDebugSettings.alwaysShowHintsKey) as? Bool,
|
|
ShortcutHintDebugSettings.defaultAlwaysShowHints
|
|
)
|
|
XCTAssertEqual(
|
|
defaults.object(forKey: ShortcutHintDebugSettings.showHintsOnCommandHoldKey) as? Bool,
|
|
ShortcutHintDebugSettings.defaultShowHintsOnCommandHold
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
final class DevBuildBannerDebugSettingsTests: XCTestCase {
|
|
func testShowSidebarBannerDefaultsToVisible() {
|
|
let suiteName = "DevBuildBannerDebugSettingsTests.Default.\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create isolated UserDefaults suite")
|
|
return
|
|
}
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
|
|
defaults.removeObject(forKey: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
|
XCTAssertTrue(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
|
|
}
|
|
|
|
func testShowSidebarBannerRespectsStoredValue() {
|
|
let suiteName = "DevBuildBannerDebugSettingsTests.Stored.\(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: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
|
XCTAssertFalse(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
|
|
|
|
defaults.set(true, forKey: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
|
XCTAssertTrue(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
|
|
}
|
|
}
|
|
|
|
|
|
final class ShortcutHintLanePlannerTests: XCTestCase {
|
|
func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() {
|
|
let intervals: [ClosedRange<CGFloat>] = [0...20, 28...40, 48...64]
|
|
XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 0, 0])
|
|
}
|
|
|
|
func testAssignLanesStacksOverlappingIntervalsIntoAdditionalLanes() {
|
|
let intervals: [ClosedRange<CGFloat>] = [0...20, 18...34, 22...38, 40...56]
|
|
XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 1, 2, 0])
|
|
}
|
|
}
|
|
|
|
|
|
final class ShortcutHintHorizontalPlannerTests: XCTestCase {
|
|
func testAssignRightEdgesResolvesOverlapWithMinimumSpacing() {
|
|
let intervals: [ClosedRange<CGFloat>] = [0...20, 18...34, 30...46]
|
|
let rightEdges = ShortcutHintHorizontalPlanner.assignRightEdges(for: intervals, minSpacing: 6)
|
|
|
|
XCTAssertEqual(rightEdges.count, intervals.count)
|
|
|
|
let adjustedIntervals = zip(intervals, rightEdges).map { interval, rightEdge in
|
|
let width = interval.upperBound - interval.lowerBound
|
|
return (rightEdge - width)...rightEdge
|
|
}
|
|
|
|
XCTAssertGreaterThanOrEqual(adjustedIntervals[1].lowerBound - adjustedIntervals[0].upperBound, 6)
|
|
XCTAssertGreaterThanOrEqual(adjustedIntervals[2].lowerBound - adjustedIntervals[1].upperBound, 6)
|
|
}
|
|
|
|
func testAssignRightEdgesKeepsAlreadySeparatedIntervalsInPlace() {
|
|
let intervals: [ClosedRange<CGFloat>] = [0...12, 20...32, 40...52]
|
|
let rightEdges = ShortcutHintHorizontalPlanner.assignRightEdges(for: intervals, minSpacing: 4)
|
|
XCTAssertEqual(rightEdges, [12, 32, 52])
|
|
}
|
|
}
|
|
|
|
|
|
final class LastSurfaceCloseShortcutSettingsTests: XCTestCase {
|
|
func testDefaultClosesWorkspace() {
|
|
let suiteName = "LastSurfaceCloseShortcutSettingsTests.Default.\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create isolated UserDefaults suite")
|
|
return
|
|
}
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
|
|
XCTAssertTrue(LastSurfaceCloseShortcutSettings.closesWorkspace(defaults: defaults))
|
|
}
|
|
|
|
func testStoredTrueClosesWorkspace() {
|
|
let suiteName = "LastSurfaceCloseShortcutSettingsTests.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: LastSurfaceCloseShortcutSettings.key)
|
|
XCTAssertTrue(LastSurfaceCloseShortcutSettings.closesWorkspace(defaults: defaults))
|
|
}
|
|
|
|
func testStoredFalseKeepsWorkspaceOpen() {
|
|
let suiteName = "LastSurfaceCloseShortcutSettingsTests.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: LastSurfaceCloseShortcutSettings.key)
|
|
XCTAssertFalse(LastSurfaceCloseShortcutSettings.closesWorkspace(defaults: defaults))
|
|
}
|
|
}
|
|
|
|
|
|
final class AppearanceSettingsTests: XCTestCase {
|
|
func testResolvedModeDefaultsToSystemWhenUnset() {
|
|
let suiteName = "AppearanceSettingsTests.Default.\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create isolated UserDefaults suite")
|
|
return
|
|
}
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
|
|
defaults.removeObject(forKey: AppearanceSettings.appearanceModeKey)
|
|
|
|
let resolved = AppearanceSettings.resolvedMode(defaults: defaults)
|
|
XCTAssertEqual(resolved, .system)
|
|
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
|
|
}
|
|
}
|
|
|
|
|
|
final class QuitWarningSettingsTests: XCTestCase {
|
|
func testDefaultWarnBeforeQuitIsEnabledWhenUnset() {
|
|
let suiteName = "QuitWarningSettingsTests.Default.\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
XCTFail("Failed to create isolated UserDefaults suite")
|
|
return
|
|
}
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
|
|
defaults.removeObject(forKey: QuitWarningSettings.warnBeforeQuitKey)
|
|
|
|
XCTAssertTrue(QuitWarningSettings.isEnabled(defaults: defaults))
|
|
}
|
|
|
|
func testStoredPreferenceOverridesDefault() {
|
|
let suiteName = "QuitWarningSettingsTests.Stored.\(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: QuitWarningSettings.warnBeforeQuitKey)
|
|
XCTAssertFalse(QuitWarningSettings.isEnabled(defaults: defaults))
|
|
|
|
defaults.set(true, forKey: QuitWarningSettings.warnBeforeQuitKey)
|
|
XCTAssertTrue(QuitWarningSettings.isEnabled(defaults: defaults))
|
|
}
|
|
}
|
|
|
|
|
|
final class UpdateChannelSettingsTests: XCTestCase {
|
|
func testResolvedFeedFallsBackWhenInfoFeedMissing() {
|
|
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: nil)
|
|
XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL)
|
|
XCTAssertFalse(resolved.isNightly)
|
|
XCTAssertTrue(resolved.usedFallback)
|
|
}
|
|
|
|
func testResolvedFeedFallsBackWhenInfoFeedEmpty() {
|
|
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: "")
|
|
XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL)
|
|
XCTAssertFalse(resolved.isNightly)
|
|
XCTAssertTrue(resolved.usedFallback)
|
|
}
|
|
|
|
func testResolvedFeedUsesInfoFeedForStableChannel() {
|
|
let infoFeed = "https://example.com/custom/appcast.xml"
|
|
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeed)
|
|
XCTAssertEqual(resolved.url, infoFeed)
|
|
XCTAssertFalse(resolved.isNightly)
|
|
XCTAssertFalse(resolved.usedFallback)
|
|
}
|
|
|
|
func testResolvedFeedDetectsNightlyFromInfoFeedURL() {
|
|
let resolved = UpdateFeedResolver.resolvedFeedURLString(
|
|
infoFeedURL: "https://example.com/nightly/appcast.xml"
|
|
)
|
|
XCTAssertEqual(resolved.url, "https://example.com/nightly/appcast.xml")
|
|
XCTAssertTrue(resolved.isNightly)
|
|
XCTAssertFalse(resolved.usedFallback)
|
|
}
|
|
}
|
|
|
|
|
|
final class UpdateSettingsTests: XCTestCase {
|
|
func testApplyEnablesAutomaticChecksAndDailySchedule() {
|
|
let defaults = makeDefaults()
|
|
UpdateSettings.apply(to: defaults)
|
|
|
|
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.automaticChecksKey))
|
|
XCTAssertEqual(defaults.double(forKey: UpdateSettings.scheduledCheckIntervalKey), UpdateSettings.scheduledCheckInterval)
|
|
XCTAssertFalse(defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey))
|
|
XCTAssertFalse(defaults.bool(forKey: UpdateSettings.sendProfileInfoKey))
|
|
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.migrationKey))
|
|
}
|
|
|
|
func testApplyRepairsLegacyDisabledAutomaticChecksOnce() {
|
|
let defaults = makeDefaults()
|
|
defaults.set(false, forKey: UpdateSettings.automaticChecksKey)
|
|
defaults.set(0, forKey: UpdateSettings.scheduledCheckIntervalKey)
|
|
defaults.set(true, forKey: UpdateSettings.automaticallyUpdateKey)
|
|
|
|
UpdateSettings.apply(to: defaults)
|
|
|
|
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.automaticChecksKey))
|
|
XCTAssertEqual(defaults.double(forKey: UpdateSettings.scheduledCheckIntervalKey), UpdateSettings.scheduledCheckInterval)
|
|
XCTAssertTrue(defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey))
|
|
|
|
defaults.set(false, forKey: UpdateSettings.automaticChecksKey)
|
|
UpdateSettings.apply(to: defaults)
|
|
|
|
XCTAssertFalse(defaults.bool(forKey: UpdateSettings.automaticChecksKey))
|
|
}
|
|
|
|
private func makeDefaults() -> UserDefaults {
|
|
let suiteName = "UpdateSettingsTests.\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
fatalError("Failed to create isolated UserDefaults suite")
|
|
}
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
return defaults
|
|
}
|
|
}
|
|
|
|
final class UpdateViewModelPresentationTests: XCTestCase {
|
|
func testDetectedBackgroundUpdateShowsPillWhileIdle() {
|
|
let viewModel = UpdateViewModel()
|
|
|
|
viewModel.detectedUpdateVersion = "9.9.9"
|
|
|
|
XCTAssertTrue(viewModel.showsPill)
|
|
XCTAssertTrue(viewModel.showsDetectedBackgroundUpdate)
|
|
XCTAssertEqual(viewModel.text, "Update Available: 9.9.9")
|
|
XCTAssertEqual(viewModel.iconName, "shippingbox.fill")
|
|
}
|
|
|
|
func testActiveUpdateStateTakesPrecedenceOverDetectedBackgroundVersion() {
|
|
let viewModel = UpdateViewModel()
|
|
|
|
viewModel.detectedUpdateVersion = "9.9.9"
|
|
viewModel.state = .checking(.init(cancel: {}))
|
|
|
|
XCTAssertTrue(viewModel.showsPill)
|
|
XCTAssertFalse(viewModel.showsDetectedBackgroundUpdate)
|
|
XCTAssertEqual(viewModel.text, "Checking for Updates…")
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class CommandPaletteOverlayPromotionPolicyTests: XCTestCase {
|
|
func testShouldPromoteWhenBecomingVisible() {
|
|
XCTAssertTrue(
|
|
CommandPaletteOverlayPromotionPolicy.shouldPromote(
|
|
previouslyVisible: false,
|
|
isVisible: true
|
|
)
|
|
)
|
|
}
|
|
|
|
func testShouldNotPromoteWhenAlreadyVisible() {
|
|
XCTAssertFalse(
|
|
CommandPaletteOverlayPromotionPolicy.shouldPromote(
|
|
previouslyVisible: true,
|
|
isVisible: true
|
|
)
|
|
)
|
|
}
|
|
|
|
func testShouldNotPromoteWhenHidden() {
|
|
XCTAssertFalse(
|
|
CommandPaletteOverlayPromotionPolicy.shouldPromote(
|
|
previouslyVisible: true,
|
|
isVisible: false
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
CommandPaletteOverlayPromotionPolicy.shouldPromote(
|
|
previouslyVisible: false,
|
|
isVisible: false
|
|
)
|
|
)
|
|
}
|
|
}
|