| // 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. |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/controlled_frame/controlled_frame_test_base.h" |
| #include "chrome/browser/extensions/browsertest_util.h" |
| #include "chrome/browser/extensions/menu_manager.h" |
| #include "chrome/browser/extensions/service_worker_apitest.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h" |
| #include "chrome/common/chrome_features.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/web_transport_simple_test_server.h" |
| #include "extensions/browser/api/web_request/extension_web_request_event_router.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/guest_view/web_view/web_view_guest.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/spawned_test_server/spawned_test_server.h" |
| #include "net/test/test_data_directory.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace controlled_frame { |
| |
| namespace { |
| |
| constexpr char kWebRequestOnBeforeRequestEventName[] = |
| "webViewInternal.onBeforeRequest"; |
| constexpr char kWebRequestOnAuthRequiredEventName[] = |
| "webViewInternal.onAuthRequired"; |
| constexpr char kEvalSuccessStr[] = "SUCCESS"; |
| |
| const extensions::MenuItem::Id CreateMenuItemId( |
| const extensions::MenuItem::ExtensionKey& extension_key, |
| const std::string& string_uid) { |
| extensions::MenuItem::Id id; |
| id.extension_key = extension_key; |
| id.string_uid = string_uid; |
| return id; |
| } |
| |
| const content::EvalJsResult CreateContextMenuItem( |
| content::RenderFrameHost* app_frame, |
| const std::string& id, |
| const std::string& title) { |
| return content::EvalJs(app_frame, content::JsReplace(R"( |
| new Promise((resolve, reject) => { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.contextMenus || !frame.contextMenus.create) { |
| reject('FAIL: frame, frame.contextMenus, or ' + |
| 'frame.contextMenus.create is undefined'); |
| return; |
| } |
| frame.contextMenus.create( |
| { title: $2, id: $1 }, |
| () => { resolve('SUCCESS'); }); |
| }); |
| )", |
| id, title)); |
| } |
| |
| const content::EvalJsResult UpdateContextMenuItemTitle( |
| content::RenderFrameHost* app_frame, |
| const std::string& id, |
| const std::string& new_title) { |
| return content::EvalJs(app_frame, content::JsReplace(R"( |
| new Promise((resolve, reject) => { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.contextMenus || !frame.contextMenus.update) { |
| reject('FAIL: frame, frame.contextMenus, or ' + |
| 'frame.contextMenus.update is undefined'); |
| return; |
| } |
| |
| frame.contextMenus.update( |
| /*id=*/$1, |
| { title: $2 }, |
| () => { resolve('SUCCESS'); }); |
| }); |
| )", |
| id, new_title)); |
| } |
| |
| const content::EvalJsResult RemoveContextMenuItem( |
| content::RenderFrameHost* app_frame, |
| const std::string& id) { |
| return content::EvalJs(app_frame, content::JsReplace(R"( |
| new Promise((resolve, reject) => { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.contextMenus || !frame.contextMenus.remove) { |
| reject('FAIL: frame, frame.contextMenus, or ' + |
| 'frame.contextMenus.remove is undefined'); |
| return; |
| } |
| |
| frame.contextMenus.remove( |
| /*id=*/$1, |
| () => { resolve('SUCCESS'); }); |
| }); |
| )", |
| id)); |
| } |
| |
| const content::EvalJsResult RemoveAllContextMenuItems( |
| content::RenderFrameHost* app_frame) { |
| return content::EvalJs(app_frame, R"( |
| new Promise((resolve, reject) => { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.contextMenus || !frame.contextMenus.removeAll) { |
| reject('FAIL: frame, frame.contextMenus, or ' + |
| 'frame.contextMenus.removeAll is undefined'); |
| return; |
| } |
| |
| frame.contextMenus.removeAll(() => { resolve('SUCCESS'); }); |
| }); |
| )"); |
| } |
| |
| const content::EvalJsResult SetBackgroundColorToWhite( |
| extensions::WebViewGuest* guest) { |
| return content::EvalJs(guest->GetGuestMainFrame(), R"( |
| (function() { |
| document.body.style.backgroundColor = 'white'; |
| return 'SUCCESS'; |
| })(); |
| )"); |
| } |
| |
| const content::EvalJsResult ExecuteScriptRedBackgroundCode( |
| content::RenderFrameHost* app_frame) { |
| return content::EvalJs(app_frame, R"( |
| new Promise((resolve, reject) => { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| reject('FAIL'); |
| return; |
| } |
| frame.executeScript( |
| {code: "document.body.style.backgroundColor = 'red';"}, |
| () => { resolve('SUCCESS') }); |
| }); |
| )"); |
| } |
| |
| const content::EvalJsResult ExecuteScriptRedBackgroundFile( |
| content::RenderFrameHost* app_frame) { |
| return content::EvalJs(app_frame, R"( |
| new Promise((resolve, reject) => { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| reject('FAIL'); |
| return; |
| } |
| frame.executeScript( |
| {file: "/execute_script.input.js"}, |
| () => { resolve('SUCCESS') }); |
| }); |
| )"); |
| } |
| |
| const content::EvalJsResult VerifyBackgroundColorIsRed( |
| extensions::WebViewGuest* guest) { |
| return content::EvalJs(guest->GetGuestMainFrame(), R"( |
| (function() { |
| if (document.body.style.backgroundColor === 'red') { |
| return 'SUCCESS'; |
| } else { |
| return 'FAIL'; |
| } |
| })(); |
| )"); |
| } |
| |
| // TODO(odejesush): Add tests for the rest of the Promise API methods. |
| const char* kControlledFramePromiseApiMethods[]{"back", "forward", "go"}; |
| |
| } // namespace |
| |
| class ControlledFrameApiTest : public ControlledFrameTestBase { |
| protected: |
| ControlledFrameApiTest() |
| : ControlledFrameTestBase( |
| /*channel=*/version_info::Channel::STABLE, |
| /*feature_setting=*/FeatureSetting::ENABLED, |
| /*flag_setting=*/FlagSetting::CONTROLLED_FRAME) {} |
| |
| ControlledFrameApiTest(const version_info::Channel& channel, |
| const FeatureSetting& feature_setting, |
| const FlagSetting& flag_setting) |
| : ControlledFrameTestBase(channel, feature_setting, flag_setting) {} |
| |
| public: |
| void SetUpOnMainThread() override { |
| ControlledFrameTestBase::SetUpOnMainThread(); |
| StartContentServer("web_apps/simple_isolated_app"); |
| } |
| |
| void ExpectMenuItemWithIdAndTitle( |
| const extensions::MenuItem::ExtensionKey& extension_key, |
| const std::string& expected_id, |
| const std::string& expected_title) { |
| auto* menu_manager = extensions::MenuManager::Get(profile()); |
| extensions::MenuItem* menu_item = |
| menu_manager->GetItemById(CreateMenuItemId(extension_key, expected_id)); |
| |
| ASSERT_TRUE(menu_item); |
| EXPECT_EQ(expected_title, menu_item->title()); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ContextMenusCreate) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame); |
| auto* menu_manager = extensions::MenuManager::Get(profile()); |
| |
| const extensions::MenuItem::ExtensionKey extension_key( |
| /*extension_id=*/"", web_view_guest->owner_rfh()->GetProcess()->GetID(), |
| web_view_guest->owner_rfh()->GetRoutingID(), |
| web_view_guest->view_instance_id()); |
| EXPECT_EQ(0u, menu_manager->MenuItemsSize(extension_key)); |
| |
| static constexpr std::string kItem1ID = "1"; |
| static constexpr std::string kItem1Title = "Test"; |
| EXPECT_EQ(kEvalSuccessStr, |
| CreateContextMenuItem(app_frame, kItem1ID, kItem1Title)); |
| ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key)); |
| ExpectMenuItemWithIdAndTitle(extension_key, kItem1ID, kItem1Title); |
| |
| static constexpr std::string kItem2ID = "2"; |
| static constexpr std::string kItem2Title = "Test2"; |
| EXPECT_EQ(kEvalSuccessStr, |
| CreateContextMenuItem(app_frame, kItem2ID, kItem2Title)); |
| ASSERT_EQ(2u, menu_manager->MenuItemsSize(extension_key)); |
| ExpectMenuItemWithIdAndTitle(extension_key, kItem2ID, kItem2Title); |
| |
| static constexpr std::string kItem3ID = "3"; |
| static constexpr std::string kItem3Title = "Test3"; |
| EXPECT_EQ(kEvalSuccessStr, |
| CreateContextMenuItem(app_frame, kItem3ID, kItem3Title)); |
| ASSERT_EQ(3u, menu_manager->MenuItemsSize(extension_key)); |
| ExpectMenuItemWithIdAndTitle(extension_key, kItem3ID, kItem3Title); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ContextMenusUpdate) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame); |
| auto* menu_manager = extensions::MenuManager::Get(profile()); |
| |
| static constexpr std::string kItem1ID = "1"; |
| static constexpr std::string kItem1Title = "Test"; |
| EXPECT_EQ(kEvalSuccessStr, |
| CreateContextMenuItem(app_frame, kItem1ID, kItem1Title)); |
| |
| const extensions::MenuItem::ExtensionKey extension_key( |
| /*extension_id=*/"", web_view_guest->owner_rfh()->GetProcess()->GetID(), |
| web_view_guest->owner_rfh()->GetRoutingID(), |
| web_view_guest->view_instance_id()); |
| ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key)); |
| ExpectMenuItemWithIdAndTitle(extension_key, kItem1ID, kItem1Title); |
| |
| static constexpr std::string kItem1NewTitle = "Test1"; |
| EXPECT_EQ(kEvalSuccessStr, |
| UpdateContextMenuItemTitle(app_frame, kItem1ID, kItem1NewTitle)); |
| |
| ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key)); |
| ExpectMenuItemWithIdAndTitle(extension_key, kItem1ID, kItem1NewTitle); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ContextMenusRemove) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame); |
| auto* menu_manager = extensions::MenuManager::Get(profile()); |
| |
| static constexpr std::string kItem1ID = "1"; |
| static constexpr std::string kItem1Title = "Test1"; |
| EXPECT_EQ(kEvalSuccessStr, |
| CreateContextMenuItem(app_frame, kItem1ID, kItem1Title)); |
| EXPECT_EQ(kEvalSuccessStr, CreateContextMenuItem(app_frame, /*id=*/"2", |
| /*title=*/"Test2")); |
| |
| EXPECT_EQ(kEvalSuccessStr, RemoveContextMenuItem(app_frame, kItem1ID)); |
| |
| const extensions::MenuItem::ExtensionKey extension_key( |
| /*extension_id=*/"", web_view_guest->owner_rfh()->GetProcess()->GetID(), |
| web_view_guest->owner_rfh()->GetRoutingID(), |
| web_view_guest->view_instance_id()); |
| ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key)); |
| |
| extensions::MenuItem* deleted_item = |
| menu_manager->GetItemById(CreateMenuItemId(extension_key, kItem1ID)); |
| EXPECT_FALSE(deleted_item); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ContextMenusRemoveAll) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame); |
| auto* menu_manager = extensions::MenuManager::Get(profile()); |
| |
| EXPECT_EQ(kEvalSuccessStr, CreateContextMenuItem(app_frame, /*id=*/"1", |
| /*title=*/"Test1")); |
| EXPECT_EQ(kEvalSuccessStr, CreateContextMenuItem(app_frame, /*id=*/"2", |
| /*title=*/"Test2")); |
| |
| EXPECT_EQ(kEvalSuccessStr, RemoveAllContextMenuItems(app_frame)); |
| |
| const extensions::MenuItem::ExtensionKey extension_key( |
| /*extension_id=*/"", web_view_guest->owner_rfh()->GetProcess()->GetID(), |
| web_view_guest->owner_rfh()->GetRoutingID(), |
| web_view_guest->view_instance_id()); |
| ASSERT_EQ(0u, menu_manager->MenuItemsSize(extension_key)); |
| } |
| |
| // This test checks if the Controlled Frame is able to intercept URL navigation |
| // requests. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, URLLoaderIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const GURL& kOriginalControlledFrameUrl = |
| embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, kOriginalControlledFrameUrl)); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| const std::string& kServerHostPort = |
| embedded_https_test_server().host_port_pair().ToString(); |
| EXPECT_EQ("SUCCESS", |
| content::EvalJs(app_frame, content::JsReplace(R"( |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return 'FAIL: frame or frame.request is undefined'; |
| } |
| frame.request.onBeforeRequest.addListener(() => { |
| return { cancel: true }; |
| }, { urls: ['https://*/controlled_frame_cancel.html'] }, ['blocking']); |
| frame.request.onBeforeRequest.addListener(() => { |
| return { cancel: false }; |
| }, { urls: ['https://*/controlled_frame_success.html'] }, ['blocking']); |
| frame.request.onBeforeRequest.addListener(() => { |
| return { |
| redirectUrl: 'https://' + $1 + '/controlled_frame_redirect_target.html' |
| }; |
| }, { urls: ['https://*/controlled_frame_redirect.html'] }, ['blocking']); |
| return 'SUCCESS'; |
| })(); |
| )", |
| kServerHostPort))); |
| EXPECT_EQ(3u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| content::WebContents* guest_web_contents = web_view_guest->web_contents(); |
| |
| // Check that navigations can be cancelled. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, net::Error::ERR_BLOCKED_BY_CLIENT, |
| content::MessageLoopRunner::QuitMode::IMMEDIATE, |
| /*ignore_uncommitted_navigations=*/false); |
| web_view_guest->NavigateGuest(embedded_https_test_server() |
| .GetURL("/controlled_frame_cancel.html") |
| .spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(net::Error::ERR_BLOCKED_BY_CLIENT, |
| navigation_observer.last_net_error_code()); |
| EXPECT_EQ(kOriginalControlledFrameUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_FALSE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Check that navigations can be redirected. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(embedded_https_test_server() |
| .GetURL("/controlled_frame_redirect.html") |
| .spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(embedded_https_test_server().GetURL( |
| "/controlled_frame_redirect_target.html"), |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Check that navigations can succeed. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, /*expected_number_of_navigations=*/1u); |
| const GURL& kControlledFrameSuccessUrl = |
| embedded_https_test_server().GetURL("/controlled_frame_success.html"); |
| web_view_guest->NavigateGuest(kControlledFrameSuccessUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kControlledFrameSuccessUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, AuthRequestIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnAuthRequiredEventName)); |
| |
| EXPECT_EQ(true, content::EvalJs(app_frame, R"( |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return false; |
| } |
| |
| const expectedUsername = 'test'; |
| const expectedPassword = 'pass'; |
| frame.request.onAuthRequired.addListener(() => { |
| return { |
| authCredentials: { |
| username: expectedUsername, |
| password: expectedPassword |
| } |
| }; |
| }, { urls: [`https://*/auth-basic*`] }, ['blocking']); |
| return true; |
| })(); |
| )")); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnAuthRequiredEventName)); |
| |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| content::WebContents* guest_web_contents = web_view_guest->web_contents(); |
| |
| // Check that the injecting the credentials through WebRequest produces a |
| // successful navigation. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| const GURL& kAuthBasicUrl = |
| embedded_https_test_server().GetURL("/auth-basic?password=pass"); |
| web_view_guest->NavigateGuest(kAuthBasicUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kAuthBasicUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Check that the injecting the wrong credentials through WebRequest produces |
| // an error. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| const GURL& kAuthBasicUrl = |
| embedded_https_test_server().GetURL("/auth-basic?password=badpass"); |
| web_view_guest->NavigateGuest(kAuthBasicUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kAuthBasicUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| // The auth request fails but keeps retrying until this error is produced. |
| // TODO(crbug.com/40942953): The error produced here should be |
| // authentication related. |
| EXPECT_EQ(net::Error::ERR_TOO_MANY_RETRIES, |
| navigation_observer.last_net_error_code()); |
| EXPECT_FALSE(navigation_observer.last_navigation_succeeded()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ExecuteScript) { |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder()) |
| .AddHtml("/execute_script.input.js", |
| "document.body.style.backgroundColor = 'red';") |
| .BuildBundle(); |
| app->TrustSigningKey(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| |
| // Verify that executeScript() using JS code can change the background color. |
| EXPECT_EQ(kEvalSuccessStr, SetBackgroundColorToWhite(web_view_guest)); |
| EXPECT_EQ(kEvalSuccessStr, ExecuteScriptRedBackgroundCode(app_frame)); |
| EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); |
| |
| // Verify that executeScript() using a JS file changes the background color. |
| EXPECT_EQ(kEvalSuccessStr, SetBackgroundColorToWhite(web_view_guest)); |
| EXPECT_EQ(kEvalSuccessStr, ExecuteScriptRedBackgroundFile(app_frame)); |
| EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInDataIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL https_url = embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, https_url)); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const src = '<!DOCTYPE html><p>data: URL</p>'; |
| const url = `data:text/html;base64,${btoa(src)}`; |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = url; |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 1); |
| ASSERT_NE(iframe, nullptr); |
| |
| ASSERT_FALSE(CreateControlledFrame(iframe, https_url)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInSandboxedIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL https_url = embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, https_url)); |
| |
| ASSERT_TRUE( |
| ExecJs(app_frame, content::JsReplace(R"( |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = $1; |
| f.sandbox = 'allow-scripts'; // for EvalJs |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )", |
| url_info.origin().Serialize()))); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 1); |
| ASSERT_NE(iframe, nullptr); |
| |
| ASSERT_FALSE(CreateControlledFrame(iframe, https_url)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInSrcdocIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const noopPolicy = trustedTypes.createPolicy("policy", { |
| createHTML: (string) => string, |
| }); |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.srcdoc = noopPolicy.createHTML('<!DOCTYPE html><p>srcdoc iframe</p>'); |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 0); |
| ASSERT_NE(iframe, nullptr); |
| |
| // Despite srcdoc iframes being same-origin, creating the <controlledframe> |
| // fails because AvailabilityCheck looks at the frame's scheme as well as |
| // its isolation level. No other IsolatedContext API does this, but it makes |
| // sense for <controlledframe> because it's not a purely JS-based API that |
| // will be blocked through CSP. |
| ASSERT_FALSE(CreateControlledFrame( |
| iframe, embedded_https_test_server().GetURL("/index.html"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInBlobIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const blob = new Blob(['<!DOCTYPE html><p>blob html page</p>'], { |
| type: 'text/html' |
| }); |
| const url = URL.createObjectURL(blob); |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = url; |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 0); |
| ASSERT_NE(iframe, nullptr); |
| |
| // As with srcdoc iframes, is blocked due to AvailabilityCheck verifying |
| // the frame's scheme as well as its isolation level. |
| ASSERT_FALSE(CreateControlledFrame( |
| iframe, embedded_https_test_server().GetURL("/index.html"))); |
| } |
| |
| class ControlledFrameWebSocketApiTest : public ControlledFrameApiTest { |
| public: |
| void SetUpOnMainThread() override { |
| ControlledFrameApiTest::SetUpOnMainThread(); |
| websocket_test_server_ = std::make_unique<net::SpawnedTestServer>( |
| net::SpawnedTestServer::TYPE_WS, net::GetWebSocketTestDataDirectory()); |
| ASSERT_TRUE(websocket_test_server_->Start()); |
| } |
| |
| net::SpawnedTestServer* websocket_test_server() { |
| return websocket_test_server_.get(); |
| } |
| |
| GURL GetWebSocketUrl(const std::string& path) { |
| GURL::Replacements replacements; |
| replacements.SetSchemeStr("ws"); |
| return websocket_test_server_->GetURL(path).ReplaceComponents(replacements); |
| } |
| |
| private: |
| std::unique_ptr<net::SpawnedTestServer> websocket_test_server_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameWebSocketApiTest, WebSocketIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const GURL& kOriginalControlledFrameUrl = |
| embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, kOriginalControlledFrameUrl)); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| // Use Web Sockets before installing a WebRequest event listener to verify |
| // that it works inside of the Controlled Frame. |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| content::WebContents* guest_web_contents = web_view_guest->web_contents(); |
| GURL::Replacements http_scheme_replacement; |
| http_scheme_replacement.SetSchemeStr("http"); |
| const GURL& kWebSocketConnectCheckUrl = |
| websocket_test_server() |
| ->GetURL("/connect_check.html") |
| .ReplaceComponents(http_scheme_replacement); |
| { |
| content::TitleWatcher title_watcher(guest_web_contents, u"PASS"); |
| title_watcher.AlsoWaitForTitle(u"FAIL"); |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(kWebSocketConnectCheckUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kWebSocketConnectCheckUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(u"PASS", title_watcher.WaitAndGetTitle()); |
| } |
| |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(kOriginalControlledFrameUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kOriginalControlledFrameUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Set up a WebRequest event listener that cancels any requests to the Web |
| // Socket server. |
| EXPECT_EQ(true, content::EvalJs(app_frame, |
| R"( |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return false; |
| } |
| frame.request.onBeforeRequest.addListener(() => { |
| return { cancel: true }; |
| }, { urls: ['ws://*/*'] }, ['blocking']); |
| return true; |
| })(); |
| )")); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| { |
| content::TitleWatcher title_watcher(guest_web_contents, u"PASS"); |
| title_watcher.AlsoWaitForTitle(u"FAIL"); |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(kWebSocketConnectCheckUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kWebSocketConnectCheckUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(u"FAIL", title_watcher.WaitAndGetTitle()); |
| } |
| } |
| |
| class ControlledFrameWebTransportApiTest : public ControlledFrameApiTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ControlledFrameApiTest::SetUpCommandLine(command_line); |
| webtransport_server_.SetUpCommandLine(command_line); |
| webtransport_server_.Start(); |
| } |
| |
| content::WebTransportSimpleTestServer& webtransport_server() { |
| return webtransport_server_; |
| } |
| |
| protected: |
| content::WebTransportSimpleTestServer webtransport_server_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameWebTransportApiTest, |
| WebTransportIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| // Use WebTransport before installing a WebRequest event listener to verify |
| // that it works inside of the Controlled Frame. |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| EXPECT_EQ(true, content::EvalJs( |
| web_view_guest->GetGuestMainFrame(), |
| content::JsReplace( |
| R"( |
| (async function() { |
| const url = 'https://localhost:' + $1 + '/echo_test'; |
| try { |
| const transport = new WebTransport(url); |
| await transport.ready; |
| } catch (e) { |
| console.log(url + ': ' + e.name + ': ' + e.message); |
| return false; |
| } |
| return true; |
| })(); |
| )", |
| webtransport_server().server_address().port()))); |
| |
| // Set up a WebRequest event listener that cancels any requests to the |
| // WebTransport server. |
| EXPECT_EQ(true, content::EvalJs(app_frame, |
| R"( |
| let cancelRequest = false; |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return false; |
| } |
| const onBeforeRequestHandler = |
| frame.request.onBeforeRequest.addListener(() => { |
| return { cancel: true }; |
| }, { urls: ['https://localhost/*'] }, ['blocking']); |
| return true; |
| })(); |
| )")); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| EXPECT_EQ(false, content::EvalJs( |
| web_view_guest->GetGuestMainFrame(), |
| content::JsReplace( |
| R"( |
| (async function() { |
| cancelRequest = true; |
| const url = 'https://localhost:' + $1 + '/echo_test'; |
| try { |
| const transport = new WebTransport(url); |
| await transport.ready; |
| } catch (e) { |
| console.log(url + ': ' + e.name + ': ' + e.message); |
| return false; |
| } |
| return true; |
| })(); |
| )", |
| webtransport_server().server_address().port()))); |
| } |
| |
| class ControlledFramePromiseApiTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface<const char*> {}; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFramePromiseApiTest, PromiseAPIs) { |
| std::unique_ptr<web_app::ScopedProxyIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder()) |
| .AddFolderFromDisk("/", "web_apps/simple_isolated_app") |
| .BuildAndStartProxyServer(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = |
| OpenApp(url_info.app_id(), "/controlled_frame_api_test.html"); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, app->proxy_server().GetURL("/controlled_frame.html"))); |
| |
| EXPECT_EQ("SUCCESS", |
| content::EvalJs(app_frame, content::JsReplace(R"( |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| testAPI(frame, $1); |
| )", |
| GetParam()))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(PromiseAPIs, |
| ControlledFramePromiseApiTest, |
| testing::ValuesIn(kControlledFramePromiseApiMethods)); |
| |
| class ControlledFrameServiceWorkerTest |
| : public extensions::ServiceWorkerBasedBackgroundTest { |
| public: |
| ControlledFrameServiceWorkerTest(const ControlledFrameServiceWorkerTest&) = |
| delete; |
| ControlledFrameServiceWorkerTest& operator=( |
| const ControlledFrameServiceWorkerTest&) = delete; |
| |
| void SetUpOnMainThread() override { |
| extensions::ServiceWorkerBasedBackgroundTest::SetUpOnMainThread(); |
| embedded_https_test_server().ServeFilesFromSourceDirectory( |
| GetChromeTestDataDir()); |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| } |
| |
| protected: |
| ControlledFrameServiceWorkerTest() {} |
| |
| ~ControlledFrameServiceWorkerTest() override = default; |
| |
| base::test::ScopedFeatureList feature_list; |
| }; |
| |
| // This test ensures that loading an extension Service Worker does not cause a |
| // crash, and that Controlled Frame is not allowed in the Service Worker |
| // context. For more details, see https://crbug.com/1462384. |
| // This test is the same as ServiceWorkerBasedBackgroundTest.Basic. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameServiceWorkerTest, PRE_Basic) { |
| ExtensionTestMessageListener newtab_listener("CREATED"); |
| newtab_listener.set_failure_message("CREATE_FAILED"); |
| ExtensionTestMessageListener worker_listener("WORKER_RUNNING"); |
| worker_listener.set_failure_message("NON_WORKER_SCOPE"); |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII( |
| "service_worker/worker_based_background/basic")); |
| ASSERT_TRUE(extension); |
| const extensions::ExtensionId extension_id = extension->id(); |
| EXPECT_TRUE(worker_listener.WaitUntilSatisfied()); |
| |
| const GURL url = |
| embedded_https_test_server().GetURL("/extensions/test_file.html"); |
| content::WebContents* new_web_contents = |
| extensions::browsertest_util::AddTab(browser(), url); |
| EXPECT_TRUE(new_web_contents); |
| EXPECT_TRUE(newtab_listener.WaitUntilSatisfied()); |
| |
| // Service Worker extension does not have ExtensionHost. |
| EXPECT_FALSE(process_manager()->GetBackgroundHostForExtension(extension_id)); |
| } |
| |
| // After browser restarts, this test step ensures that opening a tab fires |
| // tabs.onCreated event listener to the extension without explicitly loading the |
| // extension. This is because the extension registered a listener before browser |
| // restarted in PRE_Basic. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameServiceWorkerTest, Basic) { |
| ExtensionTestMessageListener newtab_listener("CREATED"); |
| newtab_listener.set_failure_message("CREATE_FAILED"); |
| const GURL url = |
| embedded_https_test_server().GetURL("/extensions/test_file.html"); |
| content::WebContents* new_web_contents = |
| extensions::browsertest_util::AddTab(browser(), url); |
| EXPECT_TRUE(new_web_contents); |
| EXPECT_TRUE(newtab_listener.WaitUntilSatisfied()); |
| } |
| |
| class ControlledFrameNotAvailableChannelTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface<version_info::Channel> { |
| protected: |
| ControlledFrameNotAvailableChannelTest() |
| : ControlledFrameApiTest(/*channel=*/GetParam(), |
| /*feature_setting=*/FeatureSetting::ENABLED, |
| /*flag_setting=*/FlagSetting::CONTROLLED_FRAME) { |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(ControlledFrameNotAvailableChannels, |
| ControlledFrameNotAvailableChannelTest, |
| testing::Values(version_info::Channel::STABLE, |
| version_info::Channel::BETA, |
| version_info::Channel::DEV, |
| version_info::Channel::CANARY, |
| version_info::Channel::DEFAULT)); |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameNotAvailableChannelTest, Test) { |
| // Test if Controlled Frame is not available. |
| const GURL start_url("https://app.site.test/example/index"); |
| InstallPWA(start_url); |
| content::WebContents* app_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| ASSERT_FALSE(CreateControlledFrame( |
| app_contents->GetPrimaryMainFrame(), |
| embedded_https_test_server().GetURL("/index.html"))); |
| } |
| |
| class ControlledFrameAvailabilityTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface< |
| ::std::tuple<version_info::Channel, FeatureSetting, FlagSetting>> { |
| protected: |
| ControlledFrameAvailabilityTest() |
| : ControlledFrameApiTest( |
| /*channel=*/std::get<0>(GetParam()), |
| /*feature_setting=*/std::get<1>(GetParam()), |
| /*flag_setting=*/std::get<2>(GetParam())) {} |
| |
| ~ControlledFrameAvailabilityTest() override = default; |
| |
| // |DetermineExpectedState| derives the expected enabling status based on |
| // the channel, feature, and flag inputs. |
| // |
| // Ideally, when we set a feature to enabled or disabled in the test setup, |
| // we would be altering that feature's default setting for the purposes of |
| // the test. However, ScopedFeatureList doesn't enable or disable a feature |
| // via defaults but instead by overrides. As a result, any feature that's |
| // enabled or disabled by ScopedFeatureList will appear as an override. |
| bool DetermineExpectedState() { |
| if (feature_setting() == FeatureSetting::DISABLED) { |
| return false; |
| } |
| |
| if (feature_setting() == FeatureSetting::NONE && |
| flag_setting() == FlagSetting::NONE) { |
| return false; |
| } |
| |
| if (feature_setting() == FeatureSetting::ENABLED && |
| (flag_setting() == FlagSetting::EXPERIMENTAL || |
| flag_setting() == FlagSetting::CONTROLLED_FRAME)) { |
| return true; |
| } |
| |
| // In Blink's runtime flags, if the base::Feature is overridden and that |
| // feature is enabled via the override, then the corresponding Blink |
| // runtime flag is also enabled. |
| if (feature_setting() == FeatureSetting::ENABLED && |
| flag_setting() == FlagSetting::NONE) { |
| return true; |
| } |
| |
| return false; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* */, |
| ControlledFrameAvailabilityTest, |
| /* Per-channel tests examine the extensions-based availability system. */ |
| testing::Combine( |
| /*channel=*/testing::Values(version_info::Channel::STABLE, |
| version_info::Channel::BETA, |
| version_info::Channel::DEV, |
| version_info::Channel::CANARY), |
| /*feature=*/ |
| testing::Values(FeatureSetting::NONE, |
| FeatureSetting::DISABLED, |
| FeatureSetting::ENABLED), |
| /*flag=*/ |
| testing::Values(FlagSetting::NONE, |
| FlagSetting::EXPERIMENTAL, |
| FlagSetting::CONTROLLED_FRAME))); |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameAvailabilityTest, Verify) { |
| const bool expected_enabled = DetermineExpectedState(); |
| EXPECT_EQ(expected_enabled, |
| base::FeatureList::IsEnabled(blink::features::kControlledFrame)); |
| |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const bool actual_enabled = CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html")); |
| EXPECT_EQ(expected_enabled, actual_enabled) |
| << "Test failure for case: " << ConfigToString(); |
| |
| // Uncomment for debugging information: |
| // DLOG(ERROR) << ConfigToString(); |
| // DLOG(ERROR) << "expected_enabled=" << expected_enabled |
| // << "; actual_enabled=" << actual_enabled; |
| |
| if (expected_enabled && actual_enabled) { |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| EXPECT_EQ(kEvalSuccessStr, SetBackgroundColorToWhite(web_view_guest)); |
| EXPECT_EQ(kEvalSuccessStr, ExecuteScriptRedBackgroundCode(app_frame)); |
| EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); |
| } |
| } |
| |
| } // namespace controlled_frame |