From 8be53a8c6c342cb1fa8db378fe9b092ff7521624 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Mon, 20 Oct 2025 14:19:56 +0300 Subject: [PATCH] Automatically detect and apply OS theme on Login screen (#19928) * Automatically detect and apply OS theme in Login screen https://community.openproject.org/work_packages/66594 * add a migration to set the default theme of anonymous user to sync_with_os * add a migration spec * add a feature spec * write migrations in raw sql * update anonymous user pref before testing the theme applied --------- Co-authored-by: Behrokh Satarnejad <62008897+bsatarnejad@users.noreply.github.com> Co-authored-by: Behrokh Satarnejad --- ...et_anonymous_user_theme_to_sync_with_os.rb | 25 ++++++ ...onymous_user_theme_to_sync_with_os_spec.rb | 83 +++++++++++++++++++ spec/views/account/login.html.erb_spec.rb | 18 ++++ 3 files changed, 126 insertions(+) create mode 100644 db/migrate/20251017111720_set_anonymous_user_theme_to_sync_with_os.rb create mode 100644 spec/migrations/migrate_set_anonymous_user_theme_to_sync_with_os_spec.rb diff --git a/db/migrate/20251017111720_set_anonymous_user_theme_to_sync_with_os.rb b/db/migrate/20251017111720_set_anonymous_user_theme_to_sync_with_os.rb new file mode 100644 index 00000000000..4bcf3340a16 --- /dev/null +++ b/db/migrate/20251017111720_set_anonymous_user_theme_to_sync_with_os.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class SetAnonymousUserThemeToSyncWithOs < ActiveRecord::Migration[8.0] + def up + say "Set anonymous user theme to sync_with_os" + execute <<~SQL.squish + UPDATE user_preferences + SET settings = settings || '{"theme": "sync_with_os"}'::jsonb + WHERE user_id = ( + SELECT id FROM users WHERE type = 'AnonymousUser' LIMIT 1 + ); + SQL + end + + def down + say "Rollback: Reset anonymous user theme to light" + execute <<~SQL.squish + UPDATE user_preferences + SET settings = settings || '{"theme": "light"}'::jsonb + WHERE user_id = ( + SELECT id FROM users WHERE type = 'AnonymousUser' LIMIT 1 + ); + SQL + end +end diff --git a/spec/migrations/migrate_set_anonymous_user_theme_to_sync_with_os_spec.rb b/spec/migrations/migrate_set_anonymous_user_theme_to_sync_with_os_spec.rb new file mode 100644 index 00000000000..9c7966bd22e --- /dev/null +++ b/spec/migrations/migrate_set_anonymous_user_theme_to_sync_with_os_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require Rails.root.join("db/migrate/20251017111720_set_anonymous_user_theme_to_sync_with_os.rb") + +RSpec.describe SetAnonymousUserThemeToSyncWithOs, type: :model do + let(:anonymous_user) { User.anonymous } + + before do + # Ensure the anonymous user has a preference entry + anonymous_user.pref.update(settings: { "theme" => "light" }) + end + + describe "up migration" do + it "sets the anonymous user theme to sync_with_os" do + expect(anonymous_user.pref.settings["theme"]).to eq("light") + + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + anonymous_user.pref.reload + expect(anonymous_user.pref.settings["theme"]).to eq("sync_with_os") + end + + it "does not affect other users" do + other_user = create(:user) + other_user.pref.update(settings: { "theme" => "dark" }) + + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + other_user.pref.reload + expect(other_user.pref.settings["theme"]).to eq("dark") + end + end + + describe "down migration" do + it "reverts the anonymous user theme back to light" do + anonymous_user.pref.update(settings: { "theme" => "sync_with_os" }) + + ActiveRecord::Migration.suppress_messages { described_class.migrate(:down) } + + anonymous_user.pref.reload + expect(anonymous_user.pref.settings["theme"]).to eq("light") + end + + it "does not modify other user preferences" do + other_user = create(:user) + other_user.pref.update(settings: { "theme" => "dark" }) + + ActiveRecord::Migration.suppress_messages { described_class.migrate(:down) } + + other_user.pref.reload + expect(other_user.pref.settings["theme"]).to eq("dark") + end + end +end diff --git a/spec/views/account/login.html.erb_spec.rb b/spec/views/account/login.html.erb_spec.rb index a26c16cf5ec..1df2a60b1bd 100644 --- a/spec/views/account/login.html.erb_spec.rb +++ b/spec/views/account/login.html.erb_spec.rb @@ -51,4 +51,22 @@ RSpec.describe "account/login" do expect(rendered).not_to include "Password" end end + + context "if user is not logged in" do + before do + User.anonymous.pref.update(settings: { "theme" => "sync_with_os" }) + end + + it "uses the OS-synced theme preference by default" do + theme_data = view.user_theme_data_attributes + + expect(theme_data[:auto_theme_switcher_theme_value]).to eq("sync_with_os") + # Check that contrast flags exist + expect(theme_data).to have_key(:auto_theme_switcher_force_light_contrast_value) + expect(theme_data).to have_key(:auto_theme_switcher_force_dark_contrast_value) + # Check logo classes + expect(theme_data[:auto_theme_switcher_desktop_light_high_contrast_logo_class]).to eq("op-logo--link_high_contrast") + expect(theme_data[:auto_theme_switcher_mobile_white_logo_class]).to eq("op-logo--icon_white") + end + end end