chromium/ios/chrome/browser/ui/save_to_drive/save_to_drive_egtest.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 "base/strings/stringprintf.h"
#import "base/test/ios/wait_util.h"
#import "components/policy/policy_constants.h"
#import "ios/chrome/browser/account_picker/ui_bundled/account_picker_confirmation/account_picker_confirmation_screen_constants.h"
#import "ios/chrome/browser/account_picker/ui_bundled/account_picker_screen/account_picker_screen_constants.h"
#import "ios/chrome/browser/download/ui_bundled/download_manager_constants.h"
#import "ios/chrome/browser/drive/model/drive_policy.h"
#import "ios/chrome/browser/drive/model/test_constants.h"
#import "ios/chrome/browser/policy/model/policy_earl_grey_utils.h"
#import "ios/chrome/browser/policy/model/scoped_policy_list.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/ui/authentication/signin_earl_grey.h"
#import "ios/chrome/browser/ui/authentication/signin_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/ui/authentication/signin_matchers.h"
#import "ios/chrome/browser/ui/authentication/views/views_constants.h"
#import "ios/chrome/browser/ui/save_to_drive/file_destination_picker_constants.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/embedded_test_server_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/request_handler_util.h"

namespace {

id<GREYMatcher> IdentityButtonMatcherForIdentity(id<SystemIdentity> identity) {
  NSString* accessibility_label = [NSString
      stringWithFormat:@"%@, %@", identity.userFullName, identity.userEmail];
  return grey_allOf(grey_accessibilityID(kIdentityButtonControlIdentifier),
                    grey_accessibilityLabel(accessibility_label), nil);
}

// Matcher for "SAVE..." button on Download Manager UI, which is presented
// instead of the "DOWNLOAD" button when multiple destinations are available for
// downloads.
id<GREYMatcher> SaveEllipsisButton() {
  return grey_accessibilityID(
      kDownloadManagerSaveEllipsisAccessibilityIdentifier);
}

// Matcher for "DOWNLOAD" button when one destination is available for
// downloads.
id<GREYMatcher> DownloadButton() {
  return grey_accessibilityID(kDownloadManagerDownloadAccessibilityIdentifier);
}

// Matcher for "Files" destination button in File destination picker UI.
id<GREYMatcher> FileDestinationFilesButton() {
  return grey_allOf(
      grey_accessibilityID(kFileDestinationPickerFilesAccessibilityIdentifier),
      grey_interactable(), nil);
}

// Matcher for "Drive" destination button in File destination picker UI.
id<GREYMatcher> FileDestinationDriveButton() {
  return grey_allOf(
      grey_accessibilityID(kFileDestinationPickerDriveAccessibilityIdentifier),
      grey_interactable(), nil);
}

// Matcher for "GET THE APP" button on Download Manager UI.
id<GREYMatcher> DownloadManagerGetTheAppButton() {
  return grey_allOf(
      grey_accessibilityID(kDownloadManagerInstallAppAccessibilityIdentifier),
      grey_interactable(), nil);
}

// Matcher for "TRY AGAIN" button on Download Manager UI.
id<GREYMatcher> DownloadManagerTryAgainButton() {
  return grey_allOf(
      grey_accessibilityID(kDownloadManagerTryAgainAccessibilityIdentifier),
      grey_interactable(), nil);
}

// Matcher for the account picker.
id<GREYMatcher> AccountPicker() {
  return grey_allOf(
      grey_accessibilityID(
          kAccountPickerScreenNavigationControllerAccessibilityIdentifier),
      grey_sufficientlyVisible(), nil);
}

// Matcher for the "Save" button in the account picker.
id<GREYMatcher> AccountPickerPrimaryButton() {
  return grey_allOf(
      grey_accessibilityID(kAccountPickerPrimaryButtonAccessibilityIdentifier),
      grey_interactable(), nil);
}

// Provides downloads landing page with download link.
std::unique_ptr<net::test_server::HttpResponse> GetResponse(
    const net::test_server::HttpRequest& request) {
  auto result = std::make_unique<net::test_server::BasicHttpResponse>();
  result->set_code(net::HTTP_OK);
  result->set_content(
      "<a id='download' href='/download-example?50000'>Download</a>");
  return result;
}

}  // namespace

@interface SaveToDriveTestCase : ChromeTestCase
@end

@implementation SaveToDriveTestCase

- (void)setUp {
  [super setUp];

  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&net::test_server::HandlePrefixedRequest, "/",
                          base::BindRepeating(&GetResponse)));

  self.testServer->RegisterRequestHandler(base::BindRepeating(
      &net::test_server::HandlePrefixedRequest, "/download-example",
      base::BindRepeating(&testing::HandleDownload)));

  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
}

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration configuration;
  configuration.features_enabled.push_back(kIOSSaveToDrive);
  if ([self isRunningTest:@selector(testCanRetryDownloadToDrive)]) {
    const std::string commandLineSwitch =
        std::string(kTestDriveFileUploaderCommandLineSwitch);
    const std::string commandLineValue =
        std::string(kTestDriveFileUploaderCommandLineSwitchFailAndThenSucceed);
    configuration.additional_args.push_back(base::StringPrintf(
        "--%s=%s", commandLineSwitch.c_str(), commandLineValue.c_str()));
  }
  return configuration;
}

// Tests that when the user is signed-in, they can choose "Files" as destination
// for their download in the file destination picker, tap "Save" in the account
// picker. Tests that after a few seconds, the file has been downloaded
// successfully and a "OPEN IN..." button is displayed.
- (void)testCanDownloadToFiles {
  // Sign-in.
  FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
  // Load a page with a download button and tap the download button.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "Drive" button is presented and tap it.
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:SaveEllipsisButton()];
  [[EarlGrey selectElementWithMatcher:SaveEllipsisButton()]
      performAction:grey_tap()];
  // Wait for the account picker to appear, select "Files" and tap "Save".
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:AccountPicker()];
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:FileDestinationFilesButton()];
  [[EarlGrey selectElementWithMatcher:FileDestinationFilesButton()]
      performAction:grey_tap()];
  [[EarlGrey selectElementWithMatcher:AccountPickerPrimaryButton()]
      performAction:grey_tap()];
  // Wait for the account picker to disappear.
  [ChromeEarlGrey waitForUIElementToDisappearWithMatcher:AccountPicker()];
  // Check that after a few seconds, the "OPEN IN..." button appears.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:chrome_test_util::OpenInButton()
                                  timeout:base::test::ios::
                                              kWaitForDownloadTimeout];
}

// Tests that when the user is signed-in, they can choose "Drive" as destination
// for their download in the file destination picker, tap "Save" in the account
// picker. Tests that after a few seconds, the file has been downloaded
// successfully and a "GET THE APP" button is displayed.
- (void)testCanDownloadToDrive {
  // Sign-in.
  FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
  // Load a page with a download button and tap the download button.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "Drive" button is presented and tap it.
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:SaveEllipsisButton()];
  [[EarlGrey selectElementWithMatcher:SaveEllipsisButton()]
      performAction:grey_tap()];
  // Wait for the account picker to appear, select "Drive" and tap "Save".
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:AccountPicker()];
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:FileDestinationDriveButton()];
  [[EarlGrey selectElementWithMatcher:FileDestinationDriveButton()]
      performAction:grey_tap()];
  [[EarlGrey selectElementWithMatcher:AccountPickerPrimaryButton()]
      performAction:grey_tap()];
  // Wait for the account picker to disappear.
  [ChromeEarlGrey waitForUIElementToDisappearWithMatcher:AccountPicker()];
  // Check that after a few seconds, the "GET THE APP" button appears.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:DownloadManagerGetTheAppButton()
                                  timeout:base::test::ios::
                                              kWaitForDownloadTimeout];
}

// Tests that when the user is signed-in, they can choose Drive as destination
// for their download, tap "Save" in the account picker. Tests that after a few
// seconds, if the file upload fails, the user can tap "TRY AGAIN..." in the
// download manager, and after a few seconds, when the upload succeeds, an "GET
// THE APP" button is displayed.
- (void)testCanRetryDownloadToDrive {
  // Sign-in.
  FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
  // Load a page with a download button and tap the download button. The file
  // name of the file to download is set to
  // `kTestDriveFileUploaderTryAgainFileName` so that the upload fails during
  // the first attempt.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "Drive" button is presented and tap it.
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:SaveEllipsisButton()];
  [[EarlGrey selectElementWithMatcher:SaveEllipsisButton()]
      performAction:grey_tap()];
  // Wait for the account picker to appear, select "Drive" and tap "Save".
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:AccountPicker()];
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:FileDestinationDriveButton()];
  [[EarlGrey selectElementWithMatcher:FileDestinationDriveButton()]
      performAction:grey_tap()];
  [[EarlGrey selectElementWithMatcher:AccountPickerPrimaryButton()]
      performAction:grey_tap()];
  // Wait for the account picker to disappear.
  [ChromeEarlGrey waitForUIElementToDisappearWithMatcher:AccountPicker()];
  // Check that after a few seconds, when the uplaod fails, the "Try Again..."
  // button appears.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:DownloadManagerTryAgainButton()
                                  timeout:base::test::ios::
                                              kWaitForDownloadTimeout];
  [[EarlGrey selectElementWithMatcher:DownloadManagerTryAgainButton()]
      performAction:grey_tap()];
  // Check that after a few seconds, when the upload succeeds, the "GET THE APP"
  // button appears.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:DownloadManagerGetTheAppButton()
                                  timeout:base::test::ios::
                                              kWaitForDownloadTimeout];
}

// Tests that "DOWNLOAD" button is presented instead of "SAVE..." if signed-out.
- (void)testSignedOutDisablesSaveToDrive {
  // Load a page with a download button and tap the download button.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "DOWNLOAD" button is presented instead of "SAVE...".
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:DownloadButton()];
}

// Tests that "DOWNLOAD" button is presented instead of "SAVE..." in Incognito.
- (void)testIncognitoDisablesSaveToDrive {
  // Sign-in.
  FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
  // Switch to Incognito.
  [ChromeEarlGrey openNewIncognitoTab];
  // Load a page with a download button and tap the download button.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "DOWNLOAD" button is presented instead of "SAVE...".
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:DownloadButton()];
}

// Tests that "DOWNLOAD" button is presented instead of "SAVE..." when
// enterprise policy explicitly disables Save to Drive.
- (void)testPolicyDisablesSaveToDrive {
  // Temporary disable Save to Drive using policy.
  ScopedPolicyList disableSaveToDrive;
  disableSaveToDrive.SetPolicy(
      static_cast<int>(SaveToDrivePolicySettings::kDisabled),
      policy::key::kDownloadManagerSaveToDriveSettings);
  // Sign-in.
  FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
  // Load a page with a download button and tap the download button.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "DOWNLOAD" button is presented instead of "SAVE...".
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:DownloadButton()];
}

// Tests that when the user taps "Save" in the account selection, the selected
// account and file destination are memorized and presented as the default
// option for the next time.
- (void)testSaveToDriveMemorizesLastSelectedAccount {
  [ChromeEarlGrey clearUserPrefWithName:prefs::kIosSaveToDriveDefaultGaiaId];
  // Sign-in.
  FakeSystemIdentity* fakeIdentity1 = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity1];
  // Add a second fake identity to the device.
  FakeSystemIdentity* fakeIdentity2 = [FakeSystemIdentity fakeIdentity2];
  [SigninEarlGrey addFakeIdentity:fakeIdentity2];
  // Load a page with a download button and tap the download button.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/")];
  [ChromeEarlGrey waitForWebStateContainingText:"Download"];
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  // Check that the "SAVE..." button is presented and tap it.
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:SaveEllipsisButton()];
  [[EarlGrey selectElementWithMatcher:SaveEllipsisButton()]
      performAction:grey_tap()];
  // Check that the identity button is hidden.
  [[EarlGrey
      selectElementWithMatcher:IdentityButtonMatcherForIdentity(fakeIdentity1)]
      assertWithMatcher:grey_notVisible()];
  // Select "Drive" as destination.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:FileDestinationDriveButton()];
  [[EarlGrey selectElementWithMatcher:FileDestinationDriveButton()]
      performAction:grey_tap()];
  // Check that the selected identity is initially the signed-in identity.
  [[EarlGrey
      selectElementWithMatcher:IdentityButtonMatcherForIdentity(fakeIdentity1)]
      assertWithMatcher:grey_interactable()];
  // Tap the identity button and select the second account.
  [[EarlGrey
      selectElementWithMatcher:IdentityButtonMatcherForIdentity(fakeIdentity1)]
      performAction:grey_tap()];
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::IdentityCellMatcherForEmail(
                                   fakeIdentity2.userEmail)]
      performAction:grey_tap()];
  // Check that the second identity is now selected.
  [[EarlGrey
      selectElementWithMatcher:IdentityButtonMatcherForIdentity(fakeIdentity2)]
      assertWithMatcher:grey_interactable()];
  // Tap "Save".
  [[EarlGrey selectElementWithMatcher:AccountPickerPrimaryButton()]
      performAction:grey_tap()];
  // Wait for the account picker to disappear.
  [ChromeEarlGrey waitForUIElementToDisappearWithMatcher:AccountPicker()];
  // Check that after a few seconds, the "GET THE APP" button appears.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:DownloadManagerGetTheAppButton()
                                  timeout:base::test::ios::
                                              kWaitForDownloadTimeout];
  // Tap the download button and the "SAVE..." button again.
  [ChromeEarlGrey tapWebStateElementWithID:@"download"];
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:SaveEllipsisButton()];
  [[EarlGrey selectElementWithMatcher:SaveEllipsisButton()]
      performAction:grey_tap()];
  // Check that the second identity is now selected by default.
  [[EarlGrey
      selectElementWithMatcher:IdentityButtonMatcherForIdentity(fakeIdentity2)]
      assertWithMatcher:grey_interactable()];
}

@end