blob: a6ba49f692d84be31a51fff02c9d98fbde77973a [file]
// 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/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "components/guest_contents/browser/guest_contents_handle.h"
#include "components/surface_embed/browser/surface_embed_host.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_view.h"
#include "content/public/browser/web_contents.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 "mojo/public/cpp/bindings/binder_map.h"
#include "net/dns/mock_host_resolver.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/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 kInnerPageUrl = "/surface_embed/inner_page.html";
constexpr size_t kSingleEmbedCount = 1;
constexpr float kTestDeviceScaleFactor = 1.5f;
// 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 RegisterBrowserInterfaceBindersForFrame(
content::RenderFrameHost* render_frame_host,
mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
content::ContentBrowserTestContentBrowserClient::
RegisterBrowserInterfaceBindersForFrame(render_frame_host, map);
map->Add<mojom::SurfaceEmbedHost>(base::BindRepeating(
[](SurfaceEmbedHostTracker* tracker,
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<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_)));
}
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) {
guest_contents::GuestContentsHandle* guest_handle =
guest_contents::GuestContentsHandle::CreateForWebContents(
child_contents);
ASSERT_NE(guest_handle, nullptr);
std::string script = "createEmbed('" + guest_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));
}
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();
guest_contents::GuestContentsHandle* guest_handle =
guest_contents::GuestContentsHandle::CreateForWebContents(
child_contents.get());
ASSERT_NE(guest_handle, nullptr);
std::string script =
content::JsReplace("createEmbed($1);", guest_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()));
// Try attaching a new child. Still shouldn't crash.
// (Handling data-content-id changes isn't implemented yet, but once it is,
// this should help make sure we don't get confused).
auto child_contents2 = CreateChildWebContents();
NavigateChildToUrl(child_contents2.get(), kBlueBoxUrl);
guest_contents::GuestContentsHandle* guest_handle2 =
guest_contents::GuestContentsHandle::CreateForWebContents(
child_contents2.get());
EXPECT_TRUE(content::ExecJs(
web_contents(),
content::JsReplace(
"document.embeds[0].setAttribute('data-content-id', $1)",
guest_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,
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.
guest_contents::GuestContentsHandle* child_guest_handle =
guest_contents::GuestContentsHandle::CreateForWebContents(
child_contents.get());
ASSERT_NE(child_guest_handle, nullptr);
std::string attach_child_script = "createEmbed('" +
child_guest_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.
guest_contents::GuestContentsHandle* child_guest_handle =
guest_contents::GuestContentsHandle::CreateForWebContents(
child_contents.get());
ASSERT_NE(child_guest_handle, nullptr);
std::string attach_child_script = "createEmbed('" +
child_guest_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, 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.
guest_contents::GuestContentsHandle* guest_handle =
guest_contents::GuestContentsHandle::CreateForWebContents(
child_contents.get());
ASSERT_NE(guest_handle, nullptr);
std::string script = "createEmbed('" + guest_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, 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());
}
} // namespace surface_embed