chromium/ios/chrome/browser/lens_overlay/coordinator/lens_overlay_coordinator_unittest.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/lens_overlay/coordinator/lens_overlay_coordinator.h"

#import "base/run_loop.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "components/lens/lens_overlay_permission_utils.h"
#import "ios/chrome/browser/lens_overlay/model/lens_overlay_tab_helper.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_overlay_consent_view_controller.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/browser_state/test_chrome_browser_state_manager.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/lens_overlay_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/browser/snapshots/model/fake_snapshot_generator_delegate.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/chrome/test/scoped_key_window.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

using base::test::ios::kWaitForUIElementTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

@interface LensOverlayCoordinator ()
- (BOOL)isUICreated;
@end

namespace {

class LensOverlayCoordinatorTest : public PlatformTest {
 public:
  LensOverlayCoordinatorTest() {
    feature_list_.InitAndEnableFeature(kEnableLensOverlay);

    root_view_controller_ = [[UIViewController alloc] init];
    root_view_controller_.definesPresentationContext = YES;
    scoped_window_.Get().rootViewController = root_view_controller_;

    // Browser state
    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    browser_state_ = profile_manager_.AddProfileWithBuilder(std::move(builder));

    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        browser_state_, std::make_unique<FakeAuthenticationServiceDelegate>());

    AuthenticationService* authentication_service =
        AuthenticationServiceFactory::GetForBrowserState(browser_state_);

    browser_ = std::make_unique<TestBrowser>(browser_state_);
    dispatcher_ = [[CommandDispatcher alloc] init];

    GetApplicationContext()->GetLocalState()->SetInteger(
        lens::prefs::kLensOverlaySettings,
        static_cast<int>(
            lens::prefs::LensOverlaySettingsPolicyValue::kEnabled));

    base_view_controller_ = [[UIViewController alloc] init];

    // LensOverlayCoordinator
    coordinator_ = [[LensOverlayCoordinator alloc]
        initWithBaseViewController:base_view_controller_
                           browser:browser_.get()];

    [dispatcher_ startDispatchingToTarget:coordinator_
                              forProtocol:@protocol(LensOverlayCommands)];

    // Tab helper
    web::WebState::CreateParams params(browser_state_);
    web_state_ = web::WebState::Create(params);
    LensOverlayTabHelper::CreateForWebState(web_state_.get());
    SnapshotTabHelper::CreateForWebState(web_state_.get());
    tab_helper_ = LensOverlayTabHelper::FromWebState(web_state_.get());

    // Attach SnapshotTabHelper to allow snapshot generation.
    SnapshotTabHelper::CreateForWebState(web_state_.get());
    delegate_ = [[FakeSnapshotGeneratorDelegate alloc] init];
    SnapshotTabHelper::FromWebState(web_state_.get())->SetDelegate(delegate_);

    // Add a fake view to the delgate, which will be used to capture snapshots.
    CGRect frame = {CGPointZero, CGSizeMake(300, 400)};
    delegate_.view = [[UIView alloc] initWithFrame:frame];
    delegate_.view.backgroundColor = [UIColor blueColor];

    // Mark the only web state as active.
    browser_.get()->GetWebStateList()->InsertWebState(std::move(web_state_));
    browser_.get()->GetWebStateList()->ActivateWebStateAt(0);

    // Increment the fullscreen disabled counter.
    FullscreenController* fullscreen_controller =
        FullscreenController::FromBrowser(browser_.get());
    fullscreen_controller->IncrementDisabledCounter();

    // Log in with a fake identity.
    id<SystemIdentity> identity = [FakeSystemIdentity fakeIdentity1];
    FakeSystemIdentityManager* fake_system_identity_manager =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    fake_system_identity_manager->AddIdentity(identity);
    authentication_service->SignIn(
        identity, signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);

    // Wait for the base view controller to be presented.
    base_view_controller_.modalPresentationStyle =
        UIModalPresentationOverCurrentContext;
    __block bool presentation_finished = NO;
    [root_view_controller_ presentViewController:base_view_controller_
                                        animated:NO
                                      completion:^{
                                        presentation_finished = YES;
                                      }];
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
      return presentation_finished;
    }));
  }

  ~LensOverlayCoordinatorTest() override {
    // Dismisses `base_view_controller_` and waits for the dismissal to finish.
    __block bool dismissal_finished = NO;
    [root_view_controller_ dismissViewControllerAnimated:NO
                                              completion:^{
                                                dismissal_finished = YES;
                                              }];
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
      return dismissal_finished;
    }));
  }

 protected:
  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO};
  base::RunLoop run_loop_;
  FakeSnapshotGeneratorDelegate* delegate_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  TestProfileManagerIOS profile_manager_;
  LensOverlayCoordinator* coordinator_;
  TestChromeBrowserState* browser_state_;
  std::unique_ptr<TestBrowser> browser_;
  std::unique_ptr<web::WebState> web_state_;
  UIViewController* base_view_controller_;
  base::test::ScopedFeatureList feature_list_;
  ScopedKeyWindow scoped_window_;
  UIViewController* root_view_controller_ = nil;
  id dispatcher_;
  LensOverlayTabHelper* tab_helper_;

  void DeliverMemoryWarningNotification() {
    [[NSNotificationCenter defaultCenter]
        postNotificationName:UIApplicationDidReceiveMemoryWarningNotification
                      object:nil];
  }
};

// The overlay should appear shown only after the UI is created.
TEST_F(LensOverlayCoordinatorTest, ShouldMarkOverlayShownWhenUICreated) {
  // Given a started `LensOverlayCoordinator`.
  [coordinator_ start];

  // Then the UI should not be shown to the user.
  EXPECT_FALSE(tab_helper_->IsLensOverlayShown());

  // When the coordinator is asked to create and show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kLocationBar];

  // Then the UI should appear created and shown to the user.
  EXPECT_TRUE(tab_helper_->IsLensOverlayShown());
}

// When the UI is destroyed the overlay should not appear shown.
TEST_F(LensOverlayCoordinatorTest, ShouldDestroyTheUIUponRequest) {
  // Given a started `LensOverlayCoordinator`.
  [coordinator_ start];

  // When the coordinator is asked to create and show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kLocationBar];

  // Then the UI should appear created and shown to the user.
  EXPECT_TRUE(tab_helper_->IsLensOverlayShown());

  // When the destroy command is dispatched.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands) destroyLensUI:NO];

  // Then the UI should not appear shown anymore.
  EXPECT_FALSE(tab_helper_->IsLensOverlayShown());
}

// When the UI is not created the `show` command should do nothing.
TEST_F(LensOverlayCoordinatorTest, ShouldNotShowTheOverlayWhenUIIsNotCreated) {
  // Given a started `LensOverlayCoordinator` without a created UI.
  [coordinator_ start];

  // When the coordinator is asked to show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands) showLensUI:NO];

  // Then nothing should be presented.
  EXPECT_TRUE(base_view_controller_.presentedViewController == nil);
}

// Showing the overlay should present the container view controller.
TEST_F(LensOverlayCoordinatorTest, ShouldPresentVCOnShowCommandDispatched) {
  // Given a started `LensOverlayCoordinator` without a created UI.
  [coordinator_ start];

  // Before showing anything nothing should appear presented.
  EXPECT_TRUE(base_view_controller_.presentedViewController == nil);

  // Dispatch the create & show command.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kLocationBar];

  // After dispatching the create & show command, a view controller should
  // appear presented.
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return base_view_controller_.presentedViewController == nil;
  }));
}

// Hiding the overlay should trigger dismissing the container VC.
TEST_F(LensOverlayCoordinatorTest, ShouldDismissVCOnHideCommandDispatched) {
  // Given a started `LensOverlayCoordinator` with a created and shown UI.
  [coordinator_ start];

  // Dispatch the create & show command.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kLocationBar];

  // After dispatching the create & show command, a view controller should
  // appear presented.
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return base_view_controller_.presentedViewController == nil;
  }));

  [HandlerForProtocol(dispatcher_, LensOverlayCommands) hideLensUI:NO];

  // The presented view controller is set to `nil` when the dismiss is over.
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return base_view_controller_.presentedViewController == nil;
  }));
}

// When the UI is created but not shown, then the memory warning should destroy
// the UI.
TEST_F(LensOverlayCoordinatorTest,
       ShouldDestroyUIOnMemoryWarningWhenUIIsNotShown) {
  // Given a started `LensOverlayCoordinator`.
  [coordinator_ start];

  // When the coordinator is asked to create and show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kOverflowMenu];

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop_.QuitClosure());
  run_loop_.Run();

  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return base_view_controller_.presentedViewController != nil;
  }));

  // Then the UI should appear created.
  EXPECT_TRUE([coordinator_ isUICreated]);

  // Given a hidden lens overlay.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands) hideLensUI:NO];
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return base_view_controller_.presentedViewController == nil;
  }));

  // When UIKit delivers a low-memory warning notification.
  DeliverMemoryWarningNotification();

  // Then the UI should get destroyed.
  EXPECT_FALSE([coordinator_ isUICreated]);
}

// When the UI is created and visible to the user the memory warning should not
// destroy the UI.
TEST_F(LensOverlayCoordinatorTest,
       ShouldNotDestroyUIOnMemoryWarningWhenUIIsShown) {
  // Given a started `LensOverlayCoordinator`.
  [coordinator_ start];

  // When the coordinator is asked to create and show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kOverflowMenu];

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop_.QuitClosure());
  run_loop_.Run();

  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return base_view_controller_.presentedViewController != nil;
  }));

  // Then the UI should appear created and shown to the user.
  EXPECT_TRUE(tab_helper_->IsLensOverlayShown());
  EXPECT_TRUE([coordinator_ isUICreated]);

  // When UIKit delivers a low-memory warning notification.
  DeliverMemoryWarningNotification();

  // Then the UI should not be destroyed.
  EXPECT_TRUE([coordinator_ isUICreated]);
}

// When the user consent have not been received yet, lens coordinator should
// present the consent view controller.
TEST_F(LensOverlayCoordinatorTest, ShouldPresentConsentDialog) {
  browser_state_->GetPrefs()->SetBoolean(prefs::kLensOverlayConditionsAccepted,
                                         false);

  // Given a started `LensOverlayCoordinator`.
  [coordinator_ start];

  // When the coordinator is asked to create and show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kOverflowMenu];

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop_.QuitClosure());
  run_loop_.Run();

  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^bool {
    return coordinator_.viewController.presentedViewController != nil;
  }));

  UIViewController* presentedVC =
      [coordinator_.viewController presentedViewController];

  EXPECT_TRUE(
      [presentedVC isKindOfClass:[LensOverlayConsentViewController class]]);
}

// When the user consent accepted TOS, lens coordinator shouldn't present the
// consent view controller.
TEST_F(LensOverlayCoordinatorTest, DoesntPromptForConsentWhenAlreadyReceived) {
  browser_state_->GetPrefs()->SetBoolean(prefs::kLensOverlayConditionsAccepted,
                                         true);

  // Given a started `LensOverlayCoordinator`.
  [coordinator_ start];

  // When the coordinator is asked to create and show the UI.
  [HandlerForProtocol(dispatcher_, LensOverlayCommands)
      createAndShowLensUI:NO
               entrypoint:LensOverlayEntrypoint::kOverflowMenu];

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop_.QuitClosure());
  run_loop_.Run();

  EXPECT_TRUE([coordinator_ isUICreated]);
  UIViewController* presentedVC =
      [coordinator_.viewController presentedViewController];

  EXPECT_FALSE(
      [presentedVC isKindOfClass:[LensOverlayConsentViewController class]]);
}

}  // namespace