blob: 8e90ab9b65ec9dfe5b77de5c6e62d4e3d6cdb3d6 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/ios/wait_util.h"
#include "base/bind.h"
#import "base/ios/ios_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/web/js_messaging/java_script_feature_manager.h"
#include "ios/web/public/js_messaging/java_script_feature_util.h"
#include "ios/web/public/js_messaging/script_message.h"
#import "ios/web/public/js_messaging/web_frame_util.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/test/web_view_content_test_util.h"
#import "ios/web/test/fakes/fake_java_script_feature.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
static NSString* kPageHTML =
@"<html><body>"
" <div id=\"div\">contents1</div><div id=\"div2\">contents2</div>"
"</body></html>";
namespace web {
// typedef WebTestWithWebState JavaScriptFeatureTest;
// Sets up a FakeJavaScriptFeature in the page content world.
class JavaScriptFeaturePageContentWorldTest : public WebTestWithWebState {
protected:
JavaScriptFeaturePageContentWorldTest()
: WebTestWithWebState(std::make_unique<web::FakeWebClient>()),
feature_(JavaScriptFeature::ContentWorld::kPageContentWorld) {}
void SetUp() override {
WebTestWithWebState::SetUp();
static_cast<web::FakeWebClient*>(WebTestWithWebState::GetWebClient())
->SetJavaScriptFeatures({feature()});
}
FakeJavaScriptFeature* feature() { return &feature_; }
private:
FakeJavaScriptFeature feature_;
};
// Tests that a JavaScriptFeature executes its injected JavaScript when
// configured in the page content world.
TEST_F(JavaScriptFeaturePageContentWorldTest,
JavaScriptFeatureInjectJavaScript) {
LoadHtml(kPageHTML);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents1"));
EXPECT_TRUE(test::WaitForWebViewContainingText(
web_state(), kFakeJavaScriptFeatureLoadedText));
}
// Tests that a JavaScriptFeature correctly calls JavaScript functions when
// configured in the page content world.
TEST_F(JavaScriptFeaturePageContentWorldTest,
JavaScriptFeatureExecuteJavaScript) {
LoadHtml(kPageHTML);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents1"));
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents2"));
feature()->ReplaceDivContents(GetMainFrame(web_state()));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "updated"));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents2"));
}
// Tests that a JavaScriptFeature receives post messages from JavaScript for
// registered names in the page content world.
TEST_F(JavaScriptFeaturePageContentWorldTest,
MessageHandlerInPageContentWorld) {
LoadHtml(kPageHTML);
ASSERT_FALSE(feature()->last_received_web_state());
ASSERT_FALSE(feature()->last_received_message());
std::vector<base::Value> parameters;
parameters.push_back(
base::Value(kFakeJavaScriptFeaturePostMessageReplyValue));
feature()->ReplyWithPostMessage(GetMainFrame(web_state()), parameters);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return feature()->last_received_web_state();
}));
EXPECT_EQ(web_state(), feature()->last_received_web_state());
ASSERT_TRUE(feature()->last_received_message()->body());
const std::string* reply =
feature()->last_received_message()->body()->GetIfString();
ASSERT_TRUE(reply);
EXPECT_STREQ(kFakeJavaScriptFeaturePostMessageReplyValue, reply->c_str());
}
// Tests that a JavaScriptFeature with
// ReinjectionBehavior::kReinjectOnDocumentRecreation re-injects JavaScript in
// the page content world.
TEST_F(JavaScriptFeaturePageContentWorldTest,
ReinjectionBehaviorPageContentWorld) {
LoadHtml(kPageHTML);
ASSERT_FALSE(feature()->last_received_web_state());
ASSERT_FALSE(feature()->last_received_message());
__block bool count_received = false;
feature()->GetErrorCount(GetMainFrame(web_state()),
base::BindOnce(^void(const base::Value* count) {
ASSERT_TRUE(count);
ASSERT_TRUE(count->is_double());
ASSERT_EQ(0ul, count->GetDouble());
count_received = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return count_received;
}));
ExecuteJavaScript(@"invalidFunction();");
count_received = false;
feature()->GetErrorCount(GetMainFrame(web_state()),
base::BindOnce(^void(const base::Value* count) {
ASSERT_TRUE(count);
ASSERT_TRUE(count->is_double());
ASSERT_EQ(1ul, count->GetDouble());
count_received = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return count_received;
}));
ASSERT_TRUE(ExecuteJavaScript(
@"document.open(); document.write('<p></p>'); document.close(); true;"));
ExecuteJavaScript(@"invalidFunction();");
count_received = false;
feature()->GetErrorCount(GetMainFrame(web_state()),
base::BindOnce(^void(const base::Value* count) {
ASSERT_TRUE(count);
ASSERT_TRUE(count->is_double());
EXPECT_EQ(2ul, count->GetDouble());
count_received = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return count_received;
}));
}
// Sets up a FakeJavaScriptFeature in an isolated world.
class JavaScriptFeatureAnyContentWorldTest : public WebTestWithWebState {
protected:
JavaScriptFeatureAnyContentWorldTest()
: WebTestWithWebState(std::make_unique<web::FakeWebClient>()),
feature_(JavaScriptFeature::ContentWorld::kAnyContentWorld) {}
void SetUp() override {
WebTestWithWebState::SetUp();
static_cast<web::FakeWebClient*>(WebTestWithWebState::GetWebClient())
->SetJavaScriptFeatures({feature()});
}
FakeJavaScriptFeature* feature() { return &feature_; }
private:
FakeJavaScriptFeature feature_;
};
// Tests that a JavaScriptFeature executes its injected JavaScript when
// configured in an isolated world.
TEST_F(JavaScriptFeatureAnyContentWorldTest,
JavaScriptFeatureInjectJavaScriptIsolatedWorld) {
LoadHtml(kPageHTML);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents1"));
EXPECT_TRUE(test::WaitForWebViewContainingText(
web_state(), kFakeJavaScriptFeatureLoadedText));
}
// Tests that a JavaScriptFeature correctly calls JavaScript functions when
// configured in an isolated world.
TEST_F(JavaScriptFeatureAnyContentWorldTest,
JavaScriptFeatureExecuteJavaScriptInIsolatedWorld) {
LoadHtml(kPageHTML);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents1"));
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents2"));
feature()->ReplaceDivContents(GetMainFrame(web_state()));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "updated"));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents2"));
}
// Tests that a JavaScriptFeature receives post messages from JavaScript for
// registered names in an isolated world.
TEST_F(JavaScriptFeatureAnyContentWorldTest, MessageHandlerInIsolatedWorld) {
LoadHtml(kPageHTML);
ASSERT_FALSE(feature()->last_received_web_state());
ASSERT_FALSE(feature()->last_received_message());
std::vector<base::Value> parameters;
parameters.push_back(
base::Value(kFakeJavaScriptFeaturePostMessageReplyValue));
feature()->ReplyWithPostMessage(GetMainFrame(web_state()), parameters);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return feature()->last_received_web_state();
}));
EXPECT_EQ(web_state(), feature()->last_received_web_state());
ASSERT_TRUE(feature()->last_received_message()->body());
const std::string* reply =
feature()->last_received_message()->body()->GetIfString();
ASSERT_TRUE(reply);
EXPECT_STREQ(kFakeJavaScriptFeaturePostMessageReplyValue, reply->c_str());
}
// Tests that a JavaScriptFeature with
// ReinjectionBehavior::kReinjectOnDocumentRecreation re-injects JavaScript in
// an isolated world.
TEST_F(JavaScriptFeatureAnyContentWorldTest, ReinjectionBehaviorIsolatedWorld) {
LoadHtml(kPageHTML);
ASSERT_FALSE(feature()->last_received_web_state());
ASSERT_FALSE(feature()->last_received_message());
__block bool count_received = false;
feature()->GetErrorCount(GetMainFrame(web_state()),
base::BindOnce(^void(const base::Value* count) {
ASSERT_TRUE(count);
ASSERT_TRUE(count->is_double());
ASSERT_EQ(0ul, count->GetDouble());
count_received = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return count_received;
}));
ExecuteJavaScript(@"invalidFunction();");
count_received = false;
feature()->GetErrorCount(GetMainFrame(web_state()),
base::BindOnce(^void(const base::Value* count) {
ASSERT_TRUE(count);
ASSERT_TRUE(count->is_double());
ASSERT_EQ(1ul, count->GetDouble());
count_received = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return count_received;
}));
ASSERT_TRUE(ExecuteJavaScript(
@"document.open(); document.write('<p></p>'); document.close(); true;"));
ExecuteJavaScript(@"invalidFunction();");
count_received = false;
feature()->GetErrorCount(GetMainFrame(web_state()),
base::BindOnce(^void(const base::Value* count) {
ASSERT_TRUE(count);
ASSERT_TRUE(count->is_double());
EXPECT_EQ(2ul, count->GetDouble());
count_received = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return count_received;
}));
}
// Sets up a FakeJavaScriptFeature in an isolated world using
// |ContentWorld::kIsolatedWorldOnly|.
class JavaScriptFeatureIsolatedWorldTest : public WebTestWithWebState {
protected:
JavaScriptFeatureIsolatedWorldTest()
: WebTestWithWebState(std::make_unique<web::FakeWebClient>()),
feature_(JavaScriptFeature::ContentWorld::kIsolatedWorldOnly) {}
void SetUp() override {
WebTestWithWebState::SetUp();
static_cast<web::FakeWebClient*>(WebTestWithWebState::GetWebClient())
->SetJavaScriptFeatures({feature()});
}
FakeJavaScriptFeature* feature() { return &feature_; }
private:
FakeJavaScriptFeature feature_;
};
// Tests that a JavaScriptFeature correctly calls JavaScript functions when
// configured in an isolated world only.
TEST_F(JavaScriptFeatureIsolatedWorldTest,
JavaScriptFeatureExecuteJavaScriptInIsolatedWorldOnly) {
// Using ContentWorld::kIsolatedWorldOnly on older versions of iOS will
// trigger a DCHECK, so return early.
if (!base::ios::IsRunningOnIOS14OrLater()) {
return;
}
LoadHtml(kPageHTML);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents1"));
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents2"));
feature()->ReplaceDivContents(GetMainFrame(web_state()));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "updated"));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "contents2"));
}
} // namespace web