// 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_java_script_feature.h"
#import <Foundation/Foundation.h>
#import "base/ios/ios_util.h"
#import "base/memory/raw_ptr.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_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>";
}
class TestWebSelectionJavaScriptFeatureObserver
: public WebSelectionJavaScriptFeatureObserver {
public:
TestWebSelectionJavaScriptFeatureObserver(web::WebState* web_state)
: web_state_(web_state) {}
void OnSelectionRetrieved(web::WebState* web_state,
WebSelectionResponse* response) override {
EXPECT_EQ(web_state, web_state_);
response_ = response;
}
WebSelectionResponse* GetLastResponse() { return response_; }
private:
raw_ptr<web::WebState> web_state_;
WebSelectionResponse* response_;
};
// Tests for the WebSelectionJavaScriptFeature.
class WebSelectionJavaScriptFeatureTest : public PlatformTest {
public:
WebSelectionJavaScriptFeatureTest()
: 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);
selection_observer_ =
std::make_unique<TestWebSelectionJavaScriptFeatureObserver>(
web_state_.get());
}
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(WebSelectionJavaScriptFeatureTest, GetNoSelection) {
if (!base::ios::IsRunningOnIOS16OrLater()) {
// Script is only injected on iOS16+.
return;
}
WebSelectionJavaScriptFeature::GetInstance()->GetSelectedText(web_state());
task_environment_.AdvanceClock(base::Seconds(1));
task_environment_.RunUntilIdle();
WebSelectionResponse* response = selection_observer_->GetLastResponse();
// There is no selection, so the observer should not be called.
ASSERT_FALSE(response);
}
// Tests that selection in main frame is returned correctly.
TEST_F(WebSelectionJavaScriptFeatureTest, GetSelectionMainFrame) {
if (!base::ios::IsRunningOnIOS16OrLater()) {
// Script is only injected on iOS16+.
return;
}
web::test::ExecuteJavaScript(@"window.getSelection().selectAllChildren("
"document.getElementById('selectid'));",
web_state());
__block WebSelectionResponse* response = nil;
WebSelectionJavaScriptFeature::GetInstance()->GetSelectedText(web_state());
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForJSCompletionTimeout, ^{
response = selection_observer_->GetLastResponse();
return response != nil;
}));
EXPECT_TRUE(response.valid);
EXPECT_NSEQ(@"selection", response.selectedText);
EXPECT_FALSE(CGRectEqualToRect(response.sourceRect, CGRectZero));
}
// Tests that selection in iframe is returned correctly.
TEST_F(WebSelectionJavaScriptFeatureTest, GetSelectionIFrame) {
if (!base::ios::IsRunningOnIOS16OrLater()) {
// Script is only injected 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;
WebSelectionJavaScriptFeature::GetInstance()->GetSelectedText(web_state());
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForJSCompletionTimeout, ^{
response = selection_observer_->GetLastResponse();
return response != nil;
}));
EXPECT_TRUE(response.valid);
EXPECT_NSEQ(@"frame selection", response.selectedText);
EXPECT_FALSE(CGRectEqualToRect(response.sourceRect, CGRectZero));
}