// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_WEB_APPLICATIONS_SYSTEM_MEDIA_CONTROLS_BRIDGE_BROWSERTEST_H_
#define CHROME_BROWSER_WEB_APPLICATIONS_SYSTEM_MEDIA_CONTROLS_BRIDGE_BROWSERTEST_H_
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_shim/app_shim_host_mac.h"
#include "chrome/browser/apps/app_shim/app_shim_manager_mac.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/system_media_controls/system_media_controls.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/common/content_features.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/media_start_stop_observer.h"
#include "content/public/test/system_media_controls_bridge_test_utils.h"
#include "media/base/media_switches.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace system_media_controls {
namespace testing {
// Runs on macOS only. Tests SystemMedia
class SystemMediaControlsBridgeBrowsertest
: public web_app::WebAppBrowserTestBase {
public:
SystemMediaControlsBridgeBrowsertest() {
feature_list_.InitWithFeatures({features::kWebAppSystemMediaControls}, {});
}
SystemMediaControlsBridgeBrowsertest(
const SystemMediaControlsBridgeBrowsertest&) = delete;
SystemMediaControlsBridgeBrowsertest& operator=(
const SystemMediaControlsBridgeBrowsertest&) = delete;
~SystemMediaControlsBridgeBrowsertest() override = default;
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
// Start the test server so we can use the test media session pages.
https_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(https_server()->Start());
wait_for_bridge_creation_run_loop_.emplace();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(
switches::kAutoplayPolicy,
switches::autoplay::kNoUserGestureRequiredPolicy);
}
void MaybeWaitForAppShimConnection(AppShimHost* app_shim_host) {
// If the app shim has not yet connected, set the callback for testing.
if (!app_shim_host->HasBootstrapConnected()) {
base::test::TestFuture<void> future;
app_shim_host->SetOnShimConnectedForTesting(future.GetCallback());
EXPECT_TRUE(future.Wait());
}
}
void TearDownOnMainThread() override {
system_media_controls::SystemMediaControls::
SetVisibilityChangedCallbackForTesting(nullptr);
}
void StartPlaybackAndWaitForStart(Browser* browser,
const std::string& media_id) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
// Play the given media
web_contents->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
base::ASCIIToUTF16(content::JsReplace(
"document.getElementById($1).play();", media_id)),
base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL);
// Wait for start
content::MediaStartStopObserver observer(
web_contents, content::MediaStartStopObserver::Type::kStart);
observer.Wait();
}
void WaitForStop(Browser* browser, const std::string& id) {
if (!IsPlaying(browser, id)) {
return;
}
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
content::MediaStartStopObserver observer(
web_contents, content::MediaStartStopObserver::Type::kStop);
observer.Wait();
}
bool IsPlaying(Browser* browser, const std::string& id) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
return EvalJs(web_contents, content::JsReplace(
"!document.getElementById($1).paused;", id))
.ExtractBool();
}
void SetUpOnBridgeCreatedCallback(bool for_web_app) {
auto on_bridge_created = base::BindLambdaForTesting([&]() {
num_bridges_created_++;
wait_for_bridge_creation_run_loop_->Quit();
});
if (for_web_app) {
content::SetOnSystemMediaControlsBridgeCreatedCallbackForTesting(
std::move(on_bridge_created));
} else {
content::SetOnBrowserSystemMediaControlsBridgeCreatedCallbackForTesting(
std::move(on_bridge_created));
}
}
protected:
std::optional<base::RunLoop> wait_for_bridge_creation_run_loop_;
int num_bridges_created_ = 0;
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest, TwoApps) {
// Install and launch a test media session PWA.
webapps::AppId app_id1 =
InstallPWA(https_server()->GetURL("/media/session/media-session.html"));
Browser* web_app_browser1 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser1);
// Wait for the app shim to connect.
apps::AppShimManager* app_shim_manager = apps::AppShimManager::Get();
AppShimHost* app_shim_host =
app_shim_manager->FindHost(web_app_browser1->profile(), app_id1);
MaybeWaitForAppShimConnection(app_shim_host);
// At this point, WebAppSystemMediaControlsManager exists,
// but no SystemMediaControls have been made yet. Before playing media, tell
// the Manager where to call us back once it starts making SMCs/Bridges
SetUpOnBridgeCreatedCallback(/*for_web_app=*/true);
// Start playing the audio.
StartPlaybackAndWaitForStart(web_app_browser1, "long-video-loop");
// Wait for the SystemMediaControlsBridge to be created.
EXPECT_EQ(num_bridges_created_, 0);
wait_for_bridge_creation_run_loop_->Run();
// Verify that 1 bridge has been created.
EXPECT_EQ(num_bridges_created_, 1);
wait_for_bridge_creation_run_loop_.emplace(); // Reset run loop for reuse.
// Install and launch a different test media session PWA.
webapps::AppId app_id2 = InstallPWA(https_server()->GetURL(
"/media/session/media_controls/media-session2.html"));
Browser* web_app_browser2 = LaunchWebAppBrowserAndWait(app_id2);
EXPECT_TRUE(web_app_browser2);
// Wait for 2nd app shim to connect.
AppShimHost* app_shim_host2 =
app_shim_manager->FindHost(web_app_browser2->profile(), app_id2);
MaybeWaitForAppShimConnection(app_shim_host2);
// Start playing the audio.
StartPlaybackAndWaitForStart(web_app_browser2, "short-video-loop");
// Wait for the 2nd SystemMediaControlsBridge to be created.
wait_for_bridge_creation_run_loop_->Run();
// Verify that 2 bridges have been created.
EXPECT_EQ(num_bridges_created_, 2);
}
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest, OneBrowser) {
// Set up the browser SMCBridge listener.
SetUpOnBridgeCreatedCallback(/*for_web_app=*/false);
// Navigate the browser to the test media page.
NavigateParams params(
browser(), https_server()->GetURL("/media/session/media-session.html"),
ui::PAGE_TRANSITION_LINK);
ui_test_utils::NavigateToURL(¶ms);
StartPlaybackAndWaitForStart(browser(), "long-video-loop");
// Wait for the browser's SystemMediaControlsBridge to be made.
EXPECT_EQ(num_bridges_created_, 0);
wait_for_bridge_creation_run_loop_->Run();
EXPECT_EQ(num_bridges_created_, 1);
}
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest, OneBrowserOneApp) {
// Set up the browser SMCBridge listener.
SetUpOnBridgeCreatedCallback(/*for_web_app=*/false);
// Navigate the browser to the test media page.
NavigateParams params(
browser(), https_server()->GetURL("/media/session/media-session.html"),
ui::PAGE_TRANSITION_LINK);
ui_test_utils::NavigateToURL(¶ms);
StartPlaybackAndWaitForStart(browser(), "long-video-loop");
// Wait for the browser's SystemMediaControlsBridge to be made.
EXPECT_EQ(num_bridges_created_, 0);
wait_for_bridge_creation_run_loop_->Run();
EXPECT_EQ(num_bridges_created_, 1);
wait_for_bridge_creation_run_loop_.emplace();
// Install and launch a test media session PWA.
webapps::AppId app_id1 =
InstallPWA(https_server()->GetURL("/media/session/media-session.html"));
Browser* web_app_browser1 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser1);
// Wait for the app shim to connect.
apps::AppShimManager* app_shim_manager = apps::AppShimManager::Get();
AppShimHost* app_shim_host =
app_shim_manager->FindHost(web_app_browser1->profile(), app_id1);
MaybeWaitForAppShimConnection(app_shim_host);
// At this point, WebAppSystemMediaControlsManager exists,
// but no SystemMediaControls have been made yet. Before playing media, tell
// the Manager where to call us back once it starts making SMCs/Bridges
SetUpOnBridgeCreatedCallback(/*for_web_app=*/true);
// Start playing the audio.
StartPlaybackAndWaitForStart(web_app_browser1, "long-video-loop");
// Wait for the SystemMediaControlsBridge to be created.
EXPECT_EQ(num_bridges_created_, 1); // Should be 1 because of browser bridge
wait_for_bridge_creation_run_loop_->Run();
// Verify that another bridge has been created.
EXPECT_EQ(num_bridges_created_, 2);
}
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest, DuplicateApp) {
// Install and launch a test media session PWA.
webapps::AppId app_id1 =
InstallPWA(https_server()->GetURL("/media/session/media-session.html"));
Browser* web_app_browser1 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser1);
// Wait for the app shim to connect.
apps::AppShimManager* app_shim_manager = apps::AppShimManager::Get();
AppShimHost* app_shim_host =
app_shim_manager->FindHost(web_app_browser1->profile(), app_id1);
MaybeWaitForAppShimConnection(app_shim_host);
// At this point, WebAppSystemMediaControlsManager exists,
// but no SystemMediaControls have been made yet. Before playing media, tell
// the Manager where to call us back once it starts making SMCs/Bridges
SetUpOnBridgeCreatedCallback(/*for_web_app=*/true);
// Start playing the audio.
StartPlaybackAndWaitForStart(web_app_browser1, "long-video-loop");
// Wait for the SystemMediaControlsBridge to be created.
EXPECT_EQ(num_bridges_created_, 0);
wait_for_bridge_creation_run_loop_->Run();
// Verify that 1 bridge got made.
EXPECT_EQ(num_bridges_created_, 1);
wait_for_bridge_creation_run_loop_.emplace(); // Reset run loop for reuse.
// Launch THE SAME test media session PWA.
Browser* web_app_browser2 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser2);
// We don't need to wait for the app shim connection here because duplicate
// apps share 1 app shim.
// Start playing the audio.
StartPlaybackAndWaitForStart(web_app_browser2, "long-video-loop");
// Verify that still only 1 bridge got made, as duplicate apps also share 1
// SMCBridge.
EXPECT_EQ(num_bridges_created_, 1);
}
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest,
CommandQuitOneApp) {
// Install and launch a test media session PWA.
webapps::AppId app_id1 =
InstallPWA(https_server()->GetURL("/media/session/media-session.html"));
Browser* web_app_browser1 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser1);
// Wait for the app shim to connect.
apps::AppShimManager* app_shim_manager = apps::AppShimManager::Get();
AppShimHost* app_shim_host =
app_shim_manager->FindHost(web_app_browser1->profile(), app_id1);
MaybeWaitForAppShimConnection(app_shim_host);
// Start playing the audio.
StartPlaybackAndWaitForStart(web_app_browser1, "long-video-loop");
// Simulate Command+Q to quit the app
content::SimulateKeyPress(
web_app_browser1->tab_strip_model()->GetActiveWebContents(),
ui::DomKey::FromCharacter('Q'), ui::DomCode::US_Q, ui::VKEY_Q, false,
false, false, /*command=*/true);
}
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest,
NowPlayingInfoHiddenOnNavigationAway) {
// Install and launch a test media session PWA.
webapps::AppId app_id1 =
InstallPWA(https_server()->GetURL("/media/session/media-session.html"));
Browser* web_app_browser1 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser1);
// Wait for the app shim to connect.
apps::AppShimManager* app_shim_manager = apps::AppShimManager::Get();
AppShimHost* app_shim_host =
app_shim_manager->FindHost(web_app_browser1->profile(), app_id1);
MaybeWaitForAppShimConnection(app_shim_host);
// Register for a callback when the bridge is made. We don't really care about
// the bridge itself here, but this ensures everything will be set up.
SetUpOnBridgeCreatedCallback(/*for_web_app=*/true);
// Start the media session.
StartPlaybackAndWaitForStart(web_app_browser1, "long-video-loop");
// Wait for the SystemMediaControlsBridge to be created.
wait_for_bridge_creation_run_loop_->Run();
// Set up the listener so the app shim can tell us when the visibility of the
// now playing info changes.
base::RunLoop wait_for_visibility_change;
bool new_visibility = true;
auto visibility_changed_callback =
base::BindLambdaForTesting([&](bool is_visible) {
new_visibility = is_visible;
wait_for_visibility_change.Quit();
});
// Register the callback.
system_media_controls::SystemMediaControls::
SetVisibilityChangedCallbackForTesting(&visibility_changed_callback);
// Check the pwa is still playing, and navigate away to a different url.
EXPECT_TRUE(IsPlaying(web_app_browser1, "long-video-loop"));
GURL http_url2(https_server()->GetURL("/media/session/title1.html"));
NavigateParams params(web_app_browser1, http_url2, ui::PAGE_TRANSITION_LINK);
ui_test_utils::NavigateToURL(¶ms);
// Start the visibility changed run loop and wait for the app to tell us
// that the metadata has been cleared, therefore the controls should not be
// visible.
wait_for_visibility_change.Run();
EXPECT_FALSE(new_visibility);
}
IN_PROC_BROWSER_TEST_F(SystemMediaControlsBridgeBrowsertest,
NowPlayingInfoHiddenOnAudioEnd) {
// Set up a media session in 1 PWA.
// Install and launch a test media session PWA.
webapps::AppId app_id1 =
InstallPWA(https_server()->GetURL("/media/session/media-session.html"));
Browser* web_app_browser1 = LaunchWebAppBrowserAndWait(app_id1);
EXPECT_TRUE(web_app_browser1);
// Wait for the app shim to connect.
apps::AppShimManager* app_shim_manager = apps::AppShimManager::Get();
AppShimHost* app_shim_host =
app_shim_manager->FindHost(web_app_browser1->profile(), app_id1);
MaybeWaitForAppShimConnection(app_shim_host);
// Register for a callback when the bridge is made. We don't really care about
// the bridge itself here, but this ensures everything will be set up.
SetUpOnBridgeCreatedCallback(/*for_web_app=*/true);
// Start the media session and wait for the controls to become visible.
StartPlaybackAndWaitForStart(web_app_browser1, "short-video");
// Wait for the SystemMediaControlsBridge to be created.
wait_for_bridge_creation_run_loop_->Run();
// Set up the listener so the app shim can tell us when the visibility of the
// now playing info changes.
base::RunLoop wait_for_visibility_change;
bool new_visibility = true;
auto visibility_changed_callback =
base::BindLambdaForTesting([&](bool is_visible) {
new_visibility = is_visible;
wait_for_visibility_change.Quit();
});
// Register the callback.
system_media_controls::SystemMediaControls::
SetVisibilityChangedCallbackForTesting(&visibility_changed_callback);
// Wait for the audio track to end on its own.
WaitForStop(web_app_browser1, "short-video");
EXPECT_FALSE(IsPlaying(web_app_browser1, "short-video"));
// Start the visibility changed run loop and wait for the app to tell us
// that the metadata has been cleared, therefore the controls should not be
// visible.
wait_for_visibility_change.Run();
EXPECT_FALSE(new_visibility);
}
} // namespace testing
} // namespace system_media_controls
#endif // CHROME_BROWSER_WEB_APPLICATIONS_SYSTEM_MEDIA_CONTROLS_BRIDGE_MAC_BROWSERTEST_H_