import ConjureKit import Foundation import Testing @Test @MainActor func suggestionsPreferFrequencyThenRecencyAndKeepUnusedAppsAlphabetical() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } var instant = Date(timeIntervalSinceReferenceDate: 1_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let apps = ["Unused Zebra", "Recent", "Frequent", "Unused Alpha", "Older"].map(historyApplication) #expect(history.suggestions(in: apps).map(\.name) == [ "Frequent", "Older", "Recent", "Unused Alpha", "Unused Zebra", ]) history.recordLaunch(of: apps[4]) instant += 10 history.recordLaunch(of: apps[2]) history.recordLaunch(of: apps[2]) instant += 10 history.recordLaunch(of: apps[1]) #expect(history.suggestions(in: apps).map(\.name) == [ "Frequent", "Recent", "Older", "Unused Alpha", "Unused Zebra", ]) } @Test @MainActor func suggestionsRetainUsageAcrossHistoryInstancesAndDoNotReintroduceMissingApps() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 2_000) let alpha = historyApplication("Alpha") let zebra = historyApplication("Zebra") let removed = historyApplication("Removed") let history = LaunchHistory(defaults: defaults, now: { instant }) history.recordLaunch(of: zebra) history.recordLaunch(of: removed) history.recordLaunch(of: removed) let reloaded = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(reloaded.suggestions(in: [alpha, zebra]).map(\.name) == ["Zebra", "Alpha"]) reloaded.recordLaunch(of: alpha) #expect(reloaded.suggestions(in: [zebra, alpha]).map(\.name) == ["Alpha", "Zebra"]) } @Test @MainActor func usageTracksApplicationIdentityRatherThanDisplayNameAndKeepsTiesDeterministic() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 3_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let first = Application(url: URL(fileURLWithPath: "/First/Editor.app"), name: "Editor") let second = Application(url: URL(fileURLWithPath: "/Second/Editor.app"), name: "Editor") #expect(history.suggestions(in: [second, first]).map(\.url) == [first.url, second.url]) history.recordLaunch(of: second) #expect(history.suggestions(in: [first, second]).map(\.url) == [second.url, first.url]) let renamed = Application(url: second.url, name: "Renamed Editor") #expect(history.suggestions(in: [first, renamed]).map(\.url) == [second.url, first.url]) history.recordLaunch(of: first) #expect(history.suggestions(in: [second, first]).map(\.url) == [first.url, second.url]) } @Test @MainActor func applicationIdentityIgnoresDirectoryURLFormattingInLaunchHistory() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 4_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let path = "/Applications/HistoryTests/Editor.app" let plain = Application(url: URL(fileURLWithPath: path, isDirectory: false), name: "Editor") let directory = Application(url: URL(fileURLWithPath: path, isDirectory: true), name: "Editor") #expect(plain.id == directory.id) history.recordLaunch(of: directory) #expect(history.suggestions(in: [historyApplication("Alpha"), plain]).first?.id == plain.id) history.toggleFavorite(directory) #expect(history.isFavorite(plain)) let reloaded = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(reloaded.isFavorite(plain)) reloaded.toggleFavorite(plain) #expect(!reloaded.isFavorite(directory)) } @Test @MainActor func typedSearchUsesHistoryOnlyForEqualTextRanks() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } var instant = Date(timeIntervalSinceReferenceDate: 5_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let apps = ["Code", "Code Alpha", "Code Recent", "Code Frequent", "Open Code", "Unrelated"].map(historyApplication) history.recordLaunch(of: apps[3]) history.recordLaunch(of: apps[3]) instant += 10 history.recordLaunch(of: apps[2]) for _ in 0..<3 { history.recordLaunch(of: apps[4]) } history.recordLaunch(of: apps[5]) #expect(history.results(for: "code", in: apps).map(\.name) == [ "Code", "Code Frequent", "Code Recent", "Code Alpha", "Open Code", ]) instant += 10 history.recordLaunch(of: apps[1]) #expect(history.results(for: "code", in: apps).map(\.name) == [ "Code", "Code Frequent", "Code Alpha", "Code Recent", "Open Code", ]) let fuzzy = ["AyB", "AzB", "xAyB", "AxxxxB"].map(historyApplication) history.recordLaunch(of: fuzzy[1]) for _ in 0..<3 { history.recordLaunch(of: fuzzy[3]) } #expect(history.results(for: "ab", in: fuzzy).map(\.name) == [ "AzB", "AyB", "xAyB", "AxxxxB", ]) } @Test @MainActor func resetRankingClearsOnlySelectedIdentityAndPersistsAcrossRelaunch() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 6_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let alpha = historyApplication("Editor Alpha") let first = Application(url: URL(fileURLWithPath: "/First/Editor.app"), name: "Editor") let second = Application(url: URL(fileURLWithPath: "/Second/Editor.app"), name: "Editor") let apps = [alpha, first, second] history.recordLaunch(of: second) history.recordLaunch(of: first) history.recordLaunch(of: first) #expect(history.suggestions(in: apps).map(\.id) == [first.id, second.id, alpha.id]) history.resetRanking(of: first) history.resetRanking(of: first) #expect(history.suggestions(in: apps).map(\.id) == [second.id, first.id, alpha.id]) #expect(history.results(for: "editor", in: apps).map(\.id) == [second.id, first.id, alpha.id]) let reloaded = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(reloaded.suggestions(in: apps).map(\.id) == [second.id, first.id, alpha.id]) reloaded.recordLaunch(of: first) #expect(reloaded.suggestions(in: apps).map(\.id) == [first.id, second.id, alpha.id]) } @Test @MainActor func emptySearchPlacesFavoritesFirstAndPreservesUsageOrderingWithinEachGroup() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } var instant = Date(timeIntervalSinceReferenceDate: 7_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let favorites = ["Favorite Frequent", "Favorite Recent", "Favorite Older", "Favorite Unused Alpha", "Favorite Unused Zebra"] .map(historyApplication) let others = ["Other Frequent", "Other Recent", "Other Older", "Other Unused Alpha", "Other Unused Zebra"] .map(historyApplication) for application in favorites { history.toggleFavorite(application) } for group in [favorites, others] { history.recordLaunch(of: group[0]) history.recordLaunch(of: group[0]) history.recordLaunch(of: group[2]) instant += 10 history.recordLaunch(of: group[1]) } for _ in 0..<8 { history.recordLaunch(of: others[0]) } let expected = (favorites + others).map(\.id) let apps = Array((others + favorites).reversed()) #expect(history.suggestions(in: apps).map(\.id) == expected) #expect(history.results(for: "", in: apps).map(\.id) == expected) #expect(history.results(for: " \n", in: apps).map(\.id) == expected) } @Test @MainActor func typedSearchPrefersTextRankBeforeFavoritesAndFavoritesBeforeUsage() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 8_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let apps = ["AB", "AB Alpha", "AB Heavy", "AyB", "AzB", "AxxxxB", "Unrelated"].map(historyApplication) for index in [1, 3, 5, 6] { history.toggleFavorite(apps[index]) } for _ in 0..<5 { history.recordLaunch(of: apps[2]) history.recordLaunch(of: apps[4]) } #expect(history.results(for: "ab", in: Array(apps.reversed())).map(\.id) == Array(apps.dropLast()).map(\.id)) } @Test @MainActor func favoritesPersistWithoutUsageAndToggleRemovalPersistsAcrossRelaunch() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 9_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let alpha = historyApplication("Alpha") let zebra = historyApplication("Zebra") let removed = historyApplication("Removed") #expect(!history.isFavorite(zebra)) history.toggleFavorite(zebra) history.toggleFavorite(removed) #expect(history.isFavorite(zebra)) let reloaded = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(reloaded.isFavorite(zebra)) #expect(reloaded.isFavorite(removed)) #expect(reloaded.suggestions(in: [alpha, zebra]).map(\.id) == [zebra.id, alpha.id]) reloaded.toggleFavorite(zebra) #expect(!reloaded.isFavorite(zebra)) let afterRemoval = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(!afterRemoval.isFavorite(zebra)) #expect(afterRemoval.isFavorite(removed)) #expect(afterRemoval.suggestions(in: [zebra, alpha]).map(\.id) == [alpha.id, zebra.id]) } @Test @MainActor func resettingUsageRetainsFavoritesAndTheirPersistence() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 10_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let alpha = historyApplication("Alpha") let zebra = historyApplication("Zebra") let other = historyApplication("Other") let apps = [other, zebra, alpha] history.toggleFavorite(alpha) history.toggleFavorite(zebra) history.recordLaunch(of: zebra) history.recordLaunch(of: zebra) for _ in 0..<5 { history.recordLaunch(of: other) } #expect(history.suggestions(in: apps).map(\.id) == [zebra.id, alpha.id, other.id]) history.resetRanking(of: zebra) history.resetRanking(of: zebra) #expect(history.isFavorite(zebra)) #expect(history.suggestions(in: apps).map(\.id) == [alpha.id, zebra.id, other.id]) let reloaded = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(reloaded.isFavorite(alpha)) #expect(reloaded.isFavorite(zebra)) #expect(reloaded.suggestions(in: apps).map(\.id) == [alpha.id, zebra.id, other.id]) reloaded.recordLaunch(of: zebra) #expect(reloaded.isFavorite(zebra)) #expect(reloaded.suggestions(in: apps).map(\.id) == [zebra.id, alpha.id, other.id]) } @Test @MainActor func favoritesTrackApplicationURLsRatherThanDisplayNames() { let suite = "ConjureHistoryTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let instant = Date(timeIntervalSinceReferenceDate: 11_000) let history = LaunchHistory(defaults: defaults, now: { instant }) let first = Application(url: URL(fileURLWithPath: "/First/Editor.app"), name: "Editor") let second = Application(url: URL(fileURLWithPath: "/Second/Editor.app"), name: "Editor") for _ in 0..<5 { history.recordLaunch(of: first) } history.toggleFavorite(second) #expect(!history.isFavorite(first)) #expect(history.isFavorite(second)) #expect(history.results(for: "editor", in: [first, second]).map(\.id) == [second.id, first.id]) let renamed = Application(url: second.url, name: "Renamed Editor") let reloaded = LaunchHistory(defaults: UserDefaults(suiteName: suite)!, now: { instant }) #expect(!reloaded.isFavorite(first)) #expect(reloaded.isFavorite(renamed)) #expect(reloaded.suggestions(in: [first, renamed]).map(\.id) == [second.id, first.id]) reloaded.toggleFavorite(renamed) #expect(!reloaded.isFavorite(second)) #expect(reloaded.suggestions(in: [second, first]).map(\.id) == [first.id, second.id]) } private func historyApplication(_ name: String) -> Application { Application(url: URL(fileURLWithPath: "/Applications/HistoryTests/\(name).app"), name: name) }