blob: c4408320f3b4258db166b4669810182f73865198 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <memory>
#import <string>
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/run_loop.h"
#import "base/task/single_thread_task_runner.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "ios/web/grit/ios_web_resources.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/test/navigation_test_util.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/webui/web_ui_ios_controller.h"
#import "ios/web/public/webui/web_ui_ios_controller_factory.h"
#import "ios/web/public/webui/web_ui_ios_data_source.h"
#import "ios/web/test/grit/test_resources.h"
#import "ios/web/test/mojo_test.mojom.h"
#import "ios/web/test/test_url_constants.h"
#import "ios/web/test/web_int_test.h"
#import "mojo/public/cpp/bindings/pending_remote.h"
#import "mojo/public/cpp/bindings/receiver_set.h"
#import "mojo/public/cpp/bindings/remote.h"
#import "url/gurl.h"
#import "url/scheme_host_port.h"
namespace web {
namespace {
// Hostname for test WebUI page.
const char kTestWebUIURLHost[] = "testwebui";
// Timeout to wait for a successful message exchange between native code and
// a web page using Mojo.
constexpr base::TimeDelta kMessageTimeout = base::Seconds(5);
// UI handler class which communicates with test WebUI page as follows:
// - page sends "syn" message to `TestUIHandler`
// - `TestUIHandler` replies with "ack" message
// - page replies back with "fin"
//
// Once "fin" is received `IsFinReceived()` call will return true, indicating
// that communication was successful. See test WebUI page code here:
// ios/web/test/data/mojo_test.js
class TestUIHandler : public mojom::TestUIHandlerMojo {
public:
TestUIHandler() {}
~TestUIHandler() override {}
// Returns true if "fin" has been received.
bool IsFinReceived() { return fin_received_; }
// TestUIHandlerMojo overrides.
void SetClientPage(mojo::PendingRemote<mojom::TestPage> page) override {
page_.Bind(std::move(page));
}
void HandleJsMessage(const std::string& message) override {
if (message == "syn") {
// Received "syn" message from WebUI page, send "ack" as reply.
DCHECK(!syn_received_);
DCHECK(!fin_received_);
syn_received_ = true;
mojom::NativeMessageResultMojoPtr result(
mojom::NativeMessageResultMojo::New());
result->message = "ack";
page_->HandleNativeMessage(std::move(result));
} else if (message == "fin") {
// Received "fin" from the WebUI page in response to "ack".
DCHECK(syn_received_);
DCHECK(!fin_received_);
fin_received_ = true;
} else {
NOTREACHED();
}
}
void BindTestHandler(
mojo::PendingReceiver<mojom::TestUIHandlerMojo> receiver) {
receivers_.Add(this, std::move(receiver));
}
private:
mojo::ReceiverSet<mojom::TestUIHandlerMojo> receivers_;
mojo::Remote<mojom::TestPage> page_;
// `true` if "syn" has been received.
bool syn_received_ = false;
// `true` if "fin" has been received.
bool fin_received_ = false;
};
// Controller for test WebUI.
class TestUI : public WebUIIOSController {
public:
// Constructs controller from `web_ui` and `ui_handler` which will communicate
// with test WebUI page.
TestUI(WebUIIOS* web_ui, const std::string& host, TestUIHandler* ui_handler)
: WebUIIOSController(web_ui, host) {
web::WebUIIOSDataSource* source =
web::WebUIIOSDataSource::Create(kTestWebUIURLHost);
source->AddResourcePath("mojo_test.js", IDR_MOJO_TEST_JS);
source->AddResourcePath("mojo_bindings.js", IDR_IOS_MOJO_BINDINGS_JS);
source->AddResourcePath("mojo_test.mojom.js", IDR_MOJO_TEST_MOJO_JS);
source->SetDefaultResource(IDR_MOJO_TEST_HTML);
web::WebState* web_state = web_ui->GetWebState();
web::WebUIIOSDataSource::Add(web_state->GetBrowserState(), source);
web_state->GetInterfaceBinderForMainFrame()->AddInterface(
base::BindRepeating(&TestUIHandler::BindTestHandler,
base::Unretained(ui_handler)));
}
~TestUI() override = default;
};
// Factory that creates TestUI controller.
class TestWebUIControllerFactory : public WebUIIOSControllerFactory {
public:
// Constructs a controller factory which will eventually create `ui_handler`.
explicit TestWebUIControllerFactory(TestUIHandler* ui_handler)
: ui_handler_(ui_handler) {}
// WebUIIOSControllerFactory overrides.
std::unique_ptr<WebUIIOSController> CreateWebUIIOSControllerForURL(
WebUIIOS* web_ui,
const GURL& url) const override {
if (!url.SchemeIs(kTestWebUIScheme))
return nullptr;
DCHECK_EQ(url.host(), kTestWebUIURLHost);
return std::make_unique<TestUI>(web_ui, url.host(), ui_handler_);
}
NSInteger GetErrorCodeForWebUIURL(const GURL& url) const override {
if (url.SchemeIs(kTestWebUIScheme))
return 0;
return NSURLErrorUnsupportedURL;
}
private:
// UI handler class which communicates with test WebUI page.
raw_ptr<TestUIHandler> ui_handler_;
};
} // namespace
// A test fixture for verifying mojo communication for WebUI.
class WebUIMojoTest : public WebIntTest {
protected:
void SetUp() override {
WebIntTest::SetUp();
@autoreleasepool {
ui_handler_ = std::make_unique<TestUIHandler>();
WebState::CreateParams params(GetBrowserState());
web_state_ = WebState::Create(params);
factory_ =
std::make_unique<TestWebUIControllerFactory>(ui_handler_.get());
WebUIIOSControllerFactory::RegisterFactory(factory_.get());
}
}
void TearDown() override {
@autoreleasepool {
// WebState owns CRWWebUIManager. When WebState is destroyed,
// CRWWebUIManager is autoreleased and will be destroyed upon autorelease
// pool purge. However in this test, WebTest destructor is called before
// PlatformTest, thus CRWWebUIManager outlives the WebTaskenvironment.
// However, CRWWebUIManager owns a URLFetcherImpl, which DCHECKs that its
// destructor is called on UI web thread. Hence, URLFetcherImpl has to
// outlive the WebTaskenvironment, since [NSThread mainThread] will not be
// WebThread::UI once WebTaskenvironment is destroyed.
web_state_.reset();
ui_handler_.reset();
WebUIIOSControllerFactory::DeregisterFactory(factory_.get());
}
WebIntTest::TearDown();
}
// Returns WebState which loads test WebUI page.
WebState* web_state() { return web_state_.get(); }
// Returns UI handler which communicates with WebUI page.
TestUIHandler* test_ui_handler() { return ui_handler_.get(); }
private:
std::unique_ptr<WebState> web_state_;
std::unique_ptr<TestUIHandler> ui_handler_;
std::unique_ptr<TestWebUIControllerFactory> factory_;
};
// Tests that JS can send messages to the native code and vice versa.
// TestUIHandler is used for communication and test succeeds only when
// `TestUIHandler` successfully receives "ack" message from WebUI page.
TEST_F(WebUIMojoTest, MessageExchange) {
@autoreleasepool {
url::SchemeHostPort tuple(kTestWebUIScheme, kTestWebUIURLHost, 0);
GURL url(tuple.Serialize());
test::LoadUrl(web_state(), url);
// LoadIfNecessary is needed because the view is not created (but needed)
// when loading the page. TODO(crbug.com/41309809): Remove this call.
web_state()->GetNavigationManager()->LoadIfNecessary();
// Wait until `TestUIHandler` receives "fin" message from WebUI page.
bool fin_received =
base::test::ios::WaitUntilConditionOrTimeout(kMessageTimeout, ^{
// Flush any pending tasks. Don't RunUntilIdle() because
// RunUntilIdle() is incompatible with mojo::SimpleWatcher's
// automatic arming behavior, which Mojo JS still depends upon.
//
// TODO(crbug.com/41307566): Introduce the full watcher API to JS and
// get rid of this hack.
base::RunLoop loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, loop.QuitClosure());
loop.Run();
return test_ui_handler()->IsFinReceived();
});
ASSERT_TRUE(fin_received);
EXPECT_FALSE(web_state()->IsLoading());
EXPECT_EQ(url, web_state()->GetLastCommittedURL());
}
}
} // namespace web