blob: baa0b844a52f8a10d58a2a5464e2254a0210e3c5 [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 "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/extensions/chrome_extension_test_notification_observer.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_action_test_util.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/declarative/rules_registry.h"
#include "extensions/browser/api/declarative/rules_registry_service.h"
#include "extensions/browser/api/declarative_webrequest/webrequest_constants.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/rules_registry_ids.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/api/extension_action/action_info_test_util.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions {
namespace {
static constexpr char kDeclarativeContentManifest[] =
R"({
"name": "Declarative Content apitest",
"version": "0.1",
"manifest_version": 2,
"description": "end-to-end browser test for the declarative Content API",
"background": {
"scripts": ["background.js"],
"persistent": true
},
"page_action": {},
"permissions": ["declarativeContent", "bookmarks"],
"incognito": "%s"
})";
constexpr char kIncognitoSpecificBackground[] =
R"(var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;
var ShowAction = chrome.declarativeContent.ShowAction;
var inIncognitoContext = chrome.extension.inIncognitoContext;
var hostPrefix = chrome.extension.inIncognitoContext ?
'test_split' : 'test_normal';
var rule = {
conditions: [new PageStateMatcher({
pageUrl: {hostPrefix: hostPrefix}})],
actions: [new ShowAction()]
};
var onPageChanged = chrome.declarativeContent.onPageChanged;
onPageChanged.removeRules(undefined, function() {
onPageChanged.addRules([rule], function() {
chrome.test.sendMessage(
!inIncognitoContext ? 'ready' : 'ready (split)');
});
});)";
constexpr char kBackgroundHelpers[] =
R"(var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;
var ShowAction = chrome.declarativeContent.ShowAction;
var onPageChanged = chrome.declarativeContent.onPageChanged;
function setRulesInPageEnvironment(rules, responseString) {
return new Promise(resolve => {
onPageChanged.removeRules(undefined, function() {
onPageChanged.addRules(rules, function() {
if (chrome.runtime.lastError) {
resolve(chrome.runtime.lastError.message);
return;
}
resolve(responseString);
});
});
});
};
function setRules(rules, responseString) {
onPageChanged.removeRules(undefined, function() {
onPageChanged.addRules(rules, function() {
if (chrome.runtime.lastError) {
chrome.test.sendScriptResult(chrome.runtime.lastError.message);
return;
}
chrome.test.sendScriptResult(responseString);
});
});
};
function addRules(rules, responseString) {
onPageChanged.addRules(rules, function() {
if (chrome.runtime.lastError) {
chrome.test.sendScriptResult(chrome.runtime.lastError.message);
return;
}
chrome.test.sendScriptResult(responseString);
});
};
function removeRule(id, responseString) {
onPageChanged.removeRules([id], function() {
if (chrome.runtime.lastError) {
chrome.test.sendScriptResult(chrome.runtime.lastError.message);
return;
}
chrome.test.sendScriptResult(responseString);
});
};)";
#if BUILDFLAG(ENABLE_EXTENSIONS)
constexpr char kRulesExtensionName[] =
"Declarative content persistence apitest";
#endif
using ContextType = extensions::browser_test_util::ContextType;
class DeclarativeContentApiTest : public ExtensionApiTest {
public:
explicit DeclarativeContentApiTest(
ContextType context_type = ContextType::kNone)
: ExtensionApiTest(context_type) {}
DeclarativeContentApiTest(const DeclarativeContentApiTest&) = delete;
DeclarativeContentApiTest& operator=(const DeclarativeContentApiTest&) =
delete;
protected:
enum IncognitoMode { SPANNING, SPLIT };
// Checks that the rules are correctly evaluated for an extension in incognito
// mode |mode| when the extension's incognito enable state is
// |is_enabled_in_incognito|.
void CheckIncognito(IncognitoMode mode, bool is_enabled_in_incognito);
// Checks that the rules matching a bookmarked state of |is_bookmarked| are
// correctly evaluated on bookmark events.
void CheckBookmarkEvents(bool is_bookmarked);
static std::string FormatManifest(IncognitoMode mode);
TestExtensionDir ext_dir_;
};
std::string DeclarativeContentApiTest::FormatManifest(IncognitoMode mode) {
const char* const mode_string = mode == SPANNING ? "spanning" : "split";
return base::StringPrintf(kDeclarativeContentManifest, mode_string);
}
void DeclarativeContentApiTest::CheckIncognito(IncognitoMode mode,
bool is_enabled_in_incognito) {
std::string manifest = FormatManifest(mode);
ext_dir_.WriteManifest(manifest);
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"),
kIncognitoSpecificBackground);
ExtensionTestMessageListener ready("ready");
ExtensionTestMessageListener ready_incognito("ready (split)");
const Extension* extension = LoadExtension(
ext_dir_.UnpackedPath(), {.allow_in_incognito = is_enabled_in_incognito});
ASSERT_TRUE(extension);
// Cache the profile and active tab before opening the incognito tab.
Profile* regular_profile = profile();
ASSERT_FALSE(regular_profile->IsOffTheRecord());
content::WebContents* regular_tab = GetActiveWebContents();
ASSERT_FALSE(regular_tab->GetBrowserContext()->IsOffTheRecord());
content::WebContents* incognito_tab =
PlatformOpenURLOffTheRecord(regular_profile, GURL("about:blank"));
const ExtensionAction* incognito_action =
ExtensionActionManager::Get(incognito_tab->GetBrowserContext())
->GetExtensionAction(*extension);
ASSERT_TRUE(incognito_action);
ASSERT_TRUE(ready.WaitUntilSatisfied());
if (is_enabled_in_incognito && mode == SPLIT)
ASSERT_TRUE(ready_incognito.WaitUntilSatisfied());
const int incognito_tab_id = ExtensionTabUtil::GetTabId(incognito_tab);
EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id));
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(incognito_tab, GURL("http://test_split/")));
if (mode == SPLIT) {
EXPECT_EQ(is_enabled_in_incognito,
incognito_action->GetIsVisible(incognito_tab_id));
} else {
EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id));
}
EXPECT_FALSE(NavigateInRenderer(incognito_tab, GURL("http://test_normal/")));
if (mode != SPLIT) {
EXPECT_EQ(is_enabled_in_incognito,
incognito_action->GetIsVisible(incognito_tab_id));
} else {
EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id));
}
// Verify that everything works as expected in the non-incognito browser.
const ExtensionAction* action = ExtensionActionManager::Get(regular_profile)
->GetExtensionAction(*extension);
const int tab_id = ExtensionTabUtil::GetTabId(regular_tab);
EXPECT_FALSE(action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(regular_tab, GURL("http://test_normal/")));
EXPECT_TRUE(action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(regular_tab, GURL("http://test_split/")));
EXPECT_FALSE(action->GetIsVisible(tab_id));
}
void DeclarativeContentApiTest::CheckBookmarkEvents(bool match_is_bookmarked) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test1/")));
EXPECT_FALSE(action->GetIsVisible(tab_id));
static constexpr char kSetIsBookmarkedRule[] =
R"(setRules([{
conditions: [new PageStateMatcher({isBookmarked: %s})],
actions: [new ShowAction()]
}], 'test_rule');)";
EXPECT_EQ("test_rule",
ExecuteScriptInBackgroundPage(
extension->id(),
base::StringPrintf(kSetIsBookmarkedRule,
base::ToString(match_is_bookmarked))));
EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id));
// Check rule evaluation on add/remove bookmark.
bookmarks::BookmarkModel* bookmark_model =
BookmarkModelFactory::GetForBrowserContext(profile());
const bookmarks::BookmarkNode* node = bookmark_model->AddURL(
bookmark_model->other_node(), 0, u"title", GURL("http://test1/"));
EXPECT_EQ(match_is_bookmarked, action->GetIsVisible(tab_id));
bookmark_model->Remove(
node, bookmarks::metrics::BookmarkEditSource::kExtension, FROM_HERE);
EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id));
// Check rule evaluation on navigate to bookmarked and non-bookmarked URL.
bookmark_model->AddURL(bookmark_model->other_node(), 0, u"title",
GURL("http://test2/"));
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test2/")));
EXPECT_EQ(match_is_bookmarked, action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test3/")));
EXPECT_EQ(!match_is_bookmarked, action->GetIsVisible(tab_id));
}
class DeclarativeContentApiTestWithContextType
: public DeclarativeContentApiTest,
public testing::WithParamInterface<ContextType> {
public:
DeclarativeContentApiTestWithContextType()
: DeclarativeContentApiTest(GetParam()) {}
DeclarativeContentApiTestWithContextType(
const DeclarativeContentApiTestWithContextType&) = delete;
DeclarativeContentApiTestWithContextType& operator=(
const DeclarativeContentApiTestWithContextType&) = delete;
};
#if !BUILDFLAG(IS_ANDROID)
// Android only supports service worker.
INSTANTIATE_TEST_SUITE_P(PersistentBackground,
DeclarativeContentApiTestWithContextType,
::testing::Values(ContextType::kPersistentBackground));
#endif
// These tests use page_action, which is unavailable in MV3.
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
DeclarativeContentApiTestWithContextType,
::testing::Values(ContextType::kServiceWorkerMV2));
// TODO(crbug.com/40260777): Convert this to run in both modes.
IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, Overview) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(
FILE_PATH_LITERAL("background.js"),
R"(var declarative = chrome.declarative;
var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;
var ShowAction = chrome.declarativeContent.ShowAction;
var rule = {
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test1'}}),
new PageStateMatcher({
css: ["input[type='password']"]})],
actions: [new ShowAction()]
}
var testEvent = chrome.declarativeContent.onPageChanged;
testEvent.removeRules(undefined, function() {
testEvent.addRules([rule], function() {
chrome.test.sendMessage('ready')
});
});)");
ExtensionTestMessageListener ready("ready");
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
ASSERT_TRUE(ready.WaitUntilSatisfied());
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
// Observer to track page action visibility. This helps avoid
// flakes by waiting to check visibility until there is an
// actual update to the page action.
ChromeExtensionTestNotificationObserver test_observer(profile());
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test1/")));
// The declarative API should show the page action instantly, rather
// than waiting for the extension to run.
test_observer.WaitForPageActionVisibilityChangeTo(tab, 1);
EXPECT_TRUE(action->GetIsVisible(tab_id));
// Make sure leaving a matching page unshows the page action.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://not_checked/")));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 0);
EXPECT_FALSE(action->GetIsVisible(tab_id));
// Insert a password field to make sure that's noticed.
// Notice that we touch offsetTop to force a synchronous layout.
ASSERT_TRUE(content::ExecJs(
tab, R"(document.body.innerHTML = '<input type="password">';
document.body.offsetTop;)"));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 1);
EXPECT_TRUE(action->GetIsVisible(tab_id))
<< "Adding a matching element should show the page action.";
// Remove it again to make sure that reverts the action.
// Notice that we touch offsetTop to force a synchronous layout.
ASSERT_TRUE(content::ExecJs(tab, R"(document.body.innerHTML = 'Hello world';
document.body.offsetTop;)"));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 0);
EXPECT_FALSE(action->GetIsVisible(tab_id))
<< "Removing the matching element should hide the page action again.";
}
// Test that adds two rules pointing to single action instance.
// Regression test for http://crbug.com/574149.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
ReusedActionInstance) {
static constexpr char kBackgroundScript[] =
R"(var declarative = chrome.declarative;
var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;
var ShowAction = chrome.declarativeContent.ShowAction;
var actionInstance = new ShowAction();
var rule1 = {
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test1'}})],
actions: [actionInstance]
};
var rule2 = {
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test'}})],
actions: [actionInstance]
};
var testEvent = chrome.declarativeContent.onPageChanged;
testEvent.removeRules(undefined, function() {
testEvent.addRules([rule1, rule2], function() {
chrome.test.sendMessage('ready');
});
});)";
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundScript);
ExtensionTestMessageListener ready("ready");
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
// Wait for declarative rules to be set up.
profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting();
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
ASSERT_TRUE(ready.WaitUntilSatisfied());
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
// This navigation matches both rules.
// TODO(crbug.com/40764017): Understand why this is EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test1/")));
EXPECT_TRUE(action->GetIsVisible(tab_id));
}
// Tests that the rules are evaluated at the time they are added or removed.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
RulesEvaluatedOnAddRemove) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test1/")));
const std::string kAddTestRules =
R"(addRules([{
id: '1',
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test1'}})],
actions: [new ShowAction()]
}, {
id: '2',
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test2'}})],
actions: [new ShowAction()]
}], 'add_rules');)";
EXPECT_EQ("add_rules",
ExecuteScriptInBackgroundPage(extension->id(), kAddTestRules));
EXPECT_TRUE(action->GetIsVisible(tab_id));
const std::string kRemoveTestRule1 = "removeRule('1', 'remove_rule1');";
EXPECT_EQ("remove_rule1",
ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule1));
EXPECT_FALSE(action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test2/")));
EXPECT_TRUE(action->GetIsVisible(tab_id));
const std::string kRemoveTestRule2 = "removeRule('2', 'remove_rule2');";
EXPECT_EQ("remove_rule2",
ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule2));
EXPECT_FALSE(action->GetIsVisible(tab_id));
}
struct ShowActionParams {
constexpr ShowActionParams(const char* show_type, ContextType context_type)
: show_type(show_type), context_type(context_type) {}
const char* show_type;
ContextType context_type;
};
class ParameterizedShowActionDeclarativeContentApiTest
: public DeclarativeContentApiTest,
public testing::WithParamInterface<ShowActionParams> {
public:
ParameterizedShowActionDeclarativeContentApiTest()
: DeclarativeContentApiTest(GetParam().context_type) {}
ParameterizedShowActionDeclarativeContentApiTest(
const ParameterizedShowActionDeclarativeContentApiTest&) = delete;
ParameterizedShowActionDeclarativeContentApiTest& operator=(
const ParameterizedShowActionDeclarativeContentApiTest&) = delete;
~ParameterizedShowActionDeclarativeContentApiTest() override = default;
void TestShowAction(std::optional<ActionInfo::Type> action_type);
};
void ParameterizedShowActionDeclarativeContentApiTest::TestShowAction(
std::optional<ActionInfo::Type> action_type) {
static constexpr char kManifestTemplate[] =
R"({
"name": "Declarative Content Show Action",
"version": "0.1",
"manifest_version": %d,
%s
"permissions": ["declarativeContent"]
})";
std::string action_declaration;
int manifest_version = 2;
if (action_type) {
action_declaration = base::StringPrintf(
R"("%s": {},)", ActionInfo::GetManifestKeyForActionType(*action_type));
manifest_version = GetManifestVersionForActionType(*action_type);
}
// Since this test uses the action API (which is restricted to MV3), we drive
// the interaction through pages, rather than the background script.
ext_dir_.WriteManifest(base::StringPrintf(kManifestTemplate, manifest_version,
action_declaration.c_str()));
ext_dir_.WriteFile(FILE_PATH_LITERAL("page.html"),
R"("<html><script src="page.js"></script></html>)");
ext_dir_.WriteFile(FILE_PATH_LITERAL("page.js"), kBackgroundHelpers);
ChromeTestExtensionLoader loader(profile());
scoped_refptr<const Extension> extension =
loader.LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
// Wait for declarative rules to be set up.
profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting();
ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
// Extensions that don't provide an action are given a page action by default
// (for visibility reasons).
ASSERT_TRUE(action);
// Ensure actions are hidden (so that the ShowAction() rule has an effect).
if (action->default_state() == ActionInfo::DefaultState::kDisabled) {
action->SetIsVisible(ExtensionAction::kDefaultTabId, false);
}
// Open the tab to invoke the APIs, as well as test the action visibility.
content::WebContents* tab = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(tab, extension->GetResourceURL("page.html")));
static constexpr char kScript[] =
R"(setRulesInPageEnvironment([{
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test'}})],
actions: [new chrome.declarativeContent.%s()]
}], 'test_rule');)";
static constexpr char kSuccessStr[] = "test_rule";
std::string result =
content::EvalJs(tab, base::StringPrintf(kScript, GetParam().show_type))
.ExtractString();
// Since extensions with no action provided are given a page action by default
// (for visibility reasons) and ShowAction() should also work with
// browser actions, both of these should pass.
EXPECT_THAT(result, testing::HasSubstr(kSuccessStr));
// TODO(crbug.com/40764017): Understand why this is EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test/")));
const int tab_id = sessions::SessionTabHelper::IdForTab(tab).id();
EXPECT_TRUE(action->GetIsVisible(tab_id));
// If an extension had no action specified in the manifest, it will get a
// synthesized page action.
ActionInfo::Type expected_type =
action_type.value_or(ActionInfo::Type::kPage);
EXPECT_EQ(expected_type, action->action_type());
EXPECT_EQ(expected_type == ActionInfo::Type::kPage ? 1u : 0u,
extension_action_test_util::GetActivePageActionCount(tab));
}
IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest,
NoActionInManifest) {
TestShowAction(std::nullopt);
}
IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest,
PageActionInManifest) {
TestShowAction(ActionInfo::Type::kPage);
}
IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest,
BrowserActionInManifest) {
TestShowAction(ActionInfo::Type::kBrowser);
}
IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest,
ActionInManifest) {
TestShowAction(ActionInfo::Type::kAction);
}
// Tests that rules from manifest are added and evaluated properly.
IN_PROC_BROWSER_TEST_P(ParameterizedShowActionDeclarativeContentApiTest,
RulesAddedFromManifest) {
static constexpr char manifest[] =
R"({
"name": "Declarative Content apitest",
"version": "0.1",
"manifest_version": 2,
"page_action": {},
"permissions": ["declarativeContent"],
"event_rules": [{
"event": "declarativeContent.onPageChanged",
"actions": [{
"type": "declarativeContent.%s"
}],
"conditions": [{
"type": "declarativeContent.PageStateMatcher",
"pageUrl": {"hostPrefix": "test1"}
}]
}]
})";
ext_dir_.WriteManifest(base::StringPrintf(manifest, GetParam().show_type));
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://blank/")));
EXPECT_FALSE(action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test1/")));
EXPECT_TRUE(action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test2/")));
EXPECT_FALSE(action->GetIsVisible(tab_id));
}
#if !BUILDFLAG(IS_ANDROID)
// ShowPageAction() is deprecated and isn't tested on Android.
INSTANTIATE_TEST_SUITE_P(
LegacyShowActionKey_PB,
ParameterizedShowActionDeclarativeContentApiTest,
::testing::Values(ShowActionParams("ShowPageAction",
ContextType::kPersistentBackground)));
#endif
INSTANTIATE_TEST_SUITE_P(
ModernShowActionKey_PB,
ParameterizedShowActionDeclarativeContentApiTest,
::testing::Values(ShowActionParams("ShowAction",
ContextType::kPersistentBackground)));
// Tests that rules are not evaluated in incognito browser windows when the
// extension specifies spanning incognito mode but is not enabled for incognito.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
DisabledForSpanningIncognito) {
CheckIncognito(SPANNING, false);
}
// Tests that rules are evaluated in incognito browser windows when the
// extension specifies spanning incognito mode and is enabled for incognito.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
EnabledForSpanningIncognito) {
CheckIncognito(SPANNING, true);
}
// Tests that rules are not evaluated in incognito browser windows when the
// extension specifies split incognito mode but is not enabled for incognito.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
DisabledForSplitIncognito) {
CheckIncognito(SPLIT, false);
}
// Tests that rules are evaluated in incognito browser windows when the
// extension specifies split incognito mode and is enabled for incognito.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
EnabledForSplitIncognito) {
CheckIncognito(SPLIT, true);
}
// Tests that rules are evaluated for an incognito tab that exists at the time
// the rules are added.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
RulesEvaluatedForExistingIncognitoTab) {
content::WebContents* const incognito_tab =
PlatformOpenURLOffTheRecord(profile(), GURL("about:blank"));
const int incognito_tab_id = ExtensionTabUtil::GetTabId(incognito_tab);
// TODO(crbug.com/40764017): Understand why this is EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(incognito_tab, GURL("http://test_normal/")));
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"),
kIncognitoSpecificBackground);
ExtensionTestMessageListener ready("ready");
const Extension* extension =
LoadExtension(ext_dir_.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
ASSERT_TRUE(ready.WaitUntilSatisfied());
const ExtensionAction* incognito_action =
ExtensionActionManager::Get(incognito_tab->GetBrowserContext())
->GetExtensionAction(*extension);
ASSERT_TRUE(incognito_action);
// The page action should be shown.
EXPECT_TRUE(incognito_action->GetIsVisible(incognito_tab_id));
}
// TODO(crbug.com/40488499): Android does not support PRE_ steps.
#if BUILDFLAG(ENABLE_EXTENSIONS)
// TODO(crbug.com/41189874): Flaky on Windows release builds.
#if BUILDFLAG(IS_WIN) && defined(NDEBUG)
#define MAYBE_PRE_RulesPersistence DISABLED_PRE_RulesPersistence
#else
#define MAYBE_PRE_RulesPersistence PRE_RulesPersistence
#endif
// Sets up rules matching http://test1/ in a normal and incognito browser.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
MAYBE_PRE_RulesPersistence) {
ExtensionTestMessageListener ready("ready");
ExtensionTestMessageListener ready_split("ready (split)");
// An on-disk extension is required so that it can be reloaded later in the
// RulesPersistence test.
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII("declarative_content")
.AppendASCII("persistence"),
{.allow_in_incognito = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kRulesExtensionName, extension->name());
ASSERT_TRUE(ready.WaitUntilSatisfied());
PlatformOpenURLOffTheRecord(profile(), GURL("about:blank"));
ASSERT_TRUE(ready_split.WaitUntilSatisfied());
}
// TODO(crbug.com/41189874): Flaky on Windows release builds.
#if BUILDFLAG(IS_WIN) && defined(NDEBUG)
#define MAYBE_RulesPersistence DISABLED_RulesPersistence
#else
#define MAYBE_RulesPersistence RulesPersistence
#endif
// Reloads the extension from PRE_RulesPersistence and checks that the rules
// continue to work as expected after being persisted and reloaded.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
MAYBE_RulesPersistence) {
const Extension* extension = GetSingleLoadedExtension();
// TODO(crbug.com/40200835): On desktop Android this assert fails because the
// extension was not loaded. It's not clear why.
ASSERT_TRUE(extension) << message_;
ASSERT_EQ(kRulesExtensionName, extension->name());
// Check non-incognito browser.
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
// Observer to track page action visibility. This helps avoid
// flakes by waiting to check visibility until there is an
// actual update to the page action.
ChromeExtensionTestNotificationObserver test_observer(profile());
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
EXPECT_FALSE(action->GetIsVisible(tab_id));
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test_normal/")));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 1);
EXPECT_TRUE(action->GetIsVisible(tab_id));
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test_split/")));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 0);
EXPECT_FALSE(action->GetIsVisible(tab_id));
ExtensionTestMessageListener ready_split("second run ready (split)");
// Check incognito browser.
content::WebContents* const incognito_tab =
PlatformOpenURLOffTheRecord(profile(), GURL("about:blank"));
ASSERT_TRUE(ready_split.WaitUntilSatisfied());
const int incognito_tab_id = ExtensionTabUtil::GetTabId(incognito_tab);
ChromeExtensionTestNotificationObserver incognito_test_observer(
incognito_tab->GetBrowserContext());
const ExtensionAction* incognito_action =
ExtensionActionManager::Get(incognito_tab->GetBrowserContext())
->GetExtensionAction(*extension);
ASSERT_TRUE(incognito_action);
EXPECT_FALSE(NavigateInRenderer(incognito_tab, GURL("http://test_split/")));
incognito_test_observer.WaitForPageActionVisibilityChangeTo(incognito_tab, 1);
EXPECT_TRUE(incognito_action->GetIsVisible(incognito_tab_id));
EXPECT_FALSE(NavigateInRenderer(incognito_tab, GURL("http://test_normal/")));
incognito_test_observer.WaitForPageActionVisibilityChangeTo(incognito_tab, 0);
EXPECT_FALSE(incognito_action->GetIsVisible(incognito_tab_id));
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
// http://crbug.com/304373
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
UninstallWhileActivePageAction) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
std::string script =
kBackgroundHelpers + std::string("\nchrome.test.sendMessage('ready');");
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), script);
ExtensionTestMessageListener ready_listener("ready");
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
const std::string extension_id = extension->id();
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
const std::string kTestRule =
R"(setRules([{
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test'}})],
actions: [new ShowAction()]
}], 'test_rule');)";
EXPECT_EQ("test_rule",
ExecuteScriptInBackgroundPage(extension_id, kTestRule));
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
// TODO(crbug.com/40764017): Understand why these are EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test/")));
EXPECT_TRUE(action->GetIsVisible(tab_id));
EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(1));
EXPECT_EQ(1u, extension_action_test_util::GetActivePageActionCount(tab));
EXPECT_EQ(1u, extension_action_test_util::GetTotalPageActionCount(tab));
ExtensionTestMessageListener reload_ready_listener("ready");
ReloadExtension(extension_id); // Invalidates action and extension.
ASSERT_TRUE(reload_ready_listener.WaitUntilSatisfied());
EXPECT_EQ("test_rule",
ExecuteScriptInBackgroundPage(extension_id, kTestRule));
// TODO(jyasskin): Apply new rules to existing tabs, without waiting for a
// navigation.
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test/")));
EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(1));
EXPECT_EQ(1u, extension_action_test_util::GetActivePageActionCount(tab));
EXPECT_EQ(1u, extension_action_test_util::GetTotalPageActionCount(tab));
UnloadExtension(extension_id);
// Wait for declarative rules to be removed.
profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting();
EXPECT_FALSE(NavigateInRenderer(tab, GURL("http://test/")));
EXPECT_TRUE(WaitForPageActionVisibilityChangeTo(0));
EXPECT_EQ(0u, extension_action_test_util::GetActivePageActionCount(tab));
EXPECT_EQ(0u, extension_action_test_util::GetTotalPageActionCount(tab));
}
// This tests against a renderer crash that was present during development.
// TODO(crbug.com/40260777): Convert this to run in both modes.
IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest,
AddExtensionMatchingExistingTabWithDeadFrames) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
content::WebContents* const tab = GetActiveWebContents();
const int tab_id = ExtensionTabUtil::GetTabId(tab);
ASSERT_TRUE(content::ExecJs(
tab, R"(document.body.innerHTML = '<iframe src="http://test2">';)"));
// Replace the iframe to destroy its WebFrame.
ASSERT_TRUE(content::ExecJs(
tab, R"(document.body.innerHTML = '<span class="foo">';)"));
// Observer to track page action visibility. This helps avoid flakes by
// waiting to check visibility until there is an update to the page action.
ChromeExtensionTestNotificationObserver test_observer(profile());
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
EXPECT_FALSE(action->GetIsVisible(tab_id));
static constexpr char kRuleScript[] =
R"(setRules([{
conditions: [new PageStateMatcher({css: ["span[class=foo]"]})],
actions: [new ShowAction()]
}], 'rule0');)";
EXPECT_EQ("rule0",
ExecuteScriptInBackgroundPage(extension->id(), kRuleScript));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 1);
EXPECT_FALSE(tab->IsCrashed());
EXPECT_TRUE(action->GetIsVisible(tab_id))
<< "Loading an extension when an open page matches its rules "
<< "should show the page action.";
static constexpr char kRemoveScript[] =
R"(onPageChanged.removeRules(undefined, function() {
chrome.test.sendScriptResult('removed');
});)";
EXPECT_EQ("removed",
ExecuteScriptInBackgroundPage(extension->id(), kRemoveScript));
test_observer.WaitForPageActionVisibilityChangeTo(tab, 0);
EXPECT_FALSE(action->GetIsVisible(tab_id));
}
// TODO(crbug.com/40260777): Convert this to run in both modes.
IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest,
CanonicalizesPageStateMatcherCss) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(
FILE_PATH_LITERAL("background.js"),
R"(var PageStateMatcher = chrome.declarativeContent.PageStateMatcher;
function Return(obj) {
chrome.test.sendScriptResult('' + obj);
})");
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
static constexpr char kValidSelectorScript[] =
R"(var psm = new PageStateMatcher({css: ["input[type='password']"]});
Return(psm.css);)";
EXPECT_EQ(
R"(input[type="password"])",
ExecuteScriptInBackgroundPage(extension->id(), kValidSelectorScript));
static constexpr char kSelectorNotAnArrayScript[] =
R"(try {
new PageStateMatcher({css: 'Not-an-array'});
Return('Failed to throw');
} catch (e) {
Return(e.message);
})";
base::Value result =
ExecuteScriptInBackgroundPage(extension->id(), kSelectorNotAnArrayScript);
ASSERT_TRUE(result.is_string());
EXPECT_THAT(result.GetString(),
testing::ContainsRegex("css.*xpected '?array'?"));
// CSS selector is not a string.
static constexpr char kSelectorNotStringScript[] =
R"(try {
new PageStateMatcher({css: [null]});
Return('Failed to throw');
} catch (e) {
Return(e.message);
})";
result =
ExecuteScriptInBackgroundPage(extension->id(), kSelectorNotStringScript);
ASSERT_TRUE(result.is_string());
EXPECT_THAT(result.GetString(),
testing::ContainsRegex("css.*0.*xpected '?string'?"));
// Invalid CSS selector.
static constexpr char kInvalidSelectorScript[] =
R"(try {
new PageStateMatcher({css: ["input''"]});
Return('Failed to throw');
} catch (e) {
Return(e.message);
})";
result =
ExecuteScriptInBackgroundPage(extension->id(), kInvalidSelectorScript);
ASSERT_TRUE(result.is_string());
EXPECT_THAT(result.GetString(), testing::ContainsRegex("valid.*: input''$"));
// "Complex" CSS selector.
static constexpr char kComplexSelectorScript[] =
R"(try {
new PageStateMatcher({css: ['div input']});
Return('Failed to throw');
} catch (e) {
Return(e.message);
})";
result =
ExecuteScriptInBackgroundPage(extension->id(), kComplexSelectorScript);
ASSERT_TRUE(result.is_string());
EXPECT_THAT(result.GetString(),
testing::ContainsRegex("selector.*: div input$"));
}
// Tests that the rules with isBookmarked: true are evaluated when handling
// bookmarking events.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
IsBookmarkedRulesEvaluatedOnBookmarkEvents) {
CheckBookmarkEvents(true);
}
// Tests that the rules with isBookmarked: false are evaluated when handling
// bookmarking events.
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
NotBookmarkedRulesEvaluatedOnBookmarkEvents) {
CheckBookmarkEvents(false);
}
// https://crbug.com/497586
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
WebContentsWithoutTabAddedNotificationAtOnLoaded) {
// Add a web contents to the tab strip in a way that doesn't trigger
// NOTIFICATION_TAB_ADDED.
ASSERT_TRUE(NavigateToURLInNewTab(GURL("about:blank")));
ASSERT_TRUE(content::WaitForLoadStop(GetActiveWebContents()));
// The actual extension contents don't matter here -- we're just looking to
// trigger OnExtensionLoaded.
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
ASSERT_TRUE(LoadExtension(ext_dir_.UnpackedPath()));
}
// https://crbug.com/501225
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
PendingWebContentsClearedOnRemoveRules) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const ExtensionAction* action =
ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
ASSERT_TRUE(action);
// Create two tabs.
content::WebContents* const tab1 = GetActiveWebContents();
content::RenderFrameHost* rfh = NavigateToURLInNewTab(GURL("http://test2/"));
content::WebContents* tab2 = content::WebContents::FromRenderFrameHost(rfh);
// Add a rule matching the second tab.
const std::string kAddTestRules =
R"(addRules([{
id: '1',
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test1'}})],
actions: [new ShowAction()]
}, {
id: '2',
conditions: [new PageStateMatcher({pageUrl: {hostPrefix: 'test2'}})],
actions: [new ShowAction()]
}], 'add_rules');)";
EXPECT_EQ("add_rules",
ExecuteScriptInBackgroundPage(extension->id(), kAddTestRules));
EXPECT_TRUE(action->GetIsVisible(ExtensionTabUtil::GetTabId(tab2)));
// Remove the rule.
const std::string kRemoveTestRule1 = "removeRule('2', 'remove_rule1');";
EXPECT_EQ("remove_rule1",
ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule1));
// Remove the second tab, then trigger a rule evaluation for the remaining
// tab.
CloseTabForWebContents(tab2);
// TODO(crbug.com/40764017): Understand why this is EXPECT_FALSE.
EXPECT_FALSE(NavigateInRenderer(tab1, GURL("http://test1/")));
EXPECT_TRUE(action->GetIsVisible(ExtensionTabUtil::GetTabId(tab1)));
}
// https://crbug.com/517492
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
RemoveAllRulesAfterExtensionUninstall) {
ext_dir_.WriteManifest(FormatManifest(SPANNING));
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundHelpers);
// Load the extension, add a rule, then uninstall the extension.
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
const std::string kAddTestRule =
R"(addRules([{
id: '1',
conditions: [],
actions: [new ShowAction()]
}], 'add_rule');)";
EXPECT_EQ("add_rule",
ExecuteScriptInBackgroundPage(extension->id(), kAddTestRule));
std::u16string error;
ASSERT_TRUE(extension_registrar()->UninstallExtension(
extension->id(), UNINSTALL_REASON_FOR_TESTING, &error));
ASSERT_EQ(u"", error);
// Reload the extension, then add and remove a rule.
extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
EXPECT_EQ("add_rule",
ExecuteScriptInBackgroundPage(extension->id(), kAddTestRule));
const std::string kRemoveTestRule1 = "removeRule('1', 'remove_rule1');";
EXPECT_EQ("remove_rule1",
ExecuteScriptInBackgroundPage(extension->id(), kRemoveTestRule1));
}
// Test that an extension with a RequestContentScript rule in its manifest can
// be loaded.
// Regression for crbug.com/1211316, which could cause this test to flake if
// RulesRegistry::OnExtensionLoaded() was called before
// UserScriptManager::OnExtensionLoaded().
IN_PROC_BROWSER_TEST_P(DeclarativeContentApiTestWithContextType,
RequestContentScriptRule) {
constexpr char kManifest[] = R"(
{
"name": "Declarative Content apitest",
"version": "0.1",
"manifest_version": 2,
"background": {
"scripts": ["background.js"],
"persistent": true
},
"page_action": {},
"permissions": [
"declarativeContent"
],
"event_rules": [{
"event": "declarativeContent.onPageChanged",
"actions": [{
"type": "declarativeContent.RequestContentScript",
"js": ["asdf.js"]
}],
"conditions": [{
"type": "declarativeContent.PageStateMatcher",
"pageUrl": {"hostPrefix": "test1"}
}]
}]
}
)";
ext_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), "");
ext_dir_.WriteManifest(kManifest);
const Extension* extension = LoadExtension(ext_dir_.UnpackedPath());
ASSERT_TRUE(extension);
RulesRegistryService* rules_registry_service =
extensions::RulesRegistryService::Get(profile());
scoped_refptr<RulesRegistry> rules_registry =
rules_registry_service->GetRulesRegistry(
rules_registry_ids::kDefaultRulesRegistryID,
"declarativeContent.onPageChanged");
DCHECK(rules_registry);
std::vector<const api::events::Rule*> rules;
rules_registry->GetAllRules(extension->id(), &rules);
EXPECT_EQ(1u, rules.size());
}
// TODO(wittman): Once ChromeContentRulesRegistry operates on condition and
// action interfaces, add a test that checks that a navigation always evaluates
// consistent URL state for all conditions. i.e.: if condition1 evaluates to
// false on url0 and true on url1, and condition2 evaluates to true on url0 and
// false on url1, navigate from url0 to url1 and validate that no action is
// triggered. Do the same when navigating back to url0. This kind of test is
// unfortunately not feasible with the current implementation and the existing
// supported conditions and actions.
} // namespace
} // namespace extensions