blob: c01ce3cc22f1889cd01305435ce9b2fd4aacb49d [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/api/tab_capture/tab_capture_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/permissions/active_tab_permission_granter.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/alert/tab_alert.h"
#include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/switches.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/gl/gl_switches.h"
#include "url/url_constants.h"
namespace extensions {
namespace {
constexpr char kExtensionId[] = "ddchlicdkolnonkihahngkmmmjnjlkkf";
constexpr char kValidChromeURL[] = "chrome://version";
class TabCaptureApiTest : public ExtensionApiTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
ExtensionApiTest::SetUpCommandLine(command_line);
// Specify smallish window size to make testing of tab capture less CPU
// intensive.
command_line->AppendSwitchASCII(::switches::kWindowSize, "300,300");
// MSan and GL do not get along so avoid using the GPU with MSan.
// TODO(crbug.com/40260482): Remove this after fixing feature
// detection in 0c tab capture path as it'll no longer be needed.
#if !BUILDFLAG(IS_CHROMEOS) && !defined(MEMORY_SANITIZER)
command_line->AppendSwitch(::switches::kUseGpuInTests);
#endif
}
void AddExtensionToCommandLineAllowlist() {
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
switches::kAllowlistedExtensionID, kExtensionId);
}
protected:
std::vector<tabs::TabAlert> GetTabAlertStatesForContents(
content::WebContents* web_contents) {
return tabs::TabAlertController::From(
tabs::TabInterface::GetFromContents(web_contents))
->GetAllActiveAlerts();
}
void SimulateMouseClickInCurrentTab() {
content::SimulateMouseClick(
browser()->tab_strip_model()->GetActiveWebContents(), 0,
blink::WebMouseEvent::Button::kLeft);
}
};
// Tests API behaviors, including info queries, and constraints violations.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, ApiTests) {
AddExtensionToCommandLineAllowlist();
ASSERT_TRUE(RunExtensionTest("tab_capture/api_tests",
{.extension_url = "api_tests.html"}))
<< message_;
}
// Tests that tab capture video frames can be received in a VIDEO element.
// TODO(crbug.com/216820236): This test is flaky.
// Possible culprit:
// The script uses a complicated animation loop to create frames, when simpler
// CSS animations would be fine.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, DISABLED_EndToEndWithoutRemoting) {
AddExtensionToCommandLineAllowlist();
// Note: The range of acceptable colors is quite large because there's no way
// to know whether software compositing is being used for screen capture; and,
// if software compositing is being used, there is no color space management
// and color values can be off by a lot. That said, color accuracy is being
// tested by a suite of content_browsertests.
ASSERT_TRUE(RunExtensionTest(
"tab_capture/end_to_end",
{.extension_url = "end_to_end.html?method=local&colorDeviation=50"}))
<< message_;
}
// Tests that getUserMedia() is NOT a way to start tab capture.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, GetUserMediaTest) {
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ASSERT_TRUE(RunExtensionTest("tab_capture/get_user_media_test",
{.extension_url = "get_user_media_test.html"}))
<< message_;
EXPECT_TRUE(listener.WaitUntilSatisfied());
content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK, false);
content::WebContents* web_contents =
browser()->OpenURL(params, /*navigation_handle_callback=*/{});
content::RenderFrameHost* const main_frame =
web_contents->GetPrimaryMainFrame();
ASSERT_TRUE(main_frame);
listener.Reply(base::StringPrintf("web-contents-media-stream://%i:%i",
main_frame->GetProcess()->GetDeprecatedID(),
main_frame->GetRoutingID()));
ResultCatcher catcher;
catcher.RestrictToBrowserContext(profile());
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
// Make sure tabCapture.capture only works if the tab has been granted
// permission via an extension icon click or the extension is allowlisted.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, ActiveTabPermission) {
ExtensionTestMessageListener before_open_tab("ready1",
ReplyBehavior::kWillReply);
ExtensionTestMessageListener before_grant_permission(
"ready2", ReplyBehavior::kWillReply);
ExtensionTestMessageListener before_open_new_tab("ready3",
ReplyBehavior::kWillReply);
ExtensionTestMessageListener before_allowlist_extension(
"ready4", ReplyBehavior::kWillReply);
ASSERT_TRUE(
RunExtensionTest("tab_capture/active_tab_permission_test",
{.extension_url = "active_tab_permission_test.html"}))
<< message_;
// Open a new tab and make sure capture is denied.
EXPECT_TRUE(before_open_tab.WaitUntilSatisfied());
content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK, false);
content::WebContents* web_contents =
browser()->OpenURL(params, /*navigation_handle_callback=*/{});
ASSERT_TRUE(web_contents) << "Failed to open new tab";
before_open_tab.Reply("");
// Grant permission and make sure capture succeeds.
EXPECT_TRUE(before_grant_permission.WaitUntilSatisfied());
const Extension* extension = ExtensionRegistry::Get(
web_contents->GetBrowserContext())->enabled_extensions().GetByID(
kExtensionId);
ActiveTabPermissionGranter::FromWebContents(web_contents)
->GrantIfRequested(extension);
before_grant_permission.Reply("");
// Open a new tab and make sure capture is denied.
EXPECT_TRUE(before_open_new_tab.WaitUntilSatisfied());
browser()->OpenURL(params, /*navigation_handle_callback=*/{});
before_open_new_tab.Reply("");
// Add extension to allowlist and make sure capture succeeds.
EXPECT_TRUE(before_allowlist_extension.WaitUntilSatisfied());
AddExtensionToCommandLineAllowlist();
before_allowlist_extension.Reply("");
ResultCatcher catcher;
catcher.RestrictToBrowserContext(profile());
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
// Tests that fullscreen transitions during a tab capture session dispatch
// events to the onStatusChange listener. The test loads a page that toggles
// fullscreen mode, using the Fullscreen Javascript API, in response to mouse
// clicks.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, FullscreenEvents) {
AddExtensionToCommandLineAllowlist();
ExtensionTestMessageListener capture_started("tab_capture_started");
ExtensionTestMessageListener entered_fullscreen("entered_fullscreen");
ASSERT_TRUE(RunExtensionTest("tab_capture/fullscreen_test",
{.extension_url = "fullscreen_test.html"}))
<< message_;
EXPECT_TRUE(capture_started.WaitUntilSatisfied());
// Click on the page to trigger the Javascript that will toggle the tab into
// fullscreen mode.
SimulateMouseClickInCurrentTab();
EXPECT_TRUE(entered_fullscreen.WaitUntilSatisfied());
// Click again to exit fullscreen mode.
SimulateMouseClickInCurrentTab();
// Wait until the page examines its results and calls chrome.test.succeed().
ResultCatcher catcher;
catcher.RestrictToBrowserContext(profile());
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, GrantForChromePages) {
ExtensionTestMessageListener before_open_tab("ready1",
ReplyBehavior::kWillReply);
ASSERT_TRUE(
RunExtensionTest("tab_capture/active_tab_chrome_pages",
{.extension_url = "active_tab_chrome_pages.html"}))
<< message_;
EXPECT_TRUE(before_open_tab.WaitUntilSatisfied());
// Open a tab on a chrome:// page and make sure we can capture.
NavigateToURLInNewTab(GURL(kValidChromeURL));
content::WebContents* web_contents = GetActiveWebContents();
const Extension* extension = ExtensionRegistry::Get(
web_contents->GetBrowserContext())->enabled_extensions().GetByID(
kExtensionId);
ActiveTabPermissionGranter::FromWebContents(web_contents)
->GrantIfRequested(extension);
before_open_tab.Reply("");
ResultCatcher catcher;
catcher.RestrictToBrowserContext(profile());
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
// Tests that a tab in incognito mode can be captured.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, CaptureInSplitIncognitoMode) {
AddExtensionToCommandLineAllowlist();
ASSERT_TRUE(RunExtensionTest(
"tab_capture/start_tab_capture",
{.extension_url = "start_tab_capture.html", .open_in_incognito = true},
{.allow_in_incognito = true}))
<< message_;
}
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, Constraints) {
AddExtensionToCommandLineAllowlist();
ASSERT_TRUE(RunExtensionTest("tab_capture/constraints",
{.extension_url = "constraints.html"}))
<< message_;
}
// Tests that the tab indicator (in the tab strip) is shown during tab capture.
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, TabIndicator) {
content::WebContents* const contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_THAT(GetTabAlertStatesForContents(contents), ::testing::IsEmpty());
// A TabStripModelObserver that quits the MessageLoop whenever the
// UI's model is sent an event that might change the indicator status.
class IndicatorChangeObserver : public TabStripModelObserver {
public:
explicit IndicatorChangeObserver(Browser* browser) : browser_(browser) {
browser_->tab_strip_model()->AddObserver(this);
}
void TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) override {
std::move(on_tab_changed_).Run();
}
void WaitForTabChange() {
base::RunLoop run_loop;
on_tab_changed_ = run_loop.QuitClosure();
run_loop.Run();
}
private:
const raw_ptr<Browser> browser_;
base::OnceClosure on_tab_changed_;
};
ASSERT_THAT(GetTabAlertStatesForContents(contents), ::testing::IsEmpty());
// Run an extension test that just turns on tab capture, which should cause
// the indicator to turn on.
AddExtensionToCommandLineAllowlist();
ASSERT_TRUE(RunExtensionTest("tab_capture/start_tab_capture",
{.extension_url = "start_tab_capture.html"}))
<< message_;
// Run the browser until the indicator turns on.
const base::TimeTicks start_time = base::TimeTicks::Now();
IndicatorChangeObserver observer(browser());
while (!base::Contains(GetTabAlertStatesForContents(contents),
tabs::TabAlert::TAB_CAPTURING)) {
if (base::TimeTicks::Now() - start_time >
TestTimeouts::action_max_timeout()) {
EXPECT_THAT(GetTabAlertStatesForContents(contents),
::testing::Contains(tabs::TabAlert::TAB_CAPTURING));
return;
}
observer.WaitForTabChange();
}
}
IN_PROC_BROWSER_TEST_F(TabCaptureApiTest, MultipleExtensions) {
ASSERT_TRUE(embedded_test_server()->Start());
// Load both extensions and wait for them to be ready.
base::FilePath base_path = test_data_dir_.AppendASCII("tab_capture")
.AppendASCII("multiple_extensions");
ExtensionTestMessageListener extension_a_ready("ready",
ReplyBehavior::kWillReply);
auto* extension_a = LoadExtension(base_path.AppendASCII("a"));
ASSERT_TRUE(extension_a);
extension_a_ready.set_extension_id(extension_a->id());
ASSERT_TRUE(extension_a_ready.WaitUntilSatisfied());
ExtensionTestMessageListener extension_b_ready("ready",
ReplyBehavior::kWillReply);
auto* extension_b = LoadExtension(base_path.AppendASCII("b"));
ASSERT_TRUE(extension_b);
extension_b_ready.set_extension_id(extension_b->id());
ASSERT_TRUE(extension_b_ready.WaitUntilSatisfied());
// Open a page and grant permissions.
content::OpenURLParams params(embedded_test_server()->GetURL("/simple.html"),
content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK, false);
content::WebContents* web_contents =
browser()->OpenURL(params, /*navigation_handle_callback=*/{});
ASSERT_TRUE(web_contents) << "Failed to open new tab";
auto* perm_granter =
ActiveTabPermissionGranter::FromWebContents(web_contents);
// It doesn't seem to work to grant permissions for both extensions at the
// same time. We start with extension_a.
perm_granter->GrantIfRequested(extension_a);
// Set up success listeners.
ExtensionTestMessageListener extension_a_success("success");
extension_a_success.set_extension_id(extension_a->id());
ExtensionTestMessageListener extension_b_success("success");
extension_b_success.set_extension_id(extension_b->id());
// Start a tab capture from extension_a.
extension_a_ready.Reply("");
extension_a_ready.Reset();
ASSERT_TRUE(extension_a_ready.WaitUntilSatisfied());
perm_granter->GrantIfRequested(extension_b);
// To reproduce crbug.com/1370338, we have extension_b spam tab capture
// requests until one or the other extension successfully captures the tab.
while (!extension_a_success.was_satisfied() &&
!extension_b_success.was_satisfied()) {
extension_b_ready.Reply("");
extension_b_ready.Reset();
ASSERT_TRUE(extension_b_ready.WaitUntilSatisfied());
}
// Only one capture should succeed.
// TODO(crbug.com/40874553): Remove this restriction.
ASSERT_TRUE(extension_a_success.was_satisfied() !=
extension_b_success.was_satisfied());
// Avoid CHECK for forgotten reply in ExtensionTestMessageListener destructor.
extension_a_ready.Reply("");
extension_b_ready.Reply("");
}
} // namespace
} // namespace extensions