| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "base/command_line.h" | |
| #include "base/memory/raw_ptr.h" | |
| #include "base/run_loop.h" | |
| #include "base/task/single_thread_task_runner.h" | |
| #include "base/test/bind.h" | |
| #include "base/test/run_until.h" | |
| #include "base/test/scoped_feature_list.h" | |
| #include "base/test/test_future.h" | |
| #include "base/time/time.h" | |
| #include "build/build_config.h" | |
| #include "components/surface_embed/browser/surface_embed_handle.h" | |
| #include "components/surface_embed/browser/surface_embed_host.h" | |
| #include "components/surface_embed/common/constants.h" | |
| #include "components/surface_embed/common/features.h" | |
| #include "components/surface_embed/common/surface_embed.mojom.h" | |
| #include "components/viz/common/frame_sinks/copy_output_result.h" | |
| #include "content/public/browser/render_frame_host.h" | |
| #include "content/public/browser/render_widget_host.h" | |
| #include "content/public/browser/render_widget_host_view.h" | |
| #include "content/public/browser/surface_embed_connector.h" | |
| #include "content/public/browser/web_contents.h" | |
| #include "content/public/browser/web_contents_delegate.h" | |
| #include "content/public/common/result_codes.h" | |
| #include "content/public/test/browser_test.h" | |
| #include "content/public/test/browser_test_utils.h" | |
| #include "content/public/test/content_browser_test.h" | |
| #include "content/public/test/content_browser_test_content_browser_client.h" | |
| #include "content/public/test/content_browser_test_utils.h" | |
| #include "content/public/test/hit_test_region_observer.h" | |
| #include "content/public/test/no_renderer_crashes_assertion.h" | |
| #include "content/shell/browser/shell.h" | |
| #include "net/dns/mock_host_resolver.h" | |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" | |
| #include "third_party/blink/public/common/switches.h" | |
| #include "third_party/skia/include/core/SkBitmap.h" | |
| #include "third_party/skia/include/core/SkColor.h" | |
| #include "ui/accessibility/accessibility_switches.h" | |
| #include "ui/accessibility/ax_tree_id.h" | |
| #include "ui/gfx/geometry/rect.h" | |
| namespace surface_embed { | |
| namespace { | |
| constexpr std::string_view kAttachHarnessUrl = | |
| "/surface_embed/attach_harness.html"; | |
| constexpr std::string_view kBlueBoxUrl = "/surface_embed/blue_box.html"; | |
| constexpr std::string_view kRedBoxUrl = "/surface_embed/red_box.html"; | |
| constexpr std::string_view kFocusHarnessUrl = | |
| "/surface_embed/focus_harness.html"; | |
| constexpr std::string_view kMultilevelHarnessUrl = | |
| "/surface_embed/multilevel_harness.html"; | |
| constexpr std::string_view kMultilevelParentUrl = | |
| "/surface_embed/multilevel_parent.html"; | |
| constexpr std::string_view kInnerPageUrl = "/surface_embed/inner_page.html"; | |
| constexpr std::string_view kIntersectionObserverTargetUrl = | |
| "/surface_embed/intersection_observer_target.html"; | |
| constexpr size_t kSingleEmbedCount = 1; | |
| constexpr float kTestDeviceScaleFactor = 1.5f; | |
| class TakeFocusTrackingDelegate : public content::WebContentsDelegate { | |
| public: | |
| bool TakeFocus(content::WebContents* source, bool reverse) override { | |
| take_focus_called_ = true; | |
| take_focus_reverse_ = reverse; | |
| if (run_loop_) { | |
| run_loop_->Quit(); | |
| } | |
| return true; | |
| } | |
| void WaitForTakeFocus() { | |
| if (take_focus_called_) { | |
| return; | |
| } | |
| run_loop_ = std::make_unique<base::RunLoop>(); | |
| run_loop_->Run(); | |
| } | |
| bool take_focus_called() const { return take_focus_called_; } | |
| bool take_focus_reverse() const { return take_focus_reverse_; } | |
| private: | |
| bool take_focus_called_ = false; | |
| bool take_focus_reverse_ = false; | |
| std::unique_ptr<base::RunLoop> run_loop_; | |
| }; | |
| // Helper class for tracking SurfaceEmbedHost instances. | |
| class SurfaceEmbedHostTracker { | |
| public: | |
| SurfaceEmbedHostTracker() = default; | |
| SurfaceEmbedHostTracker(const SurfaceEmbedHostTracker&) = delete; | |
| SurfaceEmbedHostTracker& operator=(const SurfaceEmbedHostTracker&) = delete; | |
| ~SurfaceEmbedHostTracker() = default; | |
| void AddHost(SurfaceEmbedHost* host) { hosts_.push_back(host); } | |
| void RemoveHost(SurfaceEmbedHost* host) { std::erase(hosts_, host); } | |
| SurfaceEmbedHost* GetHost(size_t index) const { | |
| if (index < hosts_.size()) { | |
| return hosts_[index]; | |
| } | |
| return nullptr; | |
| } | |
| size_t GetHostCount() const { return hosts_.size(); } | |
| size_t GetAttachedHostCount() const { | |
| size_t attached_count = 0; | |
| for (auto host : hosts_) { | |
| if (host->IsAttachedForTesting()) { | |
| ++attached_count; | |
| } | |
| } | |
| return attached_count; | |
| } | |
| private: | |
| std::vector<raw_ptr<SurfaceEmbedHost>> hosts_; | |
| }; | |
| class SurfaceEmbedTestContentBrowserClient | |
| : public content::ContentBrowserTestContentBrowserClient { | |
| public: | |
| explicit SurfaceEmbedTestContentBrowserClient( | |
| SurfaceEmbedHostTracker* tracker) | |
| : tracker_(tracker) {} | |
| ~SurfaceEmbedTestContentBrowserClient() override = default; | |
| void RegisterAssociatedInterfaceBindersForRenderFrameHost( | |
| content::RenderFrameHost& render_frame_host, | |
| blink::AssociatedInterfaceRegistry& associated_registry) override { | |
| content::ContentBrowserTestContentBrowserClient:: | |
| RegisterAssociatedInterfaceBindersForRenderFrameHost( | |
| render_frame_host, associated_registry); | |
| associated_registry.RemoveInterface(mojom::SurfaceEmbedHost::Name_); | |
| associated_registry.AddInterface<mojom::SurfaceEmbedHost>( | |
| base::BindRepeating( | |
| [](SurfaceEmbedHostTracker* tracker, | |
| content::RenderFrameHost* render_frame_host, | |
| mojo::PendingAssociatedReceiver<mojom::SurfaceEmbedHost> | |
| receiver) { | |
| SurfaceEmbedHost* host = SurfaceEmbedHost::Create( | |
| render_frame_host, std::move(receiver)); | |
| host->SetDestructionCallbackForTesting( | |
| base::BindOnce(&SurfaceEmbedHostTracker::RemoveHost, | |
| base::Unretained(tracker), host)); | |
| tracker->AddHost(host); | |
| }, | |
| base::Unretained(tracker_), &render_frame_host)); | |
| } | |
| private: | |
| raw_ptr<SurfaceEmbedHostTracker> tracker_; | |
| }; | |
| } // namespace | |
| class SurfaceEmbedBrowserTest : public content::ContentBrowserTest { | |
| public: | |
| // If `enable_binder` is true, SurfaceEmbedTestContentBrowserClient will be | |
| // installed to provide a binder for SurfaceEmbedHost interface. | |
| explicit SurfaceEmbedBrowserTest(bool enable_binder = true) | |
| : enable_binder_(enable_binder) {} | |
| void SetUp() override { | |
| scoped_feature_list_.InitAndEnableFeature(features::kSurfaceEmbed); | |
| content::ContentBrowserTest::SetUp(); | |
| } | |
| void CreatedBrowserMainParts( | |
| content::BrowserMainParts* browser_main_parts) override { | |
| content::ContentBrowserTest::CreatedBrowserMainParts(browser_main_parts); | |
| if (enable_binder_) { | |
| test_browser_client_ = | |
| std::make_unique<SurfaceEmbedTestContentBrowserClient>(&tracker_); | |
| } | |
| } | |
| void SetUpCommandLine(base::CommandLine* command_line) override { | |
| content::ContentBrowserTest::SetUpCommandLine(command_line); | |
| // Enable pixel output in tests to allow CopyFromSurface to capture actual | |
| // rendered content instead of returning empty/black bitmaps. | |
| // Note that we force a device scale factor of 1.5 to also test scaling of | |
| // the surface embed plugin. | |
| EnablePixelOutput(kTestDeviceScaleFactor); | |
| command_line->AppendSwitchASCII(blink::switches::kJavaScriptFlags, | |
| "--expose-gc"); | |
| } | |
| void SetUpOnMainThread() override { | |
| content::ContentBrowserTest::SetUpOnMainThread(); | |
| host_resolver()->AddRule("*", "127.0.0.1"); | |
| embedded_test_server()->ServeFilesFromSourceDirectory( | |
| "components/test/data"); | |
| ASSERT_TRUE(embedded_test_server()->Start()); | |
| } | |
| content::WebContents* web_contents() { return shell()->web_contents(); } | |
| void NavigateToTestUrl(std::string_view url) { | |
| const GURL test_url = embedded_test_server()->GetURL(url); | |
| ASSERT_TRUE(content::NavigateToURL(web_contents(), test_url)); | |
| } | |
| int CountEmbedElementsInPage() { | |
| return content::EvalJs(web_contents(), "document.embeds.length") | |
| .ExtractInt(); | |
| } | |
| void WaitForHostCount(size_t expected_count) { | |
| ASSERT_TRUE(base::test::RunUntil( | |
| [&]() { return GetHostCount() == expected_count; })); | |
| } | |
| bool WaitForHostAttachment(size_t expected_count) { | |
| // After host creation, the attachment happens asynchronously when | |
| // SurfaceEmbedHost::AttachConnector() is called. Poll until the expected | |
| // number of hosts are attached. | |
| return base::test::RunUntil( | |
| [&]() { return GetAttachedHostCount() >= expected_count; }); | |
| } | |
| void NavigateToAttachHarness() { | |
| const GURL harness_url = embedded_test_server()->GetURL(kAttachHarnessUrl); | |
| ASSERT_TRUE(NavigateToURL(web_contents(), harness_url)); | |
| } | |
| std::unique_ptr<content::WebContents> CreateChildWebContents() { | |
| content::WebContents::CreateParams create_params( | |
| shell()->web_contents()->GetBrowserContext()); | |
| std::unique_ptr<content::WebContents> child_contents = | |
| content::WebContents::Create(create_params); | |
| EXPECT_NE(child_contents, nullptr); | |
| return child_contents; | |
| } | |
| // Navigate child WebContents to a URL. Wait for load to complete. | |
| void NavigateChildToUrl(content::WebContents* child_contents, | |
| const std::string_view& path) { | |
| const GURL child_url = embedded_test_server()->GetURL(path); | |
| ASSERT_TRUE(NavigateToURL(child_contents, child_url)); | |
| ASSERT_TRUE(content::WaitForLoadStop(child_contents)); | |
| } | |
| // Setup child with harness navigation and content loading. | |
| std::unique_ptr<content::WebContents> SetupHarnessAndChildWithContent( | |
| const std::string_view& child_path) { | |
| NavigateToAttachHarness(); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), child_path); | |
| return child_contents; | |
| } | |
| // Setup child with harness navigation and load a blank html page. | |
| std::unique_ptr<content::WebContents> SetupHarnessAndChild() { | |
| return SetupHarnessAndChildWithContent(kRedBoxUrl); | |
| } | |
| // Attach a child to an embed element and wait for SurfaceEmbedHost creation. | |
| void AttachChildToEmbed(content::WebContents* child_contents) { | |
| AttachChildToEmbedWithId(child_contents, std::nullopt); | |
| } | |
| // Attach a child to an embed element with an optional ID. | |
| void AttachChildToEmbedWithId(content::WebContents* child_contents, | |
| std::optional<std::string> embed_id) { | |
| SurfaceEmbedHandle* embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents); | |
| ASSERT_NE(embedded_handle, nullptr); | |
| std::string script = "createEmbed('" + embedded_handle->id().ToString(); | |
| if (embed_id.has_value()) { | |
| script += "', '" + embed_id.value(); | |
| } | |
| script += "');"; | |
| size_t expected_attachments = GetAttachedHostCount() + 1; | |
| ASSERT_TRUE(content::ExecJs(web_contents(), script)); | |
| ASSERT_TRUE(WaitForHostAttachment(expected_attachments)); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| } | |
| SurfaceEmbedHost* GetHost(size_t index) { return tracker_.GetHost(index); } | |
| size_t GetHostCount() const { return tracker_.GetHostCount(); } | |
| size_t GetAttachedHostCount() const { | |
| return tracker_.GetAttachedHostCount(); | |
| } | |
| protected: | |
| // Take a screenshot of the given rectangle area of the main web contents. | |
| // Empty rectangle captures the full area. | |
| SkBitmap TakeScreenshot(const gfx::Rect& capture_rect) { | |
| base::test::TestFuture<const content::CopyFromSurfaceResult&> future_bitmap; | |
| web_contents()->GetRenderWidgetHostView()->CopyFromSurface( | |
| capture_rect, gfx::Size(), base::TimeDelta(), | |
| future_bitmap.GetCallback()); | |
| return future_bitmap.Take() | |
| .value_or(viz::CopyOutputBitmapWithMetadata()) | |
| .bitmap; | |
| } | |
| // Check if the given color is rendered without waiting. | |
| bool HasPixelInColor(SkColor target_color) { | |
| content::WaitForCopyableViewInWebContents(web_contents()); | |
| auto bitmap = TakeScreenshot(gfx::Rect()); | |
| for (int x = 0; x < bitmap.width(); ++x) { | |
| for (int y = 0; y < bitmap.height(); ++y) { | |
| if (bitmap.getColor(x, y) == target_color) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // Check if the given color is rendered. | |
| bool CheckHasPixelInColor(SkColor target_color) { | |
| return CheckHasPixelInColorInBitmapBounds(target_color); | |
| } | |
| // Check if the given color is rendered. | |
| bool CheckHasPixelInColorInBitmapBounds( | |
| SkColor target_color, | |
| gfx::Rect check_bitmap_bounds = gfx::Rect(), | |
| SkBitmap* out_bitmap = nullptr) { | |
| content::WaitForCopyableViewInWebContents(web_contents()); | |
| // Retry finding the pixel since it might take a moment to propagate. | |
| return base::test::RunUntil([&]() { | |
| auto bitmap = TakeScreenshot(gfx::Rect()); | |
| if (check_bitmap_bounds == gfx::Rect()) { | |
| check_bitmap_bounds.set_width(bitmap.width()); | |
| check_bitmap_bounds.set_height(bitmap.height()); | |
| } | |
| if (out_bitmap) { | |
| *out_bitmap = bitmap; | |
| } | |
| const int min_x = std::max(0, check_bitmap_bounds.x()); | |
| const int max_x = std::min(bitmap.width(), check_bitmap_bounds.right()); | |
| const int min_y = std::max(0, check_bitmap_bounds.y()); | |
| const int max_y = std::min(bitmap.height(), check_bitmap_bounds.bottom()); | |
| for (int x = min_x; x < max_x; ++x) { | |
| for (int y = min_y; y < max_y; ++y) { | |
| if (bitmap.getColor(x, y) == target_color) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| }); | |
| } | |
| void CalculateBitmapBoundsToCheck(const gfx::Rect embed_bounds, | |
| gfx::Rect* out_scaled_bounds) { | |
| ASSERT_TRUE(out_scaled_bounds); | |
| content::RenderWidgetHostView* const view = | |
| web_contents()->GetRenderWidgetHostView(); | |
| ASSERT_TRUE(view); | |
| // On Android forcing device scale factor might not work for tests, | |
| // therefore query the actual scale factor from the view. On other | |
| // platforms, verify it matches the forced value. | |
| const float device_scale_factor = view->GetDeviceScaleFactor(); | |
| #if !BUILDFLAG(IS_ANDROID) | |
| ASSERT_FLOAT_EQ(kTestDeviceScaleFactor, device_scale_factor); | |
| #endif | |
| *out_scaled_bounds = | |
| gfx::ScaleToRoundedRect(embed_bounds, device_scale_factor); | |
| } | |
| void VerifyRedPixelInBounds(const gfx::Rect embed_bounds, | |
| SkBitmap* out_bitmap = nullptr) { | |
| gfx::Rect scaled_embed_bounds; | |
| CalculateBitmapBoundsToCheck(embed_bounds, &scaled_embed_bounds); | |
| EXPECT_TRUE(CheckHasPixelInColorInBitmapBounds( | |
| SK_ColorRED, scaled_embed_bounds, out_bitmap)); | |
| } | |
| // Waits until the active element in |wc| has the expected id. | |
| void WaitForActiveElement(content::WebContents* wc, | |
| const std::string& expected_id) { | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(wc, "document.activeElement.id") == expected_id; | |
| })); | |
| } | |
| void WaitForMultilevelFocusState(content::WebContents* parent_contents, | |
| content::WebContents* child_contents, | |
| const std::string& root_active_element_id) { | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::GetFocusedWebContents(web_contents()) == child_contents; | |
| })); | |
| WaitForActiveElement(child_contents, "inner"); | |
| WaitForActiveElement(parent_contents, "child_embed"); | |
| WaitForActiveElement(web_contents(), root_active_element_id); | |
| for (content::WebContents* contents : | |
| {web_contents(), parent_contents, child_contents}) { | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(contents, "document.hasFocus()").ExtractBool(); | |
| })); | |
| } | |
| } | |
| void WaitForMultilevelViewSizes(content::WebContents* parent_contents, | |
| content::WebContents* child_contents) { | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| auto* parent_view = parent_contents->GetRenderWidgetHostView(); | |
| return parent_view && | |
| parent_view->GetViewBounds().size() == gfx::Size(200, 150); | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| auto* child_view = child_contents->GetRenderWidgetHostView(); | |
| return child_view && | |
| child_view->GetViewBounds().size() == gfx::Size(100, 100); | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| auto* connector = parent_contents->GetSurfaceEmbedConnector(); | |
| return connector && connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(300, 225); | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| auto* connector = child_contents->GetSurfaceEmbedConnector(); | |
| return connector && connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(150, 150); | |
| })); | |
| } | |
| private: | |
| base::test::ScopedFeatureList scoped_feature_list_; | |
| SurfaceEmbedHostTracker tracker_; | |
| bool enable_binder_; | |
| std::unique_ptr<SurfaceEmbedTestContentBrowserClient> test_browser_client_; | |
| }; | |
| // A fixture where the browser-side support isn't provided. | |
| class SurfaceEmbedBrowserTestNoHost : public SurfaceEmbedBrowserTest { | |
| public: | |
| SurfaceEmbedBrowserTestNoHost() | |
| : SurfaceEmbedBrowserTest(/*enable_binder=*/false) {} | |
| }; | |
| // Test that trying to create a web plugin w/o providing support via | |
| // SurfaceEmbedTestContentBrowserClient doesn't crash. This will imply | |
| // that content_shell won't crash in similar circumstances. | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTestNoHost, NoCrash) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| SurfaceEmbedHandle* embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(embedded_handle, nullptr); | |
| std::string script = | |
| content::JsReplace("createEmbed($1);", embedded_handle->id().ToString()); | |
| ASSERT_TRUE(content::ExecJs(web_contents(), script)); | |
| // Access an unknown property on the embed to force plugin creation | |
| // (since otherwise it's on a timer). | |
| EXPECT_EQ( | |
| base::Value(), | |
| content::EvalJs(web_contents(), "document.embeds[0].noSuchProperty")); | |
| // Check that the sad plugin page got rendered. | |
| EXPECT_TRUE(CheckHasPixelInColor(SkColors::kGray.toSkColor())); | |
| // Changing data-content-id after plugin creation must not crash. | |
| // TODO(crbug.com/561637127): Handling data-content-id changes isn't | |
| // implemented yet. | |
| auto child_contents2 = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents2.get(), kBlueBoxUrl); | |
| SurfaceEmbedHandle* embedded_handle2 = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents2.get()); | |
| EXPECT_TRUE(content::ExecJs( | |
| web_contents(), | |
| content::JsReplace( | |
| "document.embeds[0].setAttribute('data-content-id', $1)", | |
| embedded_handle2->id().ToString()))); | |
| EXPECT_TRUE(CheckHasPixelInColor(SkColors::kGray.toSkColor())); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, EmbedTagCreatesPlugin) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| // Verify that the host is created. | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_EQ(kSingleEmbedCount, GetHostCount()); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(nullptr, host); | |
| // Expect the stub plugin code to render a red square. | |
| EXPECT_TRUE(CheckHasPixelInColor(SK_ColorRED)); | |
| } | |
| // Make sure we don't crash on invalid content ID. | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, EmbedTagInvalidContentId) { | |
| NavigateToAttachHarness(); | |
| ASSERT_TRUE(content::ExecJs(web_contents(), "createEmbed('bad')")); | |
| EXPECT_EQ(1, CountEmbedElementsInPage()); | |
| // Access an unknown property on the embed to force plugin creation | |
| // (since otherwise it's on a timer). | |
| EXPECT_EQ( | |
| base::Value(), | |
| content::EvalJs(web_contents(), "document.embeds[0].noSuchProperty")); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, EmbedPixelTest) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| // Verify that the host is created. | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_EQ(kSingleEmbedCount, GetHostCount()); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(nullptr, host); | |
| // The embed element is at 10,10 with size 100x100 in red_box.html. | |
| const gfx::Rect embed_bounds(10, 10, 100, 100); | |
| SkBitmap last_bitmap; | |
| VerifyRedPixelInBounds(embed_bounds, &last_bitmap); | |
| // Check a pixel outside the embed element. | |
| EXPECT_EQ(last_bitmap.getColor(1, 1), SK_ColorWHITE); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, VisibilityHiddenPropagates) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(content::Visibility::VISIBLE, child_contents->GetVisibility()); | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), "document.embeds[0].style.visibility = 'hidden';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), "document.embeds[0].style.visibility = 'visible';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, DisplayNonePropagates) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(content::Visibility::VISIBLE, child_contents->GetVisibility()); | |
| ASSERT_TRUE(content::ExecJs(web_contents(), | |
| "document.embeds[0].style.display = 'none';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| ASSERT_TRUE(content::ExecJs(web_contents(), | |
| "document.embeds[0].style.display = 'block';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| } | |
| IN_PROC_BROWSER_TEST_F( | |
| SurfaceEmbedBrowserTest, | |
| IntersectionObserverIgnoresEmbedderViewportUnlikeIframe) { | |
| auto child_contents = | |
| SetupHarnessAndChildWithContent(kIntersectionObserverTargetUrl); | |
| AttachChildToEmbed(child_contents.get()); | |
| // Move the SurfaceEmbed host outside of the outer viewport. | |
| ASSERT_TRUE(content::ExecJs(web_contents(), R"( | |
| const embed = document.querySelector('embed'); | |
| embed.style.top = '10000px'; | |
| new Promise(resolve => { | |
| requestAnimationFrame(() => requestAnimationFrame(resolve)); | |
| }); | |
| )")); | |
| // Confirm that the SurfaceEmbed host is outside of the outer viewport. | |
| EXPECT_TRUE( | |
| content::EvalJs(web_contents(), | |
| "document.querySelector('embed').getBoundingClientRect()." | |
| "top >= window.innerHeight") | |
| .ExtractBool()); | |
| // SurfaceEmbed uses the embedded content's viewport, ignoring the outer | |
| // viewport. If this were an iframe, it would use the outer viewport and be | |
| // non-intersecting. | |
| EXPECT_TRUE( | |
| content::EvalJs(child_contents.get(), "observeViewport()").ExtractBool()); | |
| } | |
| IN_PROC_BROWSER_TEST_F( | |
| SurfaceEmbedBrowserTest, | |
| IntersectionObserverIgnoresEmbedderScrollOffsetUnlikeIframe) { | |
| auto child_contents = | |
| SetupHarnessAndChildWithContent(kIntersectionObserverTargetUrl); | |
| AttachChildToEmbed(child_contents.get()); | |
| ASSERT_TRUE(content::ExecJs(web_contents(), R"( | |
| const embed = document.querySelector('embed'); | |
| const scroller = document.createElement('div'); | |
| scroller.id = 'embed_scroller'; | |
| scroller.style.cssText = 'width: 100px; height: 100px; overflow: hidden'; | |
| const spacer = document.createElement('div'); | |
| spacer.style.height = '200px'; | |
| embed.style.position = 'static'; | |
| embed.style.left = ''; | |
| embed.style.top = ''; | |
| embed.style.display = 'block'; | |
| scroller.append(spacer, embed); | |
| document.body.appendChild(scroller); | |
| scroller.scrollTop = 200; | |
| new Promise(resolve => { | |
| requestAnimationFrame(() => requestAnimationFrame(resolve)); | |
| }); | |
| )")); | |
| // Start with the SurfaceEmbed host fully in view. | |
| EXPECT_TRUE(content::EvalJs(web_contents(), R"( | |
| (() => { | |
| const embed = document.querySelector('embed').getBoundingClientRect(); | |
| const scroller = document.querySelector('#embed_scroller') | |
| .getBoundingClientRect(); | |
| return embed.top >= scroller.top && embed.bottom <= scroller.bottom; | |
| })() | |
| )") | |
| .ExtractBool()); | |
| EXPECT_TRUE( | |
| content::EvalJs(child_contents.get(), "observeViewport()").ExtractBool()); | |
| // Scroll so the SurfaceEmbed host is partially clipped. | |
| ASSERT_TRUE(content::ExecJs(web_contents(), R"( | |
| document.querySelector('#embed_scroller').scrollTop = 150; | |
| new Promise(resolve => { | |
| requestAnimationFrame(() => requestAnimationFrame(resolve)); | |
| }); | |
| )")); | |
| // Verify the SurfaceEmbed host is partially outside the scroller's viewport. | |
| EXPECT_TRUE(content::EvalJs(web_contents(), R"( | |
| (() => { | |
| const embed = document.querySelector('embed').getBoundingClientRect(); | |
| const scroller = document.querySelector('#embed_scroller') | |
| .getBoundingClientRect(); | |
| return embed.top > scroller.top && embed.top < scroller.bottom && | |
| embed.bottom > scroller.bottom; | |
| })() | |
| )") | |
| .ExtractBool()); | |
| // Even though the SurfaceEmbed host (embed) is partially clipped, its | |
| // embedded contents remains fully intersecting. This is different from an | |
| // iframe where the iframe contents would be partially intersecting. | |
| EXPECT_TRUE( | |
| content::EvalJs(child_contents.get(), "observeViewport()").ExtractBool()); | |
| EXPECT_NEAR(1.0, | |
| content::EvalJs(child_contents.get(), | |
| "observeViewportIntersectionRatio()") | |
| .ExtractDouble(), | |
| 0.01); | |
| // Scroll so the SurfaceEmbed host is fully out of view. | |
| ASSERT_TRUE(content::ExecJs(web_contents(), R"( | |
| document.querySelector('#embed_scroller').scrollTop = 0; | |
| new Promise(resolve => { | |
| requestAnimationFrame(() => requestAnimationFrame(resolve)); | |
| }); | |
| )")); | |
| // Verify the host is fully outside the scroller's viewport. | |
| EXPECT_TRUE(content::EvalJs(web_contents(), R"( | |
| (() => { | |
| const embed = document.querySelector('embed').getBoundingClientRect(); | |
| const scroller = document.querySelector('#embed_scroller') | |
| .getBoundingClientRect(); | |
| return embed.top >= scroller.bottom; | |
| })() | |
| )") | |
| .ExtractBool()); | |
| // SurfaceEmbed remains fully intersecting. An iframe would become non- | |
| // intersecting. | |
| EXPECT_TRUE( | |
| content::EvalJs(child_contents.get(), "observeViewport()").ExtractBool()); | |
| EXPECT_NEAR(1.0, | |
| content::EvalJs(child_contents.get(), | |
| "observeViewportIntersectionRatio()") | |
| .ExtractDouble(), | |
| 0.01); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| MultilevelVisibilityHiddenPropagates) { | |
| NavigateToAttachHarness(); | |
| // Setup Parent (P) WebContents. | |
| auto parent_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(parent_contents.get(), kAttachHarnessUrl); | |
| AttachChildToEmbedWithId(parent_contents.get(), "parent_embed"); | |
| // Setup Child (C) WebContents. | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kRedBoxUrl); | |
| // Attach C to P. | |
| SurfaceEmbedHandle* child_embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(child_embedded_handle, nullptr); | |
| std::string attach_child_script = "createEmbed('" + | |
| child_embedded_handle->id().ToString() + | |
| "', 'child_embed');"; | |
| size_t expected_attachments = GetAttachedHostCount() + 1; | |
| ASSERT_TRUE(content::ExecJs(parent_contents.get(), attach_child_script)); | |
| ASSERT_TRUE(WaitForHostAttachment(expected_attachments)); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| EXPECT_EQ(content::Visibility::VISIBLE, parent_contents->GetVisibility()); | |
| EXPECT_EQ(content::Visibility::VISIBLE, child_contents->GetVisibility()); | |
| // Hide Parent embed in Grandparent. P and C should become HIDDEN. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('parent_embed').style.visibility = 'hidden';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return parent_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| // Make Parent embed visible again. P and C should become VISIBLE. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('parent_embed').style.visibility = 'visible';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return parent_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| // Hide Child embed in Parent. Only C should become HIDDEN, P remains VISIBLE. | |
| ASSERT_TRUE(content::ExecJs( | |
| parent_contents.get(), | |
| "document.getElementById('child_embed').style.visibility = 'hidden';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| EXPECT_EQ(content::Visibility::VISIBLE, parent_contents->GetVisibility()); | |
| // Make Child embed visible again. C should become VISIBLE. | |
| ASSERT_TRUE(content::ExecJs( | |
| parent_contents.get(), | |
| "document.getElementById('child_embed').style.visibility = 'visible';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| EXPECT_EQ(content::Visibility::VISIBLE, parent_contents->GetVisibility()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| MultilevelDisplayNonePropagates) { | |
| NavigateToAttachHarness(); | |
| // Setup Parent (P) WebContents. | |
| auto parent_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(parent_contents.get(), kAttachHarnessUrl); | |
| AttachChildToEmbedWithId(parent_contents.get(), "parent_embed"); | |
| // Setup Child (C) WebContents. | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kRedBoxUrl); | |
| // Attach C to P. | |
| SurfaceEmbedHandle* child_embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(child_embedded_handle, nullptr); | |
| std::string attach_child_script = "createEmbed('" + | |
| child_embedded_handle->id().ToString() + | |
| "', 'child_embed');"; | |
| size_t expected_attachments = GetAttachedHostCount() + 1; | |
| ASSERT_TRUE(content::ExecJs(parent_contents.get(), attach_child_script)); | |
| ASSERT_TRUE(WaitForHostAttachment(expected_attachments)); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| EXPECT_EQ(content::Visibility::VISIBLE, parent_contents->GetVisibility()); | |
| EXPECT_EQ(content::Visibility::VISIBLE, child_contents->GetVisibility()); | |
| // Hide Parent embed in Grandparent using display = 'none'. P and C should | |
| // become HIDDEN. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('parent_embed').style.display = 'none';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return parent_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| // Restore Parent embed in Grandparent. P and C should become VISIBLE. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('parent_embed').style.display = 'block';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return parent_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| // Hide Child embed in Parent using display = 'none'. Only C should become | |
| // HIDDEN, P remains VISIBLE. | |
| ASSERT_TRUE(content::ExecJs( | |
| parent_contents.get(), | |
| "document.getElementById('child_embed').style.display = 'none';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::HIDDEN; | |
| })); | |
| EXPECT_EQ(content::Visibility::VISIBLE, parent_contents->GetVisibility()); | |
| // Restore Child embed in Parent. C should become VISIBLE. | |
| ASSERT_TRUE(content::ExecJs( | |
| parent_contents.get(), | |
| "document.getElementById('child_embed').style.display = 'block';")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| EXPECT_EQ(content::Visibility::VISIBLE, parent_contents->GetVisibility()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| EmbedMultipleTagsCreatesMultiplePlugins) { | |
| constexpr size_t kMultipleEmbedCount = 2; | |
| NavigateToAttachHarness(); | |
| auto child_contents1 = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents1.get(), kRedBoxUrl); | |
| AttachChildToEmbed(child_contents1.get()); | |
| auto child_contents2 = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents2.get(), kRedBoxUrl); | |
| AttachChildToEmbed(child_contents2.get()); | |
| EXPECT_EQ(kMultipleEmbedCount, CountEmbedElementsInPage()); | |
| // Verify that the hosts are created. | |
| WaitForHostCount(kMultipleEmbedCount); | |
| ASSERT_EQ(kMultipleEmbedCount, GetHostCount()); | |
| for (size_t i = 0; i < kMultipleEmbedCount; ++i) { | |
| SurfaceEmbedHost* host = GetHost(i); | |
| ASSERT_NE(nullptr, host); | |
| } | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| ChildDestroyedBeforeEmbedAttachment) { | |
| NavigateToAttachHarness(); | |
| auto child_contents = CreateChildWebContents(); | |
| SurfaceEmbedHandle* embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(embedded_handle, nullptr); | |
| const std::string stale_content_id = embedded_handle->id().ToString(); | |
| child_contents.reset(); | |
| ASSERT_TRUE(content::ExecJs(web_contents(), | |
| "createEmbed('" + stale_content_id + "');")); | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_TRUE(base::test::RunUntil( | |
| [&]() { return GetHost(0)->HasReceivedAttachConnectorForTesting(); })); | |
| EXPECT_EQ(0u, GetAttachedHostCount()); | |
| // Verify that processing the stale attachment did not terminate the parent | |
| // renderer or browser. | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, EmbedTagRemovedDestroysHost) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| // Verify that the host is created. | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_EQ(kSingleEmbedCount, GetHostCount()); | |
| // Destroy the child contents before the host is destroyed to prevent a | |
| // dangling pointer in SurfaceEmbedConnectorImpl. | |
| child_contents.reset(); | |
| // Remove the embed element from the page. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.querySelector('embed').remove();")); | |
| // Verify that the host is destroyed. | |
| WaitForHostCount(0); | |
| EXPECT_EQ(0u, GetHostCount()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, Crash) { | |
| auto child_contents = SetupHarnessAndChildWithContent(kRedBoxUrl); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_TRUE(CheckHasPixelInColor(SK_ColorRED)); | |
| // Simulate a crash. | |
| content::ScopedAllowRendererCrashes testing_crashes_here( | |
| child_contents->GetPrimaryMainFrame()); | |
| child_contents->GetPrimaryMainFrame()->GetProcess()->Shutdown( | |
| content::RESULT_CODE_KILLED); | |
| EXPECT_TRUE( | |
| base::test::RunUntil([&]() { return child_contents->IsCrashed(); })); | |
| // The crashed frame gets drawn with a gray background, with an image | |
| // in the middle (which doesn't seem configured for tests). The gray in | |
| // question is a bit different than SK_ColorGRAY. | |
| EXPECT_TRUE(CheckHasPixelInColor(SkColors::kGray.toSkColor())); | |
| EXPECT_FALSE(HasPixelInColor(SK_ColorRED)); | |
| // Remove the embed element from the page. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.querySelector('embed').remove();")); | |
| // Trigger garbage collection. The C++ side of the heap has | |
| // blink::PendingLayer which refer to a PictureLayer we use, which was caught | |
| // with a dangling pointer in review. This call would have raw_ptr catch it | |
| // if the bug were still there. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), "gc()")); | |
| // Verify that the host is destroyed. | |
| WaitForHostCount(0); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, CrashThenReattach) { | |
| auto child_contents = SetupHarnessAndChildWithContent(kRedBoxUrl); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_TRUE(CheckHasPixelInColor(SK_ColorRED)); | |
| // Simulate a crash. | |
| content::ScopedAllowRendererCrashes testing_crashes_here( | |
| child_contents->GetPrimaryMainFrame()); | |
| child_contents->GetPrimaryMainFrame()->GetProcess()->Shutdown( | |
| content::RESULT_CODE_KILLED); | |
| EXPECT_TRUE( | |
| base::test::RunUntil([&]() { return child_contents->IsCrashed(); })); | |
| // The crashed frame gets drawn with a gray background, with an image | |
| // in the middle (which doesn't seem configured for tests). The gray in | |
| // question is a bit different than SK_ColorGRAY. | |
| EXPECT_TRUE(CheckHasPixelInColor(SkColors::kGray.toSkColor())); | |
| EXPECT_FALSE(HasPixelInColor(SK_ColorRED)); | |
| auto child_contents2 = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents2.get(), kBlueBoxUrl); | |
| AttachChildToEmbed(child_contents2.get()); | |
| EXPECT_TRUE(CheckHasPixelInColor(SK_ColorBLUE)); | |
| } | |
| // Test case where child process crashed before the attach. | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, CrashEarly) { | |
| auto child_contents = SetupHarnessAndChildWithContent(kRedBoxUrl); | |
| // Simulate a crash. | |
| content::ScopedAllowRendererCrashes testing_crashes_here( | |
| child_contents->GetPrimaryMainFrame()); | |
| child_contents->GetPrimaryMainFrame()->GetProcess()->Shutdown( | |
| content::RESULT_CODE_KILLED); | |
| EXPECT_TRUE( | |
| base::test::RunUntil([&]() { return child_contents->IsCrashed(); })); | |
| // Now try to attach. This doesn't actually attach successfully, so can't | |
| // use the usual helper. | |
| SurfaceEmbedHandle* embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(embedded_handle, nullptr); | |
| std::string script = | |
| "createEmbed('" + embedded_handle->id().ToString() + "')"; | |
| EXPECT_TRUE(content::ExecJs(web_contents(), script)); | |
| // Should have a gray background. | |
| EXPECT_TRUE(CheckHasPixelInColor(SkColors::kGray.toSkColor())); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, CrashThenReload) { | |
| auto child_contents = SetupHarnessAndChildWithContent(kRedBoxUrl); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_TRUE(CheckHasPixelInColor(SK_ColorRED)); | |
| // Simulate a crash. | |
| content::ScopedAllowRendererCrashes testing_crashes_here( | |
| child_contents->GetPrimaryMainFrame()); | |
| child_contents->GetPrimaryMainFrame()->GetProcess()->Shutdown( | |
| content::RESULT_CODE_KILLED); | |
| EXPECT_TRUE( | |
| base::test::RunUntil([&]() { return child_contents->IsCrashed(); })); | |
| // The crashed frame gets drawn with a gray background, with an image | |
| // in the middle (which doesn't seem configured for tests). The gray in | |
| // question is a bit different than SK_ColorGRAY. | |
| EXPECT_TRUE(CheckHasPixelInColor(SkColors::kGray.toSkColor())); | |
| EXPECT_FALSE(HasPixelInColor(SK_ColorRED)); | |
| // Reload the child contents. | |
| child_contents->GetController().Reload(content::ReloadType::NORMAL, false); | |
| EXPECT_TRUE(content::WaitForLoadStop(child_contents.get())); | |
| EXPECT_FALSE(child_contents->IsCrashed()); | |
| EXPECT_TRUE(CheckHasPixelInColor(SK_ColorRED)); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, VisualPropertiesSync) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| auto* connector = child_contents->GetSurfaceEmbedConnector(); | |
| ASSERT_NE(nullptr, connector); | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| // Wait for the initial size to propagate to the child. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(150, 150) && | |
| connector->GetCssZoomFactorForTesting() == 1.0; | |
| })); | |
| // Change the size of the embed element. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "let embed = document.querySelector('embed');" | |
| "embed.style.width = '250px';" | |
| "embed.style.height = '150px';")); | |
| // Wait for the new size to propagate to the child. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(375, 225); | |
| })); | |
| // Change the zoom of the embed element. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "let embed = document.querySelector('embed');" | |
| "embed.style.zoom = 2.0;")); | |
| // Wait for the new zoom to propagate to the child's local frame size. | |
| // The layout size of 250x150 with zoom 2.0 and dsf 1.5 is 250 * 2.0 * 1.5 = | |
| // 750, 150 * 2.0 * 1.5 = 450. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(750, 450); | |
| })); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, ResizeEmbedPixelTest) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| // Verify that the host is created. | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_EQ(kSingleEmbedCount, GetHostCount()); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(nullptr, host); | |
| // The embed element is at 10,10 with size 100x100 in red_box.html. | |
| const gfx::Rect embed_bounds(10, 10, 100, 100); | |
| VerifyRedPixelInBounds(embed_bounds); | |
| // Resize the embed element to 200x200. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "let embed = document.querySelector('embed');" | |
| "embed.style.width = '200px';" | |
| "embed.style.height = '200px';")); | |
| // Wait for the new size to propagate to the child's local frame size. | |
| // The layout size of 200x200 with dsf 1.5 is 200 * 1.5 = 300. | |
| auto* connector = child_contents->GetSurfaceEmbedConnector(); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(300, 300); | |
| })); | |
| // The bounds should eventually be 200x200, so check a pixel that's outside | |
| // the original 100x100 but inside 200x200. For example, x=150, y=150. | |
| gfx::Rect new_embed_pixel_bounds(150, 150, 10, 10); | |
| VerifyRedPixelInBounds(new_embed_pixel_bounds); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| CrossProcessNavigationPixelTest) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| EXPECT_EQ(kSingleEmbedCount, CountEmbedElementsInPage()); | |
| // Verify that the host is created. | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_EQ(kSingleEmbedCount, GetHostCount()); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(nullptr, host); | |
| // The embed element is at 10,10 with size 100x100 in red_box.html. | |
| const gfx::Rect embed_bounds(10, 10, 100, 100); | |
| VerifyRedPixelInBounds(embed_bounds); | |
| // Navigate the child to a different site to force a cross-process navigation. | |
| GURL cross_site_url = embedded_test_server()->GetURL("a.test", kRedBoxUrl); | |
| ASSERT_TRUE(content::NavigateToURL(child_contents.get(), cross_site_url)); | |
| ASSERT_TRUE(content::WaitForLoadStop(child_contents.get())); | |
| // We can just verify that it still renders the red box. | |
| VerifyRedPixelInBounds(embed_bounds); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, FocusByClick) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| // Click to focus outer1 in the outer page. | |
| content::SimulateMouseClickOrTapElementWithId(web_contents(), "outer1"); | |
| EXPECT_EQ(true, content::EvalJsAfterLifecycleUpdate(web_contents(), "", | |
| "document.hasFocus()")); | |
| EXPECT_EQ("outer1", content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.activeElement.id")); | |
| // The outer WebContents should be the focused WebContents and child | |
| // WebContents should not be able to see the focused frame. | |
| EXPECT_EQ(web_contents(), content::GetFocusedWebContents(web_contents())); | |
| EXPECT_EQ(web_contents()->GetPrimaryMainFrame(), | |
| web_contents()->GetFocusedFrame()); | |
| EXPECT_EQ(nullptr, content::GetFocusedWebContents(child_contents.get())); | |
| EXPECT_EQ(nullptr, child_contents->GetFocusedFrame()); | |
| // The outer WebContents should receive keyboard events. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('a'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("a", | |
| content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.getElementById('outer1').value")); | |
| // Wait for the child's hit test data to be available so the click can be | |
| // properly handled by it. | |
| content::WaitForHitTestData(child_contents.get()); | |
| // Click at the location of <input id="inner"> in the inner page. This should | |
| // change focus to the embed element in the outer page. | |
| auto inner_center_point = gfx::ToFlooredPoint( | |
| GetCenterCoordinatesOfElementWithId(child_contents.get(), "inner")); | |
| // Embed is at 10, 50 in parent. | |
| inner_center_point.Offset(10, 50); | |
| // Simulate a mouse click to parent web contents at the location of the inner | |
| // input element. The simulated event will be dispatched to the child via the | |
| // parent WebContents's input event router. This is the same as what happens | |
| // in real production code. | |
| content::SimulateMouseClickAt(web_contents(), 0, | |
| blink::WebMouseEvent::Button::kLeft, | |
| inner_center_point); | |
| EXPECT_EQ(true, content::EvalJsAfterLifecycleUpdate(web_contents(), "", | |
| "document.hasFocus()")); | |
| EXPECT_EQ("my_embed", content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.activeElement.id")); | |
| // The inner page should has page focus. | |
| EXPECT_EQ(true, content::EvalJsAfterLifecycleUpdate(child_contents.get(), "", | |
| "document.hasFocus()")); | |
| // The inner page's "inner" element should become the active element. | |
| EXPECT_EQ("inner", | |
| content::EvalJsAfterLifecycleUpdate(child_contents.get(), "", | |
| "document.activeElement.id")); | |
| // The child WebContents should be the focused WebContents and child | |
| // WebContents should be able to see the focused frame. | |
| EXPECT_EQ(child_contents.get(), | |
| content::GetFocusedWebContents(web_contents())); | |
| EXPECT_EQ(child_contents->GetPrimaryMainFrame(), | |
| web_contents()->GetFocusedFrame()); | |
| EXPECT_EQ(child_contents.get(), | |
| content::GetFocusedWebContents(child_contents.get())); | |
| EXPECT_EQ(child_contents->GetPrimaryMainFrame(), | |
| child_contents->GetFocusedFrame()); | |
| // The child WebContents should receive keyboard events. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('b'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("b", content::EvalJsAfterLifecycleUpdate( | |
| child_contents.get(), "", | |
| "document.getElementById('inner').value")); | |
| // Destroy the child WebContents and verify that the focus moves to outer | |
| // WebContents. | |
| child_contents.reset(); | |
| EXPECT_EQ(web_contents(), content::GetFocusedWebContents(web_contents())); | |
| EXPECT_EQ(web_contents()->GetPrimaryMainFrame(), | |
| web_contents()->GetFocusedFrame()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| HtmlPopupOnSurfaceEmbeddedPage) { | |
| auto child_contents = | |
| SetupHarnessAndChildWithContent("/surface_embed/inner_page.html"); | |
| AttachChildToEmbed(child_contents.get()); | |
| // Replace child page contents with a select element. | |
| EXPECT_TRUE(content::ExecJs(child_contents.get(), R"( | |
| document.body.innerHTML = "<select><option>option1</option></select>"; | |
| )")); | |
| // Ensure visibility and activation of the child WebContents. | |
| child_contents->WasShown(); | |
| child_contents->GetPrimaryMainFrame()->GetRenderWidgetHost()->SetActive(true); | |
| content::ShowPopupWidgetWaiter waiter(child_contents.get(), | |
| child_contents->GetPrimaryMainFrame()); | |
| EXPECT_TRUE(content::ExecJs(child_contents.get(), | |
| "document.querySelector('select').showPicker()")); | |
| // Android sometimes times out when waiting for the popup. | |
| #if !BUILDFLAG(IS_ANDROID) | |
| waiter.Wait(); | |
| #endif | |
| } | |
| // Runs the browser with renderer accessibility forced on so the plugin sends | |
| // real parent-accessibility info over mojo to the SurfaceEmbedHost. | |
| class SurfaceEmbedAccessibilityBrowserTest : public SurfaceEmbedBrowserTest { | |
| public: | |
| void SetUpCommandLine(base::CommandLine* command_line) override { | |
| SurfaceEmbedBrowserTest::SetUpCommandLine(command_line); | |
| command_line->AppendSwitch(::switches::kForceRendererAccessibility); | |
| } | |
| }; | |
| // Verifies the renderer->browser accessibility path. With accessibility | |
| // enabled, the renderer plugin sends SetParentAccessibilityInfo over mojo and | |
| // the host receives it. | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedAccessibilityBrowserTest, | |
| PluginSendsParentAccessibilityInfoToHost) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| WaitForHostCount(kSingleEmbedCount); | |
| ASSERT_EQ(kSingleEmbedCount, GetHostCount()); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(nullptr, host); | |
| EXPECT_NE(web_contents()->GetPrimaryMainFrame()->GetAXTreeID(), | |
| ui::AXTreeIDUnknown()); | |
| // The renderer plugin sends SetParentAccessibilityInfo over mojo; wait until | |
| // the host receives it. | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return host->HasReceivedParentAccessibilityInfoForTesting(); })); | |
| } | |
| // TODO(crbug.com/555865331): Flaky on Android. | |
| #if BUILDFLAG(IS_ANDROID) | |
| #define MAYBE_FocusPreservedAfterNavigation \ | |
| DISABLED_FocusPreservedAfterNavigation | |
| #else | |
| #define MAYBE_FocusPreservedAfterNavigation FocusPreservedAfterNavigation | |
| #endif | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| MAYBE_FocusPreservedAfterNavigation) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| // Click/focus the outer page first to ensure the window is focused. | |
| content::SimulateMouseClickOrTapElementWithId(web_contents(), "outer1"); | |
| EXPECT_TRUE( | |
| content::EvalJs(web_contents(), "document.hasFocus()").ExtractBool()); | |
| // Script focus within the child should not move page focus across the | |
| // WebContents boundary. | |
| EXPECT_TRUE(content::ExecJs(child_contents.get(), | |
| "document.getElementById('inner').focus()")); | |
| // Wait a little to ensure that focus really does not change. | |
| base::RunLoop focus_settle_loop; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, focus_settle_loop.QuitClosure(), base::Milliseconds(100)); | |
| focus_settle_loop.Run(); | |
| EXPECT_EQ(web_contents(), content::GetFocusedWebContents(web_contents())); | |
| EXPECT_TRUE( | |
| content::EvalJs(web_contents(), "document.hasFocus()").ExtractBool()); | |
| EXPECT_EQ("inner", | |
| content::EvalJs(child_contents.get(), "document.activeElement.id")); | |
| // Click the child's input element to focus it. | |
| content::WaitForHitTestData(child_contents.get()); | |
| auto inner_center = content::GetCenterCoordinatesOfElementWithId( | |
| child_contents.get(), "inner"); | |
| // Child embed is at (10, 50) in the parent coordinate space. | |
| gfx::Point click_point(static_cast<int>(inner_center.x()) + 10, | |
| static_cast<int>(inner_center.y()) + 50); | |
| content::SimulateMouseClickAt( | |
| web_contents(), 0, blink::WebMouseEvent::Button::kLeft, click_point); | |
| // Wait for the focus change to propagate. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool(); | |
| })); | |
| EXPECT_TRUE( | |
| content::EvalJs(web_contents(), "document.hasFocus()").ExtractBool()); | |
| EXPECT_TRUE(content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool()); | |
| // Navigate the child to a different site to force a | |
| // cross-process/cross-domain navigation. | |
| GURL cross_site_url = embedded_test_server()->GetURL("a.test", kInnerPageUrl); | |
| ASSERT_TRUE(content::NavigateToURL(child_contents.get(), cross_site_url)); | |
| ASSERT_TRUE(content::WaitForLoadStop(child_contents.get())); | |
| // Verify that the focus is preserved on the navigated child page. | |
| EXPECT_TRUE( | |
| content::EvalJs(web_contents(), "document.hasFocus()").ExtractBool()); | |
| EXPECT_TRUE(content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| AttachToFocusedEmbedHasPageFocus) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| // 1. Create an <embed> element without content ID and insert it into the DOM. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| content::JsReplace(R"( | |
| const embed = document.createElement('embed'); | |
| embed.setAttribute('type', $1); | |
| embed.style.position = 'absolute'; | |
| embed.style.left = '10px'; | |
| embed.style.top = '50px'; | |
| embed.style.width = '100px'; | |
| embed.style.height = '100px'; | |
| embed.id = 'my_embed'; | |
| const outer2 = document.getElementById('outer2'); | |
| document.body.insertBefore(embed, outer2); | |
| )", | |
| kInternalPluginMimeType))); | |
| WaitForHostCount(1); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(host, nullptr); | |
| // 2. Focus the <embed> element in the outer page before attaching. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('my_embed').focus()")); | |
| ASSERT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "my_embed" && | |
| content::EvalJs(web_contents(), "document.hasFocus()").ExtractBool(); | |
| })); | |
| // 3. Create the inner child WebContents. | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(child_contents.get()).Wait(); | |
| SurfaceEmbedHandle* embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(embedded_handle, nullptr); | |
| // 4. Attach the inner WebContents to the already focused <embed> by setting | |
| // data-content-id and re-triggering plugin creation. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| content::JsReplace(R"( | |
| const embed = document.getElementById('my_embed'); | |
| embed.setAttribute('data-content-id', $1); | |
| embed.removeAttribute('type'); | |
| embed.setAttribute('type', $2); | |
| )", embedded_handle->id().ToString(), kInternalPluginMimeType))); | |
| // 5. Verify that the child WebContents has page focus after attaching to the | |
| // already focused <embed>. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetSurfaceEmbedConnector() != nullptr && | |
| content::GetFocusedWebContents(web_contents()) == | |
| child_contents.get() && | |
| web_contents()->GetFocusedFrame() == | |
| child_contents->GetPrimaryMainFrame() && | |
| content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "my_embed" && | |
| content::EvalJs(web_contents(), "document.hasFocus()") | |
| .ExtractBool() && | |
| content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool(); | |
| })); | |
| EXPECT_TRUE( | |
| content::EvalJs(web_contents(), "document.hasFocus()").ExtractBool()); | |
| EXPECT_TRUE(content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool()); | |
| EXPECT_EQ(child_contents.get(), | |
| content::GetFocusedWebContents(web_contents())); | |
| EXPECT_EQ(child_contents->GetPrimaryMainFrame(), | |
| web_contents()->GetFocusedFrame()); | |
| EXPECT_EQ(child_contents.get(), | |
| content::GetFocusedWebContents(child_contents.get())); | |
| EXPECT_EQ(child_contents->GetPrimaryMainFrame(), | |
| child_contents->GetFocusedFrame()); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, FocusByTabKey) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(child_contents.get()).Wait(); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| content::WaitForHitTestData(child_contents.get()); | |
| // Focus outer1 which is before the embed tag. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('outer1').focus()")); | |
| ASSERT_EQ("outer1", | |
| content::EvalJs(web_contents(), "document.activeElement.id")); | |
| // Press tab, it should move into the embed element, which focuses the child | |
| // WebContents. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, false, false, false); | |
| // Focus should go to the embed element in the parent WebContents. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::GetFocusedWebContents(web_contents()) == | |
| child_contents.get() && | |
| web_contents()->GetFocusedFrame() == | |
| child_contents->GetPrimaryMainFrame() && | |
| content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "my_embed" && | |
| content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool() && | |
| content::EvalJs(child_contents.get(), "document.activeElement.id") == | |
| "inner"; | |
| })); | |
| // Keep pressing tab: focus should leave the embed element and move to the | |
| // next focusable element in the embedder page ('outer2'). | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, false, false, false); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "outer2" && | |
| content::EvalJs(web_contents(), "document.hasFocus()") | |
| .ExtractBool() && | |
| !content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool(); | |
| })); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, FocusByShiftTabKey) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(child_contents.get()).Wait(); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| ASSERT_TRUE(content::ExecJs(child_contents.get(), R"( | |
| const input = document.createElement('input'); | |
| input.id = 'inner2'; | |
| document.body.appendChild(input); | |
| )")); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| content::WaitForHitTestData(child_contents.get()); | |
| // Focus outer2, which follows the embed element in document order. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('outer2').focus()")); | |
| ASSERT_EQ("outer2", | |
| content::EvalJs(web_contents(), "document.activeElement.id")); | |
| // Shift+Tab from outer2 moves focus into the embed element, focusing the last | |
| // element ('inner2') in child WebContents. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, true, false, false); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::GetFocusedWebContents(web_contents()) == | |
| child_contents.get() && | |
| web_contents()->GetFocusedFrame() == | |
| child_contents->GetPrimaryMainFrame() && | |
| content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "my_embed" && | |
| content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool() && | |
| content::EvalJs(child_contents.get(), "document.activeElement.id") == | |
| "inner2"; | |
| })); | |
| // Shift+Tab again moves focus from inner2 to inner inside child WebContents. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, true, false, false); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool() && | |
| content::EvalJs(child_contents.get(), "document.activeElement.id") == | |
| "inner"; | |
| })); | |
| // Shift+Tab from inner moves focus out of the embed element backward to | |
| // outer1. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, true, false, false); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "outer1" && | |
| content::EvalJs(web_contents(), "document.hasFocus()") | |
| .ExtractBool() && | |
| !content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool(); | |
| })); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| FocusByTabKeyNoNextFocusableElement) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| // Remove outer2 so the embed element is the last focusable element. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('outer2').remove()")); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(child_contents.get()).Wait(); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| content::WaitForHitTestData(child_contents.get()); | |
| TakeFocusTrackingDelegate delegate; | |
| web_contents()->SetDelegate(&delegate); | |
| // Focus outer1. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('outer1').focus()")); | |
| ASSERT_EQ("outer1", | |
| content::EvalJs(web_contents(), "document.activeElement.id")); | |
| // Press tab to move into the embed element and focus child's 'inner'. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, false, false, false); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::GetFocusedWebContents(web_contents()) == | |
| child_contents.get() && | |
| web_contents()->GetFocusedFrame() == | |
| child_contents->GetPrimaryMainFrame() && | |
| content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "my_embed" && | |
| content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool() && | |
| content::EvalJs(child_contents.get(), "document.activeElement.id") == | |
| "inner"; | |
| })); | |
| // Press tab again. Since there is no next focusable element after the embed | |
| // element, focus should be handed off to the embedder WebContents' TakeFocus. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, false, false, false); | |
| delegate.WaitForTakeFocus(); | |
| EXPECT_TRUE(delegate.take_focus_called()); | |
| EXPECT_FALSE(delegate.take_focus_reverse()); | |
| EXPECT_FALSE(content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool()); | |
| web_contents()->SetDelegate(nullptr); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| FocusByShiftTabKeyNoPreviousFocusableElement) { | |
| NavigateToTestUrl(kFocusHarnessUrl); | |
| // Remove outer1 so the embed element is the first focusable element. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('outer1').remove()")); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::ReadyForInputObserver(child_contents.get()).Wait(); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| content::WaitForHitTestData(child_contents.get()); | |
| TakeFocusTrackingDelegate delegate; | |
| web_contents()->SetDelegate(&delegate); | |
| // Focus outer2. | |
| EXPECT_TRUE(content::ExecJs(web_contents(), | |
| "document.getElementById('outer2').focus()")); | |
| ASSERT_EQ("outer2", | |
| content::EvalJs(web_contents(), "document.activeElement.id")); | |
| // Press Shift+Tab to move into the embed element and focus child's 'inner'. | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, true, false, false); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::GetFocusedWebContents(web_contents()) == | |
| child_contents.get() && | |
| web_contents()->GetFocusedFrame() == | |
| child_contents->GetPrimaryMainFrame() && | |
| content::EvalJs(web_contents(), "document.activeElement.id") == | |
| "my_embed" && | |
| content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool() && | |
| content::EvalJs(child_contents.get(), "document.activeElement.id") == | |
| "inner"; | |
| })); | |
| // Press Shift+Tab again. Since there is no previous focusable element before | |
| // the embed element, focus should be handed off to the embedder WebContents' | |
| // TakeFocus (with reverse = true). | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, | |
| ui::VKEY_TAB, false, true, false, false); | |
| delegate.WaitForTakeFocus(); | |
| EXPECT_TRUE(delegate.take_focus_called()); | |
| EXPECT_TRUE(delegate.take_focus_reverse()); | |
| EXPECT_FALSE(content::EvalJs(child_contents.get(), "document.hasFocus()") | |
| .ExtractBool()); | |
| web_contents()->SetDelegate(nullptr); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, MultilevelDetachIntermediate) { | |
| NavigateToAttachHarness(); | |
| auto parent_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(parent_contents.get(), kAttachHarnessUrl); | |
| AttachChildToEmbedWithId(parent_contents.get(), "parent_embed"); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kRedBoxUrl); | |
| SurfaceEmbedHandle* child_embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(child_embedded_handle, nullptr); | |
| std::string attach_child_script = "createEmbed('" + | |
| child_embedded_handle->id().ToString() + | |
| "', 'child_embed');"; | |
| size_t expected_attachments = GetAttachedHostCount() + 1; | |
| ASSERT_TRUE(content::ExecJs(parent_contents.get(), attach_child_script)); | |
| ASSERT_TRUE(WaitForHostAttachment(expected_attachments)); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| EXPECT_NE(parent_contents->GetSurfaceEmbedConnector(), nullptr); | |
| EXPECT_EQ(content::Visibility::VISIBLE, child_contents->GetVisibility()); | |
| // Verify the child's red content renders through the 2-level chain. | |
| const gfx::Rect embed_bounds(10, 10, 100, 100); | |
| VerifyRedPixelInBounds(embed_bounds); | |
| // Remove the parent embed from the grandparent page, detaching the parent. | |
| EXPECT_TRUE(content::ExecJs( | |
| web_contents(), "document.getElementById('parent_embed').remove();")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return parent_contents->GetSurfaceEmbedConnector() == nullptr; | |
| })); | |
| // The child remains attached to the parent. | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, MultilevelFocusAndInput) { | |
| NavigateToTestUrl(kMultilevelHarnessUrl); | |
| auto parent_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(parent_contents.get(), kMultilevelParentUrl); | |
| AttachChildToEmbedWithId(parent_contents.get(), "parent_embed"); | |
| EXPECT_NE(parent_contents->GetSurfaceEmbedConnector(), nullptr); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| SurfaceEmbedHandle* child_embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(child_embedded_handle, nullptr); | |
| std::string attach_child_script = "createEmbed('" + | |
| child_embedded_handle->id().ToString() + | |
| "', 'child_embed');"; | |
| size_t expected_attachments = GetAttachedHostCount() + 1; | |
| ASSERT_TRUE(content::ExecJs(parent_contents.get(), attach_child_script)); | |
| ASSERT_TRUE(WaitForHostAttachment(expected_attachments)); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| // Focus outer1 in the grandparent and verify keyboard input. | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| content::SimulateMouseClickOrTapElementWithId(web_contents(), "outer1"); | |
| EXPECT_EQ("outer1", content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.activeElement.id")); | |
| EXPECT_EQ(web_contents(), content::GetFocusedWebContents(web_contents())); | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('g'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("g", | |
| content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.getElementById('outer1').value")); | |
| // Click the child's input through grandparent -> parent -> child. | |
| WaitForMultilevelViewSizes(parent_contents.get(), child_contents.get()); | |
| // Parent is at (10, 50) in the root, and child is at (10, 40) in | |
| // parent, so the child's bounds in root coordinates are (20, 90, 100, 100). | |
| const gfx::Rect child_embed_bounds(20, 90, 100, 100); | |
| VerifyRedPixelInBounds(child_embed_bounds); | |
| content::WaitForHitTestData(parent_contents.get()); | |
| content::WaitForHitTestData(child_contents.get()); | |
| auto inner_center = content::GetCenterCoordinatesOfElementWithId( | |
| child_contents.get(), "inner"); | |
| // Offset by embed positions: child in parent (10, 40) + parent in | |
| // grandparent (10, 50). | |
| gfx::Point click_point(static_cast<int>(inner_center.x()) + 10 + 10, | |
| static_cast<int>(inner_center.y()) + 40 + 50); | |
| content::SimulateMouseClickAt( | |
| web_contents(), 0, blink::WebMouseEvent::Button::kLeft, click_point); | |
| WaitForMultilevelFocusState(parent_contents.get(), child_contents.get(), | |
| "parent_embed"); | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('c'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("c", content::EvalJsAfterLifecycleUpdate( | |
| child_contents.get(), "", | |
| "document.getElementById('inner').value")); | |
| // Detach parent from grandparent and re-attach with child still connected. | |
| EXPECT_TRUE(content::ExecJs( | |
| web_contents(), "document.getElementById('parent_embed').remove();")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return parent_contents->GetSurfaceEmbedConnector() == nullptr; | |
| })); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| EXPECT_TRUE( | |
| base::test::RunUntil([&]() { return !HasPixelInColor(SK_ColorRED); })); | |
| AttachChildToEmbedWithId(parent_contents.get(), "reattached_embed"); | |
| EXPECT_NE(child_contents->GetSurfaceEmbedConnector(), nullptr); | |
| // Wait for child to become visible again after re-attach. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetVisibility() == content::Visibility::VISIBLE; | |
| })); | |
| // Click the child's input after re-attach. | |
| WaitForMultilevelViewSizes(parent_contents.get(), child_contents.get()); | |
| VerifyRedPixelInBounds(child_embed_bounds); | |
| content::WaitForHitTestData(parent_contents.get()); | |
| content::WaitForHitTestData(child_contents.get()); | |
| inner_center = content::GetCenterCoordinatesOfElementWithId( | |
| child_contents.get(), "inner"); | |
| click_point = gfx::Point(static_cast<int>(inner_center.x()) + 10 + 10, | |
| static_cast<int>(inner_center.y()) + 40 + 50); | |
| content::SimulateMouseClickAt( | |
| web_contents(), 0, blink::WebMouseEvent::Button::kLeft, click_point); | |
| WaitForMultilevelFocusState(parent_contents.get(), child_contents.get(), | |
| "reattached_embed"); | |
| EXPECT_TRUE(content::ExecJs(child_contents.get(), | |
| "document.getElementById('inner').value = '';")); | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('r'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("r", content::EvalJsAfterLifecycleUpdate( | |
| child_contents.get(), "", | |
| "document.getElementById('inner').value")); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| FocusAndInputWithInnerWebContents) { | |
| NavigateToTestUrl(kMultilevelHarnessUrl); | |
| auto parent_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(parent_contents.get(), kMultilevelParentUrl); | |
| AttachChildToEmbedWithId(parent_contents.get(), "parent_embed"); | |
| ASSERT_NE(parent_contents->GetSurfaceEmbedConnector(), nullptr); | |
| ASSERT_TRUE(content::ExecJs(parent_contents.get(), R"( | |
| new Promise(resolve => { | |
| const iframe = document.createElement('iframe'); | |
| iframe.id = 'child_frame'; | |
| iframe.style.position = 'absolute'; | |
| iframe.style.left = '10px'; | |
| iframe.style.top = '40px'; | |
| iframe.style.width = '100px'; | |
| iframe.style.height = '100px'; | |
| iframe.style.border = '0'; | |
| iframe.onload = resolve; | |
| iframe.src = 'about:blank'; | |
| document.body.appendChild(iframe); | |
| }); | |
| )")); | |
| content::RenderFrameHost* child_frame = content::ChildFrameAt( | |
| parent_contents->GetPrimaryMainFrame(), /*index=*/0); | |
| ASSERT_NE(child_frame, nullptr); | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kInnerPageUrl); | |
| content::WebContents* child_contents_ptr = child_contents.get(); | |
| base::test::TestFuture<content::RenderFrameHost*> prepared_frame; | |
| child_frame->PrepareForInnerWebContentsAttach(prepared_frame.GetCallback()); | |
| child_frame = prepared_frame.Get(); | |
| ASSERT_NE(child_frame, nullptr); | |
| parent_contents->AttachInnerWebContents(std::move(child_contents), | |
| child_frame, | |
| /*is_full_page=*/false); | |
| ASSERT_EQ(child_contents_ptr->GetOuterWebContents(), parent_contents.get()); | |
| EXPECT_EQ(child_contents_ptr->GetSurfaceEmbedConnector(), nullptr); | |
| content::ReadyForInputObserver(web_contents()).Wait(); | |
| content::SimulateMouseClickOrTapElementWithId(web_contents(), "outer1"); | |
| EXPECT_EQ("outer1", content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.activeElement.id")); | |
| EXPECT_EQ(web_contents(), content::GetFocusedWebContents(web_contents())); | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('g'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("g", | |
| content::EvalJsAfterLifecycleUpdate( | |
| web_contents(), "", "document.getElementById('outer1').value")); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| auto* parent_view = parent_contents->GetRenderWidgetHostView(); | |
| auto* child_view = child_contents_ptr->GetRenderWidgetHostView(); | |
| return parent_view && child_view && | |
| parent_view->GetViewBounds().size() == gfx::Size(200, 150) && | |
| child_view->GetViewBounds().size() == gfx::Size(100, 100); | |
| })); | |
| const gfx::Rect child_embed_bounds(20, 90, 100, 100); | |
| VerifyRedPixelInBounds(child_embed_bounds); | |
| content::WaitForHitTestData(parent_contents.get()); | |
| content::WaitForHitTestData(child_contents_ptr); | |
| auto inner_center = | |
| content::GetCenterCoordinatesOfElementWithId(child_contents_ptr, "inner"); | |
| gfx::Point click_point(static_cast<int>(inner_center.x()) + 10 + 10, | |
| static_cast<int>(inner_center.y()) + 40 + 50); | |
| content::SimulateMouseClickAt( | |
| web_contents(), 0, blink::WebMouseEvent::Button::kLeft, click_point); | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::GetFocusedWebContents(web_contents()) == child_contents_ptr; | |
| })); | |
| WaitForActiveElement(child_contents_ptr, "inner"); | |
| WaitForActiveElement(parent_contents.get(), "child_frame"); | |
| WaitForActiveElement(web_contents(), "parent_embed"); | |
| for (content::WebContents* contents : | |
| {web_contents(), parent_contents.get(), child_contents_ptr}) { | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return content::EvalJs(contents, "document.hasFocus()").ExtractBool(); | |
| })); | |
| } | |
| content::SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('c'), | |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, | |
| false); | |
| EXPECT_EQ("c", content::EvalJsAfterLifecycleUpdate( | |
| child_contents_ptr, "", | |
| "document.getElementById('inner').value")); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, ThrottlingPropagation) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbed(child_contents.get()); | |
| auto* connector = child_contents->GetSurfaceEmbedConnector(); | |
| ASSERT_NE(nullptr, connector); | |
| // Wait for initial visual properties to propagate to the child. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(150, 150); | |
| })); | |
| EXPECT_FALSE(connector->IsThrottledForTesting()); | |
| EXPECT_FALSE(connector->IsSubtreeThrottledForTesting()); | |
| EXPECT_FALSE(connector->IsDisplayLockedForTesting()); | |
| SurfaceEmbedHost* host = GetHost(0); | |
| ASSERT_NE(nullptr, host); | |
| host->OnEmbedElementThrottlingStatusChanged( | |
| mojom::RenderThrottlingStatus::New( | |
| /*is_throttled=*/true, | |
| /*subtree_throttled=*/true, | |
| /*display_locked=*/true)); | |
| EXPECT_TRUE(connector->IsThrottledForTesting()); | |
| EXPECT_TRUE(connector->IsSubtreeThrottledForTesting()); | |
| EXPECT_TRUE(connector->IsDisplayLockedForTesting()); | |
| } | |
| // Setting content-visibility:hidden directly on the <embed> element display- | |
| // locks the element in its own document. This lock is not reflected in the | |
| // containing frame's throttling bits, so it exercises the element-level | |
| // display-lock path in WebPluginContainerImpl. | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| DisplayLockThrottlingViaContentVisibility) { | |
| auto child_contents = SetupHarnessAndChild(); | |
| AttachChildToEmbedWithId(child_contents.get(), "my_embed"); | |
| auto* connector = child_contents->GetSurfaceEmbedConnector(); | |
| ASSERT_NE(nullptr, connector); | |
| // Wait for initial visual properties to propagate to the child. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { | |
| return connector->GetLocalFrameSizeInPixelsForTesting() == | |
| gfx::Size(150, 150); | |
| })); | |
| EXPECT_FALSE(connector->IsDisplayLockedForTesting()); | |
| // Display-lock the embed element itself. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('my_embed').style.contentVisibility = " | |
| "'hidden';")); | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return connector->IsDisplayLockedForTesting(); })); | |
| // Unlock and confirm the display-lock status clears. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('my_embed').style.contentVisibility = " | |
| "'visible';")); | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return !connector->IsDisplayLockedForTesting(); })); | |
| } | |
| // Display-locking the parent embed in the root document propagates display-lock | |
| // throttling to both the parent (directly, via the element-level path) and the | |
| // child (via the frame-level bit pushed down from the parent frame). | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| MultilevelDisplayLockThrottlingViaContentVisibility) { | |
| NavigateToAttachHarness(); | |
| // Setup Parent (P) WebContents. | |
| auto parent_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(parent_contents.get(), kAttachHarnessUrl); | |
| AttachChildToEmbedWithId(parent_contents.get(), "parent_embed"); | |
| // Setup Child (C) WebContents and attach it to P. | |
| auto child_contents = CreateChildWebContents(); | |
| NavigateChildToUrl(child_contents.get(), kRedBoxUrl); | |
| SurfaceEmbedHandle* child_embedded_handle = | |
| SurfaceEmbedHandle::CreateForWebContents(child_contents.get()); | |
| ASSERT_NE(child_embedded_handle, nullptr); | |
| std::string attach_child_script = "createEmbed('" + | |
| child_embedded_handle->id().ToString() + | |
| "', 'child_embed');"; | |
| size_t expected_attachments = GetAttachedHostCount() + 1; | |
| ASSERT_TRUE(content::ExecJs(parent_contents.get(), attach_child_script)); | |
| ASSERT_TRUE(WaitForHostAttachment(expected_attachments)); | |
| auto* parent_connector = parent_contents->GetSurfaceEmbedConnector(); | |
| auto* child_connector = child_contents->GetSurfaceEmbedConnector(); | |
| ASSERT_NE(nullptr, parent_connector); | |
| ASSERT_NE(nullptr, child_connector); | |
| EXPECT_FALSE(parent_connector->IsDisplayLockedForTesting()); | |
| EXPECT_FALSE(child_connector->IsDisplayLockedForTesting()); | |
| // Display-lock the parent embed in the root document. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('parent_embed').style.contentVisibility = " | |
| "'hidden';")); | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return parent_connector->IsDisplayLockedForTesting(); })); | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return child_connector->IsDisplayLockedForTesting(); })); | |
| // Unlock and confirm both connectors clear. | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.getElementById('parent_embed').style.contentVisibility = " | |
| "'visible';")); | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return !parent_connector->IsDisplayLockedForTesting(); })); | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return !child_connector->IsDisplayLockedForTesting(); })); | |
| } | |
| IN_PROC_BROWSER_TEST_F(SurfaceEmbedBrowserTest, | |
| TransparentChildWebContentsAndUnsubmittedSurface) { | |
| NavigateToAttachHarness(); | |
| ASSERT_TRUE(content::ExecJs( | |
| web_contents(), | |
| "document.body.style.backgroundColor = 'rgb(0, 255, 0)';")); | |
| // 1. Attach an unnavigated child WebContents (which has no active surface | |
| // yet, matching `webui_examples --webshell`). The parent's green background | |
| // should show through the <embed> bounds rather than being occluded/culled. | |
| std::unique_ptr<content::WebContents> child_contents = | |
| CreateChildWebContents(); | |
| AttachChildToEmbedWithId(child_contents.get(), "embed1"); | |
| gfx::Rect scaled_embed_bounds; | |
| CalculateBitmapBoundsToCheck(gfx::Rect(10, 10, 100, 100), | |
| &scaled_embed_bounds); | |
| EXPECT_TRUE( | |
| CheckHasPixelInColorInBitmapBounds(SK_ColorGREEN, scaled_embed_bounds)); | |
| EXPECT_FALSE(HasPixelInColor(SK_ColorBLUE)); | |
| // 2. Navigate the child WebContents, make its background transparent, and | |
| // render a 50x50 red box <div>. The red box is intentional to verify that | |
| // opaque foreground content in the child is still painted (checked in the | |
| // top-left region below), while the surrounding background is transparent | |
| // and shows the parent's green background (checked in the bottom-right | |
| // region). | |
| // | |
| // Transparency must be set at 3 places corresponding to the nested | |
| // container levels: | |
| // - RenderWidgetHostView (window/compositor default background) | |
| // - html / documentElement (styled opaque red by `kRedBoxUrl`) | |
| // - body (styled opaque red by `kRedBoxUrl`) | |
| // - 50x50 red box <div> | |
| // If any level above the <div> uses an opaque background, the resulting | |
| // background will be opaque. | |
| NavigateChildToUrl(child_contents.get(), kRedBoxUrl); | |
| ASSERT_NE(nullptr, child_contents->GetRenderWidgetHostView()); | |
| // TODO(crbug.com/565576788): Remove the `RunUntil` wait below (and allow | |
| // calling `SetBackgroundColor(SK_ColorTRANSPARENT)` at any time, e.g. before | |
| // `NavigateChildToUrl` or after `ExecJs`) once | |
| // `RenderWidgetHostViewChildFrame::UpdateBackgroundColor()` stops using | |
| // `content_background_color_` to overwrite `SetBackgroundOpaque(false)`. | |
| // Until then, wait for `kRedBoxUrl`'s initial frame metadata (`SK_ColorRED`) | |
| // to arrive at the browser and set `SK_ColorTRANSPARENT` before clearing the | |
| // DOM backgrounds in JS so an in-flight opaque `content_background_color_` | |
| // update does not clobber `SetBackgroundOpaque(false)`. | |
| ASSERT_TRUE(base::test::RunUntil([&]() { | |
| return child_contents->GetRenderWidgetHostView()->GetBackgroundColor() == | |
| SK_ColorRED; | |
| })); | |
| child_contents->GetRenderWidgetHostView()->SetBackgroundColor( | |
| SK_ColorTRANSPARENT); | |
| ASSERT_TRUE(content::ExecJs(child_contents.get(), R"( | |
| document.documentElement.style.backgroundColor = 'transparent'; | |
| document.body.style.backgroundColor = 'transparent'; | |
| const box = document.createElement('div'); | |
| box.style.width = '50px'; | |
| box.style.height = '50px'; | |
| box.style.backgroundColor = 'rgb(255, 0, 0)'; | |
| document.body.appendChild(box); | |
| )")); | |
| gfx::Rect scaled_top_left_bounds; | |
| CalculateBitmapBoundsToCheck(gfx::Rect(15, 15, 30, 30), | |
| &scaled_top_left_bounds); | |
| gfx::Rect scaled_bottom_right_bounds; | |
| CalculateBitmapBoundsToCheck(gfx::Rect(70, 70, 30, 30), | |
| &scaled_bottom_right_bounds); | |
| // Wait until both the child's red box and the parent's green background | |
| // (showing through the transparent region of the child) are visible. | |
| EXPECT_TRUE( | |
| CheckHasPixelInColorInBitmapBounds(SK_ColorRED, scaled_top_left_bounds)); | |
| EXPECT_TRUE(CheckHasPixelInColorInBitmapBounds(SK_ColorGREEN, | |
| scaled_bottom_right_bounds)); | |
| } | |
| } // namespace surface_embed |