Add visualization for Chrome configuration changes (fixes #3892)

- Add new API to retrieve/observe configuration values.
- cefclient: Add https://tests/config to inspect configuration
  values in real time.
This commit is contained in:
Marshall Greenblatt
2025-03-10 15:50:46 +00:00
parent ec31b2b505
commit 549e8fe05c
25 changed files with 1887 additions and 40 deletions

View File

@ -677,6 +677,8 @@ source_set("libcef_static") {
"libcef/browser/scheme_impl.cc",
"libcef/browser/server_impl.cc",
"libcef/browser/server_impl.h",
"libcef/browser/setting_helper.cc",
"libcef/browser/setting_helper.h",
"libcef/browser/simple_menu_model_impl.cc",
"libcef/browser/simple_menu_model_impl.h",
"libcef/browser/ssl_info_impl.cc",

View File

@ -247,6 +247,8 @@
'tests/cefclient/browser/client_prefs.cc',
'tests/cefclient/browser/client_prefs.h',
'tests/cefclient/browser/client_types.h',
'tests/cefclient/browser/config_test.cc',
'tests/cefclient/browser/config_test.h',
'tests/cefclient/browser/default_client_handler.cc',
'tests/cefclient/browser/default_client_handler.h',
'tests/cefclient/browser/dialog_test.cc',
@ -324,6 +326,7 @@
'cefclient_sources_resources': [
'tests/cefclient/resources/binary_transfer.html',
'tests/cefclient/resources/binding.html',
'tests/cefclient/resources/config.html',
'tests/cefclient/resources/dialogs.html',
'tests/cefclient/resources/draggable.html',
'tests/cefclient/resources/hang.html',

View File

@ -41,6 +41,7 @@
#include <vector>
#include "include/cef_base.h"
#include "include/cef_registration.h"
#include "include/cef_values.h"
///
@ -65,6 +66,24 @@ class CefPreferenceRegistrar : public CefBaseScoped {
CefRefPtr<CefValue> default_value) = 0;
};
#if CEF_API_ADDED(CEF_NEXT)
///
/// Implemented by the client to observe preference changes and registered via
/// CefPreferenceManager::AddPreferenceObserver. The methods of this class will
/// be called on the browser process UI thread.
///
/*--cef(source=client,added=next)--*/
class CefPreferenceObserver : public virtual CefBaseRefCounted {
public:
///
/// Called when a preference has changed. The new value can be retrieved using
/// CefPreferenceManager::GetPreference.
///
/*--cef()--*/
virtual void OnPreferenceChanged(const CefString& name) = 0;
};
#endif
///
/// Manage access to preferences. Many built-in preferences are registered by
/// Chromium. Custom preferences can be registered in
@ -73,6 +92,36 @@ class CefPreferenceRegistrar : public CefBaseScoped {
/*--cef(source=library,no_debugct_check)--*/
class CefPreferenceManager : public virtual CefBaseRefCounted {
public:
#if CEF_API_ADDED(CEF_NEXT)
///
/// Returns the current Chrome Variations configuration (combination of field
/// trials and chrome://flags) as equivalent command-line switches
/// (`--[enable|disable]-features=XXXX`, etc). These switches can be used to
/// apply the same configuration when launching a CEF-based application. See
/// https://developer.chrome.com/docs/web-platform/chrome-variations for
/// background and details. Note that field trial tests are disabled by
/// default in Official CEF builds (via the
/// `disable_fieldtrial_testing_config=true` GN flag). This method must be
/// called on the browser process UI thread.
///
/*--cef(added=next)--*/
static void GetChromeVariationsAsSwitches(std::vector<CefString>& switches);
///
/// Returns the current Chrome Variations configuration (combination of field
/// trials and chrome://flags) as human-readable strings. This is the
/// human-readable equivalent of the "Active Variations" section of
/// chrome://version. See
/// https://developer.chrome.com/docs/web-platform/chrome-variations for
/// background and details. Note that field trial tests are disabled by
/// default in Official CEF builds (via the
/// `disable_fieldtrial_testing_config=true` GN flag). This method must be
/// called on the browser process UI thread.
///
/*--cef(added=next)--*/
static void GetChromeVariationsAsStrings(std::vector<CefString>& strings);
#endif
///
/// Returns the global preference manager object.
///
@ -129,6 +178,21 @@ class CefPreferenceManager : public virtual CefBaseRefCounted {
virtual bool SetPreference(const CefString& name,
CefRefPtr<CefValue> value,
CefString& error) = 0;
#if CEF_API_ADDED(CEF_NEXT)
///
/// Add an observer for preference changes. |name| is the name of the
/// preference to observe. If |name| is empty then all preferences will
/// be observed. Observing all preferences has performance consequences and
/// is not recommended outside of testing scenarios. The observer will remain
/// registered until the returned Registration object is destroyed. This
/// method must be called on the browser process UI thread.
///
/*--cef(optional_param=name,added=next)--*/
virtual CefRefPtr<CefRegistration> AddPreferenceObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer) = 0;
#endif
};
#endif // CEF_INCLUDE_CEF_PREFERENCE_H_

View File

@ -44,6 +44,7 @@
#include "include/cef_cookie.h"
#include "include/cef_media_router.h"
#include "include/cef_preference.h"
#include "include/cef_registration.h"
#include "include/cef_values.h"
class CefRequestContextHandler;
@ -66,6 +67,27 @@ class CefResolveCallback : public virtual CefBaseRefCounted {
const std::vector<CefString>& resolved_ips) = 0;
};
#if CEF_API_ADDED(CEF_NEXT)
///
/// Implemented by the client to observe content and website setting changes and
/// registered via CefRequestContext::AddSettingObserver. The methods of this
/// class will be called on the browser process UI thread.
///
/*--cef(source=client,added=next)--*/
class CefSettingObserver : public virtual CefBaseRefCounted {
public:
///
/// Called when a content or website setting has changed. The new value can be
/// retrieved using CefRequestContext::GetContentSetting or
/// CefRequestContext::GetWebsiteSetting.
///
/*--cef(optional_param=requesting_url,optional_param=top_level_url)--*/
virtual void OnSettingChanged(const CefString& requesting_url,
const CefString& top_level_url,
cef_content_setting_types_t content_type) = 0;
};
#endif
///
/// A request context provides request handling for a set of related browser
/// or URL request objects. A request context can be specified when creating a
@ -292,6 +314,17 @@ class CefRequestContext : public CefPreferenceManager {
cef_content_setting_types_t content_type,
cef_content_setting_values_t value) = 0;
#if CEF_API_ADDED(CEF_NEXT)
///
/// Add an observer for content and website setting changes. The observer will
/// remain registered until the returned Registration object is destroyed.
/// This method must be called on the browser process UI thread.
///
/*--cef(added=next)--*/
virtual CefRefPtr<CefRegistration> AddSettingObserver(
CefRefPtr<CefSettingObserver> observer) = 0;
#endif
///
/// Sets the Chrome color scheme for all browsers that share this request
/// context. |variant| values of SYSTEM, LIGHT and DARK change the underlying

View File

@ -12,10 +12,12 @@
#include "base/task/current_thread.h"
#include "base/threading/thread_restrictions.h"
#include "cef/libcef/browser/browser_info_manager.h"
#include "cef/libcef/browser/prefs/pref_helper.h"
#include "cef/libcef/browser/request_context_impl.h"
#include "cef/libcef/browser/thread_util.h"
#include "cef/libcef/browser/trace_subscriber.h"
#include "cef/libcef/common/cef_switches.h"
#include "chrome/browser/browser_process_impl.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "ui/base/ui_base_switches.h"
@ -570,12 +572,24 @@ CefTraceSubscriber* CefContext::GetTraceSubscriber() {
if (shutting_down_) {
return nullptr;
}
if (!trace_subscriber_.get()) {
if (!trace_subscriber_) {
trace_subscriber_ = std::make_unique<CefTraceSubscriber>();
}
return trace_subscriber_.get();
}
pref_helper::Registrar* CefContext::GetPrefRegistrar() {
CEF_REQUIRE_UIT();
if (shutting_down_) {
return nullptr;
}
if (!pref_registrar_) {
pref_registrar_ = std::make_unique<pref_helper::Registrar>();
pref_registrar_->Init(g_browser_process->local_state());
}
return pref_registrar_.get();
}
void CefContext::PopulateGlobalRequestContextSettings(
CefRequestContextSettings* settings) {
CefRefPtr<CefCommandLine> command_line =
@ -645,12 +659,15 @@ void CefContext::ShutdownOnUIThread() {
observer.OnContextDestroyed();
}
if (trace_subscriber_.get()) {
trace_subscriber_.reset(nullptr);
if (trace_subscriber_) {
trace_subscriber_.reset();
}
if (pref_registrar_) {
pref_registrar_.reset();
}
}
void CefContext::FinalizeShutdown() {
browser_info_manager_.reset(nullptr);
browser_info_manager_.reset();
application_ = nullptr;
}

View File

@ -16,6 +16,10 @@
#include "cef/libcef/browser/main_runner.h"
#include "third_party/skia/include/core/SkColor.h"
namespace pref_helper {
class Registrar;
}
class CefBrowserInfoManager;
class CefTraceSubscriber;
@ -73,6 +77,7 @@ class CefContext {
cef_state_t windowless_state) const;
CefTraceSubscriber* GetTraceSubscriber();
pref_helper::Registrar* GetPrefRegistrar();
// Populate request context settings for the global system context based on
// CefSettings and command-line flags.
@ -112,6 +117,7 @@ class CefContext {
std::unique_ptr<CefMainRunner> main_runner_;
std::unique_ptr<CefTraceSubscriber> trace_subscriber_;
std::unique_ptr<pref_helper::Registrar> pref_registrar_;
std::unique_ptr<CefBrowserInfoManager> browser_info_manager_;
// Observers that want to be notified of changes to this object.

View File

@ -4,10 +4,28 @@
#include "cef/libcef/browser/global_preference_manager_impl.h"
#include "base/metrics/field_trial_list_including_low_anonymity.h"
#include "base/strings/string_util.h"
#include "cef/libcef/browser/context.h"
#include "cef/libcef/browser/prefs/pref_helper.h"
#include "cef/libcef/browser/thread_util.h"
#include "cef/libcef/common/api_version_util.h"
#include "chrome/browser/about_flags.h"
#include "chrome/browser/browser_process.h"
#include "components/flags_ui/pref_service_flags_storage.h"
#include "components/variations/synthetic_trials_active_group_id_provider.h"
namespace {
std::string GetActiveGroupNameAsString(
const base::FieldTrial::ActiveGroup& group) {
constexpr std::string_view kNonBreakingHyphenUTF8 = "\xE2\x80\x91";
std::string result = group.trial_name + ":" + group.group_name;
base::ReplaceChars(result, "-", kNonBreakingHyphenUTF8, &result);
return result;
}
} // namespace
bool CefGlobalPreferenceManagerImpl::HasPreference(const CefString& name) {
CEF_REQUIRE_UIT_RETURN(false);
@ -40,6 +58,77 @@ bool CefGlobalPreferenceManagerImpl::SetPreference(const CefString& name,
value, error);
}
CefRefPtr<CefRegistration>
CefGlobalPreferenceManagerImpl::AddPreferenceObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer) {
CEF_API_REQUIRE_ADDED(CEF_NEXT);
CEF_REQUIRE_UIT_RETURN(nullptr);
return CefContext::Get()->GetPrefRegistrar()->AddObserver(name, observer);
}
// static
void CefPreferenceManager::GetChromeVariationsAsSwitches(
std::vector<CefString>& switches) {
CEF_API_REQUIRE_ADDED(CEF_NEXT);
// Verify that the context is in a valid state.
if (!CONTEXT_STATE_VALID()) {
DCHECK(false) << "context not valid";
return;
}
switches.clear();
// Based on ChromeFeatureListCreator::ConvertFlagsToSwitches().
flags_ui::PrefServiceFlagsStorage flags_storage(
g_browser_process->local_state());
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
about_flags::ConvertFlagsToSwitches(&flags_storage, &command_line,
flags_ui::kNoSentinels);
for (const auto& arg : command_line.argv()) {
if (!arg.empty()) {
switches.push_back(arg);
}
}
}
// static
void CefPreferenceManager::GetChromeVariationsAsStrings(
std::vector<CefString>& strings) {
CEF_API_REQUIRE_ADDED(CEF_NEXT);
// Verify that the context is in a valid state.
if (!CONTEXT_STATE_VALID()) {
DCHECK(false) << "context not valid";
return;
}
strings.clear();
// Based on components/webui/version/version_handler_helper.cc
// GetVariationsList().
base::FieldTrial::ActiveGroups active_groups;
// Include low anonymity trial groups in the version string, as it is only
// displayed locally (and is useful for diagnostics purposes).
base::FieldTrialListIncludingLowAnonymity::
GetActiveFieldTrialGroupsForTesting(&active_groups);
for (const auto& group : active_groups) {
strings.push_back(GetActiveGroupNameAsString(group));
}
// Synthetic field trials.
for (const auto& group :
variations::SyntheticTrialsActiveGroupIdProvider::GetInstance()
->GetGroups()) {
strings.push_back(GetActiveGroupNameAsString(group.active_group()));
}
}
// static
CefRefPtr<CefPreferenceManager>
CefPreferenceManager::GetGlobalPreferenceManager() {

View File

@ -27,6 +27,9 @@ class CefGlobalPreferenceManagerImpl : public CefPreferenceManager {
bool SetPreference(const CefString& name,
CefRefPtr<CefValue> value,
CefString& error) override;
CefRefPtr<CefRegistration> AddPreferenceObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer) override;
private:
IMPLEMENT_REFCOUNTING(CefGlobalPreferenceManagerImpl);

View File

@ -6,6 +6,7 @@
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "cef/include/cef_preference.h"
#include "cef/libcef/browser/thread_util.h"
#include "cef/libcef/common/values_impl.h"
#include "components/prefs/pref_service.h"
@ -118,4 +119,145 @@ bool SetPreference(PrefService* pref_service,
return true;
}
class RegistrationImpl final : public Registration, public CefRegistration {
public:
RegistrationImpl(Registrar* registrar,
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer)
: registrar_(registrar), name_(name), observer_(observer) {
DCHECK(registrar_);
DCHECK(observer_);
}
RegistrationImpl(const RegistrationImpl&) = delete;
RegistrationImpl& operator=(const RegistrationImpl&) = delete;
~RegistrationImpl() override {
CEF_REQUIRE_UIT();
if (registrar_) {
registrar_->RemoveObserver(name_.ToString(), this);
}
}
void Detach() override {
registrar_ = nullptr;
observer_ = nullptr;
}
void RunCallback() const override { RunCallback(name_); }
void RunCallback(const CefString& name) const override {
observer_->OnPreferenceChanged(name);
}
private:
raw_ptr<Registrar> registrar_;
CefString name_;
CefRefPtr<CefPreferenceObserver> observer_;
IMPLEMENT_REFCOUNTING_DELETE_ON_UIT(RegistrationImpl);
};
Registrar::~Registrar() {
RemoveAll();
}
void Registrar::Init(PrefService* service) {
DCHECK(service);
DCHECK(IsEmpty() || service_ == service);
service_ = service;
}
void Registrar::Reset() {
RemoveAll();
service_ = nullptr;
}
void Registrar::RemoveAll() {
if (!name_observers_.empty()) {
for (auto& [name, registrations] : name_observers_) {
service_->RemovePrefObserver(name, this);
for (auto& registration : registrations) {
registration.Detach();
}
}
name_observers_.clear();
}
if (!all_observers_.empty()) {
service_->RemovePrefObserverAllPrefs(this);
for (auto& registration : all_observers_) {
registration.Detach();
}
all_observers_.Clear();
}
}
bool Registrar::IsEmpty() const {
return name_observers_.empty() && all_observers_.empty();
}
CefRefPtr<CefRegistration> Registrar::AddObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer) {
CHECK(service_);
RegistrationImpl* impl = new RegistrationImpl(this, name, observer);
if (name.empty()) {
if (all_observers_.empty()) {
service_->AddPrefObserverAllPrefs(this);
}
all_observers_.AddObserver(impl);
} else {
const std::string& name_str = name.ToString();
if (!name_observers_.contains(name_str)) {
service_->AddPrefObserver(name_str, this);
}
name_observers_[name_str].AddObserver(impl);
}
return impl;
}
void Registrar::RemoveObserver(std::string_view name,
Registration* registration) {
CHECK(service_);
if (name.empty()) {
all_observers_.RemoveObserver(registration);
if (all_observers_.empty()) {
service_->RemovePrefObserverAllPrefs(this);
}
} else {
auto it = name_observers_.find(std::string(name));
DCHECK(it != name_observers_.end());
it->second.RemoveObserver(registration);
if (it->second.empty()) {
name_observers_.erase(it);
service_->RemovePrefObserver(name, this);
}
}
}
void Registrar::OnPreferenceChanged(PrefService* service,
std::string_view pref_name) {
std::string pref_name_str(pref_name);
if (!name_observers_.empty()) {
auto it = name_observers_.find(pref_name_str);
if (it != name_observers_.end()) {
for (Registration& registration : it->second) {
registration.RunCallback();
}
}
}
if (!all_observers_.empty()) {
CefString name_str(pref_name_str);
for (Registration& registration : all_observers_) {
registration.RunCallback(name_str);
}
}
}
} // namespace pref_helper

View File

@ -5,8 +5,16 @@
#ifndef CEF_LIBCEF_BROWSER_PREFS_PREF_HELPER_H_
#define CEF_LIBCEF_BROWSER_PREFS_PREF_HELPER_H_
#include "cef/include/cef_values.h"
#include <unordered_map>
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "cef/include/cef_registration.h"
#include "cef/include/cef_values.h"
#include "components/prefs/pref_observer.h"
class CefPreferenceObserver;
class CefRegistration;
class PrefService;
namespace pref_helper {
@ -28,6 +36,69 @@ bool SetPreference(PrefService* pref_service,
CefRefPtr<CefValue> value,
CefString& error);
class Registration : public base::CheckedObserver {
public:
virtual void Detach() = 0;
virtual void RunCallback() const = 0;
virtual void RunCallback(const CefString& name) const = 0;
};
class RegistrationImpl;
// Automatically manages the registration of one or more CefPreferenceObserver
// objects with a PrefService. When the Registrar is destroyed, all registered
// observers are automatically unregistered with the PrefService. Loosely based
// on PrefChangeRegistrar.
class Registrar final : public PrefObserver {
public:
Registrar() = default;
Registrar(const Registrar&) = delete;
Registrar& operator=(const Registrar&) = delete;
~Registrar();
// Must be called before adding or removing observers. Can be called more
// than once as long as the value of |service| doesn't change.
void Init(PrefService* service);
// Removes all observers and clears the reference to the PrefService.
// `Init` must be called before adding or removing any observers.
void Reset();
// Removes all observers that have been previously added with a call to Add.
void RemoveAll();
// Returns true if no observers are registered.
bool IsEmpty() const;
// Adds a pref |observer| for the specified pref |name|. All registered
// observers will be automatically unregistered and detached when the
// Registrar's destructor is called.
CefRefPtr<CefRegistration> AddObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer);
private:
friend class RegistrationImpl;
void RemoveObserver(std::string_view name, Registration* registration);
// PrefObserver:
void OnPreferenceChanged(PrefService* service,
std::string_view pref_name) override;
raw_ptr<PrefService, AcrossTasksDanglingUntriaged> service_ = nullptr;
// Observers registered for a preference by name.
using ObserverMap =
std::unordered_map<std::string, base::ObserverList<Registration>>;
ObserverMap name_observers_;
// Observers registered for all preferences.
base::ObserverList<Registration> all_observers_;
};
} // namespace pref_helper
#endif // CEF_LIBCEF_BROWSER_PREFS_PREF_HELPER_H_

View File

@ -10,7 +10,9 @@
#include "cef/libcef/browser/browser_context.h"
#include "cef/libcef/browser/context.h"
#include "cef/libcef/browser/prefs/pref_helper.h"
#include "cef/libcef/browser/setting_helper.h"
#include "cef/libcef/browser/thread_util.h"
#include "cef/libcef/common/api_version_util.h"
#include "cef/libcef/common/app_manager.h"
#include "cef/libcef/common/task_runner_impl.h"
#include "cef/libcef/common/values_impl.h"
@ -459,6 +461,22 @@ bool CefRequestContextImpl::SetPreference(const CefString& name,
return pref_helper::SetPreference(pref_service, name, value, error);
}
CefRefPtr<CefRegistration> CefRequestContextImpl::AddPreferenceObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer) {
CEF_API_REQUIRE_ADDED(CEF_NEXT);
if (!VerifyBrowserContext()) {
return nullptr;
}
if (!pref_registrar_) {
pref_registrar_ = std::make_unique<pref_helper::Registrar>();
pref_registrar_->Init(browser_context()->AsProfile()->GetPrefs());
}
return pref_registrar_->AddObserver(name, observer);
}
void CefRequestContextImpl::ClearCertificateExceptions(
CefRefPtr<CefCompletionCallback> callback) {
GetBrowserContext(
@ -588,6 +606,26 @@ void CefRequestContextImpl::SetContentSetting(
requesting_url, top_level_url, content_type, value));
}
CefRefPtr<CefRegistration> CefRequestContextImpl::AddSettingObserver(
CefRefPtr<CefSettingObserver> observer) {
CEF_API_REQUIRE_ADDED(CEF_NEXT);
if (!VerifyBrowserContext()) {
return nullptr;
}
if (!setting_registrar_) {
auto* settings_map = HostContentSettingsMapFactory::GetForProfile(
browser_context()->AsProfile());
if (!settings_map) {
return nullptr;
}
setting_registrar_ = std::make_unique<setting_helper::Registrar>();
setting_registrar_->Init(settings_map);
}
return setting_registrar_->AddObserver(observer);
}
void CefRequestContextImpl::SetChromeColorScheme(cef_color_variant_t variant,
cef_color_t user_color) {
GetBrowserContext(

View File

@ -17,6 +17,14 @@ namespace content {
struct GlobalRenderFrameHostId;
}
namespace pref_helper {
class Registrar;
}
namespace setting_helper {
class Registrar;
}
class CefBrowserContext;
// Implementation of the CefRequestContext interface. All methods are thread-
@ -80,6 +88,20 @@ class CefRequestContextImpl : public CefRequestContext {
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
BrowserContextCallback callback);
// CefPreferenceManager methods.
bool HasPreference(const CefString& name) override;
CefRefPtr<CefValue> GetPreference(const CefString& name) override;
CefRefPtr<CefDictionaryValue> GetAllPreferences(
bool include_defaults) override;
bool CanSetPreference(const CefString& name) override;
bool SetPreference(const CefString& name,
CefRefPtr<CefValue> value,
CefString& error) override;
CefRefPtr<CefRegistration> AddPreferenceObserver(
const CefString& name,
CefRefPtr<CefPreferenceObserver> observer) override;
// CefRequestContext methods.
bool IsSame(CefRefPtr<CefRequestContext> other) override;
bool IsSharingWith(CefRefPtr<CefRequestContext> other) override;
bool IsGlobal() override;
@ -92,14 +114,6 @@ class CefRequestContextImpl : public CefRequestContext {
const CefString& domain_name,
CefRefPtr<CefSchemeHandlerFactory> factory) override;
bool ClearSchemeHandlerFactories() override;
bool HasPreference(const CefString& name) override;
CefRefPtr<CefValue> GetPreference(const CefString& name) override;
CefRefPtr<CefDictionaryValue> GetAllPreferences(
bool include_defaults) override;
bool CanSetPreference(const CefString& name) override;
bool SetPreference(const CefString& name,
CefRefPtr<CefValue> value,
CefString& error) override;
void ClearCertificateExceptions(
CefRefPtr<CefCompletionCallback> callback) override;
void ClearHttpAuthCredentials(
@ -125,6 +139,8 @@ class CefRequestContextImpl : public CefRequestContext {
const CefString& top_level_url,
cef_content_setting_types_t content_type,
cef_content_setting_values_t value) override;
CefRefPtr<CefRegistration> AddSettingObserver(
CefRefPtr<CefSettingObserver> observer) override;
void SetChromeColorScheme(cef_color_variant_t variant,
cef_color_t user_color) override;
cef_color_variant_t GetChromeColorSchemeMode() override;
@ -218,6 +234,9 @@ class CefRequestContextImpl : public CefRequestContext {
Config config_;
std::unique_ptr<pref_helper::Registrar> pref_registrar_;
std::unique_ptr<setting_helper::Registrar> setting_registrar_;
IMPLEMENT_REFCOUNTING_DELETE_ON_UIT(CefRequestContextImpl);
};

View File

@ -0,0 +1,118 @@
// Copyright (c) 2025 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that can
// be found in the LICENSE file.
#include "cef/libcef/browser/setting_helper.h"
#include "cef/include/cef_request_context.h"
#include "cef/libcef/browser/thread_util.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "url/gurl.h"
namespace setting_helper {
class RegistrationImpl final : public Registration, public CefRegistration {
public:
RegistrationImpl(Registrar* registrar, CefRefPtr<CefSettingObserver> observer)
: registrar_(registrar), observer_(observer) {
DCHECK(registrar_);
DCHECK(observer_);
}
RegistrationImpl(const RegistrationImpl&) = delete;
RegistrationImpl& operator=(const RegistrationImpl&) = delete;
~RegistrationImpl() override {
CEF_REQUIRE_UIT();
if (registrar_) {
registrar_->RemoveObserver(this);
}
}
void Detach() override {
registrar_ = nullptr;
observer_ = nullptr;
}
void RunCallback(const CefString& requesting_url,
const CefString& top_level_url,
cef_content_setting_types_t content_type) const override {
observer_->OnSettingChanged(requesting_url, top_level_url, content_type);
}
private:
raw_ptr<Registrar> registrar_;
CefRefPtr<CefSettingObserver> observer_;
IMPLEMENT_REFCOUNTING_DELETE_ON_UIT(RegistrationImpl);
};
Registrar::~Registrar() {
RemoveAll();
}
void Registrar::Init(HostContentSettingsMap* settings) {
DCHECK(settings);
DCHECK(IsEmpty() || settings_ == settings);
settings_ = settings;
}
void Registrar::Reset() {
RemoveAll();
settings_ = nullptr;
}
void Registrar::RemoveAll() {
if (!observers_.empty()) {
settings_->RemoveObserver(this);
for (auto& registration : observers_) {
registration.Detach();
}
observers_.Clear();
}
}
bool Registrar::IsEmpty() const {
return observers_.empty();
}
CefRefPtr<CefRegistration> Registrar::AddObserver(
CefRefPtr<CefSettingObserver> observer) {
CHECK(settings_);
RegistrationImpl* impl = new RegistrationImpl(this, observer);
if (observers_.empty()) {
settings_->AddObserver(this);
}
observers_.AddObserver(impl);
return impl;
}
void Registrar::RemoveObserver(Registration* registration) {
CHECK(settings_);
observers_.RemoveObserver(registration);
if (observers_.empty()) {
settings_->RemoveObserver(this);
}
}
void Registrar::OnContentSettingChanged(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsTypeSet content_type_set) {
DCHECK(!IsEmpty());
const CefString requesting_url(primary_pattern.ToRepresentativeUrl().spec());
const CefString top_level_url(secondary_pattern.ToRepresentativeUrl().spec());
const auto content_type =
static_cast<cef_content_setting_types_t>(content_type_set.GetType());
for (Registration& registration : observers_) {
registration.RunCallback(requesting_url, top_level_url, content_type);
}
}
} // namespace setting_helper

View File

@ -0,0 +1,81 @@
// Copyright (c) 2025 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that can
// be found in the LICENSE file.
#ifndef CEF_LIBCEF_BROWSER_SETTING_HELPER_H_
#define CEF_LIBCEF_BROWSER_SETTING_HELPER_H_
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "cef/include/cef_registration.h"
#include "components/content_settings/core/browser/content_settings_observer.h"
class CefSettingObserver;
class CefRegistration;
class HostContentSettingsMap;
namespace setting_helper {
class Registration : public base::CheckedObserver {
public:
virtual void Detach() = 0;
virtual void RunCallback(const CefString& requesting_url,
const CefString& top_level_url,
cef_content_setting_types_t content_type) const = 0;
};
class RegistrationImpl;
// Automatically manages the registration of one or more CefSettingObserver
// objects with a HostContentSettingsMap. When the Registrar is destroyed, all
// registered observers are automatically unregistered with the
// HostContentSettingsMap. Loosely based on PrefChangeRegistrar.
class Registrar final : public content_settings::Observer {
public:
Registrar() = default;
Registrar(const Registrar&) = delete;
Registrar& operator=(const Registrar&) = delete;
~Registrar();
// Must be called before adding or removing observers. Can be called more
// than once as long as the value of |settings| doesn't change.
void Init(HostContentSettingsMap* settings);
// Removes all observers and clears the reference to the
// HostContentSettingsMap. `Init` must be called before adding or removing any
// observers.
void Reset();
// Removes all observers that have been previously added with a call to Add.
void RemoveAll();
// Returns true if no observers are registered.
bool IsEmpty() const;
// Adds a setting observer. All registered observers will be automatically
// unregistered and detached when the Registrar's destructor is called.
CefRefPtr<CefRegistration> AddObserver(
CefRefPtr<CefSettingObserver> observer);
private:
friend class RegistrationImpl;
void RemoveObserver(Registration* registration);
// content_settings::Observer:
void OnContentSettingChanged(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsTypeSet content_type_set) override;
raw_ptr<HostContentSettingsMap, AcrossTasksDanglingUntriaged> settings_ =
nullptr;
base::ObserverList<Registration> observers_;
};
} // namespace setting_helper
#endif // CEF_LIBCEF_BROWSER_SETTING_HELPER_H_

View File

@ -764,5 +764,14 @@ patches = [
# Expose Mojo Connector error state to Receiver disconnect handlers.
# https://github.com/chromiumembedded/cef/issues/3664
'name': 'mojo_connect_result_3664'
},
{
# Support for configuration accessors/observers.
# - Allow pref_service::Registrar access to
# PrefService::[Add|Remove]PrefObserver.
# - Enable SyntheticTrialsActiveGroupIdProvider::GetGroups in Release
# builds.
# https://github.com/chromiumembedded/cef/issues/3892
'name': 'config_3892'
}
]

View File

@ -0,0 +1,85 @@
diff --git components/prefs/pref_service.h components/prefs/pref_service.h
index f1856d6ee4419..413b77e80d84d 100644
--- components/prefs/pref_service.h
+++ components/prefs/pref_service.h
@@ -52,6 +52,10 @@ namespace base {
class FilePath;
}
+namespace pref_helper {
+class Registrar;
+}
+
namespace prefs {
class ScopedDictionaryPrefUpdate;
}
@@ -441,6 +445,8 @@ class COMPONENTS_PREFS_EXPORT PrefService {
// declared as a friend, too.
friend class PrefChangeRegistrar;
friend class subtle::PrefMemberBase;
+ // CEF registration manager.
+ friend class pref_helper::Registrar;
// These are protected so they can only be accessed by the friend
// classes listed above.
diff --git components/variations/synthetic_trials_active_group_id_provider.cc components/variations/synthetic_trials_active_group_id_provider.cc
index bd51697297471..3e669cd080457 100644
--- components/variations/synthetic_trials_active_group_id_provider.cc
+++ components/variations/synthetic_trials_active_group_id_provider.cc
@@ -27,7 +27,7 @@ SyntheticTrialsActiveGroupIdProvider::GetActiveGroupIds() {
return group_ids_;
}
-#if !defined(NDEBUG)
+#if !defined(NDEBUG) || BUILDFLAG(ENABLE_CEF)
std::vector<SyntheticTrialGroup>
SyntheticTrialsActiveGroupIdProvider::GetGroups() {
base::AutoLock scoped_lock(lock_);
@@ -38,7 +38,7 @@ SyntheticTrialsActiveGroupIdProvider::GetGroups() {
void SyntheticTrialsActiveGroupIdProvider::ResetForTesting() {
base::AutoLock scoped_lock(lock_);
group_ids_.clear();
-#if !defined(NDEBUG)
+#if !defined(NDEBUG) || BUILDFLAG(ENABLE_CEF)
groups_.clear();
#endif // !defined(NDEBUG)
}
@@ -53,7 +53,7 @@ void SyntheticTrialsActiveGroupIdProvider::OnSyntheticTrialsChanged(
for (const auto& group : groups) {
group_ids_.push_back(group.id());
}
-#if !defined(NDEBUG)
+#if !defined(NDEBUG) || BUILDFLAG(ENABLE_CEF)
groups_ = groups;
#endif // !defined(NDEBUG)
}
diff --git components/variations/synthetic_trials_active_group_id_provider.h components/variations/synthetic_trials_active_group_id_provider.h
index b4a999731d996..0bbac173fddf1 100644
--- components/variations/synthetic_trials_active_group_id_provider.h
+++ components/variations/synthetic_trials_active_group_id_provider.h
@@ -10,6 +10,7 @@
#include "base/component_export.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
+#include "cef/libcef/features/features.h"
#include "components/variations/active_field_trials.h"
#include "components/variations/synthetic_trials.h"
@@ -36,7 +37,7 @@ class COMPONENT_EXPORT(VARIATIONS) SyntheticTrialsActiveGroupIdProvider
// Returns currently active synthetic trial group IDs.
std::vector<ActiveGroupId> GetActiveGroupIds();
-#if !defined(NDEBUG)
+#if !defined(NDEBUG) || BUILDFLAG(ENABLE_CEF)
// In debug mode, not only the group IDs are tracked but also the full group
// info, to display the names unhashed in chrome://version.
std::vector<SyntheticTrialGroup> GetGroups();
@@ -60,7 +61,7 @@ class COMPONENT_EXPORT(VARIATIONS) SyntheticTrialsActiveGroupIdProvider
base::Lock lock_;
std::vector<ActiveGroupId> group_ids_; // GUARDED_BY(lock_);
-#if !defined(NDEBUG)
+#if !defined(NDEBUG) || BUILDFLAG(ENABLE_CEF)
// In debug builds, keep the full group information to be able to display it
// in chrome://version.
std::vector<SyntheticTrialGroup> groups_; // GUARDED_BY(lock_);

View File

@ -0,0 +1,334 @@
// Copyright (c) 2025 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "tests/cefclient/browser/config_test.h"
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include "include/base/cef_logging.h"
#include "include/cef_parser.h"
#include "include/cef_request_context.h"
#include "tests/cefclient/browser/test_runner.h"
namespace client::config_test {
namespace {
const char kTestUrlPath[] = "/config";
// Application-specific error codes.
const int kMessageFormatError = 1;
const int kRequestFailedError = 2;
// Common to all messages.
const char kNameKey[] = "name";
const char kNameGlobalConfig[] = "global_config";
const char kNameSubscribe[] = "subscribe";
// Convert a dictionary value to a JSON string.
CefString GetJSON(CefRefPtr<CefDictionaryValue> dictionary) {
CefRefPtr<CefValue> value = CefValue::Create();
value->SetDictionary(dictionary);
return CefWriteJSON(value, JSON_WRITER_DEFAULT);
}
using CallbackType = CefMessageRouterBrowserSide::Callback;
void SendSuccess(CefRefPtr<CallbackType> callback,
CefRefPtr<CefDictionaryValue> result) {
callback->Success(GetJSON(result));
}
void SendFailure(CefRefPtr<CallbackType> callback,
int error_code,
const std::string& error_message) {
callback->Failure(error_code, error_message);
}
class PreferenceObserver : public CefPreferenceObserver {
public:
PreferenceObserver(CefRefPtr<CefPreferenceManager> manager,
bool global,
CefRefPtr<CallbackType> callback)
: manager_(manager), global_(global), callback_(callback) {}
PreferenceObserver(const PreferenceObserver&) = delete;
PreferenceObserver& operator=(const PreferenceObserver&) = delete;
void OnPreferenceChanged(const CefString& name) override {
CEF_REQUIRE_UI_THREAD();
auto value = manager_->GetPreference(name);
auto payload = CefDictionaryValue::Create();
payload->SetString("type", "preference");
payload->SetBool("global", global_);
payload->SetString("name", name);
if (value) {
payload->SetInt("value_type", value->GetType());
payload->SetValue("value", value);
} else {
payload->SetInt("value_type", VTYPE_NULL);
payload->SetNull("value");
}
SendSuccess(callback_, payload);
}
private:
const CefRefPtr<CefPreferenceManager> manager_;
const bool global_;
const CefRefPtr<CallbackType> callback_;
IMPLEMENT_REFCOUNTING(PreferenceObserver);
};
class SettingObserver : public CefSettingObserver {
public:
SettingObserver(CefRefPtr<CefRequestContext> context,
CefRefPtr<CallbackType> callback)
: context_(context), callback_(callback) {}
SettingObserver(const SettingObserver&) = delete;
SettingObserver& operator=(const SettingObserver&) = delete;
void OnSettingChanged(const CefString& requesting_url,
const CefString& top_level_url,
cef_content_setting_types_t content_type) override {
CEF_REQUIRE_UI_THREAD();
auto value = context_->GetWebsiteSetting(requesting_url, top_level_url,
content_type);
auto payload = CefDictionaryValue::Create();
payload->SetString("type", "setting");
payload->SetString("requesting_url", requesting_url);
payload->SetString("top_level_url", top_level_url);
payload->SetInt("content_type", static_cast<int>(content_type));
if (value) {
payload->SetInt("value_type", value->GetType());
payload->SetValue("value", value);
} else {
payload->SetInt("value_type", VTYPE_NULL);
payload->SetNull("value");
}
SendSuccess(callback_, payload);
}
private:
const CefRefPtr<CefRequestContext> context_;
const CefRefPtr<CallbackType> callback_;
IMPLEMENT_REFCOUNTING(SettingObserver);
};
// Handle messages in the browser process. Only accessed on the UI thread.
class Handler : public CefMessageRouterBrowserSide::Handler {
public:
typedef std::vector<std::string> NameVector;
Handler() { CEF_REQUIRE_UI_THREAD(); }
Handler(const Handler&) = delete;
Handler& operator=(const Handler&) = delete;
~Handler() override {
for (auto& pair : subscription_state_map_) {
delete pair.second;
}
}
// Called due to cefQuery execution in config.html.
bool OnQuery(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64_t query_id,
const CefString& request,
bool persistent,
CefRefPtr<Callback> callback) override {
CEF_REQUIRE_UI_THREAD();
// Only handle messages from the test URL.
const std::string& url = frame->GetURL();
if (!test_runner::IsTestURL(url, kTestUrlPath)) {
return false;
}
// Parse |request| as a JSON dictionary.
CefRefPtr<CefDictionaryValue> request_dict = ParseJSON(request);
if (!request_dict) {
callback->Failure(kMessageFormatError, "Incorrect message format");
return true;
}
// Verify the "name" key.
if (!VerifyKey(request_dict, kNameKey, VTYPE_STRING, callback)) {
return true;
}
const std::string& message_name = request_dict->GetString(kNameKey);
if (message_name == kNameGlobalConfig) {
// JavaScript is requesting a JSON representation of the global config.
SendGlobalConfig(callback);
return true;
} else if (message_name == kNameSubscribe) {
// Subscribe to notifications from observers.
if (!persistent) {
SendFailure(callback, kMessageFormatError,
"Subscriptions must be persistent");
return true;
}
if (!CreateSubscription(browser, query_id, callback)) {
SendFailure(callback, kRequestFailedError,
"Browser is already subscribed");
}
return true;
}
return false;
}
void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64_t query_id) override {
CEF_REQUIRE_UI_THREAD();
RemoveSubscription(browser->GetIdentifier(), query_id);
}
private:
// Convert a JSON string to a dictionary value.
static CefRefPtr<CefDictionaryValue> ParseJSON(const CefString& string) {
CefRefPtr<CefValue> value = CefParseJSON(string, JSON_PARSER_RFC);
if (value.get() && value->GetType() == VTYPE_DICTIONARY) {
return value->GetDictionary();
}
return nullptr;
}
// Verify that |key| exists in |dictionary| and has type |value_type|. Fails
// |callback| and returns false on failure.
static bool VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,
const char* key,
cef_value_type_t value_type,
CefRefPtr<Callback> callback) {
if (!dictionary->HasKey(key) || dictionary->GetType(key) != value_type) {
callback->Failure(
kMessageFormatError,
"Missing or incorrectly formatted message key: " + std::string(key));
return false;
}
return true;
}
static CefRefPtr<CefListValue> MakeListValue(std::vector<CefString> vec) {
if (vec.empty()) {
return nullptr;
}
auto list = CefListValue::Create();
list->SetSize(vec.size());
size_t idx = 0;
for (const auto& val : vec) {
list->SetString(idx++, val);
}
return list;
}
void SendGlobalConfig(CefRefPtr<Callback> callback) {
std::vector<CefString> switches;
CefPreferenceManager::GetChromeVariationsAsSwitches(switches);
std::vector<CefString> strings;
CefPreferenceManager::GetChromeVariationsAsStrings(strings);
auto payload = CefDictionaryValue::Create();
if (auto list = MakeListValue(switches)) {
payload->SetList("switches", list);
} else {
payload->SetNull("switches");
}
if (auto list = MakeListValue(strings)) {
payload->SetList("strings", list);
} else {
payload->SetNull("strings");
}
SendSuccess(callback, payload);
}
// Subscription state associated with a single browser.
struct SubscriptionState {
int64_t query_id;
CefRefPtr<PreferenceObserver> global_pref_observer;
CefRefPtr<CefRegistration> global_pref_registration;
CefRefPtr<PreferenceObserver> context_pref_observer;
CefRefPtr<CefRegistration> context_pref_registration;
CefRefPtr<SettingObserver> context_setting_observer;
CefRefPtr<CefRegistration> context_setting_registration;
};
bool CreateSubscription(CefRefPtr<CefBrowser> browser,
int64_t query_id,
CefRefPtr<Callback> callback) {
const int browser_id = browser->GetIdentifier();
if (subscription_state_map_.find(browser_id) !=
subscription_state_map_.end()) {
// An subscription already exists for this browser.
return false;
}
auto global_pref_manager =
CefPreferenceManager::GetGlobalPreferenceManager();
auto request_context = browser->GetHost()->GetRequestContext();
SubscriptionState* state = new SubscriptionState();
state->query_id = query_id;
state->global_pref_observer =
new PreferenceObserver(global_pref_manager, /*global=*/true, callback);
state->global_pref_registration =
global_pref_manager->AddPreferenceObserver(CefString(),
state->global_pref_observer);
state->context_pref_observer =
new PreferenceObserver(request_context, /*global=*/false, callback);
state->context_pref_registration = request_context->AddPreferenceObserver(
CefString(), state->context_pref_observer);
state->context_setting_observer =
new SettingObserver(request_context, callback);
state->context_setting_registration =
request_context->AddSettingObserver(state->context_setting_observer);
subscription_state_map_[browser_id] = state;
return true;
}
void RemoveSubscription(int browser_id, int64_t query_id) {
SubscriptionStateMap::iterator it =
subscription_state_map_.find(browser_id);
if (it != subscription_state_map_.end() &&
it->second->query_id == query_id) {
delete it->second;
subscription_state_map_.erase(it);
}
}
// Map of browser ID to SubscriptionState object.
typedef std::map<int, SubscriptionState*> SubscriptionStateMap;
SubscriptionStateMap subscription_state_map_;
};
} // namespace
void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers) {
handlers.insert(new Handler());
}
} // namespace client::config_test

View File

@ -0,0 +1,18 @@
// Copyright (c) 2025 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#ifndef CEF_TESTS_CEFCLIENT_BROWSER_CONFIG_TEST_H_
#define CEF_TESTS_CEFCLIENT_BROWSER_CONFIG_TEST_H_
#pragma once
#include "tests/cefclient/browser/test_runner.h"
namespace client::config_test {
// Create message handlers. Called from test_runner.cc.
void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers);
} // namespace client::config_test
#endif // CEF_TESTS_CEFCLIENT_BROWSER_CONFIG_TEST_H_

View File

@ -46,32 +46,33 @@
#define IDC_STATIC -1
#define IDS_BINARY_TRANSFER_HTML 1000
#define IDS_BINDING_HTML 1001
#define IDS_DIALOGS_HTML 1002
#define IDS_DRAGGABLE_HTML 1003
#define IDS_HANG_HTML 1004
#define IDS_IPC_PERFORMANCE_HTML 1005
#define IDS_LOCALSTORAGE_HTML 1006
#define IDS_LOGO_PNG 1007
#define IDS_MEDIA_ROUTER_HTML 1008
#define IDS_MENU_ICON_1X_PNG 1009
#define IDS_MENU_ICON_2X_PNG 1010
#define IDS_OSRTEST_HTML 1011
#define IDS_OTHER_TESTS_HTML 1012
#define IDS_PDF_HTML 1013
#define IDS_PDF_PDF 1014
#define IDS_PERFORMANCE_HTML 1015
#define IDS_PERFORMANCE2_HTML 1016
#define IDS_PREFERENCES_HTML 1017
#define IDS_RESPONSE_FILTER_HTML 1018
#define IDS_SERVER_HTML 1019
#define IDS_TASK_MANAGER_HTML 1020
#define IDS_TRANSPARENCY_HTML 1021
#define IDS_URLREQUEST_HTML 1022
#define IDS_WEBSOCKET_HTML 1023
#define IDS_WINDOW_HTML 1024
#define IDS_WINDOW_ICON_1X_PNG 1025
#define IDS_WINDOW_ICON_2X_PNG 1026
#define IDS_XMLHTTPREQUEST_HTML 1027
#define IDS_CONFIG_HTML 1002
#define IDS_DIALOGS_HTML 1003
#define IDS_DRAGGABLE_HTML 1004
#define IDS_HANG_HTML 1005
#define IDS_IPC_PERFORMANCE_HTML 1006
#define IDS_LOCALSTORAGE_HTML 1007
#define IDS_LOGO_PNG 1008
#define IDS_MEDIA_ROUTER_HTML 1009
#define IDS_MENU_ICON_1X_PNG 1010
#define IDS_MENU_ICON_2X_PNG 1011
#define IDS_OSRTEST_HTML 1012
#define IDS_OTHER_TESTS_HTML 1013
#define IDS_PDF_HTML 1014
#define IDS_PDF_PDF 1015
#define IDS_PERFORMANCE_HTML 1016
#define IDS_PERFORMANCE2_HTML 1017
#define IDS_PREFERENCES_HTML 1018
#define IDS_RESPONSE_FILTER_HTML 1019
#define IDS_SERVER_HTML 1020
#define IDS_TASK_MANAGER_HTML 1021
#define IDS_TRANSPARENCY_HTML 1022
#define IDS_URLREQUEST_HTML 1023
#define IDS_WEBSOCKET_HTML 1024
#define IDS_WINDOW_HTML 1025
#define IDS_WINDOW_ICON_1X_PNG 1026
#define IDS_WINDOW_ICON_2X_PNG 1027
#define IDS_XMLHTTPREQUEST_HTML 1028
// Next default values for new objects
//

View File

@ -15,6 +15,7 @@ int GetResourceId(const char* resource_name) {
int id;
} resource_map[] = {{"binary_transfer.html", IDS_BINARY_TRANSFER_HTML},
{"binding.html", IDS_BINDING_HTML},
{"config.html", IDS_CONFIG_HTML},
{"dialogs.html", IDS_DIALOGS_HTML},
{"draggable.html", IDS_DRAGGABLE_HTML},
{"hang.html", IDS_HANG_HTML},

View File

@ -21,6 +21,7 @@
#include "tests/cefclient/browser/base_client_handler.h"
#include "tests/cefclient/browser/binary_transfer_test.h"
#include "tests/cefclient/browser/binding_test.h"
#include "tests/cefclient/browser/config_test.h"
#include "tests/cefclient/browser/dialog_test.h"
#include "tests/cefclient/browser/hang_test.h"
#include "tests/cefclient/browser/main_context.h"
@ -865,6 +866,9 @@ void CreateMessageHandlers(MessageHandlerSet& handlers) {
// Create the binding test handlers.
binding_test::CreateMessageHandlers(handlers);
// Create the config test handlers.
config_test::CreateMessageHandlers(handlers);
// Create the dialog test handlers.
dialog_test::CreateMessageHandlers(handlers);

View File

@ -0,0 +1,703 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Configuration Test</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<style>
body {
font-family: Verdana, Arial;
font-size: 12px;
}
#message {
color: red;
font-weight: bold;
font-size: 14px;
}
.desc {
font-size: 14px;
}
.foot {
font-size: 11px;
}
.mono {
font-family: monospace;
}
.cat_header_0 {
font-weight: bold;
font-size: 14px;
}
.cat_header_1 {
font-weight: bold;
}
.cat_header_2 {
font-family: Verdana, Arial;
}
.cat_body {
font-family: monospace;
white-space: pre;
margin-left: 10px;
}
#temp-message {
display: none;
background-color: #f0f0f0;
border: 1px solid #ccc;
padding: 10px;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.hr-container {
display: flex;
align-items: center;
text-align: center;
}
.hr-line {
border-top: 1px solid black;
width: 100%;
margin: 0 3px;
}
.hr-text {
padding: 0;
white-space: nowrap;
}
</style>
<script>
function onLoad() {
if (location.hostname != 'tests') {
onCefError(0, 'This page can only be run from tests.');
// Disable all form elements.
var elements = document.getElementById("form").elements;
for (var i = 0, element; element = elements[i++]; ) {
element.disabled = true;
}
return;
}
getGlobalConfig();
updateFilter();
startSubscription();
}
function onUnload() {
stopSubscription();
}
function onCefError(code, message) {
val = 'ERROR: ' + message;
if (code !== 0) {
val += ' (' + code + ')';
}
document.getElementById('message').innerHTML = val + '<br/><br/>';
}
function sendCefQuery(payload, onSuccess, onFailure=onCefError, persistent=false) {
// Results in a call to the OnQuery method in config_test.cc
return window.cefQuery({
request: JSON.stringify(payload),
onSuccess: onSuccess,
onFailure: onFailure,
persistent: persistent
});
}
// Request the global configuration.
function getGlobalConfig() {
sendCefQuery(
{name: 'global_config'},
(message) => onGlobalConfigMessage(JSON.parse(message)),
);
}
// Display the global configuration response.
function onGlobalConfigMessage(message) {
document.getElementById('global_switches').innerHTML =
message.switches !== null ? message.switches.join('<br/>') : '(none)';
if (message.strings !== null) {
document.getElementById('global_strings').innerHTML = message.strings.join('<br/>');
document.getElementById('global_strings_ct').textContent = message.strings.length;
}
}
var currentSubscriptionId = null;
// Subscribe to ongoing message notifications from the native code.
function startSubscription() {
currentSubscriptionId = sendCefQuery(
{name: 'subscribe'},
(message) => onSubscriptionMessage(JSON.parse(message)),
(code, message) => {
onCefError(code, message);
currentSubscriptionId = null;
},
true
);
}
// Unsubscribe from message notifications.
function stopSubscription() {
if (currentSubscriptionId !== null) {
// Results in a call to the OnQueryCanceled method in config_test.cc
window.cefQueryCancel(currentSubscriptionId);
}
}
// Returns a nice timestamp for display purposes.
function getNiceTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
var paused = false;
var paused_messages = [];
var first_after_pause = false;
// Toggle whether messages are displayed or queued.
function togglePause() {
paused = !paused;
document.getElementById("pause_button").value = paused ? "Resume" : "Pause";
if (!paused) {
first_after_pause = true;
while (paused_messages.length > 0) {
onSubscriptionMessage(paused_messages.shift());
}
}
}
function doPause() {
if (!paused) {
togglePause();
showTempMessage('Event processing is paused. Click Resume to continue.');
}
}
var filter = {}
var filtered_ct = 0;
var filter_updating = false;
// Populate |filter| based on form control state.
function updateFilter() {
if (filter_updating) {
// Ignore changes triggered from individual elements while we're updating multiple.
return;
}
filter.text = document.getElementById("filter_text").value.trim().toLowerCase();
filter.global_prefs = document.getElementById("filter_global_prefs").checked;
filter.context_prefs = document.getElementById("filter_context_prefs").checked;
filter.context_settings = document.getElementById("filter_context_settings").checked;
}
function doFilter(type, text, global=false) {
filter_updating = true;
document.getElementById("filter_text").value = text;
var checked = '';
if (type === 'preference') {
checked = global ? 'filter_global_prefs' : 'filter_context_prefs';
} else if (type === 'setting') {
checked = 'filter_context_settings';
}
['filter_global_prefs', 'filter_context_prefs', 'filter_context_settings'].forEach(function(id) {
document.getElementById(id).checked = id === checked;
});
filter_updating = false;
updateFilter();
}
function doFilterReset() {
filter_updating = true;
document.getElementById("filtered_ct").textContent = 0;
document.getElementById("filter_text").value = '';
document.getElementById("filter_global_prefs").checked = true;
document.getElementById("filter_context_prefs").checked = true;
document.getElementById("filter_context_settings").checked = true;
filter_updating = false;
updateFilter();
}
// Returns true if the message should be displayed based on the current filter settings.
function passesFilter(message) {
if (message.type === 'preference') {
if (message.global) {
if (!filter.global_prefs) {
return false;
}
} else if (!filter.context_prefs) {
return false;
}
} else if (message.type === 'setting' && !filter.context_settings) {
return false;
}
if (filter.text.length > 0) {
var check_text = JSON.stringify(message).toLowerCase();
if (message.type === 'setting') {
check_text += ' ' + getSettingTypeLabel(message.content_type).toLowerCase();
}
if (check_text.indexOf(filter.text) < 0) {
return false;
}
}
return true;
}
// Match the cef_value_type_t values from include/internal/cef_types.h
const value_types = [
'INVALID',
'NULL',
'BOOL',
'INT',
'DOUBLE',
'STRING',
'BINARY',
'DICTIONARY',
'LIST',
]
function getValueType(index) {
if (index < 0 || index >= value_types.length) {
return 'UNKNOWN';
}
return value_types[index];
}
// Match the cef_content_setting_types_t values from include/internal/cef_types_content_settings.h
const setting_types = [
"COOKIES",
"IMAGES",
"JAVASCRIPT",
"POPUPS",
"GEOLOCATION",
"NOTIFICATIONS",
"AUTO_SELECT_CERTIFICATE",
"MIXEDSCRIPT",
"MEDIASTREAM_MIC",
"MEDIASTREAM_CAMERA",
"PROTOCOL_HANDLERS",
"DEPRECATED_PPAPI_BROKER",
"AUTOMATIC_DOWNLOADS",
"MIDI_SYSEX",
"SSL_CERT_DECISIONS",
"PROTECTED_MEDIA_IDENTIFIER",
"APP_BANNER",
"SITE_ENGAGEMENT",
"DURABLE_STORAGE",
"USB_CHOOSER_DATA",
"BLUETOOTH_GUARD",
"BACKGROUND_SYNC",
"AUTOPLAY",
"IMPORTANT_SITE_INFO",
"PERMISSION_AUTOBLOCKER_DATA",
"ADS",
"ADS_DATA",
"MIDI",
"PASSWORD_PROTECTION",
"MEDIA_ENGAGEMENT",
"SOUND",
"CLIENT_HINTS",
"SENSORS",
"DEPRECATED_ACCESSIBILITY_EVENTS",
"PAYMENT_HANDLER",
"USB_GUARD",
"BACKGROUND_FETCH",
"INTENT_PICKER_DISPLAY",
"IDLE_DETECTION",
"SERIAL_GUARD",
"SERIAL_CHOOSER_DATA",
"PERIODIC_BACKGROUND_SYNC",
"BLUETOOTH_SCANNING",
"HID_GUARD",
"HID_CHOOSER_DATA",
"WAKE_LOCK_SCREEN",
"WAKE_LOCK_SYSTEM",
"LEGACY_COOKIE_ACCESS",
"FILE_SYSTEM_WRITE_GUARD",
"NFC",
"BLUETOOTH_CHOOSER_DATA",
"CLIPBOARD_READ_WRITE",
"CLIPBOARD_SANITIZED_WRITE",
"SAFE_BROWSING_URL_CHECK_DATA",
"VR",
"AR",
"FILE_SYSTEM_READ_GUARD",
"STORAGE_ACCESS",
"CAMERA_PAN_TILT_ZOOM",
"WINDOW_MANAGEMENT",
"INSECURE_PRIVATE_NETWORK",
"LOCAL_FONTS",
"PERMISSION_AUTOREVOCATION_DATA",
"FILE_SYSTEM_LAST_PICKED_DIRECTORY",
"DISPLAY_CAPTURE",
"FILE_SYSTEM_ACCESS_CHOOSER_DATA",
"FEDERATED_IDENTITY_SHARING",
"JAVASCRIPT_JIT",
"HTTP_ALLOWED",
"FORMFILL_METADATA",
"DEPRECATED_FEDERATED_IDENTITY_ACTIVE_SESSION",
"AUTO_DARK_WEB_CONTENT",
"REQUEST_DESKTOP_SITE",
"FEDERATED_IDENTITY_API",
"NOTIFICATION_INTERACTIONS",
"REDUCED_ACCEPT_LANGUAGE",
"NOTIFICATION_PERMISSION_REVIEW",
"PRIVATE_NETWORK_GUARD",
"PRIVATE_NETWORK_CHOOSER_DATA",
"FEDERATED_IDENTITY_IDENTITY_PROVIDER_SIGNIN_STATUS",
"REVOKED_UNUSED_SITE_PERMISSIONS",
"TOP_LEVEL_STORAGE_ACCESS",
"FEDERATED_IDENTITY_AUTO_REAUTHN_PERMISSION",
"FEDERATED_IDENTITY_IDENTITY_PROVIDER_REGISTRATION",
"ANTI_ABUSE",
"THIRD_PARTY_STORAGE_PARTITIONING",
"HTTPS_ENFORCED",
"ALL_SCREEN_CAPTURE",
"COOKIE_CONTROLS_METADATA",
"TPCD_HEURISTICS_GRANTS",
"TPCD_METADATA_GRANTS",
"TPCD_TRIAL",
"TOP_LEVEL_TPCD_TRIAL",
"TOP_LEVEL_TPCD_ORIGIN_TRIAL",
"AUTO_PICTURE_IN_PICTURE",
"FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION",
"FILE_SYSTEM_ACCESS_RESTORE_PERMISSION",
"CAPTURED_SURFACE_CONTROL",
"SMART_CARD_GUARD",
"SMART_CARD_DATA",
"WEB_PRINTING",
"AUTOMATIC_FULLSCREEN",
"SUB_APP_INSTALLATION_PROMPTS",
"SPEAKER_SELECTION",
"DIRECT_SOCKETS",
"KEYBOARD_LOCK",
"POINTER_LOCK",
"REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS",
"TRACKING_PROTECTION",
"DISPLAY_MEDIA_SYSTEM_AUDIO",
"JAVASCRIPT_OPTIMIZER",
"STORAGE_ACCESS_HEADER_ORIGIN_TRIAL",
"HAND_TRACKING",
"WEB_APP_INSTALLATION",
"DIRECT_SOCKETS_PRIVATE_NETWORK_ACCESS",
"LEGACY_COOKIE_SCOPE",
"ARE_SUSPICIOUS_NOTIFICATIONS_ALLOWLISTED_BY_USER",
"CONTROLLED_FRAME",
];
function getSettingType(index) {
if (index < 0 || index >= setting_types.length) {
return 'UNKNOWN';
}
return setting_types[index];
}
function getSettingTypeLabel(type) {
return getSettingType(type) + ' (' + type + ')'
}
function makeDetails(summaryHTML, summaryClass, contentHTML, contentClass, contentId=null, open=false) {
const newDetails = document.createElement('details');
if (open) {
newDetails.open = true;
}
const newSummary = document.createElement('summary');
newSummary.innerHTML = summaryHTML;
if (summaryClass !== null) {
newSummary.className = summaryClass;
}
newDetails.append(newSummary);
const newContent = document.createElement('p');
newContent.innerHTML = contentHTML
if (contentClass !== null) {
newContent.className = contentClass;
}
if (contentId !== null) {
newContent.id = contentId;
}
newDetails.append(newContent);
const newP = document.createElement('p');
newP.append(newDetails);
return newP;
}
function makeValueExample(value, value_type) {
code = '\n// Create a CefValue object programmatically:\n' +
'auto value = CefValue::Create();\n';
if (value === null || getValueType(value_type) == 'NULL') {
code += 'value->SetNull();\n';
} else if (typeof value === 'boolean' || getValueType(value_type) == 'BOOL') {
code += 'value->SetBool(' + (value ? 'true' : 'false') + ');\n';
} else if (Number.isInteger(value) || getValueType(value_type) == 'INT') {
code += 'value->SetInt(' + value + ');\n';
} else if (typeof value === 'number' || getValueType(value_type) == 'DOUBLE') {
code += 'value->SetDouble(' + value + ');\n';
} else if (typeof value === 'string' || getValueType(value_type) == 'STRING') {
code += 'value->SetString("' + value + '");\n';
} else if (Array.isArray(value) || getValueType(value_type) == 'LIST') {
code += 'auto listValue = CefListValue::Create();\n';
if (value.length > 0) {
code += '\n// TODO: Populate |listValue| using CefListValue::Set* methods.\n\n';
}
code += 'value->SetList(listValue);\n';
if (value.length > 0) {
code += '\n// ALTERNATELY: Create a CefValue object by parsing a JSON string:\n' +
'auto value = CefParseJSON("[ ... ]", JSON_PARSER_RFC);\n';
}
} else if (typeof value === 'object' || getValueType(value_type) == 'DICTIONARY') {
code += 'auto dictValue = CefDictionaryValue::Create();\n';
const has_value = Object.keys(value).length > 0;
if (has_value) {
code += '\n// TODO: Populate |dictValue| using CefDictionaryValue::Set* methods.\n\n';
}
code += 'value->SetDictionary(dictValue);\n';
if (has_value) {
code += '\n// ALTERNATELY: Create a CefValue object by parsing a JSON string:\n' +
'auto value = CefParseJSON("{ ... }", JSON_PARSER_RFC);\n';
}
} else {
code += '\n//TODO: Populate |value|.\n\n';
}
return code;
}
function makeCopyLink(elem_id) {
return '<a href="#" onMouseDown="copyToClipboard(\'' + elem_id + '\')" onClick="return false">[copy to clipboard]</a>';
}
function makeContent(elem_id, content) {
const content_id = 'cn-' + elem_id;
return makeDetails('Content ' + makeCopyLink(content_id), 'cat_header_2', content, 'cat_body', content_id, true);
}
function makeHR(label) {
const container = document.createElement('div');
container.className = 'hr-container';
const line1 = document.createElement('div');
line1.className = 'hr-line';
container.append(line1);
const text = document.createElement('span');
text.className = 'hr-text';
text.innerHTML = label;
container.append(text);
const line2 = document.createElement('div');
line2.className = 'hr-line';
container.append(line2);
return container;
}
function makeCodeExample(elem_id, message) {
const example_id = 'ex-' + elem_id;
var code = '// Code must be executed on the browser process UI thread.\n\n';
if (message.type === "preference") {
if (message.global) {
code += 'auto pref_manager = CefPreferenceManager::GetGlobalPreferenceManager();\n';
} else {
code += '// |browser| is an existing CefBrowser instance.\n' +
'auto pref_manager = browser->GetHost()->GetRequestContext();\n';
}
code += makeValueExample(message.value, message.value_type) + '\n' +
'CefString error;\n' +
'bool success = pref_manager->SetPreference("' + message.name + '", value, error);\n' +
'if (!success) {\n' +
' // TODO: Use |error| to diagnose the failure.\n' +
'}\n';
} else if (message.type === "setting") {
const type = getSettingType(message.content_type);
const content_type = type !== 'UNKNOWN' ? 'CEF_CONTENT_SETTING_TYPE_' + type : message.content_type;
code += '// |browser| is an existing CefBrowser instance.\n' +
'auto context = browser->GetHost()->GetRequestContext();\n' +
makeValueExample(message.value, message.value_type) + '\n' +
'context->SetWebsiteSetting("' + message.requesting_url + '", "' + message.top_level_url +
'", '+ content_type +', value);\n';
}
return makeDetails('C++ Code Example ' + makeCopyLink(example_id), 'cat_header_2', code, 'cat_body', example_id, false);
}
var message_ct = 0;
// A new message has arrived. It may be queued, filtered out or displayed.
function onSubscriptionMessage(message) {
if (paused) {
// Queue the message until the user clicks Resume.
message.timestamp = getNiceTimestamp();
paused_messages.push(message);
document.getElementById("pause_button").value = 'Resume (' + paused_messages.length + ')';
return;
}
if (!passesFilter(message)) {
// Filter out the message.
filtered_ct++;
document.getElementById("filtered_ct").innerHTML = filtered_ct;
return;
}
// Use the arrival timestamp for queued messages.
var timestamp;
if (message.timestamp) {
timestamp = message.timestamp;
delete message.timestamp;
} else {
timestamp = getNiceTimestamp();
}
// Display the message.
var label = timestamp + ': ';
var content = 'value_type=' + getValueType(message.value_type);
var search = '';
var filter = '';
if (message.type === "preference") {
label += 'Preference (' + (message.global ? 'Global' : 'Profile') +
') <span class="mono">' + message.name + '</span>';
search = '%5C%22' + message.name + '%5C%22';
filter = "'preference', '" + message.name + "', " + (message.global ? 'true' : 'false');
} else if (message.type === "setting") {
label += 'Setting <span class="mono">' + getSettingTypeLabel(message.content_type) + '</span>';
const setting_type = getSettingType(message.content_type);
search = 'ContentSettingsType::' + setting_type;
filter = "'setting', '" + setting_type + "'";
content = 'requesting_url=' + message.requesting_url +
'\ntop_level_url=' + message.top_level_url +
'\n' + content;
}
content += '\nvalue=' + JSON.stringify(message.value, null, 1);
label += ' <a href="#" onMouseDown="doFilter(' + filter + ')" onClick="return false">[filter]</a>' +
' <a href="https://source.chromium.org/search?q=' + search + '" target="_blank">[search &#x1F517]</a>';
const messages = document.getElementById('messages');
if (first_after_pause) {
messages.prepend(makeHR('RESUMED'));
first_after_pause = false;
}
const elem_id = message_ct++;
const newDetails = makeDetails(label, null, makeContent(elem_id, content).outerHTML +
makeCodeExample(elem_id, message).outerHTML, 'cat_body');
messages.prepend(newDetails);
}
// Clear filter count and displayed/pending messages.
function doClear() {
filtered_ct = 0;
document.getElementById("filtered_ct").textContent = 0;
document.getElementById('messages').innerHTML = '';
if (paused) {
paused_messages = [];
document.getElementById("pause_button").value = 'Resume';
}
message_ct = 0;
}
function showTempMessage(msg) {
const element = document.getElementById("temp-message");
element.innerHTML = msg;
element.style.display = "block";
setTimeout(function() {
element.style.display = "none";
}, 3000);
}
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
if (!element) {
return;
}
// Make all parent details nodes are open, otherwise nothing will be copied to the clipboard.
var parent = element.parentNode;
while (parent) {
if (parent.nodeName === 'DETAILS') {
if (!parent.open) {
parent.open = true;
}
}
parent = parent.parentNode;
}
navigator.clipboard.writeText(element.outerText)
.then(() => {
showTempMessage('Text copied to clipboard.');
})
.catch(err => {
showTempMessage('Failed to copy text to clipboard!');
console.error('Failed to copy text: ', err);
});
}
</script>
</head>
<body bgcolor="white" onload="onLoad()" onunload="onUnload()">
<div id="message"></div>
<details open>
<summary class="cat_header_0">Startup configuration</summary>
<p class="desc">
This section displays the global configuration (Chrome Variations) that was applied at startup.
Chrome Variations can be configured via chrome://flags <sup>[*]</sup>, via the below command-line switches, or via field trials (disabled in Official builds).
The Active Variations section below is the human-readable equivalent of the "Active Variations" section of chrome://version.
See <a href="https://developer.chrome.com/docs/web-platform/chrome-variations" target="_blank">Chrome Variations docs</a> for background.
</p>
<p class="foot">
* Flags are stored in the global <span class="mono">browser.enabled_labs_experiments</span> preference.
</p>
<p class="cat_header_1">Command-Line Switches:</p>
<p class="cat_body" id="global_switches"></p>
<details>
<summary class="cat_header_1">Active Variations (<span id="global_strings_ct">0</span>)</summary>
<p class="cat_body" id="global_strings"></p>
</details>
</details>
<br/>
<details open>
<summary class="cat_header_0">Runtime configuration</summary>
<p class="desc">
This section displays preference and site settings changes that occur during runtime.
Chromium stores both global and Profile-specific preferences.
See <a href="https://www.chromium.org/developers/design-documents/preferences/" target="_blank">Preferences docs</a> for background.
To view a snapshot of all preferences go <a href="https://tests/preferences#advanced">here</a> instead.
</p>
<p id="filter">
<form id="form">
Text Contains: <input type="text" id="filter_text"/> <input type="button" onclick="updateFilter();" value="Apply"/>
<br/>Show: <input type="checkbox" id="filter_global_prefs" onChange="updateFilter()" checked /> Global preferences
<input type="checkbox" id="filter_context_prefs" onChange="updateFilter()" checked /> Profile-specific preferences
<input type="checkbox" id="filter_context_settings" onChange="updateFilter()" checked /> Site settings <sup>[*]</sup>
<br/><input type="button" id="clear_button" onclick="doClear()" value="Clear"/>
<input type="button" id="pause_button" onclick="togglePause()" value="Pause"/> <sup>[**]</sup> Filtered out: <span id="filtered_ct">0</span>
<input type="button" id="reset_button" onclick="doFilterReset()" value="Reset"/>
<p class="foot">
* Site settings are stored in the Profile-specific <span class="mono">profile.content_settings</span> preference and can be modified via chrome://settings/content.
<br/>** Events will not be displayed or filtered out while processing is paused.
</p>
</form>
</p>
<div id="messages" onMouseDown="doPause()"></div>
<div id="temp-message"></div>
</details>
</body>
</html>

View File

@ -10,6 +10,7 @@
<li><a href="https://jigsaw.w3.org/HTTP/Basic/">Authentication (Basic)</a> - credentials returned via GetAuthCredentials</li>
<li><a href="https://jigsaw.w3.org/HTTP/Digest/">Authentication (Digest)</a> - credentials returned via GetAuthCredentials</li>
<li><a href="binary_transfer">Binary vs String Transfer Benchmark</a></li>
<li><a href="config">Chrome Configuration</a></li>
<li><a href="http://html5advent2011.digitpaint.nl/3/index.html">Cursors</a></li>
<li><a href="dialogs">Dialogs</a></li>
<li><a href="http://html5demos.com/drag">Drag & Drop</a></li>

View File

@ -13,8 +13,12 @@
<script>
function setup() {
if (location.hostname == 'tests' || location.hostname == 'localhost')
if (location.hostname === 'tests' || location.hostname === 'localhost') {
if (location.hash === '#advanced') {
toggleView();
}
return;
}
alert('This page can only be run from tests or localhost.');

View File

@ -31,6 +31,7 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
IDS_BINARY_TRANSFER_HTML BINARY "tests\\cefclient\\resources\\binary_transfer.html"
IDS_BINDING_HTML BINARY "tests\\cefclient\\resources\\binding.html"
IDS_CONFIG_HTML BINARY "tests\\cefclient\\resources\\config.html"
IDS_DIALOGS_HTML BINARY "tests\\cefclient\\resources\\dialogs.html"
IDS_DRAGGABLE_HTML BINARY "tests\\cefclient\\resources\\draggable.html"
IDS_HANG_HTML BINARY "tests\\cefclient\\resources\\hang.html"