| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "third_party/blink/public/common/switches.h" |
| #include "ui/gl/gl_switches.h" |
| |
| namespace { |
| |
| using content::WebContents; |
| |
| // Capturing page. |
| const char kCapturingPage[] = "/webrtc/conditional_focus_capturing_page.html"; |
| // Captured page. |
| const char kCapturedPage[] = "/webrtc/conditional_focus_captured_page.html"; |
| |
| // The tab to be captured is detected using this string, which is hard-coded |
| // in the HTML file. |
| const char kCapturedPageTitle[] = "Conditional Focus Test - Captured Page"; |
| |
| enum class FocusEnumValue { |
| kNoValue, // "" |
| kFocusCapturingApplication, // "focus-capturing-application" |
| kFocusCapturedSurface, // "focus-captured-surface" |
| kNoFocusChange // "no-focus-change" |
| }; |
| |
| const char* ToString(FocusEnumValue focus_enum_value) { |
| switch (focus_enum_value) { |
| case FocusEnumValue::kNoValue: |
| return ""; |
| case FocusEnumValue::kFocusCapturingApplication: |
| return "focus-capturing-application"; |
| case FocusEnumValue::kFocusCapturedSurface: |
| return "focus-captured-surface"; |
| case FocusEnumValue::kNoFocusChange: |
| return "no-focus-change"; |
| } |
| NOTREACHED(); |
| } |
| |
| enum class Tab { kUnknownTab, kCapturingTab, kCapturedTab }; |
| |
| } // namespace |
| |
| // Essentially depends on InProcessBrowserTest, but WebRtcTestBase provides |
| // detection of JS errors. |
| class ConditionalFocusInteractiveUiTest : public WebRtcTestBase { |
| public: |
| void SetUpInProcessBrowserTestFixture() override { |
| WebRtcTestBase::SetUpInProcessBrowserTestFixture(); |
| DetectErrorsInJavaScript(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| void SetUpOnMainThread() override { |
| WebRtcTestBase::SetUpOnMainThread(); |
| |
| base::RunLoop run_loop; |
| content::GetIOThreadTaskRunner({})->PostTaskAndReply( |
| FROM_HERE, base::BindOnce([]() { |
| content::SetConditionalFocusWindowForTesting(base::Seconds(5)); |
| }), |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitch( |
| switches::kEnableExperimentalWebPlatformFeatures); |
| command_line->AppendSwitchASCII( |
| switches::kAutoSelectTabCaptureSourceByTitle, kCapturedPageTitle); |
| // 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 |
| } |
| |
| WebContents* OpenTestPageInNewTab(const std::string& test_url) { |
| chrome::AddTabAt(browser(), GURL(url::kAboutBlankURL), -1, true); |
| GURL url = embedded_test_server()->GetURL(test_url); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| void SetUpTestTabs() { |
| captured_tab_ = OpenTestPageInNewTab(kCapturedPage); |
| capturing_tab_ = OpenTestPageInNewTab(kCapturingPage); |
| |
| TabStripModel* const tab_strip_model = browser()->tab_strip_model(); |
| |
| EXPECT_EQ(tab_strip_model->GetWebContentsAt(1), captured_tab_); |
| EXPECT_EQ(tab_strip_model->GetWebContentsAt(2), capturing_tab_); |
| EXPECT_EQ(tab_strip_model->GetActiveWebContents(), capturing_tab_); |
| } |
| |
| Tab ActiveTab() const { |
| WebContents* const active_web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| return active_web_contents == capturing_tab_ ? Tab::kCapturingTab |
| : active_web_contents == captured_tab_ ? Tab::kCapturedTab |
| : Tab::kUnknownTab; |
| } |
| |
| // Performs the following actions, in order: |
| // 1. Calls navigator.mediaDevices.getDisplayMedia(). The test fixture |
| // is set to simulate the user selecting |captured_tab_|. Expects success. |
| // 2. getDisplayMedia() returns a Promise<MediaStream> which resolves in |
| // a microtask. We hold up the main thread for |busy_wait_ms| so as to |
| // simulate either (a) an application which performs some non-trivial |
| // computation on that task, (b) intentional delay by the app or |
| // (c) random CPU delays. |
| // 3. Avoids calling setFocusBehavior() or does so with the appropriate |
| // value, depending on |focus_enum_value|. |
| // If !on_correct_microtask, calling setFocusBehavior() is done |
| // from a task that is scheduled to be executed later. |
| void Capture(int busy_wait_ms, |
| FocusEnumValue focus_enum_value, |
| bool on_correct_microtask = true, |
| const std::string& expected_result = "capture-success") { |
| std::string script_result; |
| EXPECT_EQ(content::EvalJs( |
| capturing_tab_->GetPrimaryMainFrame(), |
| base::StringPrintf("captureOtherTab(%d, '%s', %s);", |
| busy_wait_ms, ToString(focus_enum_value), |
| base::ToString(on_correct_microtask))), |
| expected_result); |
| } |
| |
| void Wait(base::TimeDelta timeout) { |
| base::RunLoop run_loop; |
| base::OneShotTimer timer; |
| timer.Start(FROM_HERE, timeout, run_loop.QuitClosure()); |
| run_loop.Run(); |
| timer.Stop(); |
| } |
| |
| // Repeatedly polls until a focus-switch to the captured tab is detected. |
| // If a switch is detected within 10s, returns true; otherwise, returns false. |
| bool WaitForFocusSwitchToCapturedTab() { |
| for (int i = 0; i < 20; ++i) { |
| if (ActiveTab() == Tab::kCapturedTab) { |
| return true; |
| } |
| Wait(base::Milliseconds(500)); |
| } |
| return (ActiveTab() == Tab::kCapturedTab); |
| } |
| |
| void CallFocusAndExpectError(const std::string& expected_error) { |
| EXPECT_EQ(content::EvalJs(capturing_tab_->GetPrimaryMainFrame(), |
| "callFocusAndExpectError();"), |
| expected_error); |
| } |
| |
| void CallSetFocusBehaviorBeforeCapture( |
| FocusEnumValue focus_enum_value_before_capture, |
| FocusEnumValue focus_enum_value_after_capture = FocusEnumValue::kNoValue, |
| const std::string& expected_result = "capture-success") { |
| EXPECT_EQ( |
| content::EvalJs( |
| capturing_tab_->GetPrimaryMainFrame(), |
| base::StringPrintf("callSetFocusBehaviorBeforeCapture('%s', '%s');", |
| ToString(focus_enum_value_before_capture), |
| ToString(focus_enum_value_after_capture))), |
| expected_result); |
| } |
| |
| protected: |
| raw_ptr<WebContents, AcrossTasksDanglingUntriaged> captured_tab_ = nullptr; |
| raw_ptr<WebContents, AcrossTasksDanglingUntriaged> capturing_tab_ = nullptr; |
| }; |
| |
| // Flaky on Win bots and on linux release bots http://crbug.com/1264744 |
| #if BUILDFLAG(IS_WIN) || (BUILDFLAG(IS_LINUX) && defined(NDEBUG)) |
| #define MAYBE_CapturedTabFocusedIfNoExplicitCallToFocus \ |
| DISABLED_CapturedTabFocusedIfNoExplicitCallToFocus |
| #else |
| #define MAYBE_CapturedTabFocusedIfNoExplicitCallToFocus \ |
| CapturedTabFocusedIfNoExplicitCallToFocus |
| #endif |
| IN_PROC_BROWSER_TEST_F(ConditionalFocusInteractiveUiTest, |
| MAYBE_CapturedTabFocusedIfNoExplicitCallToFocus) { |
| SetUpTestTabs(); |
| Capture(0, FocusEnumValue::kNoValue); |
| EXPECT_TRUE(WaitForFocusSwitchToCapturedTab()); |
| } |
| |
| // Flaky on Win bots and on linux release bots http://crbug.com/1264744 |
| #if BUILDFLAG(IS_WIN) || (BUILDFLAG(IS_LINUX) && defined(NDEBUG)) |
| #define MAYBE_CapturedTabFocusedIfExplicitlyCallingFocus \ |
| DISABLED_CapturedTabFocusedIfExplicitlyCallingFocus |
| #else |
| #define MAYBE_CapturedTabFocusedIfExplicitlyCallingFocus \ |
| CapturedTabFocusedIfExplicitlyCallingFocus |
| #endif |
| IN_PROC_BROWSER_TEST_F(ConditionalFocusInteractiveUiTest, |
| MAYBE_CapturedTabFocusedIfExplicitlyCallingFocus) { |
| SetUpTestTabs(); |
| Capture(0, FocusEnumValue::kFocusCapturedSurface); |
| EXPECT_TRUE(WaitForFocusSwitchToCapturedTab()); |
| } |
| |
| // This class only uses the values of FocusEnumValue that lead to the capturing |
| // application keeping focus. |
| class ConditionalFocusInteractiveUiTestWithFocusCapturingApplication |
| : public ConditionalFocusInteractiveUiTest, |
| public testing::WithParamInterface<FocusEnumValue> { |
| public: |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication() |
| : focus_behavior_(GetParam()) {} |
| |
| ~ConditionalFocusInteractiveUiTestWithFocusCapturingApplication() override = |
| default; |
| |
| protected: |
| const FocusEnumValue focus_behavior_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| _, |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication, |
| testing::Values(FocusEnumValue::kFocusCapturingApplication, |
| FocusEnumValue::kNoFocusChange)); |
| |
| // TODO(crbug.com/40913269): Flaky on a TSan bot. |
| #if BUILDFLAG(IS_LINUX) && (defined(THREAD_SANITIZER) \ |
| || defined(MEMORY_SANITIZER)) |
| #define MAYBE_CapturedTabNotFocusedIfExplicitlyCallingNoFocus \ |
| DISABLED_CapturedTabNotFocusedIfExplicitlyCallingNoFocus |
| #else |
| #define MAYBE_CapturedTabNotFocusedIfExplicitlyCallingNoFocus \ |
| CapturedTabNotFocusedIfExplicitlyCallingNoFocus |
| #endif |
| IN_PROC_BROWSER_TEST_P( |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication, |
| MAYBE_CapturedTabNotFocusedIfExplicitlyCallingNoFocus) { |
| SetUpTestTabs(); |
| Capture(0, focus_behavior_); |
| // Whereas calls to Wait() in previous tests served to minimize flakiness, |
| // this one is to prove no false-positives. Namely, we allow enough time |
| // for the focus-change, yet it does not occur. |
| Wait(base::Seconds(10)); |
| EXPECT_EQ(ActiveTab(), Tab::kCapturingTab); |
| } |
| |
| // TODO(crbug.com/40913269): Flaky on a TSan bot. |
| #if BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER) |
| #define MAYBE_CapturedTabFocusedIfAppWaitsTooLongBeforeCallingFocus \ |
| DISABLED_CapturedTabFocusedIfAppWaitsTooLongBeforeCallingFocus |
| #else |
| #define MAYBE_CapturedTabFocusedIfAppWaitsTooLongBeforeCallingFocus \ |
| CapturedTabFocusedIfAppWaitsTooLongBeforeCallingFocus |
| #endif |
| IN_PROC_BROWSER_TEST_P( |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication, |
| MAYBE_CapturedTabFocusedIfAppWaitsTooLongBeforeCallingFocus) { |
| SetUpTestTabs(); |
| Capture(15000, focus_behavior_); |
| EXPECT_TRUE(WaitForFocusSwitchToCapturedTab()); |
| } |
| |
| // This ensures that we don't have to wait for the entire duration of the |
| // conditional focus window before focus occurs. Rather, that is just the |
| // hard-limit that is employed in case the application attempts abuse by |
| // blocking the main thread for too long. |
| IN_PROC_BROWSER_TEST_F(ConditionalFocusInteractiveUiTest, |
| FocusTriggeredByMicrotask) { |
| SetUpTestTabs(); |
| Capture(0, FocusEnumValue::kNoValue); |
| // Recall that the test fixture set the conditional focus window to 5s. |
| Wait(base::Seconds(2)); |
| // Focus-change occurs before 5s elapse. |
| EXPECT_EQ(ActiveTab(), Tab::kCapturedTab); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ConditionalFocusInteractiveUiTest, |
| UserFocusChangeSuppressesFocusDecision) { |
| SetUpTestTabs(); |
| |
| // Recall that tab 0 is neither the capturing nor captured tab, |
| // and it is initially inactive. |
| ASSERT_NE(browser()->tab_strip_model()->GetWebContentsAt(0), captured_tab_); |
| ASSERT_NE(browser()->tab_strip_model()->GetWebContentsAt(0), capturing_tab_); |
| |
| // Start capturing, but give some time for a manual focus-switch by the user. |
| Capture(2500, FocusEnumValue::kFocusCapturedSurface); |
| |
| // Simulated manual user change of active tab. |
| browser()->tab_strip_model()->ActivateTabAt(0); |
| |
| // No additional focus-change - user activity has suppressed that. |
| Wait(base::Milliseconds(7500)); |
| EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents(), |
| browser()->tab_strip_model()->GetWebContentsAt(0)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ConditionalFocusInteractiveUiTest, |
| ExceptionRaisedIfFocusCalledAfterMicrotaskExecutes) { |
| // Setup. |
| SetUpTestTabs(); |
| Capture(0, FocusEnumValue::kFocusCapturedSurface, |
| /*on_correct_microtask=*/false, |
| /*expected_result=*/ |
| "InvalidStateError: Failed to execute 'setFocusBehavior' on " |
| "'CaptureController': The window of opportunity for focus-decision " |
| "is closed."); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ConditionalFocusInteractiveUiTest, FocusBeforeCapture) { |
| // Setup. |
| SetUpTestTabs(); |
| CallSetFocusBehaviorBeforeCapture(FocusEnumValue::kFocusCapturedSurface); |
| EXPECT_TRUE(WaitForFocusSwitchToCapturedTab()); |
| } |
| |
| // TODO(crbug.com/40913269): Flaky on TSan, MSan, and mac bots. |
| #if (BUILDFLAG(IS_LINUX) && \ |
| (defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER))) || \ |
| BUILDFLAG(IS_MAC) |
| #define MAYBE_NoFocusBeforeCapture DISABLED_NoFocusBeforeCapture |
| #else |
| #define MAYBE_NoFocusBeforeCapture NoFocusBeforeCapture |
| #endif |
| IN_PROC_BROWSER_TEST_P( |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication, |
| MAYBE_NoFocusBeforeCapture) { |
| // Setup. |
| SetUpTestTabs(); |
| CallSetFocusBehaviorBeforeCapture(focus_behavior_); |
| // Whereas calls to Wait() in previous tests served to minimize flakiness, |
| // this one is to prove no false-positives. Namely, we allow enough time |
| // for the focus-change, yet it does not occur. |
| Wait(base::Seconds(10)); |
| EXPECT_EQ(ActiveTab(), Tab::kCapturingTab); |
| } |
| |
| // TODO(crbug.com/40913269): Flaky on TSan and MSan bots. |
| #if BUILDFLAG(IS_LINUX) && \ |
| (defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER)) |
| #define MAYBE_NoFocusAfterCaptureOverrideFocusBeforeCapture \ |
| DISABLED_NoFocusAfterCaptureOverrideFocusBeforeCapture |
| #else |
| #define MAYBE_NoFocusAfterCaptureOverrideFocusBeforeCapture \ |
| NoFocusAfterCaptureOverrideFocusBeforeCapture |
| #endif |
| IN_PROC_BROWSER_TEST_P( |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication, |
| MAYBE_NoFocusAfterCaptureOverrideFocusBeforeCapture) { |
| // Setup. |
| SetUpTestTabs(); |
| CallSetFocusBehaviorBeforeCapture(FocusEnumValue::kFocusCapturedSurface, |
| focus_behavior_); |
| // Whereas calls to Wait() in previous tests served to minimize flakiness, |
| // this one is to prove no false-positives. Namely, we allow enough time |
| // for the focus-change, yet it does not occur. |
| Wait(base::Seconds(10)); |
| EXPECT_EQ(ActiveTab(), Tab::kCapturingTab); |
| } |
| |
| // TODO(crbug.com/40913269): Flaky on a TSan bot. |
| #if BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER) |
| #define MAYBE_FocusAfterCaptureOverrideNoFocusBeforeCapture \ |
| DISABLED_FocusAfterCaptureOverrideNoFocusBeforeCapture |
| #else |
| #define MAYBE_FocusAfterCaptureOverrideNoFocusBeforeCapture \ |
| FocusAfterCaptureOverrideNoFocusBeforeCapture |
| #endif |
| IN_PROC_BROWSER_TEST_P( |
| ConditionalFocusInteractiveUiTestWithFocusCapturingApplication, |
| MAYBE_FocusAfterCaptureOverrideNoFocusBeforeCapture) { |
| // Setup. |
| SetUpTestTabs(); |
| CallSetFocusBehaviorBeforeCapture(focus_behavior_, |
| FocusEnumValue::kFocusCapturedSurface); |
| EXPECT_TRUE(WaitForFocusSwitchToCapturedTab()); |
| } |