chromium/ios/chrome/browser/web/model/choose_file/choose_file_java_script_feature_unittest.mm

// Copyright 2023 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/web/model/choose_file/choose_file_java_script_feature.h"

#import "base/strings/sys_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/web/model/choose_file/choose_file_event.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "ios/web/public/web_state.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

namespace {
const char kPageHtml[] = "<html><body>"
                         "<input type=\"file\" id=\"choose_file\" "
                         "ACCEPT_PLACEHOLDER MULTIPLE_PLACEHOLDER/>"
                         "</body></html>";

const char kPageHtmlWithButton[] = "<html><body>"
                                   "<input type=\"button\" id=\"button\"/>"
                                   "</body></html>";
}  // namespace

// Tests metrics are logged when tapping choose file input.
class ChooseFileJavaScriptFeatureTest : public PlatformTest {
 protected:
  ChooseFileJavaScriptFeatureTest()
      : web_client_(std::make_unique<web::FakeWebClient>()) {}

  void SetUp() override {
    PlatformTest::SetUp();

    browser_state_ = TestChromeBrowserState::Builder().Build();

    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    web_state_->GetView();
    web_state_->SetKeepRenderProcessAlive(true);

    ChooseFileJavaScriptFeature* feature =
        ChooseFileJavaScriptFeature::GetInstance();
    GetWebClient()->SetJavaScriptFeatures({feature});
  }

  web::FakeWebClient* GetWebClient() {
    return static_cast<web::FakeWebClient*>(web_client_.Get());
  }

  web::WebState* web_state() { return web_state_.get(); }

  void LoadHtml(bool has_multiple, bool has_accept, NSString* accept_value) {
    NSString* html = base::SysUTF8ToNSString(kPageHtml);
    if (has_multiple) {
      html = [html stringByReplacingOccurrencesOfString:@"MULTIPLE_PLACEHOLDER"
                                             withString:@"multiple"];
    } else {
      html = [html stringByReplacingOccurrencesOfString:@"MULTIPLE_PLACEHOLDER"
                                             withString:@""];
    }
    if (has_accept) {
      html = [html
          stringByReplacingOccurrencesOfString:@"ACCEPT_PLACEHOLDER"
                                    withString:[NSString stringWithFormat:
                                                             @"accept=\"%@\"",
                                                             accept_value]];

    } else {
      html = [html stringByReplacingOccurrencesOfString:@"ACCEPT_PLACEHOLDER"
                                             withString:@""];
    }
    web::test::LoadHtml(html, web_state());
  }

  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;
};

// Tests that metrics are logged with correct bucket when tapping on an input
// file. One case for each bucket.
TEST_F(ChooseFileJavaScriptFeatureTest, TestMetricsLogging) {
  // See definitions of ChooseFileAccept enum in
  // choose_file_javascript_feature.mm for the order and values.
  NSArray* accept_attributes = @[
    @"", @".pdf,.jpg", @".invalid", @".jpg", @".avi", @".mp3", @".zip", @".pdf",
    @".doc", @".pkpass"
  ];
  NSArray* multiple_attributes = @[ @NO, @YES ];

  for (int accept_index = 0; accept_index < 9; accept_index++) {
    for (int multiple_index = 0; multiple_index < 2; multiple_index++) {
      base::HistogramTester histogram_tester;
      LoadHtml([multiple_attributes[multiple_index] boolValue], true,
               accept_attributes[accept_index]);
      ASSERT_TRUE(
          web::test::TapWebViewElementWithId(web_state(), "choose_file"));
      histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);
      histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                         2 * accept_index + multiple_index, 1);
    }
  }
}

// Extra test cases for metrics logging that test corner cases.
TEST_F(ChooseFileJavaScriptFeatureTest, TestMetricsLoggingExtra) {
  base::HistogramTester histogram_tester;
  int total_count = 0;
  // No accept, no multiple
  LoadHtml(false, false, @"");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kNoAccept*/ 0, 1);

  // No accept, multiple
  LoadHtml(true, false, @"");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kNoAcceptMultiple*/ 1, 1);

  // Multiple empty
  LoadHtml(false, true, @",,,  ,  ,");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kNoAccept*/ 0, 2);

  // Image extension with a dot
  LoadHtml(false, true, @".png");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kImageAccept*/ 6, 1);

  // Image extension without a dot
  LoadHtml(false, true, @"jpg");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kImageAccept*/ 6, 2);

  // Image mime
  LoadHtml(false, true, @"image/png");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kImageAccept*/ 6, 3);

  // Generic Image mime
  LoadHtml(false, true, @"image/*");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kImageAccept*/ 6, 4);

  // Two Image types
  LoadHtml(false, true, @"png, jpg");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kImageAccept*/ 6, 5);

  // Video with spaces
  LoadHtml(false, true, @"  .mp4  ");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kVideoAccept*/ 8, 1);

  // Audio without dot
  LoadHtml(false, true, @"mp3");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kAudioAccept*/ 10, 1);

  // Archive
  LoadHtml(false, true, @"zip , rar ");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kArchiveAccept*/ 12, 1);

  // Unknown and image
  LoadHtml(false, true, @"unknown, jpg");
  total_count++;
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "choose_file"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", total_count);
  histogram_tester.ExpectBucketCount("IOS.Web.FileInput.Clicked",
                                     /*kImageAccept*/ 6, 6);
}

// Tests that no metrics are logged when tapping on a normal button.
TEST_F(ChooseFileJavaScriptFeatureTest, TestNoMetricsLoggingOnButtonClick) {
  base::HistogramTester histogram_tester;
  web::test::LoadHtml(base::SysUTF8ToNSString(kPageHtmlWithButton),
                      web_state());
  ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "button"));
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 0);
}

// Tests that no metrics are logged when tapping on a normal button.
TEST_F(ChooseFileJavaScriptFeatureTest, TestInvalidPayload) {
  base::HistogramTester histogram_tester;
  web::test::LoadHtml(base::SysUTF8ToNSString(kPageHtmlWithButton),
                      web_state());
  // Test synchronisation
  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', "
      @"{'acceptType':0,'hasMultiple':true});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);

  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', {});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);

  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', "
      @"{'acceptType':-2, 'hasMultiple':true});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);

  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', "
      @"{'acceptType':37, 'hasMultiple':true});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);

  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', "
      @"{'acceptType':'invalid', 'hasMultiple':true});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);

  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', "
      @"{'missing':'invalid', 'hasMultiple':true});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 1);

  // Test synchronisation
  web::test::ExecuteJavaScriptForFeature(
      web_state_.get(),
      @"__gCrWeb.common.sendWebKitMessage('ChooseFileHandler', "
      @"{'acceptType':0,'hasMultiple':true});",
      ChooseFileJavaScriptFeature::GetInstance());
  histogram_tester.ExpectTotalCount("IOS.Web.FileInput.Clicked", 2);
}

// Tests that `ResetLastChooseFileEvent()` returns the expected file extensions
// and resets the last event.
TEST_F(ChooseFileJavaScriptFeatureTest,
       TestResetLastChooseFileEventFileExtensions) {
  base::test::ScopedFeatureList feature_list(kIOSChooseFromDrive);
  const std::map<std::string, std::vector<std::string>>
      accept_attributes_file_extensions = {
          {"", {}},
          {".jpg", {".jpg"}},
          {".pdf,.jpg", {".pdf", ".jpg"}},
          {".avi", {".avi"}},
          {".zip, .rar", {".zip", ".rar"}},
          {".zip, .rar, .jpg", {".zip", ".rar", ".jpg"}},
          {"video/mp4", {}},
          {"video/mp4, .jpg", {".jpg"}},
          {"video/mp4, .pdf, .jpg", {".pdf", ".jpg"}},
          {"audio/*", {}},
          {"application/zip", {}},
          {".jpg,,,", {".jpg"}},
          {".b,. b,.\",/(,.),./,.;,.=,.?,.[],.{}", {".b"}},
      };
  constexpr bool multiple_attributes[] = {false, true};

  for (const auto& [accept_attribute, expected_file_extensions] :
       accept_attributes_file_extensions) {
    for (const auto& multiple_attribute : multiple_attributes) {
      const bool has_accept = accept_attribute.empty() == false;
      LoadHtml(multiple_attribute, has_accept, @(accept_attribute.c_str()));
      ASSERT_TRUE(
          web::test::TapWebViewElementWithId(web_state(), "choose_file"));
      const std::optional<ChooseFileEvent> event =
          ChooseFileJavaScriptFeature::GetInstance()
              ->ResetLastChooseFileEvent();
      ASSERT_TRUE(event.has_value());
      EXPECT_EQ(expected_file_extensions, event->accept_file_extensions);
      EXPECT_EQ(multiple_attribute, event->allow_multiple_files);
      EXPECT_FALSE(ChooseFileJavaScriptFeature::GetInstance()
                       ->ResetLastChooseFileEvent());
    }
  }
}

// Tests that `ResetLastChooseFileEvent()` returns the expected MIME types and
// resets the last event.
TEST_F(ChooseFileJavaScriptFeatureTest, TestResetLastChooseFileEventMimeTypes) {
  base::test::ScopedFeatureList feature_list(kIOSChooseFromDrive);
  const std::map<std::string, std::vector<std::string>>
      accept_attributes_mime_types = {
          {"", {}},
          {".jpg", {}},
          {".pdf,.jpg", {}},
          {".avi", {}},
          {".zip, .rar", {}},
          {".zip, .rar, .jpg", {}},
          {"video/mp4", {"video/mp4"}},
          {"video/mp4, .jpg", {"video/mp4"}},
          {"video/mp4, .pdf, .jpg", {"video/mp4"}},
          {"audio/*", {"audio/*"}},
          {"application/zip", {"application/zip"}},
          {"application/zip,  image/*", {"application/zip", "image/*"}},
          {"application/zip, .jpg, image/*", {"application/zip", "image/*"}},
          {"application/application/", {}},
          {".jpg,,,", {}},
          {"a/b,a/ b,a/\",a/(,a/),a//,a/;,a/=,a/?,a/[],a/{}", {"a/b"}},
      };
  constexpr bool multiple_attributes[] = {false, true};

  for (const auto& [accept_attribute, expected_mime_types] :
       accept_attributes_mime_types) {
    for (const auto& multiple_attribute : multiple_attributes) {
      const bool has_accept = accept_attribute.empty() == false;
      LoadHtml(multiple_attribute, has_accept, @(accept_attribute.c_str()));
      ASSERT_TRUE(
          web::test::TapWebViewElementWithId(web_state(), "choose_file"));
      const std::optional<ChooseFileEvent> event =
          ChooseFileJavaScriptFeature::GetInstance()
              ->ResetLastChooseFileEvent();
      ASSERT_TRUE(event.has_value());
      EXPECT_EQ(expected_mime_types, event->accept_mime_types);
      EXPECT_EQ(multiple_attribute, event->allow_multiple_files);
      EXPECT_FALSE(ChooseFileJavaScriptFeature::GetInstance()
                       ->ResetLastChooseFileEvent());
    }
  }
}