diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index 11dd0b8302b5..52efd1b7e0b0 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -723,7 +723,6 @@ 8A3233FE28627446003E1C33 /* LocalDesktopFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3233FD28627446003E1C33 /* LocalDesktopFolder.swift */; }; 8A32DD5028B419B300D57C60 /* HomepageMessageCardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A32DD4F28B419B300D57C60 /* HomepageMessageCardViewModelTests.swift */; }; 8A33221F27DFE318008F809E /* TopSitesDataAdaptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A33221E27DFE318008F809E /* TopSitesDataAdaptorTests.swift */; }; - 8A33222227DFE658008F809E /* NimbusMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A33222127DFE658008F809E /* NimbusMock.swift */; }; 8A3345612BA499B7008C52AB /* disconnect-block-fingerprinting.json in Resources */ = {isa = PBXBuildFile; fileRef = 8A3345572BA499B6008C52AB /* disconnect-block-fingerprinting.json */; }; 8A3345622BA499B7008C52AB /* disconnect-block-advertising.json in Resources */ = {isa = PBXBuildFile; fileRef = 8A3345582BA499B6008C52AB /* disconnect-block-advertising.json */; }; 8A3345632BA499B7008C52AB /* disconnect-block-cookies-content.json in Resources */ = {isa = PBXBuildFile; fileRef = 8A3345592BA499B6008C52AB /* disconnect-block-cookies-content.json */; }; @@ -7298,7 +7297,6 @@ 8A3233FD28627446003E1C33 /* LocalDesktopFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalDesktopFolder.swift; sourceTree = ""; }; 8A32DD4F28B419B300D57C60 /* HomepageMessageCardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageMessageCardViewModelTests.swift; sourceTree = ""; }; 8A33221E27DFE318008F809E /* TopSitesDataAdaptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSitesDataAdaptorTests.swift; sourceTree = ""; }; - 8A33222127DFE658008F809E /* NimbusMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbusMock.swift; sourceTree = ""; }; 8A3345572BA499B6008C52AB /* disconnect-block-fingerprinting.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "disconnect-block-fingerprinting.json"; path = "../../../ContentBlockingLists/disconnect-block-fingerprinting.json"; sourceTree = ""; }; 8A3345582BA499B6008C52AB /* disconnect-block-advertising.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "disconnect-block-advertising.json"; path = "../../../ContentBlockingLists/disconnect-block-advertising.json"; sourceTree = ""; }; 8A3345592BA499B6008C52AB /* disconnect-block-cookies-content.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "disconnect-block-cookies-content.json"; path = "../../../ContentBlockingLists/disconnect-block-cookies-content.json"; sourceTree = ""; }; @@ -11288,7 +11286,6 @@ 8A33222027DFE64C008F809E /* TestingHelperClasses */ = { isa = PBXGroup; children = ( - 8A33222127DFE658008F809E /* NimbusMock.swift */, 8A6B77CB2811C468001110D2 /* URLProtocolStub.swift */, ); path = TestingHelperClasses; @@ -17162,7 +17159,6 @@ 215349062886007900FADB4D /* GleanPlumbMessageStoreTests.swift in Sources */, 2173326A29CCF901007F20C7 /* UIPanGestureRecognizerMock.swift in Sources */, 5A9A09D628B01FD500B6F51E /* MockURLBarView.swift in Sources */, - 8A33222227DFE658008F809E /* NimbusMock.swift in Sources */, 0B7FC3D32CAE811F005C5CCE /* DefaultBookmarksSaverTests.swift in Sources */, 8A8629E72880B7330096DDB1 /* LegacyBookmarksPanelTests.swift in Sources */, C8B394362A0ED55D00700E49 /* MockOnboardingCardDelegate.swift in Sources */, diff --git a/firefox-ios/Client/FeatureFlags/LegacyFeatureFlagsManager.swift b/firefox-ios/Client/FeatureFlags/LegacyFeatureFlagsManager.swift index 256f4bc1d820..41b7fa3e8c40 100644 --- a/firefox-ios/Client/FeatureFlags/LegacyFeatureFlagsManager.swift +++ b/firefox-ios/Client/FeatureFlags/LegacyFeatureFlagsManager.swift @@ -6,7 +6,7 @@ import Shared import Common // MARK: - Protocol -protocol FeatureFlaggable { } +protocol FeatureFlaggable {} extension FeatureFlaggable { var featureFlags: LegacyFeatureFlagsManager { diff --git a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift index adb5ccd5ab1f..0e67825834e0 100644 --- a/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift +++ b/firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift @@ -35,6 +35,7 @@ enum NimbusFeatureFlagID: String, CaseIterable { case nightMode case passwordGenerator case preferSwitchToOpenTabOverDuplicate + case pullToRefreshRefactor case reduxSearchSettings case closeRemoteTabs case reportSiteIssue @@ -119,6 +120,7 @@ struct NimbusFlaggableFeature: HasNimbusSearchBar { .nightMode, .passwordGenerator, .preferSwitchToOpenTabOverDuplicate, + .pullToRefreshRefactor, .reduxSearchSettings, .reportSiteIssue, .feltPrivacySimplifiedUI, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index 2f294efb36b3..499676ca6e32 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -191,7 +191,13 @@ class BrowserViewController: UIViewController, private lazy var privacyWindowHelper = PrivacyWindowHelper() var keyboardBackdrop: UIView? - lazy var scrollController = TabScrollingController(windowUUID: windowUUID) + lazy var scrollController = TabScrollingController( + windowUUID: windowUUID, + isPullToRefreshRefactorEnabled: featureFlags.isFeatureEnabled( + .pullToRefreshRefactor, + checking: .buildOnly + ) + ) private var keyboardState: KeyboardState? var pendingToast: Toast? // A toast that might be waiting for BVC to appear before displaying var downloadToast: DownloadToast? // A toast that is showing the combined download progress diff --git a/firefox-ios/Client/Frontend/Browser/TabScrollController.swift b/firefox-ios/Client/Frontend/Browser/TabScrollController.swift index 1af53234954d..ea74e08b5806 100644 --- a/firefox-ios/Client/Frontend/Browser/TabScrollController.swift +++ b/firefox-ios/Client/Frontend/Browser/TabScrollController.swift @@ -9,7 +9,6 @@ import Common private let ToolbarBaseAnimationDuration: CGFloat = 0.2 class TabScrollingController: NSObject, - FeatureFlaggable, SearchBarLocationProvider, Themeable { private struct UX { @@ -39,16 +38,8 @@ class TabScrollingController: NSObject, self.scrollView?.addGestureRecognizer(panGesture) scrollView?.delegate = self scrollView?.keyboardDismissMode = .onDrag - configureRefreshControl() - tab?.onLoading = { [weak self] in - guard let self else { return } - if tabIsLoading() { - pullToRefreshView?.stopObservingContentScroll() - pullToRefreshView?.removeFromSuperview() - } else { - configureRefreshControl() - } - } + configureRefreshControl(isEnabled: true) + setupTabOnLoadingCallback() } } @@ -58,7 +49,6 @@ class TabScrollingController: NSObject, weak var zoomPageBar: ZoomPageBar? private var observedScrollViews = WeakList() - private var webViewIsLoadingObserver: NSKeyValueObservation? var overKeyboardContainerConstraint: Constraint? var bottomContainerConstraint: Constraint? @@ -118,6 +108,7 @@ class TabScrollingController: NSObject, $0 is PullRefreshView }) as? PullRefreshView } + private let isPullToRefreshRefactorEnabled: Bool var contentOffset: CGPoint { return scrollView?.contentOffset ?? .zero } private var scrollViewHeight: CGFloat { return scrollView?.frame.height ?? 0 } private var topScrollHeight: CGFloat { header?.frame.height ?? 0 } @@ -150,7 +141,6 @@ class TabScrollingController: NSObject, } deinit { - webViewIsLoadingObserver?.invalidate() logger.log("TabScrollController deallocating", level: .info, category: .lifecycle) observedScrollViews.forEach({ stopObserving(scrollView: $0) }) guard let themeObserver else { return } @@ -158,6 +148,7 @@ class TabScrollingController: NSObject, } init(windowUUID: WindowUUID, + isPullToRefreshRefactorEnabled: Bool, themeManager: ThemeManager = AppContainer.shared.resolve(), notificationCenter: NotificationProtocol = NotificationCenter.default, logger: Logger = DefaultLogger.shared) { @@ -165,14 +156,17 @@ class TabScrollingController: NSObject, self.windowUUID = windowUUID self.notificationCenter = notificationCenter self.logger = logger + self.isPullToRefreshRefactorEnabled = isPullToRefreshRefactorEnabled super.init() setupNotifications() } func traitCollectionDidChange() { - pullToRefreshView?.stopObservingContentScroll() - pullToRefreshView?.removeFromSuperview() - configureRefreshControl() + if isPullToRefreshRefactorEnabled { + pullToRefreshView?.stopObservingContentScroll() + pullToRefreshView?.removeFromSuperview() + configureRefreshControl(isEnabled: true) + } } private func setupNotifications() { @@ -182,6 +176,21 @@ class TabScrollingController: NSObject, object: nil) } + private func setupTabOnLoadingCallback() { + if isPullToRefreshRefactorEnabled { + tab?.onLoading = { [weak self] in + guard let self else { return } + // If we are in the home page delete pull to refresh so it doesn't show on the background + if tabIsLoading() || (tab?.isFxHomeTab ?? false) { + pullToRefreshView?.stopObservingContentScroll() + pullToRefreshView?.removeFromSuperview() + } else { + configureRefreshControl(isEnabled: true) + } + } + } + } + @objc private func applicationWillTerminate(_ notification: Notification) { // Ensures that we immediately de-register KVO observations for content size changes in @@ -316,7 +325,16 @@ class TabScrollingController: NSObject, // MARK: - Private private extension TabScrollingController { - func configureRefreshControl() { + private func configureRefreshControl(isEnabled: Bool) { + if isPullToRefreshRefactorEnabled { + configureNewRefreshControl() + } else { + scrollView?.refreshControl = isEnabled ? UIRefreshControl() : nil + scrollView?.refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged) + } + } + + private func configureNewRefreshControl() { guard let scrollView, let webView = tab?.webView, !scrollView.subviews.contains(where: { $0 is PullRefreshView }) @@ -326,10 +344,10 @@ private extension TabScrollingController { } let refresh = PullRefreshView(parentScrollView: self.scrollView, - isPotraitOrientation: UIWindow.isPortrait) { - self.reload() + isPotraitOrientation: UIWindow.isPortrait) { [weak self] in + self?.reload() } - self.scrollView?.addSubview(refresh) + scrollView.addSubview(refresh) refresh.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ refresh.leadingAnchor.constraint(equalTo: webView.leadingAnchor), @@ -587,13 +605,17 @@ extension TabScrollingController: UIScrollViewDelegate { } func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { - pullToRefreshView?.stopObservingContentScroll() - pullToRefreshView?.removeFromSuperview() + if isPullToRefreshRefactorEnabled { + pullToRefreshView?.stopObservingContentScroll() + pullToRefreshView?.removeFromSuperview() + } else { + configureRefreshControl(isEnabled: false) + } self.isUserZoom = true } func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - configureRefreshControl() + configureRefreshControl(isEnabled: true) self.isUserZoom = false } diff --git a/firefox-ios/Client/Frontend/Browser/WebView/PullRefreshView.swift b/firefox-ios/Client/Frontend/Browser/WebView/PullRefreshView.swift index 3f9147d0a88d..3b368bcbb8d9 100644 --- a/firefox-ios/Client/Frontend/Browser/WebView/PullRefreshView.swift +++ b/firefox-ios/Client/Frontend/Browser/WebView/PullRefreshView.swift @@ -76,6 +76,13 @@ class PullRefreshView: UIView, let shrinkFactor = computeShrinkingFactor() let progressContainerViewPadding = UX.progressViewPadding * shrinkFactor let progressContainerViewSize = UX.progressViewAnimatedBackgroundSize * shrinkFactor + + if let scrollView, scrollView.contentOffset.y != 0 { + let threshold = UX.blinkProgressViewStandardThreshold * shrinkFactor + let initialRotationAngle = -(scrollView.contentOffset.y) / threshold + progressView.transform = CGAffineTransform(rotationAngle: initialRotationAngle) + } + NSLayoutConstraint.activate([ progressContainerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -progressContainerViewPadding), progressContainerView.centerXAnchor.constraint(equalTo: centerXAnchor), @@ -133,7 +140,6 @@ class PullRefreshView: UIView, self.progressContainerView.backgroundColor = .clear self.progressContainerView.transform = .identity self.progressView.transform = .identity - TelemetryWrapper.shared.recordEvent(category: .action, method: .pull, object: .reload) self.onRefreshCallback() }) } @@ -196,6 +202,7 @@ class PullRefreshView: UIView, func stopObservingContentScroll() { scrollObserver?.invalidate() + scrollObserver = nil } func updateEasterEggForOrientationChange(isPotrait: Bool) { diff --git a/firefox-ios/Client/Frontend/Home/LegacyHomepageViewController.swift b/firefox-ios/Client/Frontend/Home/LegacyHomepageViewController.swift index dc29bf97097b..893a6f47dac4 100644 --- a/firefox-ios/Client/Frontend/Home/LegacyHomepageViewController.swift +++ b/firefox-ios/Client/Frontend/Home/LegacyHomepageViewController.swift @@ -40,6 +40,7 @@ class LegacyHomepageViewController: private var overlayManager: OverlayModeManager private var userDefaults: UserDefaultsInterface private lazy var wallpaperView: WallpaperBackgroundView = .build { _ in } + private var wallpaperViewTopConstraint: NSLayoutConstraint? private var jumpBackInContextualHintViewController: ContextualHintViewController private var syncTabContextualHintViewController: ContextualHintViewController private var collectionView: UICollectionView! = nil @@ -192,6 +193,11 @@ class LegacyHomepageViewController: if UIDevice.current.userInterfaceIdiom == .pad { reloadOnRotation(newSize: size) } + + coordinator.animate { sin_ in + let wallpaperTopConstant: CGFloat = UIWindow.keyWindow?.safeAreaInsets.top ?? self.statusBarFrame?.height ?? 0 + self.wallpaperViewTopConstraint?.constant = -wallpaperTopConstant + } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -277,9 +283,11 @@ class LegacyHomepageViewController: // Constraint so wallpaper appears under the status bar let wallpaperTopConstant: CGFloat = UIWindow.keyWindow?.safeAreaInsets.top ?? statusBarFrame?.height ?? 0 + wallpaperViewTopConstraint = wallpaperView.topAnchor.constraint(equalTo: view.topAnchor, + constant: -wallpaperTopConstant) + wallpaperViewTopConstraint?.isActive = true NSLayoutConstraint.activate([ - wallpaperView.topAnchor.constraint(equalTo: view.topAnchor, constant: -wallpaperTopConstant), wallpaperView.leadingAnchor.constraint(equalTo: view.leadingAnchor), wallpaperView.bottomAnchor.constraint(equalTo: view.bottomAnchor), wallpaperView.trailingAnchor.constraint(equalTo: view.trailingAnchor) diff --git a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift index f84fee762b78..a6fe7c497156 100644 --- a/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift +++ b/firefox-ios/Client/Nimbus/NimbusFeatureFlagLayer.swift @@ -82,6 +82,9 @@ final class NimbusFeatureFlagLayer { case .preferSwitchToOpenTabOverDuplicate: return checkPreferSwitchToOpenTabOverDuplicate(from: nimbus) + case .pullToRefreshRefactor: + return checkPullToRefreshFeature(from: nimbus) + case .reduxSearchSettings: return checkReduxSearchSettingsFeature(from: nimbus) @@ -309,6 +312,10 @@ final class NimbusFeatureFlagLayer { return nimbus.features.homescreenFeature.value().preferSwitchToOpenTab } + private func checkPullToRefreshFeature(from nimbus: FxNimbus) -> Bool { + return nimbus.features.pullToRefreshRefactorFeature.value().enabled + } + private func checkAddressAutofillEditing(from nimbus: FxNimbus) -> Bool { let config = nimbus.features.addressAutofillEdit.value() diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/TabScrollControllerTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/TabScrollControllerTests.swift index 7899320287b2..40c0d0264f81 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/TabScrollControllerTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/TabScrollControllerTests.swift @@ -5,12 +5,11 @@ import XCTest import WebKit import Common - +import Shared @testable import Client final class TabScrollControllerTests: XCTestCase { var tab: Tab! - var subject: TabScrollingController! var mockProfile: MockProfile! var mockGesture: UIPanGestureRecognizerMock! let windowUUID: WindowUUID = .XCTestDefaultUUID @@ -21,24 +20,22 @@ final class TabScrollControllerTests: XCTestCase { super.setUp() DependencyHelperMock().bootstrapDependencies() - self.mockProfile = MockProfile() - self.subject = TabScrollingController(windowUUID: windowUUID) - self.tab = Tab(profile: mockProfile, windowUUID: windowUUID) + mockProfile = MockProfile() LegacyFeatureFlagsManager.shared.initializeDeveloperFeatures(with: mockProfile) + tab = Tab(profile: mockProfile, windowUUID: windowUUID) mockGesture = UIPanGestureRecognizerMock() } override func tearDown() { - super.tearDown() - mockProfile?.shutdown() - self.mockProfile = nil - self.subject = nil - self.tab = nil + mockProfile = nil + tab = nil + super.tearDown() } func testHandlePan_ScrollingUp() { - setupTabScroll() + let subject = createSubject() + setupTabScroll(with: subject) mockGesture.gestureTranslation = CGPoint(x: 0, y: 100) subject.handlePan(mockGesture) @@ -47,7 +44,8 @@ final class TabScrollControllerTests: XCTestCase { } func testHandlePan_ScrollingDown() { - setupTabScroll() + let subject = createSubject() + setupTabScroll(with: subject) mockGesture.gestureTranslation = CGPoint(x: 0, y: -100) subject.handlePan(mockGesture) @@ -56,7 +54,8 @@ final class TabScrollControllerTests: XCTestCase { } func testShowToolbar_AfterHidingWithScroll() { - setupTabScroll() + let subject = createSubject() + setupTabScroll(with: subject) // Hide toolbar mockGesture.gestureTranslation = CGPoint(x: 0, y: 100) @@ -69,7 +68,8 @@ final class TabScrollControllerTests: XCTestCase { } func testScrollDidEndDragging_ScrollingUp() { - setupTabScroll() + let subject = createSubject() + setupTabScroll(with: subject) // Hide toolbar mockGesture.gestureTranslation = CGPoint(x: 0, y: 100) @@ -81,7 +81,8 @@ final class TabScrollControllerTests: XCTestCase { } func testScrollDidEndDragging_ScrollingDown() { - setupTabScroll() + let subject = createSubject() + setupTabScroll(with: subject) // Hide toolbar mockGesture.gestureTranslation = CGPoint(x: 0, y: -100) @@ -91,11 +92,99 @@ final class TabScrollControllerTests: XCTestCase { XCTAssertEqual(subject.toolbarState, TabScrollingController.ToolbarState.collapsed) } - private func setupTabScroll() { + func testDidSetTab_addsPullRefreshViewToScrollView() { + let subject = createSubject() + setupTabScroll(with: subject) + + let pullRefreshView = tab.webView?.scrollView.subviews.first(where: { + $0 is PullRefreshView + }) + + XCTAssertNotNil(pullRefreshView) + XCTAssertNil(tab.webView?.scrollView.refreshControl) + } + + func testDidSetTab_addsUIRefreshControllToScrollView() { + let subject = createSubject(isPullRefreshRefactorEnabled: false) + setupTabScroll(with: subject) + + let pullRefreshView = tab.webView?.scrollView.subviews.first(where: { + $0 is PullRefreshView + }) + + XCTAssertNotNil(tab.webView?.scrollView.refreshControl) + XCTAssertNil(pullRefreshView) + } + + func testDidSetTab_setsTabOnLoadingClosure_whenPullRefreshFeatureEnabled() { + let subject = createSubject() + setupTabScroll(with: subject) + + XCTAssertNotNil(tab.onLoading) + } + + func testDidSetTab_tabHasNilOnLoadingClosure_whenPullRefreshFeatureDisabled() { + let subject = createSubject(isPullRefreshRefactorEnabled: false) + setupTabScroll(with: subject) + + XCTAssertNil(tab.onLoading) + } + + func testScrollViewWillBeginZooming_removesPullRefresh_whenPullRefreshFeatureEnabled() throws { + let subject = createSubject() + setupTabScroll(with: subject) + + let scrollView = try XCTUnwrap(tab.webView?.scrollView) + subject.scrollViewWillBeginZooming(scrollView, with: nil) + + let pullRefresh = scrollView.subviews.first { $0 is PullRefreshView } + XCTAssertNil(pullRefresh) + } + + func testScrollViewWillBeginZooming_removesUIRefreshControll_whenPullRefreshFeatureDisabled() throws { + let subject = createSubject(isPullRefreshRefactorEnabled: false) + setupTabScroll(with: subject) + + let scrollView = try XCTUnwrap(tab.webView?.scrollView) + subject.scrollViewWillBeginZooming(scrollView, with: nil) + + XCTAssertNil(scrollView.refreshControl) + } + + func testScrollViewDidEndZooming_addsPullRefresh_whenPullRefreshFeatureEnabled() throws { + let subject = createSubject() + setupTabScroll(with: subject) + + let scrollView = try XCTUnwrap(tab.webView?.scrollView) + scrollView.scrollRectToVisible(.zero, animated: true) + subject.scrollViewDidEndZooming(scrollView, with: nil, atScale: 0) + + let pullRefresh = scrollView.subviews.first { $0 is PullRefreshView } + XCTAssertNotNil(pullRefresh) + } + + func testScrollViewDidEndZooming_addsUIRefreshControll_whenPullRefreshFeatureDisabled() throws { + let subject = createSubject(isPullRefreshRefactorEnabled: false) + setupTabScroll(with: subject) + + let scrollView = try XCTUnwrap(tab.webView?.scrollView) + subject.scrollViewDidEndZooming(scrollView, with: nil, atScale: 0) + + XCTAssertNotNil(scrollView.refreshControl) + } + + private func setupTabScroll(with subject: TabScrollingController) { tab.createWebview(configuration: .init()) tab.webView?.scrollView.contentSize = CGSize(width: 200, height: 2000) tab.webView?.scrollView.delegate = subject subject.tab = tab subject.header = header } + + private func createSubject(isPullRefreshRefactorEnabled: Bool = true) -> TabScrollingController { + return TabScrollingController( + windowUUID: .XCTestDefaultUUID, + isPullToRefreshRefactorEnabled: isPullRefreshRefactorEnabled + ) + } } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TestingHelperClasses/NimbusMock.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TestingHelperClasses/NimbusMock.swift deleted file mode 100644 index b45e09864733..000000000000 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TestingHelperClasses/NimbusMock.swift +++ /dev/null @@ -1,9 +0,0 @@ -// 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 Foundation -@testable import Client -import MozillaAppServices - -class NimbusMock: FxNimbus { } diff --git a/firefox-ios/nimbus-features/pullToRefreshRefactorFeature.yaml b/firefox-ios/nimbus-features/pullToRefreshRefactorFeature.yaml new file mode 100644 index 000000000000..d5377f98fbb6 --- /dev/null +++ b/firefox-ios/nimbus-features/pullToRefreshRefactorFeature.yaml @@ -0,0 +1,18 @@ +# The configuration for the pullToRefreshRefactorFeature feature +features: + pull-to-refresh-refactor-feature: + description: > + The Feature flag to manage the roll out of the new pull to refresh feature. + variables: + enabled: + description: > + Enables the feature + type: Boolean + default: false + defaults: + - channel: beta + value: + enabled: true + - channel: developer + value: + enabled: true \ No newline at end of file diff --git a/firefox-ios/nimbus.fml.yaml b/firefox-ios/nimbus.fml.yaml index 63772b4594f8..bc98c00ade8f 100644 --- a/firefox-ios/nimbus.fml.yaml +++ b/firefox-ios/nimbus.fml.yaml @@ -32,6 +32,7 @@ include: - nimbus-features/nightModeFeature.yaml - nimbus-features/onboardingFrameworkFeature.yaml - nimbus-features/passwordGeneratorFeature.yaml + - nimbus-features/pullToRefreshRefactorFeature.yaml - nimbus-features/reduxSearchSettingsFeature.yaml - nimbus-features/remoteTabManagement.yaml - nimbus-features/searchFeature.yaml