diff --git a/BUILD.gn b/BUILD.gn index 24256a6c6d9..3616f43a6f8 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -80,6 +80,7 @@ if (!is_ios) { } else { brave_all_unit_tests_deps += [ "//brave/ios/testing:ios_brave_unit_tests", + "//brave/ios/web:ios_brave_web_inttests", "//brave/ios/web:ios_brave_web_unittests", ] } diff --git a/chromium_src/ios/web/web_state/web_state_impl.h b/chromium_src/ios/web/web_state/web_state_impl.h new file mode 100644 index 00000000000..8c63f34e495 --- /dev/null +++ b/chromium_src/ios/web/web_state/web_state_impl.h @@ -0,0 +1,24 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_WEB_STATE_WEB_STATE_IMPL_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_WEB_STATE_WEB_STATE_IMPL_H_ + +namespace web { +class WebUIIOS; +} + +// Exposes an API to obtain the main frame WebUI which is required for some +// Brave WebUI implementations +#define ClearWebUI \ + ClearWebUI(); \ + web::WebUIIOS* GetMainFrameWebUI(); \ + size_t GetWebUICountForTesting + +#include // IWYU pragma: export + +#undef ClearWebUI + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_WEB_STATE_WEB_STATE_IMPL_H_ diff --git a/chromium_src/ios/web/web_state/web_state_impl.mm b/chromium_src/ios/web/web_state/web_state_impl.mm new file mode 100644 index 00000000000..5a94282e0f6 --- /dev/null +++ b/chromium_src/ios/web/web_state/web_state_impl.mm @@ -0,0 +1,20 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include + +namespace web { + +WebUIIOS* WebStateImpl::GetMainFrameWebUI() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return RealizedState()->GetMainFrameWebUI(); +} + +size_t WebStateImpl::GetWebUICountForTesting() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return RealizedState()->GetWebUICountForTesting(); // IN-TEST +} + +} // namespace web diff --git a/chromium_src/ios/web/web_state/web_state_impl_realized_web_state.h b/chromium_src/ios/web/web_state/web_state_impl_realized_web_state.h new file mode 100644 index 00000000000..b0bd6d45194 --- /dev/null +++ b/chromium_src/ios/web/web_state/web_state_impl_realized_web_state.h @@ -0,0 +1,41 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_IOS_WEB_WEB_STATE_WEB_STATE_IMPL_REALIZED_WEB_STATE_H_ +#define BRAVE_CHROMIUM_SRC_IOS_WEB_WEB_STATE_WEB_STATE_IMPL_REALIZED_WEB_STATE_H_ + +#include + +#include "ios/web/web_state/web_state_impl.h" + +// The goal of this override is to introduce a new map of WebUIIOS so that +// WebStateImpl can handle WebUI in multiple frames in one page. It introduces +// a set of replacements for the WebUI method implementations to use said map +// +// The reason we need to allow multiple WebUI's per frame is because some of our +// WebUI applications such as AI Chat use subframes to host chrome-untrusted +// frames +#define web_ui_ \ + web_ui_; \ + std::map, std::less<>> web_uis_; \ + void TearDown_ChromiumImpl(); \ + void CreateWebUI_ChromiumImpl(const GURL& url); \ + void ClearWebUI_ChromiumImpl(); \ + bool HasWebUI_ChromiumImpl() const; \ + void HandleWebUIMessage_ChromiumImpl(const GURL&, std::string_view, \ + const base::Value::List&) +// Exposes an API to obtain the main frame WebUI which is required for some +// Brave WebUI implementations +#define ClearWebUI \ + ClearWebUI(); \ + web::WebUIIOS* GetMainFrameWebUI(); \ + size_t GetWebUICountForTesting + +#include // IWYU pragma: export + +#undef ClearWebUI +#undef web_ui_ + +#endif // BRAVE_CHROMIUM_SRC_IOS_WEB_WEB_STATE_WEB_STATE_IMPL_REALIZED_WEB_STATE_H_ diff --git a/chromium_src/ios/web/web_state/web_state_impl_realized_web_state.mm b/chromium_src/ios/web/web_state/web_state_impl_realized_web_state.mm new file mode 100644 index 00000000000..4ba12b8d831 --- /dev/null +++ b/chromium_src/ios/web/web_state/web_state_impl_realized_web_state.mm @@ -0,0 +1,85 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ios/web/web_state/web_state_impl_realized_web_state.h" + +// Support for replacing the following methods below +#define ClearWebUI ClearWebUI_ChromiumImpl +#define CreateWebUI(url) CreateWebUI_ChromiumImpl(url) +#define HandleWebUIMessage HandleWebUIMessage_ChromiumImpl +#define HasWebUI HasWebUI_ChromiumImpl +#define TearDown TearDown_ChromiumImpl + +#include + +#undef TearDown +#undef HasWebUI +#undef HandleWebUIMessage +#undef CreateWebUI +#undef ClearWebUI + +namespace web { + +// These new implementations mimic their non-Brave counterparts in +// web_state_impl_realized_web_state.mm but store and fetch from the newly added +// map instead of the `web_ui_` field +// +// See web_state_impl_realized_web_state.h for reasons why we are replacing +// these implementations + +void WebStateImpl::RealizedWebState::TearDown() { + // Call the original ClearWebUI which would have been replaced with + // ClearWebUI_ChromiumImpl + ClearWebUI(); + TearDown_ChromiumImpl(); +} + +void WebStateImpl::RealizedWebState::CreateWebUI(const GURL& url) { + const std::string_view host = url.host_piece(); + if (web_uis_.contains(host)) { + // Don't recreate WebUI for the same host. At the moment this is a required + // limitation as we don't have the neccessary info to associate a WebUI + // with a specific frame like desktop/android does. + return; + } + auto web_ui = CreateWebUIIOS(url); + if (web_ui) { + web_uis_.insert({std::string(host), std::move(web_ui)}); + } +} + +void WebStateImpl::RealizedWebState::ClearWebUI() { + web_uis_.clear(); +} + +void WebStateImpl::RealizedWebState::HandleWebUIMessage( + const GURL& source_url, + std::string_view message, + const base::Value::List& args) { + const std::string_view host = source_url.host_piece(); + auto web_ui = web_uis_.find(host); + if (web_ui != web_uis_.end() && web_ui->second) { + web_ui->second->ProcessWebUIIOSMessage(source_url, message, args); + } +} + +bool WebStateImpl::RealizedWebState::HasWebUI() const { + return !web_uis_.empty(); +} + +// Implements the new method we expose to get the main frame's WebUI +WebUIIOS* WebStateImpl::RealizedWebState::GetMainFrameWebUI() { + if (web_uis_.empty()) { + return nullptr; + } + // The first WebUI created should always be the main frame + return web_uis_.begin()->second.get(); +} + +size_t WebStateImpl::RealizedWebState::GetWebUICountForTesting() { + return web_uis_.size(); +} + +} // namespace web diff --git a/ios/web/BUILD.gn b/ios/web/BUILD.gn index 6e754a02bb4..a4df0fa7ec7 100644 --- a/ios/web/BUILD.gn +++ b/ios/web/BUILD.gn @@ -14,3 +14,13 @@ test("ios_brave_web_unittests") { "//brave/ios/web/webui:ios_web_webui_unittests", ] } + +test("ios_brave_web_inttests") { + testonly = true + deps = [ + "//ios/web:run_all_unittests", + + # Add individual test source_set targets here. + "//brave/ios/web/webui:ios_web_webui_inttests", + ] +} diff --git a/ios/web/test/BUILD.gn b/ios/web/test/BUILD.gn new file mode 100644 index 00000000000..b1d77bf2745 --- /dev/null +++ b/ios/web/test/BUILD.gn @@ -0,0 +1,23 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//brave/resources/brave_grit.gni") +import("//tools/grit/repack.gni") + +repack("packed_resources") { + testonly = true + sources = [ "$root_gen_dir/brave/ios/web/test/test_resources.pak" ] + deps = [ ":resources" ] + output = "$target_gen_dir/resources.pak" + copy_data_to_bundle = true +} + +brave_grit("resources") { + source = "test_resources.grd" + outputs = [ + "grit/test_resources.h", + "test_resources.pak", + ] +} diff --git a/ios/web/test/data/webui_test.html b/ios/web/test/data/webui_test.html new file mode 100644 index 00000000000..a82be680d10 --- /dev/null +++ b/ios/web/test/data/webui_test.html @@ -0,0 +1,7 @@ + + + + WebUI page + + + diff --git a/ios/web/test/data/webui_test_2.html b/ios/web/test/data/webui_test_2.html new file mode 100644 index 00000000000..9db3fab7516 --- /dev/null +++ b/ios/web/test/data/webui_test_2.html @@ -0,0 +1,6 @@ + + + + WebUI 2 page + + diff --git a/ios/web/test/test_resources.grd b/ios/web/test/test_resources.grd new file mode 100644 index 00000000000..0a688f08b6b --- /dev/null +++ b/ios/web/test/test_resources.grd @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ios/web/webui/BUILD.gn b/ios/web/webui/BUILD.gn index 34a9c19cb4e..890eab726de 100644 --- a/ios/web/webui/BUILD.gn +++ b/ios/web/webui/BUILD.gn @@ -48,3 +48,23 @@ source_set("ios_web_webui_unittests") { "//testing/gtest", ] } + +source_set("ios_web_webui_inttests") { + testonly = true + sources = [ "web_ui_inttest.mm" ] + deps = [ + "//base", + "//base/test:test_support", + "//brave/ios/web/test:packed_resources", + "//brave/ios/web/test:resources", + "//brave/ios/web/webui", + "//ios/web/public/test", + "//ios/web/public/test:test_fixture", + "//ios/web/public/test:util", + "//ios/web/public/test/fakes", + "//ios/web/public/webui", + "//ios/web/test:test_constants", + "//ios/web/web_state:web_state_impl_header", + "//url", + ] +} diff --git a/ios/web/webui/DEPS b/ios/web/webui/DEPS index aa27a66ad48..05394df0cd3 100644 --- a/ios/web/webui/DEPS +++ b/ios/web/webui/DEPS @@ -6,5 +6,7 @@ include_rules = [ "+ios/chrome/browser/shared/model/url", "+ios/components/webui", "+ios/web/webui", + "+ios/web/web_state", + "+ios/web/test", "+services/network/public/mojom", ] diff --git a/ios/web/webui/web_ui_inttest.mm b/ios/web/webui/web_ui_inttest.mm new file mode 100644 index 00000000000..500b9e4dd26 --- /dev/null +++ b/ios/web/webui/web_ui_inttest.mm @@ -0,0 +1,149 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include + +#include "base/functional/bind.h" +#include "base/run_loop.h" +#include "base/test/ios/wait_util.h" +#include "brave/ios/web/test/grit/test_resources.h" +#include "brave/ios/web/webui/brave_web_ui_ios_data_source.h" +#include "ios/web/public/navigation/navigation_manager.h" +#include "ios/web/public/test/navigation_test_util.h" +#include "ios/web/public/test/web_test_with_web_state.h" +#include "ios/web/public/test/web_view_content_test_util.h" +#include "ios/web/public/test/web_view_interaction_test_util.h" +#include "ios/web/public/webui/web_ui_ios_controller.h" +#include "ios/web/public/webui/web_ui_ios_controller_factory.h" +#include "ios/web/public/webui/web_ui_ios_data_source.h" +#include "ios/web/test/test_url_constants.h" +#include "ios/web/web_state/web_state_impl.h" +#include "third_party/abseil-cpp/absl/strings/str_format.h" +#include "url/gurl.h" +#include "url/scheme_host_port.h" + +using base::test::ios::kWaitForPageLoadTimeout; +using base::test::ios::WaitUntilConditionOrTimeout; +using web::test::WaitForWebViewContainingText; +using web::test::WaitForWebViewContainingTextInFrame; + +namespace web { + +namespace { + +// Hostname for test WebUI page. +const char kTestWebUIURLHost[] = "testwebui"; +const char kTestWebUIURLHost2[] = "testwebui2"; + +// Text present on the sample WebUI page. +const char kWebUIPageText[] = "WebUI page"; +const char kWebUIPage2Text[] = "WebUI 2 page"; + +// 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, int resource_id) + : WebUIIOSController(web_ui, host) { + // Need to use a BraveWebUIIOSDataSource instead of standard + // WebUIIOSDataSource because we the test html has an iframe pointing to + // WebUI and so we need to override the CSP + BraveWebUIIOSDataSource* source = BraveWebUIIOSDataSource::Create(host); + + source->SetDefaultResource(resource_id); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FrameSrc, + absl::StrFormat("frame-src %s://%s", kTestWebUIScheme, + kTestWebUIURLHost2)); + + web::WebState* web_state = web_ui->GetWebState(); + web::WebUIIOSDataSource::Add(web_state->GetBrowserState(), source); + } + + ~TestUI() override = default; +}; + +// Factory that creates TestUI controller. +class TestWebUIControllerFactory : public WebUIIOSControllerFactory { + public: + // Constructs a controller factory. + TestWebUIControllerFactory() {} + + // WebUIIOSControllerFactory overrides. + std::unique_ptr CreateWebUIIOSControllerForURL( + WebUIIOS* web_ui, + const GURL& url) const override { + if (!url.SchemeIs(kTestWebUIScheme)) { + return nullptr; + } + if (url.host() == kTestWebUIURLHost) { + return std::make_unique(web_ui, url.host(), IDR_WEBUI_TEST_HTML); + } + DCHECK_EQ(url.host(), kTestWebUIURLHost2); + return std::make_unique(web_ui, url.host(), IDR_WEBUI_TEST_HTML_2); + } + + NSInteger GetErrorCodeForWebUIURL(const GURL& url) const override { + if (url.SchemeIs(kTestWebUIScheme)) { + return 0; + } + return NSURLErrorUnsupportedURL; + } +}; +} // namespace + +// A test fixture for verifying WebUI. This test fixture is copied from +// //ios/web/webui/web_ui_inttest.mm +class WebUITest : public WebTestWithWebState { + protected: + WebUITest() : WebTestWithWebState() {} + + void SetUp() override { + WebTestWithWebState::SetUp(); + factory_ = std::make_unique(); + WebUIIOSControllerFactory::RegisterFactory(factory_.get()); + + 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(); + + ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{ + base::RunLoop().RunUntilIdle(); + return !web_state()->IsLoading(); + })); + + ASSERT_EQ(url.spec(), BaseUrl()); + } + + void TearDown() override { + WebUIIOSControllerFactory::DeregisterFactory(factory_.get()); + WebTestWithWebState::TearDown(); + } + + private: + std::unique_ptr factory_; +}; + +// Tests that a both the main web UI page and its child frame also containing +// WebUI both load correctly and that WebState is holding onto both of the +// WebUIIOS references +TEST_F(WebUITest, LoadWebUIPageWithWebUIChildFrame) { + auto* web_state_impl = WebStateImpl::FromWebState(web_state()); + ASSERT_TRUE(web_state_impl->HasWebUI()); + EXPECT_TRUE(WaitForWebViewContainingText(web_state(), kWebUIPageText)); + ASSERT_TRUE(web_state_impl->GetMainFrameWebUI() != nullptr); + EXPECT_TRUE( + WaitForWebViewContainingTextInFrame(web_state(), kWebUIPage2Text)); + size_t expected_web_ui_count = 2; + ASSERT_EQ(web_state_impl->GetWebUICountForTesting(), expected_web_ui_count); +} + +} // namespace web diff --git a/resources/resource_ids.spec b/resources/resource_ids.spec index eb51c7e6efa..8a82c41b827 100644 --- a/resources/resource_ids.spec +++ b/resources/resource_ids.spec @@ -218,6 +218,10 @@ "META": {"sizes": {"includes": [25]}}, "includes": [30580], }, + "brave/ios/web/test/test_resources.grd": { + "META": {"sizes": {"includes": [10]}}, + "includes": [30600], + }, # WARNING: The upstream ChromeOS/Ash strings currently run through 36930. We # must be careful not to exceed that maximum when adding new strings here. }