// 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 "chrome/browser/lacros/lacros_extension_apps_publisher.h"
#include <optional>
#include "base/run_loop.h"
#include "chrome/browser/apps/app_service/extension_apps_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/browser/lacros/for_which_extension_type.h"
#include "chrome/browser/lacros/lacros_extensions_util.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
#include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/crosapi/mojom/app_service_types.mojom.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/app_window/native_app_window.h"
#include "extensions/common/constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "ui/aura/window.h"
namespace {
using Apps = std::vector<apps::AppPtr>;
using Accesses = std::vector<apps::CapabilityAccessPtr>;
// This fake intercepts and tracks all calls to Publish().
class LacrosExtensionAppsPublisherFake : public LacrosExtensionAppsPublisher {
public:
LacrosExtensionAppsPublisherFake()
: LacrosExtensionAppsPublisher(InitForChromeApps()) {
// Since LacrosExtensionAppsPublisherTest run without Ash, Lacros won't get
// the Ash extension keeplist data from Ash (passed via crosapi). Therefore,
// set empty ash keeplist for test.
extensions::SetEmptyAshKeeplistForTest();
apps::EnableHostedAppsInLacrosForTesting();
}
~LacrosExtensionAppsPublisherFake() override = default;
LacrosExtensionAppsPublisherFake(const LacrosExtensionAppsPublisherFake&) =
delete;
LacrosExtensionAppsPublisherFake& operator=(
const LacrosExtensionAppsPublisherFake&) = delete;
void VerifyAppCapabilityAccess(const std::string& app_id,
size_t count,
std::optional<bool> accessing_camera,
std::optional<bool> accessing_microphone) {
ASSERT_EQ(count, accesses_history().size());
Accesses& accesses = accesses_history().back();
ASSERT_EQ(1u, accesses.size());
EXPECT_EQ(app_id, accesses[0]->app_id);
if (accessing_camera.has_value()) {
ASSERT_TRUE(accesses[0]->camera.has_value());
EXPECT_EQ(accessing_camera.value(), accesses[0]->camera.value());
} else {
ASSERT_FALSE(accesses[0]->camera.has_value());
}
if (accessing_microphone.has_value()) {
ASSERT_TRUE(accesses[0]->microphone.has_value());
EXPECT_EQ(accessing_microphone.value(), accesses[0]->microphone.value());
} else {
ASSERT_FALSE(accesses[0]->microphone.has_value());
}
}
std::vector<Apps>& apps_history() { return apps_history_; }
std::vector<Accesses>& accesses_history() { return accesses_history_; }
std::map<std::string, std::string>& app_windows() { return app_windows_; }
const apps::App* GetLastPublishedChangeForExtensionId() {
for (size_t i = apps_history_.size() - 1; i >= 0; --i) {
if (auto it = std::find_if(
apps_history_[i].begin(), apps_history_[i].end(),
[](const apps::AppPtr& app) {
Profile* profile = nullptr;
const extensions::Extension* extension = nullptr;
if (lacros_extensions_util::GetProfileAndExtension(
app->app_id, &profile, &extension)) {
return extension->id() == extensions::kWebStoreAppId;
}
return false;
});
it != apps_history_[i].end()) {
return (*it).get();
}
}
return nullptr;
}
private:
// Override to intercept calls to Publish().
void Publish(Apps apps) override { apps_history_.push_back(std::move(apps)); }
// Override to intercept calls to PublishCapabilityAccesses().
void PublishCapabilityAccesses(Accesses accesses) override {
accesses_history_.push_back(std::move(accesses));
}
// Override to intercept calls to OnAppWindowAdded().
void OnAppWindowAdded(const std::string& app_id,
const std::string& window_id) override {
app_windows_[window_id] = app_id;
}
// Override to intercept calls to OnAppWindowRemoved().
void OnAppWindowRemoved(const std::string& app_id,
const std::string& window_id) override {
EXPECT_TRUE(app_windows_.find(window_id) != app_windows_.end());
EXPECT_EQ(app_windows_[window_id], app_id);
app_windows_.erase(window_id);
}
// Override to pretend that crosapi is initialized.
bool InitializeCrosapi() override { return true; }
// Holds the contents of all calls to Publish() in chronological order.
std::vector<Apps> apps_history_;
// Holds the contents of all calls to PublishCapabilityAccesses() in
// chronological order.
std::vector<Accesses> accesses_history_;
// Holds the list of currently showing app windows, as seen by
// OnAppWindowAdded() and OnAppWindowRemoved(). The key is the window_id and
// the value is the app_id.
std::map<std::string, std::string> app_windows_;
};
const size_t kDefaultAppsSize = 1u;
// Verify that only default apps have been published. Web store app
// (hosted app) is the default app that is always loaded by chrome component
// extension loader.
void VerifyOnlyDefaultAppsPublished(
LacrosExtensionAppsPublisherFake* publisher) {
ASSERT_GE(publisher->apps_history().size(), 1u);
Apps& default_apps = publisher->apps_history()[0];
ASSERT_EQ(kDefaultAppsSize, default_apps.size());
auto& default_app = default_apps[0];
Profile* profile = nullptr;
const extensions::Extension* extension = nullptr;
bool success = lacros_extensions_util::GetProfileAndExtension(
default_app->app_id, &profile, &extension);
ASSERT_TRUE(success);
ASSERT_TRUE(extension->is_hosted_app());
ASSERT_EQ(extensions::kWebStoreAppId, extension->id());
ASSERT_TRUE(default_app->is_platform_app.has_value());
ASSERT_FALSE(default_app->is_platform_app.value());
}
// Adds a fake media device with the specified `stream_type` and starts
// capturing. Returns a closure to stop the capturing.
base::OnceClosure StartMediaCapture(content::WebContents* web_contents,
blink::mojom::MediaStreamType stream_type) {
blink::mojom::StreamDevices fake_devices;
blink::MediaStreamDevice device(stream_type, "fake_device", "fake_device");
if (blink::IsAudioInputMediaType(stream_type)) {
fake_devices.audio_device = device;
} else {
fake_devices.video_device = device;
}
std::unique_ptr<content::MediaStreamUI> ui =
MediaCaptureDevicesDispatcher::GetInstance()
->GetMediaStreamCaptureIndicator()
->RegisterMediaStream(web_contents, fake_devices);
ui->OnStarted(base::RepeatingClosure(),
content::MediaStreamUI::SourceCallback(),
/*label=*/std::string(), /*screen_capture_ids=*/{},
content::MediaStreamUI::StateChangeCallback());
return base::BindOnce(
[](std::unique_ptr<content::MediaStreamUI> ui) { ui.reset(); },
std::move(ui));
}
using LacrosExtensionAppsPublisherTest = extensions::ExtensionBrowserTest;
// When publisher is created and initialized, only chrome default apps
// should be published.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, DefaultApps) {
LoadExtension(test_data_dir_.AppendASCII("simple_with_file"));
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
ASSERT_TRUE(publisher->apps_history().empty());
publisher->Initialize();
VerifyOnlyDefaultAppsPublished(publisher.get());
}
// When publisher is created and initialized, only chrome default apps
// should be published.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest,
SystemFeaturesPoliciesWorkAsExpected) {
base::Value::List system_features;
system_features.Append(static_cast<int>(policy::SystemFeature::kWebStore));
g_browser_process->local_state()->SetList(
policy::policy_prefs::kSystemFeaturesDisableList,
std::move(system_features));
g_browser_process->local_state()->SetString(
policy::policy_prefs::kSystemFeaturesDisableMode,
policy::kBlockedDisableMode);
LoadExtension(test_data_dir_.AppendASCII("simple_with_file"));
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
ASSERT_TRUE(publisher->apps_history().empty());
publisher->Initialize();
// Verify that the Web Store has been published at least one time
ASSERT_GE(publisher->apps_history().size(), 1u);
// Set the policy to disable the Web Store app.
const apps::App* latest_web_store_app =
publisher->GetLastPublishedChangeForExtensionId();
EXPECT_EQ(latest_web_store_app->readiness,
apps::Readiness::kDisabledByPolicy);
ASSERT_TRUE(latest_web_store_app->show_in_launcher.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_launcher.value());
ASSERT_TRUE(latest_web_store_app->show_in_shelf.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_shelf.value());
ASSERT_TRUE(latest_web_store_app->show_in_search.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_search.value());
ASSERT_TRUE(latest_web_store_app->show_in_management.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_management.value());
// Set the policy to hide apps, Web Store should be republished as disable and
// hidden.
g_browser_process->local_state()->SetString(
policy::policy_prefs::kSystemFeaturesDisableMode,
policy::kHiddenDisableMode);
latest_web_store_app = publisher->GetLastPublishedChangeForExtensionId();
EXPECT_EQ(latest_web_store_app->readiness,
apps::Readiness::kDisabledByPolicy);
ASSERT_TRUE(latest_web_store_app->show_in_launcher.has_value());
EXPECT_FALSE(latest_web_store_app->show_in_launcher.value());
ASSERT_TRUE(latest_web_store_app->show_in_shelf.has_value());
EXPECT_FALSE(latest_web_store_app->show_in_shelf.value());
ASSERT_TRUE(latest_web_store_app->show_in_search.has_value());
EXPECT_FALSE(latest_web_store_app->show_in_search.value());
ASSERT_TRUE(latest_web_store_app->show_in_management.has_value());
EXPECT_FALSE(latest_web_store_app->show_in_management.value());
// Reset the policy, Web Store should be republished as enabled.
g_browser_process->local_state()->SetList(
policy::policy_prefs::kSystemFeaturesDisableList, base::Value::List());
latest_web_store_app = publisher->GetLastPublishedChangeForExtensionId();
EXPECT_EQ(latest_web_store_app->readiness, apps::Readiness::kReady);
ASSERT_TRUE(latest_web_store_app->show_in_launcher.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_launcher.value());
ASSERT_TRUE(latest_web_store_app->show_in_shelf.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_shelf.value());
ASSERT_TRUE(latest_web_store_app->show_in_search.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_search.value());
ASSERT_TRUE(latest_web_store_app->show_in_management.has_value());
EXPECT_TRUE(latest_web_store_app->show_in_management.value());
}
// If the profile has one app installed, then creating a publisher should
// immediately result in a call to Publish() with 1 entry.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, OneApp) {
LoadExtension(test_data_dir_.AppendASCII("simple_with_file"));
LoadExtension(test_data_dir_.AppendASCII("platform_apps/minimal"));
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
ASSERT_GE(publisher->apps_history().size(), 1u);
Apps& apps = publisher->apps_history()[0];
// The platform app is added after the default apps.
ASSERT_EQ(kDefaultAppsSize + 1u, apps.size());
auto& platform_app = apps.back();
ASSERT_TRUE(platform_app->is_platform_app.has_value());
ASSERT_TRUE(platform_app->is_platform_app.value());
}
// Same as OneApp, but with two pre-installed apps.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, TwoApps) {
LoadExtension(test_data_dir_.AppendASCII("simple_with_file"));
LoadExtension(test_data_dir_.AppendASCII("platform_apps/minimal"));
LoadExtension(test_data_dir_.AppendASCII("platform_apps/minimal_id"));
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
ASSERT_GE(publisher->apps_history().size(), 1u);
Apps& apps = publisher->apps_history()[0];
// The platform apps are added after the default apps.
ASSERT_EQ(kDefaultAppsSize + 2u, apps.size());
auto& platform_app_1 = apps[kDefaultAppsSize];
ASSERT_TRUE(platform_app_1->is_platform_app.has_value());
ASSERT_TRUE(platform_app_1->is_platform_app.value());
auto& platform_app_2 = apps[kDefaultAppsSize + 1];
ASSERT_TRUE(platform_app_2->is_platform_app.has_value());
ASSERT_TRUE(platform_app_2->is_platform_app.value());
}
// If an app is installed after the AppsPublisher is created, there should be a
// corresponding event.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest,
InstallAppAfterCreate) {
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
ASSERT_TRUE(publisher->apps_history().empty());
publisher->Initialize();
VerifyOnlyDefaultAppsPublished(publisher.get());
ASSERT_GE(publisher->apps_history().size(), 1u);
LoadExtension(test_data_dir_.AppendASCII("platform_apps/minimal"));
ASSERT_GE(publisher->apps_history().size(), 2u);
Apps& apps = publisher->apps_history().back();
ASSERT_EQ(1u, apps.size());
auto& platform_app = apps.back();
ASSERT_TRUE(platform_app->is_platform_app.has_value());
ASSERT_TRUE(platform_app->is_platform_app.value());
}
// If an app is unloaded, there should be a corresponding unload event.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, Unload) {
const extensions::Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("platform_apps/minimal"));
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
UnloadExtension(extension->id());
ASSERT_GE(publisher->apps_history().size(), 2u);
// The first event should be a ready event.
{
Apps& apps = publisher->apps_history()[0];
ASSERT_EQ(kDefaultAppsSize + 1u, apps.size());
ASSERT_EQ(apps.back()->readiness, apps::Readiness::kReady);
}
// The last event should be an unload event.
{
Apps& apps = publisher->apps_history().back();
ASSERT_EQ(1u, apps.size());
ASSERT_EQ(apps[0]->readiness, apps::Readiness::kDisabledByUser);
}
}
// If an app is uninstalled, there should be a corresponding uninstall event.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, Uninstall) {
const extensions::Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("platform_apps/minimal"));
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
UninstallExtension(extension->id());
ASSERT_GE(publisher->apps_history().size(), 2u);
// The first event should be a ready event.
{
Apps& apps = publisher->apps_history()[0];
ASSERT_EQ(2u, apps.size());
ASSERT_EQ(apps[1]->readiness, apps::Readiness::kReady);
}
// The last event should be an uninstall event.
{
Apps& apps = publisher->apps_history().back();
ASSERT_EQ(1u, apps.size());
ASSERT_EQ(apps[0]->readiness, apps::Readiness::kUninstalledByUser);
}
}
// If the app window is loaded after to creating the publisher, everything
// should still work.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, LaunchAppWindow) {
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
// There should be no windows tracked.
{
auto& app_windows = publisher->app_windows();
ASSERT_EQ(0u, app_windows.size());
}
// Load and launch the app.
const extensions::Extension* extension =
LoadAndLaunchApp(test_data_dir_.AppendASCII("platform_apps/minimal"));
auto* registry = extensions::AppWindowRegistry::Get(profile());
extensions::AppWindow* app_window =
registry->GetCurrentAppWindowForApp(extension->id());
ASSERT_TRUE(app_window);
// Check that the window is tracked correctly.
{
auto& app_windows = publisher->app_windows();
ASSERT_EQ(1u, app_windows.size());
EXPECT_EQ(app_windows.begin()->second, extension->id());
EXPECT_EQ(app_windows.begin()->first,
lacros_window_utility::GetRootWindowUniqueId(
app_window->GetNativeWindow()));
}
// Check that the window is no longer tracked. This process is asynchronous.
app_window->GetBaseWindow()->Close();
base::RunLoop run_loop;
run_loop.RunUntilIdle();
{
auto& app_windows = publisher->app_windows();
ASSERT_EQ(0u, app_windows.size());
}
}
// If the app window is loaded prior to creating the publisher, everything
// should still work.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, PreLaunchAppWindow) {
const extensions::Extension* extension =
LoadAndLaunchApp(test_data_dir_.AppendASCII("platform_apps/minimal"));
auto* registry = extensions::AppWindowRegistry::Get(profile());
extensions::AppWindow* app_window =
registry->GetCurrentAppWindowForApp(extension->id());
ASSERT_TRUE(app_window);
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
// Check that the window is tracked correctly.
{
auto& app_windows = publisher->app_windows();
ASSERT_EQ(1u, app_windows.size());
EXPECT_EQ(app_windows.begin()->second, extension->id());
EXPECT_EQ(app_windows.begin()->first,
lacros_window_utility::GetRootWindowUniqueId(
app_window->GetNativeWindow()));
}
// Check that the window is no longer tracked. This process is asynchronous.
app_window->GetBaseWindow()->Close();
base::RunLoop run_loop;
run_loop.RunUntilIdle();
{
auto& app_windows = publisher->app_windows();
ASSERT_EQ(0u, app_windows.size());
}
}
// Verify AppCapabilityAccess is modified for Chrome apps.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest,
RequestAccessingForPlatformApp) {
const extensions::Extension* extension =
LoadAndLaunchApp(test_data_dir_.AppendASCII("platform_apps/minimal"));
ASSERT_TRUE(extension);
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
auto* registry = extensions::AppWindowRegistry::Get(profile());
extensions::AppWindow* app_window =
registry->GetCurrentAppWindowForApp(extension->id());
ASSERT_TRUE(app_window);
content::WebContents* web_contents = app_window->web_contents();
ASSERT_TRUE(web_contents);
// Request accessing the camera for `web_contents`.
base::OnceClosure video_closure = StartMediaCapture(
web_contents, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
publisher->VerifyAppCapabilityAccess(extension->id(), 1u, true, std::nullopt);
// Request accessing the microphone for `web_contents`.
base::OnceClosure audio_closure = StartMediaCapture(
web_contents, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
publisher->VerifyAppCapabilityAccess(extension->id(), 2u, std::nullopt, true);
// Stop accessing the microphone for `web_contents`.
std::move(audio_closure).Run();
publisher->VerifyAppCapabilityAccess(extension->id(), 3u, std::nullopt,
false);
// Stop accessing the camera for `web_contents`.
std::move(video_closure).Run();
publisher->VerifyAppCapabilityAccess(extension->id(), 4u, false,
std::nullopt);
}
// Verify AppCapabilityAccess for web apps is not handled by
// LacrosExtensionAppsPublisher.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, NoAccessingForWebApp) {
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("app.com", "/ssl/google.html");
auto web_app_info =
web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(url);
web_app_info->scope = url;
auto app_id = web_app::test::InstallWebApp(browser()->profile(),
std::move(web_app_info));
// Launch `app_id` for the web app.
web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
web_app::NavigateViaLinkClickToURLAndWait(browser(), url);
content::WebContents* web_content =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_content);
// Request accessing the camera and microphone for `web_contents`.
base::OnceClosure video_closure1 = StartMediaCapture(
web_content, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
base::OnceClosure audio_closure1 = StartMediaCapture(
web_content, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
// Verify the publisher does not handle the access for the web app.
ASSERT_TRUE(publisher->accesses_history().empty());
}
// Verify AppCapabilityAccess for browser tabs is not handled by
// LacrosExtensionAppsPublisher.
IN_PROC_BROWSER_TEST_F(LacrosExtensionAppsPublisherTest, NoAccessingForTab) {
std::unique_ptr<LacrosExtensionAppsPublisherFake> publisher =
std::make_unique<LacrosExtensionAppsPublisherFake>();
publisher->Initialize();
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(),
embedded_test_server()->GetURL("app.com", "/ssl/google.html")));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
// Request accessing the camera and microphone for `web_contents`.
base::OnceClosure video_closure = StartMediaCapture(
web_contents, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
base::OnceClosure audio_closure = StartMediaCapture(
web_contents, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
// Verify the publisher does not handle the access for the tab.
ASSERT_TRUE(publisher->accesses_history().empty());
}
} // namespace