blob: 9f9e0772783c93ca5560c41a55d0417c7b8b7f81 [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/linux/dark_mode_manager_linux.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "components/dbus/xdg/portal.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ui_base_features.h"
#include "ui/linux/fake_linux_ui.h"
#include "ui/native_theme/mock_os_settings_provider.h"
#include "ui/native_theme/native_theme.h"
namespace ui {
namespace {
class MockLinuxUi : public FakeLinuxUi {
public:
MOCK_METHOD(void, SetDarkTheme, (bool dark), (override));
MOCK_METHOD(void,
SetColorScheme,
(std::optional<bool> prefer_dark),
(override));
MOCK_METHOD(void, SetAccentColor, (std::optional<SkColor> color), (override));
};
// Matches a method call to the specified dbus target.
MATCHER_P2(Calls, interface, member, "") {
return arg->GetInterface() == interface && arg->GetMember() == member;
}
} // namespace
using testing::_;
using testing::Mock;
using testing::Return;
using testing::StrictMock;
class DarkModeManagerLinuxTest : public testing::Test {
public:
~DarkModeManagerLinuxTest() override = default;
protected:
// The web color scheme is sourced from the active `OsSettingsProvider` via
// `UpdateVariablesForToolkitSettings()`, so it reflects what the manager
// pushed into the toolkit (emulated by the `SetColorScheme` wiring below).
bool PrefersDarkTheme() const {
return NativeTheme::GetInstanceForNativeUi()->preferred_color_scheme() ==
NativeTheme::PreferredColorScheme::kDark;
}
dbus::ObjectProxy::SignalCallback& setting_changed_callback() {
return setting_changed_callback_;
}
dbus::ObjectProxy::OnConnectedCallback& signal_connected_callback() {
return signal_connected_callback_;
}
dbus::ObjectProxy::ResponseOrErrorCallback& color_scheme_callback() {
return color_scheme_callback_;
}
dbus::ObjectProxy::ResponseOrErrorCallback& accent_color_callback() {
return accent_color_callback_;
}
MockOsSettingsProvider& os_settings_provider() {
return os_settings_provider_;
}
MockLinuxUi* mock_linux_ui() { return mock_linux_ui_.get(); }
private:
void SetUp() override {
dbus_xdg::SetPortalStateForTesting(
dbus_xdg::PortalRegistrarState::kSuccess);
mock_bus_ = base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
mock_dbus_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
mock_bus_.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
mock_portal_proxy_ =
base::MakeRefCounted<StrictMock<dbus::MockObjectProxy>>(
mock_bus_.get(), DarkModeManagerLinux::kFreedesktopSettingsService,
dbus::ObjectPath(
DarkModeManagerLinux::kFreedesktopSettingsObjectPath));
EXPECT_CALL(*mock_bus_, GetObjectProxy(DBUS_SERVICE_DBUS,
dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy_.get()));
EXPECT_CALL(*mock_bus_,
GetObjectProxy(
DarkModeManagerLinux::kFreedesktopSettingsService,
dbus::ObjectPath(
DarkModeManagerLinux::kFreedesktopSettingsObjectPath)))
.WillRepeatedly(Return(mock_portal_proxy_.get()));
EXPECT_CALL(
*mock_portal_proxy_,
ConnectToSignal(DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kSettingChangedSignal, _, _))
.WillOnce(
[&](const std::string& interface_name,
const std::string& signal_name,
dbus::ObjectProxy::SignalCallback signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback) {
setting_changed_callback_ = signal_callback;
signal_connected_callback_ = std::move(on_connected_callback);
});
EXPECT_CALL(*mock_portal_proxy_,
CallMethodWithErrorResponse(
Calls(DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kReadMethod),
_, _))
.WillOnce([&](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseOrErrorCallback callback) {
color_scheme_callback_ = std::move(callback);
})
.WillOnce([&](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseOrErrorCallback callback) {
accent_color_callback_ = std::move(callback);
});
mock_linux_ui_ = std::make_unique<MockLinuxUi>();
linux_ui_themes_ = std::vector<raw_ptr<LinuxUiTheme, VectorExperimental>>{
mock_linux_ui_.get()};
// In production, each toolkit's `SetColorScheme()`/`SetAccentColor()` route
// the value through its `OsSettingsProvider`, which is what drives the web
// `NativeTheme` via `UpdateVariablesForToolkitSettings()`. Emulate that
// here so the assertions below exercise the provider path rather than a
// direct write from the manager.
ON_CALL(*mock_linux_ui_, SetColorScheme(_))
.WillByDefault([this](std::optional<bool> prefer_dark) {
os_settings_provider_.SetPreferredColorScheme(
prefer_dark
? (*prefer_dark ? NativeTheme::PreferredColorScheme::kDark
: NativeTheme::PreferredColorScheme::kLight)
: NativeTheme::PreferredColorScheme::kNoPreference);
});
ON_CALL(*mock_linux_ui_, SetAccentColor(_))
.WillByDefault([this](std::optional<SkColor> color) {
if (color.has_value()) {
os_settings_provider_.SetAccentColor(color.value());
}
});
enable_portal_accent_color_.InitAndEnableFeature(
features::kUsePortalAccentColor);
// Baseline: no portal preference yet, so the toolkit-sourced scheme (here,
// the mock provider) is "no preference".
os_settings_provider_.SetPreferredColorScheme(
NativeTheme::PreferredColorScheme::kNoPreference);
manager_ =
std::make_unique<DarkModeManagerLinux>(mock_bus_, &linux_ui_themes_);
auto* const native_theme = ui::NativeTheme::GetInstanceForNativeUi();
EXPECT_FALSE(PrefersDarkTheme());
EXPECT_EQ(native_theme->preferred_color_scheme(),
NativeTheme::PreferredColorScheme::kNoPreference);
EXPECT_FALSE(native_theme->user_color().has_value());
}
void TearDown() override {
manager_.reset();
dbus_xdg::SetPortalStateForTesting(dbus_xdg::PortalRegistrarState::kIdle);
}
std::unique_ptr<MockLinuxUi> mock_linux_ui_;
std::vector<raw_ptr<LinuxUiTheme, VectorExperimental>> linux_ui_themes_;
MockOsSettingsProvider os_settings_provider_;
scoped_refptr<dbus::MockBus> mock_bus_;
scoped_refptr<dbus::MockObjectProxy> mock_dbus_proxy_;
scoped_refptr<dbus::MockObjectProxy> mock_portal_proxy_;
dbus::ObjectProxy::SignalCallback setting_changed_callback_;
dbus::ObjectProxy::OnConnectedCallback signal_connected_callback_;
dbus::ObjectProxy::ResponseOrErrorCallback color_scheme_callback_;
dbus::ObjectProxy::ResponseOrErrorCallback accent_color_callback_;
base::test::ScopedFeatureList enable_portal_accent_color_;
std::unique_ptr<DarkModeManagerLinux> manager_;
};
TEST_F(DarkModeManagerLinuxTest, UseNativeThemeSetting) {
// Without a portal preference, the toolkit-sourced scheme (the provider)
// drives the web theme directly; the manager is not involved.
os_settings_provider().SetPreferredColorScheme(
NativeTheme::PreferredColorScheme::kDark);
EXPECT_TRUE(PrefersDarkTheme());
os_settings_provider().SetPreferredColorScheme(
NativeTheme::PreferredColorScheme::kLight);
EXPECT_FALSE(PrefersDarkTheme());
// Let the manager know the DBus method call and signal connection failed.
dbus::MethodCall method_call(
DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kReadMethod);
method_call.SetSerial(123);
auto error = dbus::ErrorResponse::FromMethodCall(
&method_call, "org.freedesktop.DBus.Error.Failed", "");
std::move(color_scheme_callback()).Run(nullptr, error.get());
std::move(signal_connected_callback())
.Run(DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kSettingChangedSignal, false);
// The toolkit-sourced scheme should still drive the web theme.
os_settings_provider().SetPreferredColorScheme(
NativeTheme::PreferredColorScheme::kDark);
EXPECT_TRUE(PrefersDarkTheme());
os_settings_provider().SetPreferredColorScheme(
NativeTheme::PreferredColorScheme::kLight);
EXPECT_FALSE(PrefersDarkTheme());
}
TEST_F(DarkModeManagerLinuxTest, UsePortalSetting) {
// Let the manager know the DBus method call and signal connection succeeded.
dbus::MethodCall method_call(
DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kReadMethod);
method_call.SetSerial(123);
auto response = dbus::Response::FromMethodCall(&method_call);
dbus::MessageWriter writer(response.get());
dbus::MessageWriter variant_writer(nullptr);
writer.OpenVariant("v", &variant_writer);
variant_writer.AppendVariantOfUint32(static_cast<uint32_t>(
DarkModeManagerLinux::FreedesktopColorScheme::kDark));
writer.CloseContainer(&variant_writer);
// The manager pushes the portal preference into the toolkit: SetDarkTheme()
// for native widgets, and SetColorScheme() for the web theme (via the
// provider).
EXPECT_CALL(*mock_linux_ui(), SetDarkTheme(true));
EXPECT_CALL(*mock_linux_ui(), SetColorScheme(std::optional<bool>(true)));
std::move(color_scheme_callback()).Run(response.get(), nullptr);
EXPECT_TRUE(PrefersDarkTheme());
// Changes in the portal preference should be processed by the manager and the
// web theme should be updated.
dbus::Signal signal(DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kSettingChangedSignal);
dbus::MessageWriter signal_writer(&signal);
signal_writer.AppendString(DarkModeManagerLinux::kSettingsNamespace);
signal_writer.AppendString(DarkModeManagerLinux::kColorSchemeKey);
signal_writer.AppendVariantOfUint32(static_cast<uint32_t>(
DarkModeManagerLinux::FreedesktopColorScheme::kLight));
EXPECT_CALL(*mock_linux_ui(), SetDarkTheme(false));
EXPECT_CALL(*mock_linux_ui(), SetColorScheme(std::optional<bool>(false)));
std::move(setting_changed_callback()).Run(&signal);
EXPECT_FALSE(PrefersDarkTheme());
}
TEST_F(DarkModeManagerLinuxTest, UsePortalAccentColor) {
// Let the manager know the DBus method call and signal connection succeeded.
dbus::MethodCall method_call(
DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kReadMethod);
method_call.SetSerial(123);
auto response = dbus::Response::FromMethodCall(&method_call);
dbus::MessageWriter writer(response.get());
dbus::MessageWriter outer_variant_writer(nullptr);
writer.OpenVariant("v", &outer_variant_writer);
dbus::MessageWriter inner_variant_writer(nullptr);
outer_variant_writer.OpenVariant("(ddd)", &inner_variant_writer);
dbus::MessageWriter struct1_writer(nullptr);
inner_variant_writer.OpenStruct(&struct1_writer);
struct1_writer.AppendDouble(0.0);
struct1_writer.AppendDouble(0.5);
struct1_writer.AppendDouble(1.0);
inner_variant_writer.CloseContainer(&struct1_writer);
outer_variant_writer.CloseContainer(&inner_variant_writer);
writer.CloseContainer(&outer_variant_writer);
static constexpr std::optional<SkColor> kExpectedColor1 =
SkColorSetRGB(0, 127, 255);
EXPECT_CALL(*mock_linux_ui(), SetAccentColor(kExpectedColor1));
std::move(accent_color_callback()).Run(response.get(), nullptr);
auto* const native_theme = ui::NativeTheme::GetInstanceForNativeUi();
EXPECT_EQ(native_theme->user_color(), kExpectedColor1);
Mock::VerifyAndClearExpectations(mock_linux_ui());
// Changes in the portal accent color should be processed by the manager and
// the native theme should be updated.
dbus::Signal signal(DarkModeManagerLinux::kFreedesktopSettingsInterface,
DarkModeManagerLinux::kSettingChangedSignal);
dbus::MessageWriter signal_writer(&signal);
signal_writer.AppendString(DarkModeManagerLinux::kSettingsNamespace);
signal_writer.AppendString(DarkModeManagerLinux::kAccentColorKey);
dbus::MessageWriter variant_writer(nullptr);
signal_writer.OpenVariant("(ddd)", &variant_writer);
dbus::MessageWriter struct2_writer(nullptr);
variant_writer.OpenStruct(&struct2_writer);
struct2_writer.AppendDouble(1.0);
struct2_writer.AppendDouble(0.5);
struct2_writer.AppendDouble(0.0);
variant_writer.CloseContainer(&struct2_writer);
signal_writer.CloseContainer(&variant_writer);
static constexpr std::optional<SkColor> kExpectedColor2 =
SkColorSetRGB(255, 127, 0);
EXPECT_CALL(*mock_linux_ui(), SetAccentColor(kExpectedColor2));
std::move(setting_changed_callback()).Run(&signal);
EXPECT_EQ(native_theme->user_color(), kExpectedColor2);
}
} // namespace ui