chromium/ios/chrome/browser/web_selection/model/web_selection_tab_helper_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_selection/model/web_selection_tab_helper.h"

#import <Foundation/Foundation.h>

#import "base/ios/ios_util.h"
#import "base/test/ios/wait_util.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/chrome_web_client.h"
#import "ios/chrome/browser/web_selection/model/web_selection_java_script_feature.h"
#import "ios/chrome/browser/web_selection/model/web_selection_java_script_feature_observer.h"
#import "ios/chrome/browser/web_selection/model/web_selection_response.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 "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

namespace {
NSString* kPageHTML =
    @"<html>"
     "  <body>"
     "    This text contains a <span id='selectid'>selection</span>."
     "    <iframe id='frame' srcdoc='"
     "      <html>"
     "        <body>"
     "          This frame contains another "
     "          <span id=\"frameselectid\">frame selection</span>."
     "        </body>"
     "      </html>"
     "    '/>"
     "  </body>"
     "</html>";

NSString* kPage2HTML =
    @"<html>"
     "  <body>"
     "    This text contains a <span id='selectid'>selection2</span>."
     "  </body>"
     "</html>";

// A test Web selection observer that will count the number of call from JS.
class TestWebSelectionJavaScriptFeatureObserver
    : public WebSelectionJavaScriptFeatureObserver {
 public:
  void OnSelectionRetrieved(web::WebState* web_state,
                            WebSelectionResponse* response) override {
    number_of_calls_++;
  }

  int GetNumberOfCalls() { return number_of_calls_; }

 private:
  int number_of_calls_ = 0;
};

}  // namespace

class WebSelectionTabHelperTest : public PlatformTest {
 public:
  WebSelectionTabHelperTest()
      : web_client_(std::make_unique<ChromeWebClient>()) {
    feature_list_.InitAndEnableFeature(kIOSEditMenuPartialTranslate);
    browser_state_ = TestChromeBrowserState::Builder().Build();

    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    WebSelectionTabHelper::CreateForWebState(web_state_.get());
    selection_observer_ =
        std::make_unique<TestWebSelectionJavaScriptFeatureObserver>();
  }

  void SetUp() override {
    PlatformTest::SetUp();
    WebSelectionJavaScriptFeature::GetInstance()->AddObserver(
        selection_observer_.get());
    web::test::LoadHtml(kPageHTML, web_state());
  }

  void TearDown() override {
    WebSelectionJavaScriptFeature::GetInstance()->RemoveObserver(
        selection_observer_.get());
    PlatformTest::TearDown();
  }

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

 protected:
  web::WebTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  web::ScopedTestingWebClient web_client_;
  base::test::ScopedFeatureList feature_list_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;
  std::unique_ptr<TestWebSelectionJavaScriptFeatureObserver>
      selection_observer_;
};

// Tests that no selection is returned if nothing is selected.
TEST_F(WebSelectionTabHelperTest, GetNoSelection) {
  if (!base::ios::IsRunningOnIOS16OrLater()) {
    // The tab helper is only created on iOS16+.
    return;
  }
  __block WebSelectionResponse* response = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response = block_response;
      }));

  task_environment_.AdvanceClock(base::Milliseconds(500));
  task_environment_.RunUntilIdle();
  EXPECT_FALSE(response);

  // As a call is already in progress, the second call should only have to wait
  // for 500 more ms to get the selection (1 second total).
  __block WebSelectionResponse* response2 = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response2 = block_response;
      }));
  task_environment_.AdvanceClock(base::Milliseconds(500));
  task_environment_.RunUntilIdle();

  // The 1st query waited twice 500ms, so the response should have been received
  // at this point.
  ASSERT_TRUE(response);
  EXPECT_FALSE(response.valid);
  EXPECT_NSEQ(nil, response.selectedText);
  EXPECT_TRUE(CGRectEqualToRect(response.sourceRect, CGRectZero));

  ASSERT_TRUE(response2);
  EXPECT_FALSE(response2.valid);
  EXPECT_NSEQ(nil, response2.selectedText);
  EXPECT_TRUE(CGRectEqualToRect(response2.sourceRect, CGRectZero));

  EXPECT_EQ(0, selection_observer_->GetNumberOfCalls());
}

// Tests that selection in main frame is returned correctly.
TEST_F(WebSelectionTabHelperTest, GetSelectionMainFrame) {
  if (!base::ios::IsRunningOnIOS16OrLater()) {
    // The tab helper is only created on iOS16+.
    return;
  }
  web::test::ExecuteJavaScript(@"window.getSelection().selectAllChildren("
                                "document.getElementById('selectid'));",
                               web_state());
  __block WebSelectionResponse* response = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response = block_response;
      }));

  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForJSCompletionTimeout, /*run_message_loop=*/true,
      ^{
        return response != nil;
      }));
  EXPECT_TRUE(response.valid);
  EXPECT_NSEQ(@"selection", response.selectedText);
  EXPECT_FALSE(CGRectEqualToRect(response.sourceRect, CGRectZero));
  EXPECT_EQ(1, selection_observer_->GetNumberOfCalls());
}

// Tests that selection in iframe is returned correctly.
TEST_F(WebSelectionTabHelperTest, GetSelectionIFrame) {
  if (!base::ios::IsRunningOnIOS16OrLater()) {
    // The tab helper is only created on iOS16+.
    return;
  }
  web::test::ExecuteJavaScript(
      @"subWindow = document.getElementById('frame').contentWindow;"
       "subWindow.document.getSelection().selectAllChildren("
       "  subWindow.document.getElementById('frameselectid'));",
      web_state());
  __block WebSelectionResponse* response = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response = block_response;
      }));

  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForJSCompletionTimeout, /*run_message_loop=*/true,
      ^{
        return response != nil;
      }));
  EXPECT_TRUE(response.valid);
  EXPECT_NSEQ(@"frame selection", response.selectedText);
  EXPECT_FALSE(CGRectEqualToRect(response.sourceRect, CGRectZero));
  EXPECT_EQ(1, selection_observer_->GetNumberOfCalls());
}

// Tests that selection is passed to the correct tab helper.
// Also tests that getting twice the selection on the same webState does not
// trigger additional JS calls.
TEST_F(WebSelectionTabHelperTest, GetMultipleWebStateSelections) {
  if (!base::ios::IsRunningOnIOS16OrLater()) {
    // The tab helper is only created on iOS16+.
    return;
  }
  web::WebState::CreateParams params(browser_state_.get());
  auto web_state2 = web::WebState::Create(params);
  WebSelectionTabHelper::CreateForWebState(web_state2.get());
  web::test::LoadHtml(kPage2HTML, web_state2.get());

  web::test::ExecuteJavaScript(@"window.getSelection().selectAllChildren("
                                "document.getElementById('selectid'));",
                               web_state());
  web::test::ExecuteJavaScript(@"window.getSelection().selectAllChildren("
                                "document.getElementById('selectid'));",
                               web_state2.get());

  __block WebSelectionResponse* response = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response = block_response;
      }));

  __block WebSelectionResponse* response2 = nil;
  WebSelectionTabHelper::FromWebState(web_state2.get())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response2 = block_response;
      }));

  // No additional call to selection_observer should be done for this selection
  // retrieval.
  __block WebSelectionResponse* response3 = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response3 = block_response;
      }));

  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForJSCompletionTimeout, /*run_message_loop=*/true,
      ^{
        return response != nil && response2 != nil && response3 != nil;
      }));

  EXPECT_TRUE(response.valid);
  EXPECT_NSEQ(@"selection", response.selectedText);
  EXPECT_FALSE(CGRectEqualToRect(response.sourceRect, CGRectZero));

  EXPECT_TRUE(response2.valid);
  EXPECT_NSEQ(@"selection2", response2.selectedText);
  EXPECT_FALSE(CGRectEqualToRect(response2.sourceRect, CGRectZero));

  EXPECT_TRUE(response3.valid);
  EXPECT_NSEQ(@"selection", response3.selectedText);
  EXPECT_FALSE(CGRectEqualToRect(response3.sourceRect, CGRectZero));

  EXPECT_EQ(response, response3);

  EXPECT_EQ(2, selection_observer_->GetNumberOfCalls());

  __block WebSelectionResponse* response4 = nil;
  WebSelectionTabHelper::FromWebState(web_state())
      ->GetSelectedText(base::BindOnce(^(WebSelectionResponse* block_response) {
        response4 = block_response;
      }));

  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForJSCompletionTimeout, /*run_message_loop=*/true,
      ^{
        return response4 != nil;
      }));

  EXPECT_TRUE(response4.valid);
  EXPECT_NSEQ(@"selection", response4.selectedText);
  EXPECT_FALSE(CGRectEqualToRect(response4.sourceRect, CGRectZero));
  EXPECT_EQ(3, selection_observer_->GetNumberOfCalls());
}