From 3845520f61020da4ae9b4fbc1d78adff5416059d Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Sun, 22 Sep 2024 19:02:45 +0100 Subject: [PATCH] Add tabs to suggestions/autocomplete on iOS (#3371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/392891325557410/1208036752520497/f Tech Design URL: CC: **Description**: Adds open tabs to suggestions. I'm going to follow up with localisations and UI tests in a separate PR as I want to get this thing merged with BSK and can update iOS internal easily on its own. **Steps to test this PR**: 1. Open a bunch of sites and bookmark some of them (quick way to do this is via Hacker News Ycombinator then go through and open the first 5 sites in the background and use tab switcher to bookmark them all, then go back and open a few more from the list) 2. Open a new tab and type until you get a suggestion for an open tab 3. Switching to the open tab should close the new tab 4. Matches that have bookmarks and tabs should show both, but history should not be shown in these cases 5. Edit a bookmark so that its title starts with quotes and is something different to its URL. Check that bookmark is still suggested based on the first word in the title 6. Close all tabs (not fire) and type until you see history for sites that have not been bookmarked 7. Use the fire button 8. Typing should not show any history but should show matching bookmarks **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [x] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [x] Portrait * [ ] Landscape **Device Testing**: * [x] iPhone SE (1st Gen) * [x] iPhone 8 * [x] iPhone X * [x] iPhone 14 Pro * [x] iPad **OS Testing**: * [x] iOS 15 * [x] iOS 16 * [x] iOS 17 **Theme Testing**: * [x] Light theme * [x] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/BookmarksCachingSearch.swift | 99 ++++++++++--------- Core/FeatureFlag.swift | 3 + Core/PixelEvent.swift | 4 + DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../24px/OpenTab-24.imageset/Contents.json | 15 +++ .../24px/OpenTab-24.imageset/OpenTab-24.svg | 3 + DuckDuckGo/AutocompleteView.swift | 6 ++ DuckDuckGo/AutocompleteViewController.swift | 51 +++++++++- DuckDuckGo/MainViewController.swift | 18 +++- DuckDuckGo/SuggestionTrayViewController.swift | 12 ++- DuckDuckGo/UserText.swift | 1 + DuckDuckGo/en.lproj/Localizable.strings | 3 + .../BookmarksCachingSearchTests.swift | 63 +++++++++--- 14 files changed, 218 insertions(+), 68 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg diff --git a/Core/BookmarksCachingSearch.swift b/Core/BookmarksCachingSearch.swift index 089eec2699..a219442954 100644 --- a/Core/BookmarksCachingSearch.swift +++ b/Core/BookmarksCachingSearch.swift @@ -133,12 +133,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch { self.title = title self.url = url self.isFavorite = isFavorite - - if isFavorite { - score = 0 - } else { - score = -1 - } + self.score = 0 } init?(bookmark: [String: Any]) { @@ -191,7 +186,6 @@ public class BookmarksCachingSearch: BookmarksStringSearch { return cachedBookmarksAndFavorites } - // swiftlint:disable cyclomatic_complexity private func score(query: String, input: [ScoredBookmark]) -> [ScoredBookmark] { let query = query.lowercased() let tokens = query.split(separator: " ").filter { !$0.isEmpty }.map { String($0).lowercased() } @@ -201,55 +195,63 @@ public class BookmarksCachingSearch: BookmarksStringSearch { for index in 0.. 0 { + result.append(input[index]) } + } + return result + } - let domain = entry.url.host?.droppingWwwPrefix() ?? "" + private func score(_ query: String, _ bookmark: ScoredBookmark, _ tokens: [String]) -> Int { + let title = bookmark.title.lowercased() + let domain = bookmark.url.host?.droppingWwwPrefix() ?? "" + var score = bookmark.isFavorite ? 0 : -1 - // Tokenized matches + // Exact matches - full query + if title.leadingBoundaryStartsWith(query) { // High score for exact match from the beginning of the title + score += 200 + } else if title.contains(" \(query)") { // Exact match from the beginning of the word within string. + score += 100 + } - if tokens.count > 1 { - var matchesAllTokens = true - for token in tokens { - // Match only from the beginning of the word to avoid unintuitive matches. - if !title.starts(with: token) && !title.contains(" \(token)") && !domain.starts(with: token) { - matchesAllTokens = false - break - } + // Tokenized matches + + if tokens.count > 1 { + var matchesAllTokens = true + for token in tokens { + // Match only from the beginning of the word to avoid unintuitive matches. + if !title.leadingBoundaryStartsWith(token) && + !title.contains(" \(token)") + && !domain.starts(with: token) { + matchesAllTokens = false + break } + } - if matchesAllTokens { - // Score tokenized matches - input[index].score += 10 - - // Boost score if first token matches: - if let firstToken = tokens.first { // domain - high score boost - if domain.starts(with: firstToken) { - input[index].score += 300 - } else if title.starts(with: firstToken) { // beginning of the title - moderate score boost - input[index].score += 50 - } + if matchesAllTokens { + // Score tokenized matches + score += 10 + + // Boost score if first token matches: + if let firstToken = tokens.first { // domain - high score boost + if domain.starts(with: firstToken) { + score += 300 + } else if title.leadingBoundaryStartsWith(firstToken) { // beginning of the title - moderate score boost + score += 50 } } - } else { - // High score for matching domain in the URL - if let firstToken = tokens.first, domain.starts(with: firstToken) { - input[index].score += 300 - } } - if input[index].score > 0 { - result.append(input[index]) + } else { + // High score for matching domain in the URL + if let firstToken = tokens.first, domain.starts(with: firstToken) { + score += 300 } } - return result + + return score } - // swiftlint:enable cyclomatic_complexity public func search(query: String) -> [BookmarksStringSearchResult] { guard hasData else { @@ -265,3 +267,12 @@ public class BookmarksCachingSearch: BookmarksStringSearch { return finalResult } } + +private extension String { + + /// e.g. "Cats and Dogs" would match `Cats` or `"Cats` + func leadingBoundaryStartsWith(_ s: String) -> Bool { + return starts(with: s) || trimmingCharacters(in: .alphanumerics.inverted).starts(with: s) + } + +} diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 763b282091..6d5b363635 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -42,6 +42,7 @@ public enum FeatureFlag: String { case syncPromotionPasswords case onboardingHighlights case autofillSurveys + case autcompleteTabs } extension FeatureFlag: FeatureFlagSourceProviding { @@ -89,6 +90,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .autofillSurveys: return .remoteReleasable(.feature(.autofillSurveys)) + case .autcompleteTabs: + return .remoteReleasable(.feature(.autocompleteTabs)) } } } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2f67e300f1..d7ff07905b 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -131,9 +131,11 @@ extension Pixel { case autocompleteClickFavorite case autocompleteClickSearchHistory case autocompleteClickSiteHistory + case autocompleteClickOpenTab case autocompleteDisplayedLocalBookmark case autocompleteDisplayedLocalFavorite case autocompleteDisplayedLocalHistory + case autocompleteDisplayedOpenedTab case autocompleteSwipeToDelete case feedbackPositive @@ -946,9 +948,11 @@ extension Pixel.Event { case .autocompleteClickFavorite: return "m_autocomplete_click_favorite" case .autocompleteClickSearchHistory: return "m_autocomplete_click_history_search" case .autocompleteClickSiteHistory: return "m_autocomplete_click_history_site" + case .autocompleteClickOpenTab: return "m_autocomplete_click_switch_to_tab" case .autocompleteDisplayedLocalBookmark: return "m_autocomplete_display_local_bookmark" case .autocompleteDisplayedLocalFavorite: return "m_autocomplete_display_local_favorite" case .autocompleteDisplayedLocalHistory: return "m_autocomplete_display_local_history" + case .autocompleteDisplayedOpenedTab: return "m_autocomplete_display_switch_to_tab" case .autocompleteSwipeToDelete: return "m_autocomplete_result_deleted" case .feedbackPositive: return "mfbs_positive_submit" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 192ec061e5..c1fa3fd356 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10927,7 +10927,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 197.0.0; + version = 198.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8ef30ff3b2..db741141be 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "40f2fcc23944e028e16798a784ceff7e24ba6683", - "version" : "197.0.0" + "revision" : "6e1520bd83bbcc269b0d561c51fc92b81fe6d93b", + "version" : "198.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json new file mode 100644 index 0000000000..e83ff99786 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "OpenTab-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg new file mode 100644 index 0000000000..f00822f378 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg @@ -0,0 +1,3 @@ + + + diff --git a/DuckDuckGo/AutocompleteView.swift b/DuckDuckGo/AutocompleteView.swift index f138157461..3003807655 100644 --- a/DuckDuckGo/AutocompleteView.swift +++ b/DuckDuckGo/AutocompleteView.swift @@ -249,6 +249,11 @@ private struct SuggestionView: View { title: title ?? "", subtitle: url.formattedForSuggestion()) + case .openTab(title: let title, url: let url): + SuggestionListItem(icon: Image("OpenTab-24"), + title: title, + subtitle: "\(UserText.autocompleteSwitchToTab) · \(url.formattedForSuggestion())") + case .internalPage, .unknown: FailedAssertionView("Unknown or unsupported suggestion type") } @@ -336,6 +341,7 @@ private extension URL { let string = absoluteString .dropping(prefix: "https://") .dropping(prefix: "http://") + .droppingWwwPrefix() return pathComponents.isEmpty ? string : string.dropping(suffix: "/") } diff --git a/DuckDuckGo/AutocompleteViewController.swift b/DuckDuckGo/AutocompleteViewController.swift index 0c680525da..7c09c4da6a 100644 --- a/DuckDuckGo/AutocompleteViewController.swift +++ b/DuckDuckGo/AutocompleteViewController.swift @@ -58,19 +58,33 @@ class AutocompleteViewController: UIHostingController { CachedBookmarks(bookmarksDatabase) }() + private lazy var openTabs: [BrowserTab] = { + tabsModel.tabs.compactMap { + guard let url = $0.link?.url else { return nil } + return OpenTab(title: $0.link?.displayTitle ?? "", url: url) + } + }() + private var lastResults: SuggestionResult? private var loader: SuggestionLoader? - private var historyMessageManager: HistoryMessageManager + private var tabsModel: TabsModel + private var featureFlagger: FeatureFlagger init(historyManager: HistoryManaging, bookmarksDatabase: CoreDataDatabase, appSettings: AppSettings, - historyMessageManager: HistoryMessageManager = HistoryMessageManager()) { + historyMessageManager: HistoryMessageManager = HistoryMessageManager(), + tabsModel: TabsModel, + featureFlagger: FeatureFlagger) { + + self.tabsModel = tabsModel self.historyManager = historyManager self.bookmarksDatabase = bookmarksDatabase self.appSettings = appSettings self.historyMessageManager = historyMessageManager + self.featureFlagger = featureFlagger + self.model = AutocompleteViewModel(isAddressBarAtBottom: appSettings.currentAddressBarPosition == .bottom, showMessage: historyManager.isHistoryFeatureEnabled() && historyMessageManager.shouldShow()) super.init(rootView: AutocompleteView(model: model)) @@ -119,6 +133,7 @@ class AutocompleteViewController: UIHostingController { var bookmark = false var favorite = false var history = false + var openTab = false lastResults?.all.forEach { switch $0 { @@ -132,6 +147,9 @@ class AutocompleteViewController: UIHostingController { case .historyEntry: history = true + case .openTab: + openTab = true + default: break } } @@ -148,6 +166,10 @@ class AutocompleteViewController: UIHostingController { Pixel.fire(pixel: .autocompleteDisplayedLocalHistory) } + if openTab { + Pixel.fire(pixel: .autocompleteDisplayedOpenedTab) + } + } private func cancelInFlightRequests() { @@ -158,7 +180,7 @@ class AutocompleteViewController: UIHostingController { private func requestSuggestions(query: String) { model.selection = nil - loader = SuggestionLoader(dataSource: self, urlFactory: { phrase in + loader = SuggestionLoader(urlFactory: { phrase in guard let url = URL(trimmedAddressBarString: phrase), let scheme = url.scheme, scheme.description.hasPrefix("http"), @@ -169,7 +191,7 @@ class AutocompleteViewController: UIHostingController { return url }) - loader?.getSuggestions(query: query) { [weak self] result, error in + loader?.getSuggestions(query: query, usingDataSource: self) { [weak self] result, error in guard let self, error == nil else { return } let updatedResults = result ?? .empty self.lastResults = updatedResults @@ -228,6 +250,9 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate { case .website: Pixel.fire(pixel: .autocompleteClickWebsite) + case .openTab: + Pixel.fire(pixel: .autocompleteClickOpenTab) + default: // NO-OP break @@ -259,6 +284,10 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate { extension AutocompleteViewController: SuggestionLoadingDataSource { + var platform: Platform { + .mobile + } + func history(for suggestionLoading: Suggestions.SuggestionLoading) -> [HistorySuggestion] { return historyCoordinator.history ?? [] } @@ -271,6 +300,13 @@ extension AutocompleteViewController: SuggestionLoadingDataSource { return [] } + func openTabs(for suggestionLoading: any SuggestionLoading) -> [BrowserTab] { + if featureFlagger.isFeatureOn(.autcompleteTabs) { + return openTabs + } + return [] + } + func suggestionLoading(_ suggestionLoading: Suggestions.SuggestionLoading, suggestionDataFromUrl url: URL, withParameters parameters: [String: String], completion: @escaping (Data?, Error?) -> Void) { var queryURL = url parameters.forEach { @@ -298,3 +334,10 @@ extension HistoryEntry: HistorySuggestion { } } + +struct OpenTab: BrowserTab { + + let title: String + let url: URL + +} diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 12e5ae1e7b..dd9e7efeb5 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -398,7 +398,9 @@ class MainViewController: UIViewController { SuggestionTrayViewController(coder: coder, favoritesViewModel: self.favoritesViewModel, bookmarksDatabase: self.bookmarksDatabase, - historyManager: self.historyManager) + historyManager: self.historyManager, + tabsModel: self.tabManager.model, + featureFlagger: self.featureFlagger) }) else { assertionFailure() return @@ -2088,16 +2090,26 @@ extension MainViewController: AutocompleteViewControllerDelegate { } else { Logger.lifecycle.error("Couldn‘t form URL for suggestion: \(phrase, privacy: .public)") } + case .website(url: let url): if url.isBookmarklet() { executeBookmarklet(url) } else { loadUrl(url) } + case .bookmark(_, url: let url, _, _): loadUrl(url) + case .historyEntry(_, url: let url, _): loadUrl(url) + + case .openTab(title: _, url: let url): + if homeViewController != nil, let tab = tabManager.model.currentTab { + self.closeTab(tab) + } + loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: .noAttribution) + case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } @@ -2119,6 +2131,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { viewCoordinator.omniBar.textField.text = title case .historyEntry(title: let title, _, _): viewCoordinator.omniBar.textField.text = title + case .openTab: break // no-op case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } @@ -2136,7 +2149,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { } case .website(url: let url): viewCoordinator.omniBar.textField.text = url.absoluteString - case .bookmark(title: let title, _, _, _): + case .bookmark(title: let title, _, _, _), .openTab(title: let title, url: _): viewCoordinator.omniBar.textField.text = title if title.hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) @@ -2149,6 +2162,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { if (title ?? url.absoluteString).hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) } + case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } diff --git a/DuckDuckGo/SuggestionTrayViewController.swift b/DuckDuckGo/SuggestionTrayViewController.swift index be3bc7a0c5..f71ee490f5 100644 --- a/DuckDuckGo/SuggestionTrayViewController.swift +++ b/DuckDuckGo/SuggestionTrayViewController.swift @@ -23,6 +23,7 @@ import Bookmarks import Suggestions import Persistence import History +import BrowserServicesKit class SuggestionTrayViewController: UIViewController { @@ -50,6 +51,8 @@ class SuggestionTrayViewController: UIViewController { private let bookmarksDatabase: CoreDataDatabase private let favoritesModel: FavoritesListInteracting private let historyManager: HistoryManaging + private let tabsModel: TabsModel + private let featureFlagger: FeatureFlagger var selectedSuggestion: Suggestion? { autocompleteController?.selectedSuggestion @@ -79,10 +82,12 @@ class SuggestionTrayViewController: UIViewController { } } - required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging) { + required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, tabsModel: TabsModel, featureFlagger: FeatureFlagger) { self.favoritesModel = favoritesViewModel self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager + self.tabsModel = tabsModel + self.featureFlagger = featureFlagger super.init(coder: coder) } @@ -236,8 +241,9 @@ class SuggestionTrayViewController: UIViewController { private func installAutocompleteSuggestions() { let controller = AutocompleteViewController(historyManager: historyManager, bookmarksDatabase: bookmarksDatabase, - appSettings: appSettings) - + appSettings: appSettings, + tabsModel: tabsModel, + featureFlagger: featureFlagger) install(controller: controller) controller.delegate = autocompleteDelegate controller.presentationDelegate = self diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index b586b9f55d..c8c2fe0910 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1251,6 +1251,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let autocompleteHistoryWarningTitle = NSLocalizedString("autocomplete.history.warning.title", value: "Same privacy.\nBetter search suggestions!", comment: "Title for message show in suggestions") public static let autocompleteHistoryWarningDescription = NSLocalizedString("autocomplete.history.warning.message", value: "Search suggestions now include your recently visited sites. Turn off in Settings, or clear anytime with the 🔥 Fire Button.", comment: "The message text shown in suggestions") public static let autocompleteSearchDuckDuckGo = NSLocalizedString("autocomplete.history.search.duckduckgo", value: "Search DuckDuckGo", comment: "Subtitle for search history items") + public static let autocompleteSwitchToTab = NSLocalizedString("autocomplete.switch.to.tab", value: "Switch to Tab", comment: "Switch to tab hint") // Site not working public static let siteNotWorkingTitle = NSLocalizedString("site.not.working.title", value: "Site not working? Let DuckDuckGo know.", comment: "Prompt asking user to send report to us if we suspect site may be broken") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index affd90745c..2d4b428f71 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -250,6 +250,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Same privacy.\nBetter search suggestions!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Switch to Tab"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Disabled"; diff --git a/DuckDuckGoTests/BookmarksCachingSearchTests.swift b/DuckDuckGoTests/BookmarksCachingSearchTests.swift index 03f79cb6d2..21545ad57e 100644 --- a/DuckDuckGoTests/BookmarksCachingSearchTests.swift +++ b/DuckDuckGoTests/BookmarksCachingSearchTests.swift @@ -46,7 +46,8 @@ class BookmarksCachingSearchTests: XCTestCase { let simpleStore = MockBookmarksSearchStore() let urlStore = MockBookmarksSearchStore() - + let quotedTitleStore = MockBookmarksSearchStore() + enum Entry: String { case b1 = "bookmark test 1" case b2 = "test bookmark 2" @@ -61,6 +62,9 @@ class BookmarksCachingSearchTests: XCTestCase { case urlExample2 = "Test E 2" case urlNasa = "Test N 1 Duck" case urlDDG = "Test D 1" + + case quotedTitle1 = "\"Cats and Dogs\"" + case quotedTitle2 = "«Рукописи не горят»: первый замысел" } private var mockObjectID: NSManagedObjectID! @@ -75,20 +79,29 @@ class BookmarksCachingSearchTests: XCTestCase { mockObjectID = BookmarkUtils.fetchRootFolder(inMemoryStore.viewContext)?.objectID XCTAssertNotNil(mockObjectID) - simpleStore.dataSet = [BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b1.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b2.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12a.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f1.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f2.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12a.rawValue, url: url, isFavorite: true)] - + simpleStore.dataSet = [ + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b1.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b2.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12a.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f1.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f2.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12a.rawValue, url: url, isFavorite: true), + ] + urlStore.dataSet = [ BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlExample1.rawValue, url: URL(string: "https://example.com")!, isFavorite: true), BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlExample2.rawValue, url: URL(string: "https://example.com")!, isFavorite: true), BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlNasa.rawValue, url: URL(string: "https://www.nasa.gov")!, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlDDG.rawValue, url: url, isFavorite: true)] + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlDDG.rawValue, url: url, isFavorite: true), + ] + + quotedTitleStore.dataSet = [ + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.quotedTitle1.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.quotedTitle2.rawValue, url: url, isFavorite: false), + ] + } override func tearDown() { @@ -98,6 +111,34 @@ class BookmarksCachingSearchTests: XCTestCase { super.tearDown() } + func testWhenSearchingForCharactersThenCharactersAtTheStartAreMatched() async throws { + let engine = BookmarksCachingSearch(bookmarksStore: quotedTitleStore) + var bookmarks = engine.search(query: "\"") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "«") + XCTAssertEqual(bookmarks.count, 1) + } + + func testWhenSearchingForWordsAtStartWithQuotesThenWordsAreMatched() async throws { + + let engine = BookmarksCachingSearch(bookmarksStore: quotedTitleStore) + var bookmarks = engine.search(query: "Cats") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Р") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Ру") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Рук") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Nope") + XCTAssertEqual(bookmarks.count, 0) + } + func testWhenSearchingThenOnlyBeginingsOfWordsAreMatched() async throws { let engine = BookmarksCachingSearch(bookmarksStore: simpleStore)