From 82dcbe9bb6facbe82f337e89ecc01dcc9f42667b Mon Sep 17 00:00:00 2001 From: Isabella Date: Fri, 25 Oct 2024 14:53:36 -0600 Subject: [PATCH] Add FXIOS-10379 [Unified Search] Implement initial redux setup (#22767) * Stub placeholder button for testing search engine toggle. * Stub in basic SearchEngineSelectionState. * Stub in SearchEngineSelectionAction and SearchEngineSelectionMiddleware. * Hook up SearchEngineSelectionViewController to use new redux setup for SearchEngineSelection. * Make OpenSearchEngine test helpers public. * Add SearchEngineSelectionStateTests. Add SearchEngineSelectionMiddlewareTests. --- firefox-ios/Client.xcodeproj/project.pbxproj | 28 +++++++ .../Redux/SearchEngineSelectionAction.swift | 24 ++++++ .../SearchEngineSelectionMiddleware.swift | 50 ++++++++++++ .../Redux/SearchEngineSelectionState.swift | 75 ++++++++++++++++++ .../SearchEngineSelectionViewController.swift | 79 ++++++++++++++++++- .../GlobalState/ActiveScreenAction.swift | 1 + .../Redux/GlobalState/ActiveScreenState.swift | 7 ++ .../Client/Redux/GlobalState/AppState.swift | 2 + .../SearchEngines/OpenSearchEngineTests.swift | 18 +++-- ...SearchEngineSelectionMiddlewareTests.swift | 64 +++++++++++++++ .../SearchEngineSelectionStateTests.swift | 59 ++++++++++++++ 11 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionAction.swift create mode 100644 firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionMiddleware.swift create mode 100644 firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionState.swift create mode 100644 firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionMiddlewareTests.swift create mode 100644 firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionStateTests.swift diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index b63a67d6a0e8..d81a6a78499e 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -1832,6 +1832,11 @@ EBFDB790211C83A5005CCA2F /* BrowserViewController+FindInPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBFDB787211C83A5005CCA2F /* BrowserViewController+FindInPage.swift */; }; ED07C0E62CCACD7E006C0627 /* Locale+possibilitiesForLanguageIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0E52CCACD7E006C0627 /* Locale+possibilitiesForLanguageIdentifier.swift */; }; ED07C0E72CCACE18006C0627 /* Locale+possibilitiesForLanguageIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0E52CCACD7E006C0627 /* Locale+possibilitiesForLanguageIdentifier.swift */; }; + ED07C0EB2CCADCD5006C0627 /* SearchEngineSelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0E92CCADCD5006C0627 /* SearchEngineSelectionState.swift */; }; + ED07C0ED2CCAE745006C0627 /* SearchEngineSelectionAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0EC2CCAE745006C0627 /* SearchEngineSelectionAction.swift */; }; + ED07C0EF2CCAE856006C0627 /* SearchEngineSelectionMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0EE2CCAE856006C0627 /* SearchEngineSelectionMiddleware.swift */; }; + ED07C0F22CCAFED1006C0627 /* SearchEngineSelectionStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0F02CCAFEC2006C0627 /* SearchEngineSelectionStateTests.swift */; }; + ED07C0F52CCB020B006C0627 /* SearchEngineSelectionMiddlewareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED07C0F32CCB0200006C0627 /* SearchEngineSelectionMiddlewareTests.swift */; }; ED28DAC92C45A95F00D2641C /* TabScrollBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED28DAC82C45A95F00D2641C /* TabScrollBehaviorModel.swift */; }; ED371A682CB881EF0099F3C8 /* SearchEngineSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED371A672CB881EF0099F3C8 /* SearchEngineSelectionCoordinator.swift */; }; ED45893E2CC800D9006F2C0B /* SearchEngineSelectionViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED45893D2CC800D9006F2C0B /* SearchEngineSelectionViewControllerTests.swift */; }; @@ -8984,6 +8989,11 @@ EC934F7BB2F8B247BC714BE2 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/ClearPrivateData.strings"; sourceTree = ""; }; ECDA4593850BA4E08FDAC5AF /* kk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kk; path = kk.lproj/ClearPrivateData.strings; sourceTree = ""; }; ED07C0E52CCACD7E006C0627 /* Locale+possibilitiesForLanguageIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+possibilitiesForLanguageIdentifier.swift"; sourceTree = ""; }; + ED07C0E92CCADCD5006C0627 /* SearchEngineSelectionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEngineSelectionState.swift; sourceTree = ""; }; + ED07C0EC2CCAE745006C0627 /* SearchEngineSelectionAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngineSelectionAction.swift; sourceTree = ""; }; + ED07C0EE2CCAE856006C0627 /* SearchEngineSelectionMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngineSelectionMiddleware.swift; sourceTree = ""; }; + ED07C0F02CCAFEC2006C0627 /* SearchEngineSelectionStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngineSelectionStateTests.swift; sourceTree = ""; }; + ED07C0F32CCB0200006C0627 /* SearchEngineSelectionMiddlewareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngineSelectionMiddlewareTests.swift; sourceTree = ""; }; ED264785844AD63AD616A882 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/InfoPlist.strings; sourceTree = ""; }; ED28DAC82C45A95F00D2641C /* TabScrollBehaviorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabScrollBehaviorModel.swift; sourceTree = ""; }; ED364EECB95B96AB639A82DE /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/Localizable.strings; sourceTree = ""; }; @@ -10853,6 +10863,7 @@ 8A1E3BE428CBBF1E003388C4 /* SearchEngines */ = { isa = PBXGroup; children = ( + ED07C0EA2CCADCD5006C0627 /* Redux */, ED371A692CB885B20099F3C8 /* Views */, EDC3C2552CCAC9CB005A047F /* SearchEnginesManager.swift */, 8A1E3BE528CBBF44003388C4 /* OpenSearchEngine.swift */, @@ -10872,6 +10883,8 @@ D3FA777A1A43B2990010CD32 /* SearchTests.swift */, 96A5F734298D8EB900234E5F /* MockSearchEngineProvider.swift */, EDC3D34E2CB5E70500C62DE3 /* SearchEngineTestAssets.xcassets */, + ED07C0F02CCAFEC2006C0627 /* SearchEngineSelectionStateTests.swift */, + ED07C0F32CCB0200006C0627 /* SearchEngineSelectionMiddlewareTests.swift */, ); path = SearchEngines; sourceTree = ""; @@ -13512,6 +13525,16 @@ path = InternalSchemeHandler; sourceTree = ""; }; + ED07C0EA2CCADCD5006C0627 /* Redux */ = { + isa = PBXGroup; + children = ( + ED07C0E92CCADCD5006C0627 /* SearchEngineSelectionState.swift */, + ED07C0EC2CCAE745006C0627 /* SearchEngineSelectionAction.swift */, + ED07C0EE2CCAE856006C0627 /* SearchEngineSelectionMiddleware.swift */, + ); + path = Redux; + sourceTree = ""; + }; ED371A692CB885B20099F3C8 /* Views */ = { isa = PBXGroup; children = ( @@ -15570,6 +15593,7 @@ E1A6AB4628CA6A4C00EBEBDD /* String+Extension.swift in Sources */, 1D2F68AF2ACB272500524B92 /* RemoteTabsTableViewController.swift in Sources */, 210887CC293E8800000AB4EE /* LegacyRemoteTabsErrorCell.swift in Sources */, + ED07C0ED2CCAE745006C0627 /* SearchEngineSelectionAction.swift in Sources */, 8AABBCFC2A0010900089941E /* GleanWrapper.swift in Sources */, 8C92DE912A7128CB0090BD28 /* ProductAnalysisResponse.swift in Sources */, F85C7F0F271DD154004BDBA4 /* AppAuthenticator.swift in Sources */, @@ -15638,6 +15662,7 @@ 6ACB550C28633860007A6ABD /* TabManagerNavDelegate.swift in Sources */, E18259DF29B25E4F00E6BE76 /* UserNotificationCenterProtocol.swift in Sources */, DFFC9AD12A681FA0002A6AAD /* NimbusFakespotFeatureLayer.swift in Sources */, + ED07C0EB2CCADCD5006C0627 /* SearchEngineSelectionState.swift in Sources */, 1DEBC55E2AC4ED70006E4801 /* RemoteTabsPanel.swift in Sources */, 435D660523D794B90046EFA2 /* UpdateViewModel.swift in Sources */, 9658143C29FAB610007339BD /* CreditCardInputFieldHelper.swift in Sources */, @@ -16311,6 +16336,7 @@ C84655E42887394B00861B4A /* WallpaperMetadata.swift in Sources */, 8AB8572E27D94A1A0075C173 /* UXSizeClass.swift in Sources */, 965C3C8F29313A1B006499ED /* AppSessionManager.swift in Sources */, + ED07C0EF2CCAE856006C0627 /* SearchEngineSelectionMiddleware.swift in Sources */, 45D5EDA729269F7500311934 /* DataObserver.swift in Sources */, 81A3F6F02C2DAEE200BDD86B /* MainMenuCoordinator.swift in Sources */, 961577922A38FDB300391E8D /* SponsoredTileDataUtility.swift in Sources */, @@ -16571,6 +16597,7 @@ 8A5D1CA02A30C9D7005AD35C /* MockAppSettingsDelegate.swift in Sources */, 1D7B789F2AE088930011E9F2 /* EventQueueTests.swift in Sources */, 21A1C3C72996AFF800181B7C /* OverlayModeManagerTests.swift in Sources */, + ED07C0F52CCB020B006C0627 /* SearchEngineSelectionMiddlewareTests.swift in Sources */, C8DF92F72A14101500AA7B05 /* OnboardingViewControllerProtocolTests.swift in Sources */, 8A4EA0D92C01127C00E4E4F1 /* MicrosurveyMockModel.swift in Sources */, 1D4D79472BF2F4FD007C6796 /* Throttler.swift in Sources */, @@ -16633,6 +16660,7 @@ 8CEDF07E2BFE04B100D2617B /* AddressListViewModelTests.swift in Sources */, 21D884412A79628E00AF144C /* MockSettingsDelegate.swift in Sources */, F1BC457E2A40F6D2005541D5 /* EnhancedTrackingProtectionCoordinatorTests.swift in Sources */, + ED07C0F22CCAFED1006C0627 /* SearchEngineSelectionStateTests.swift in Sources */, 8A13FA8F2AD83F00007527AB /* DefaultBackgroundTabLoaderTests.swift in Sources */, 8A827E362C20CB5B008D5E3C /* MicrosurveyPromptMiddlewareTests.swift in Sources */, 5A31275828906422001F30FA /* BookmarksDelegateMock.swift in Sources */, diff --git a/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionAction.swift b/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionAction.swift new file mode 100644 index 000000000000..81d20da4e620 --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionAction.swift @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Common +import Foundation +import MenuKit +import Redux + +final class SearchEngineSelectionAction: Action { + let searchEngines: [OpenSearchEngine]? + + init(windowUUID: WindowUUID, actionType: ActionType, searchEngines: [OpenSearchEngine]? = nil) { + self.searchEngines = searchEngines + super.init(windowUUID: windowUUID, actionType: actionType) + } +} + +enum SearchEngineSelectionActionType: ActionType { + case viewDidLoad + case didLoadSearchEngines +} + +enum SearchEngineSelectionMiddlewareActionType: ActionType {} diff --git a/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionMiddleware.swift b/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionMiddleware.swift new file mode 100644 index 000000000000..8a643ff57d96 --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionMiddleware.swift @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Common +import Redux +import ToolbarKit + +final class SearchEngineSelectionMiddleware { + private let profile: Profile + private let logger: Logger + private let searchEnginesManager: SearchEnginesManager + + init(profile: Profile = AppContainer.shared.resolve(), + searchEnginesManager: SearchEnginesManager? = nil, + logger: Logger = DefaultLogger.shared) { + self.profile = profile + self.logger = logger + self.searchEnginesManager = searchEnginesManager ?? SearchEnginesManager(prefs: profile.prefs, files: profile.files) + } + + lazy var searchEngineSelectionProvider: Middleware = { [self] state, action in + guard let action = action as? SearchEngineSelectionAction else { return } + + switch action.actionType { + case SearchEngineSelectionActionType.viewDidLoad: + guard let searchEngines = searchEnginesManager.orderedEngines, !searchEngines.isEmpty else { + // The SearchEngineManager should have loaded these by now, but if not, attempt to fetch the search engines + self.searchEnginesManager.getOrderedEngines { [weak self] searchEngines in + self?.notifyDidLoad(windowUUID: action.windowUUID, searchEngines: searchEngines) + } + return + } + + notifyDidLoad(windowUUID: action.windowUUID, searchEngines: searchEngines) + + default: + break + } + } + + private func notifyDidLoad(windowUUID: WindowUUID, searchEngines: [OpenSearchEngine]) { + let action = SearchEngineSelectionAction( + windowUUID: windowUUID, + actionType: SearchEngineSelectionActionType.didLoadSearchEngines, + searchEngines: searchEngines + ) + store.dispatch(action) + } +} diff --git a/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionState.swift b/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionState.swift new file mode 100644 index 000000000000..830797021c41 --- /dev/null +++ b/firefox-ios/Client/Frontend/Browser/SearchEngines/Redux/SearchEngineSelectionState.swift @@ -0,0 +1,75 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Common +import Shared +import Redux + +struct SearchEngineSelectionState: ScreenState, Equatable { + var windowUUID: WindowUUID + var shouldDismiss: Bool + // Default search engine should appear in position 0 + var searchEngines: [OpenSearchEngine] + + init(appState: AppState, uuid: WindowUUID) { + guard let state = store.state.screenState( + SearchEngineSelectionState.self, + for: .searchEngineSelection, + window: uuid + ) else { + self.init(windowUUID: uuid) + return + } + + self.init( + windowUUID: state.windowUUID, + searchEngines: state.searchEngines, + shouldDismiss: state.shouldDismiss + ) + } + + init(windowUUID: WindowUUID) { + self.init(windowUUID: windowUUID, searchEngines: []) + } + + private init( + windowUUID: WindowUUID, + searchEngines: [OpenSearchEngine], + shouldDismiss: Bool = false + ) { + self.windowUUID = windowUUID + self.searchEngines = searchEngines + self.shouldDismiss = shouldDismiss + } + + /// Returns a new `SearchEngineSelectionState` which clears any transient data. + static func defaultState(fromPreviousState state: SearchEngineSelectionState) -> SearchEngineSelectionState { + return SearchEngineSelectionState( + windowUUID: state.windowUUID, + searchEngines: state.searchEngines + ) + } + + static let reducer: Reducer = { state, action in + // Only process actions for the current window + guard action.windowUUID == .unavailable || action.windowUUID == state.windowUUID else { + return defaultState(fromPreviousState: state) + } + + switch action.actionType { + case SearchEngineSelectionActionType.didLoadSearchEngines: + guard let action = action as? SearchEngineSelectionAction, + let searchEngines = action.searchEngines + else { return defaultState(fromPreviousState: state) } + + return SearchEngineSelectionState( + windowUUID: state.windowUUID, + searchEngines: searchEngines + ) + + default: + return defaultState(fromPreviousState: state) + } + } +} diff --git a/firefox-ios/Client/Frontend/Browser/SearchEngines/Views/SearchEngineSelectionViewController.swift b/firefox-ios/Client/Frontend/Browser/SearchEngines/Views/SearchEngineSelectionViewController.swift index 6742d1bb5478..d70de53bb42b 100644 --- a/firefox-ios/Client/Frontend/Browser/SearchEngines/Views/SearchEngineSelectionViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/SearchEngines/Views/SearchEngineSelectionViewController.swift @@ -11,7 +11,8 @@ import Redux class SearchEngineSelectionViewController: UIViewController, UISheetPresentationControllerDelegate, UIPopoverPresentationControllerDelegate, - Themeable { + Themeable, + StoreSubscriber { // MARK: - Properties var notificationCenter: NotificationProtocol var themeManager: ThemeManager @@ -20,6 +21,7 @@ class SearchEngineSelectionViewController: UIViewController, weak var coordinator: SearchEngineSelectionCoordinator? private let windowUUID: WindowUUID + private var state: SearchEngineSelectionState private let logger: Logger // MARK: - UI/UX elements @@ -32,6 +34,15 @@ class SearchEngineSelectionViewController: UIViewController, view.addTarget(self, action: #selector(self.didTapOpenSettings), for: .touchUpInside) } + // FIXME FXIOS-10189 This will be deleted later. + private lazy var placeholderSwitchSearchEngineButton: UIButton = .build { view in + view.setTitle("Test changing search engine", for: .normal) + view.setTitleColor(.systemPink, for: .normal) + view.titleLabel?.numberOfLines = 0 + view.titleLabel?.textAlignment = .center + + view.addTarget(self, action: #selector(self.testDidChangeSearchEngine), for: .touchUpInside) + } // MARK: - Initializers and Lifecycle @@ -42,11 +53,15 @@ class SearchEngineSelectionViewController: UIViewController, logger: Logger = DefaultLogger.shared ) { self.windowUUID = windowUUID + self.state = SearchEngineSelectionState(windowUUID: windowUUID) + self.logger = logger self.notificationCenter = notificationCenter self.themeManager = themeManager - self.logger = logger + super.init(nibName: nil, bundle: nil) + subscribeToRedux() + // TODO Additional setup to come // ... } @@ -55,6 +70,10 @@ class SearchEngineSelectionViewController: UIViewController, fatalError("init(coder:) has not been implemented") } + deinit { + unsubscribeFromRedux() + } + override func viewDidLoad() { super.viewDidLoad() @@ -63,6 +82,13 @@ class SearchEngineSelectionViewController: UIViewController, setupView() listenForThemeChange(view) + + store.dispatch( + SearchEngineSelectionAction( + windowUUID: self.windowUUID, + actionType: SearchEngineSelectionActionType.viewDidLoad + ) + ) } override func viewWillAppear(_ animated: Bool) { @@ -75,14 +101,54 @@ class SearchEngineSelectionViewController: UIViewController, private func setupView() { view.addSubview(placeholderOpenSettingsButton) + view.addSubviews(placeholderSwitchSearchEngineButton) NSLayoutConstraint.activate([ placeholderOpenSettingsButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 80), placeholderOpenSettingsButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - placeholderOpenSettingsButton.widthAnchor.constraint(equalToConstant: 200) + placeholderOpenSettingsButton.widthAnchor.constraint(equalToConstant: 200), + + placeholderSwitchSearchEngineButton.topAnchor.constraint(equalTo: placeholderOpenSettingsButton.bottomAnchor), + placeholderSwitchSearchEngineButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), ]) } + // MARK: - Redux + + func subscribeToRedux() { + store.dispatch( + ScreenAction( + windowUUID: windowUUID, + actionType: ScreenActionType.showScreen, + screen: .searchEngineSelection + ) + ) + + let uuid = windowUUID + store.subscribe(self, transform: { + return $0.select({ appState in + return SearchEngineSelectionState(appState: appState, uuid: uuid) + }) + }) + } + + func unsubscribeFromRedux() { + store.dispatch( + ScreenAction( + windowUUID: windowUUID, + actionType: ScreenActionType.closeScreen, + screen: .searchEngineSelection + ) + ) + } + + func newState(state: SearchEngineSelectionState) { + self.state = state + + // FIXME FXIOS-10189 Eventually we'll have a tableview. Placeholder for temporary testing redux. + placeholderSwitchSearchEngineButton.setTitle(state.searchEngines.last?.shortName ?? "Empty!", for: .normal) + } + // MARK: - Theme func applyTheme() { @@ -103,4 +169,11 @@ class SearchEngineSelectionViewController: UIViewController, func didTapOpenSettings(sender: UIButton) { coordinator?.navigateToSearchSettings(animated: true) } + + // FIXME FXIOS-10189 This will be deleted later. + @objc + func testDidChangeSearchEngine(sender: UIButton) { + // TODO FXIOS-10384 Push action to the toolbar to update the search engine selection for the next search and + // to focus the toolbar (if it isn't already focused). + } } diff --git a/firefox-ios/Client/Redux/GlobalState/ActiveScreenAction.swift b/firefox-ios/Client/Redux/GlobalState/ActiveScreenAction.swift index 58af1862ce86..db238c023a3b 100644 --- a/firefox-ios/Client/Redux/GlobalState/ActiveScreenAction.swift +++ b/firefox-ios/Client/Redux/GlobalState/ActiveScreenAction.swift @@ -32,6 +32,7 @@ enum AppScreen { case microsurvey case trackingProtection case toolbar + case searchEngineSelection case passwordGenerator } diff --git a/firefox-ios/Client/Redux/GlobalState/ActiveScreenState.swift b/firefox-ios/Client/Redux/GlobalState/ActiveScreenState.swift index 54f6b19ec416..e3d39b9d8a9e 100644 --- a/firefox-ios/Client/Redux/GlobalState/ActiveScreenState.swift +++ b/firefox-ios/Client/Redux/GlobalState/ActiveScreenState.swift @@ -21,6 +21,7 @@ enum AppScreenState: Equatable { case themeSettings(ThemeSettingsState) case trackingProtection(TrackingProtectionState) case toolbar(ToolbarState) + case searchEngineSelection(SearchEngineSelectionState) case passwordGenerator(PasswordGeneratorState) static let reducer: Reducer = { state, action in @@ -51,6 +52,8 @@ enum AppScreenState: Equatable { return .trackingProtection(TrackingProtectionState.reducer(state, action)) case .toolbar(let state): return .toolbar(ToolbarState.reducer(state, action)) + case .searchEngineSelection(let state): + return .searchEngineSelection(SearchEngineSelectionState.reducer(state, action)) case .passwordGenerator(let state): return .passwordGenerator(PasswordGeneratorState.reducer(state, action)) } @@ -72,6 +75,7 @@ enum AppScreenState: Equatable { case .themeSettings: return .themeSettings case .trackingProtection: return .trackingProtection case .toolbar: return .toolbar + case .searchEngineSelection: return .searchEngineSelection case .passwordGenerator: return .passwordGenerator } } @@ -91,6 +95,7 @@ enum AppScreenState: Equatable { case .themeSettings(let state): return state.windowUUID case .trackingProtection(let state): return state.windowUUID case .toolbar(let state): return state.windowUUID + case .searchEngineSelection(let state): return state.windowUUID case .passwordGenerator(let state): return state.windowUUID } } @@ -156,6 +161,8 @@ struct ActiveScreensState: Equatable { screens.append(.trackingProtection(TrackingProtectionState(windowUUID: uuid))) case .toolbar: screens.append(.toolbar(ToolbarState(windowUUID: uuid))) + case .searchEngineSelection: + screens.append(.searchEngineSelection(SearchEngineSelectionState(windowUUID: uuid))) case .passwordGenerator: screens.append(.passwordGenerator(PasswordGeneratorState(windowUUID: uuid))) } diff --git a/firefox-ios/Client/Redux/GlobalState/AppState.swift b/firefox-ios/Client/Redux/GlobalState/AppState.swift index 099f77ea9fa9..af6b75f943e4 100644 --- a/firefox-ios/Client/Redux/GlobalState/AppState.swift +++ b/firefox-ios/Client/Redux/GlobalState/AppState.swift @@ -30,6 +30,7 @@ struct AppState: StateType { case (.tabsTray(let state), .tabsTray): return state as? S case (.themeSettings(let state), .themeSettings): return state as? S case (.toolbar(let state), .toolbar): return state as? S + case (.searchEngineSelection(let state), .searchEngineSelection): return state as? S case (.trackingProtection(let state), .trackingProtection): return state as? S case (.passwordGenerator(let state), .passwordGenerator): return state as? S default: return nil @@ -63,6 +64,7 @@ let middlewares = [ TabManagerMiddleware().tabsPanelProvider, ThemeManagerMiddleware().themeManagerProvider, ToolbarMiddleware().toolbarProvider, + SearchEngineSelectionMiddleware().searchEngineSelectionProvider, TrackingProtectionMiddleware().trackingProtectionProvider, PasswordGeneratorMiddleware().passwordGeneratorProvider, PocketMiddleware().pocketSectionProvider diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/OpenSearchEngineTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/OpenSearchEngineTests.swift index 2dfe60bb65aa..be9650505488 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/OpenSearchEngineTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/OpenSearchEngineTests.swift @@ -10,7 +10,7 @@ import Storage class OpenSearchEngineTests: XCTestCase { func testEncodeDecodeOpenSearchEngine_withBundledImages_Single() throws { - let searchEngine = try generateOpenSearchEngine(type: .wikipedia) + let searchEngine = try Self.generateOpenSearchEngine(type: .wikipedia) // Encode the data let data = try NSKeyedArchiver.archivedData(withRootObject: searchEngine, requiringSecureCoding: true) @@ -30,8 +30,8 @@ class OpenSearchEngineTests: XCTestCase { } func testEncodeDecodeOpenSearchEngine_withBundledImages_Array() throws { - let searchEngine1 = try generateOpenSearchEngine(type: .wikipedia) - let searchEngine2 = try generateOpenSearchEngine(type: .youtube) + let searchEngine1 = try Self.generateOpenSearchEngine(type: .wikipedia) + let searchEngine2 = try Self.generateOpenSearchEngine(type: .youtube) let dataToEncode = [searchEngine1, searchEngine2] @@ -52,8 +52,8 @@ class OpenSearchEngineTests: XCTestCase { func testCustomSearchEnginesSavedToFile_canRetrievesImageData() throws { // Test reading and writing OpenSearchEngines to the same customEngines plist file as done within the app. - let searchEngine1 = try generateOpenSearchEngine(type: .wikipedia) - let searchEngine2 = try generateOpenSearchEngine(type: .youtube) + let searchEngine1 = try Self.generateOpenSearchEngine(type: .wikipedia) + let searchEngine2 = try Self.generateOpenSearchEngine(type: .youtube) // Encode the data let searchEngines = [searchEngine1, searchEngine2] @@ -90,7 +90,7 @@ class OpenSearchEngineTests: XCTestCase { } /// For generating test `OpenSearchEngine` data. - private enum TestSearchEngine { + public enum TestSearchEngine { case youtube, wikipedia var engineID: String { @@ -119,7 +119,7 @@ class OpenSearchEngineTests: XCTestCase { } /// Creates a single `OpenSearchEngine` with valid image data pulled from the test's asset catalog. - private func generateOpenSearchEngine(type: TestSearchEngine) throws -> OpenSearchEngine { + public static func generateOpenSearchEngine(type: TestSearchEngine) throws -> OpenSearchEngine { guard let testImage = UIImage( named: type.imageName, in: Bundle(for: OpenSearchEngineTests.self), @@ -128,6 +128,10 @@ class OpenSearchEngineTests: XCTestCase { throw OpenSearchEngineError.imageNotInBundle } + return generateOpenSearchEngine(type: type, withImage: testImage) + } + + public static func generateOpenSearchEngine(type: TestSearchEngine, withImage testImage: UIImage) -> OpenSearchEngine { return OpenSearchEngine( engineID: type.engineID, shortName: type.name, diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionMiddlewareTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionMiddlewareTests.swift new file mode 100644 index 000000000000..5f9dda8eebcf --- /dev/null +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionMiddlewareTests.swift @@ -0,0 +1,64 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Redux +import XCTest + +@testable import Client + +final class SearchEngineSelectionMiddlewareTests: XCTestCase { + var mockProfile: MockProfile! + var mockSearchEngines: [OpenSearchEngine]! + + override func setUp() { + super.setUp() + DependencyHelperMock().bootstrapDependencies() + mockProfile = MockProfile() + mockSearchEngines = [ + OpenSearchEngineTests.generateOpenSearchEngine(type: .wikipedia, withImage: UIImage()), + OpenSearchEngineTests.generateOpenSearchEngine(type: .youtube, withImage: UIImage()), + ] + } + + override func tearDown() { + DependencyHelperMock().reset() + super.tearDown() + } + + func testDismissMenuAction() throws { + let mockSearchEnginesManager = SearchEnginesManager(prefs: mockProfile.prefs, files: mockProfile.files) + mockSearchEnginesManager.orderedEngines = mockSearchEngines + + let subject = createSubject(mockSearchEnginesManager: mockSearchEnginesManager) + let action = getAction(for: .viewDidLoad) + + let testStore = Store( + state: AppState(), + reducer: AppState.reducer, + middlewares: [subject.searchEngineSelectionProvider] + ) + + testStore.dispatch(action) + + // Currently we have a testability problem with our redux archicture: + // 1) Every middleware calls the global `store` + // 2) We have one global store so every test (including tests running in parallel) accesses the same store + // + // Ideally we would be able to check that the middleware fired an action of a specific type with a specific payload. + throw XCTSkip("Need Store architecture changes if we want to implement tests") + } + + // MARK: - Helpers + + private func createSubject(mockSearchEnginesManager: SearchEnginesManager) -> SearchEngineSelectionMiddleware { + return SearchEngineSelectionMiddleware(profile: mockProfile, searchEnginesManager: mockSearchEnginesManager) + } + + private func getAction(for actionType: SearchEngineSelectionActionType) -> SearchEngineSelectionAction { + return SearchEngineSelectionAction( + windowUUID: .XCTestDefaultUUID, + actionType: actionType + ) + } +} diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionStateTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionStateTests.swift new file mode 100644 index 000000000000..e1c1ab6b8297 --- /dev/null +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/SearchEngines/SearchEngineSelectionStateTests.swift @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import Redux +import XCTest + +@testable import Client + +final class SearchEngineSelectionStateTests: XCTestCase { + override func setUp() { + super.setUp() + DependencyHelperMock().bootstrapDependencies() + } + + override func tearDown() { + DependencyHelperMock().reset() + super.tearDown() + } + + func testInitialization() { + let initialState = createSubject() + + XCTAssertFalse(initialState.shouldDismiss) + XCTAssertEqual(initialState.searchEngines, []) + } + + func testDidLoadSearchEngines() { + let initialState = createSubject() + let reducer = searchEngineSelectionReducer() + + let expectedResult: [OpenSearchEngine] = [ + OpenSearchEngineTests.generateOpenSearchEngine(type: .wikipedia, withImage: UIImage()), + OpenSearchEngineTests.generateOpenSearchEngine(type: .youtube, withImage: UIImage()) + ] + + XCTAssertEqual(initialState.searchEngines, []) + + let newState = reducer( + initialState, + SearchEngineSelectionAction( + windowUUID: .XCTestDefaultUUID, + actionType: SearchEngineSelectionActionType.didLoadSearchEngines, + searchEngines: expectedResult + ) + ) + + XCTAssertEqual(newState.searchEngines, expectedResult) + } + + // MARK: - Private + private func createSubject() -> SearchEngineSelectionState { + return SearchEngineSelectionState(windowUUID: .XCTestDefaultUUID) + } + + private func searchEngineSelectionReducer() -> Reducer { + return SearchEngineSelectionState.reducer + } +}