Skip to content

Fix body element theme class not updating when user changes theme#26

Merged
haru merged 4 commits into
developfrom
bugfix/25-body-class-update
Dec 23, 2025
Merged

Fix body element theme class not updating when user changes theme#26
haru merged 4 commits into
developfrom
bugfix/25-body-class-update

Conversation

@haru

@haru haru commented Dec 23, 2025

Copy link
Copy Markdown
Owner

fix #25

@haru haru requested a review from Copilot December 23, 2025 13:05
@codecov

codecov Bot commented Dec 23, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (734a240) to head (288f016).
⚠️ Report is 8 commits behind head on develop.

Additional details and impacted files
@@            Coverage Diff            @@
##           develop       #26   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            5         6    +1     
  Lines           57        72   +15     
=========================================
+ Hits            57        72   +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes issue #25 where the CSS class on the body element was not updating when users changed their theme preference. The fix introduces a new ApplicationHelper patch that overrides the body_css_classes method to inject user-specific theme classes into the HTML body element.

Key Changes:

  • Added new ThemeChangerApplicationHelperPatch module to override body_css_classes and inject user-specific theme CSS classes
  • Added comprehensive unit tests for the new body_css_classes functionality
  • Refactored test helper to improve fixture loading and remove deprecated simplecov-rcov dependency

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
lib/theme_changer_application_helper_patch.rb New patch module that overrides body_css_classes to dynamically inject user's theme class into the body element
init.rb Registers the new application helper patch module in the plugin initialization
test/unit/application_helper_patch_test.rb New test file with unit tests for body_css_classes behavior with different user themes and anonymous users
test/test_helper.rb Refactored to use double quotes, automatic fixture loading, and removed deprecated simplecov-rcov formatter
.gitignore Added local development files (.claude/settings.local.json and docs/) to ignore list

Comment thread test/test_helper.rb
Comment thread lib/theme_changer_application_helper_patch.rb
Comment on lines +23 to +42
# Only modify theme class for logged-in users
return css_classes unless User.current.logged?

# Get user's theme setting
setting = ThemeChangerUserSetting.find_theme_by_user_id(User.current.id)

# If no setting or system setting is selected, use default behavior
return css_classes unless setting
return css_classes if setting.theme == ThemeChangerUserSetting::SYSTEM_SETTING

# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip

# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
theme = Redmine::Themes.theme(setting.theme)
if theme
css_classes = "theme-#{theme.name.tr(' ', '_')} #{css_classes}"
end
end

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The body_css_classes patch duplicates theme resolution logic that already exists in ThemeChangerThemesPatch (get_theme and current_theme methods). Instead of re-implementing theme lookup with ThemeChangerUserSetting.find_theme_by_user_id and handling SYSTEM_SETTING/DEFAULT_THEME cases, consider reusing the existing current_theme method which already handles this logic. This would make the code more maintainable and ensure consistent behavior across the plugin.

Suggested change
# Only modify theme class for logged-in users
return css_classes unless User.current.logged?
# Get user's theme setting
setting = ThemeChangerUserSetting.find_theme_by_user_id(User.current.id)
# If no setting or system setting is selected, use default behavior
return css_classes unless setting
return css_classes if setting.theme == ThemeChangerUserSetting::SYSTEM_SETTING
# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip
# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
theme = Redmine::Themes.theme(setting.theme)
if theme
css_classes = "theme-#{theme.name.tr(' ', '_')} #{css_classes}"
end
end
# Use the centralized theme resolution logic from ThemeChangerThemesPatch
theme = current_theme
return css_classes unless theme
# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip
# Add new theme class at the beginning based on the resolved theme
css_classes = "theme-#{theme.name.tr(' ', '_')} #{css_classes}".strip

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +50
def test_body_css_classes_with_user_theme
User.current = User.find(1)
css_classes = body_css_classes
assert_match(/\Atheme-Classic/, css_classes)
end

def test_body_css_classes_with_alternate_theme
User.current = User.find(2)
css_classes = body_css_classes
assert_match(/\Atheme-Alternate/, css_classes)
end

def test_body_css_classes_when_not_logged_in
User.current = User.anonymous
with_settings :ui_theme => 'classic' do
css_classes = body_css_classes
assert_match(/\Atheme-/, css_classes)
end
end

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite is missing coverage for important edge cases of the body_css_classes method. Consider adding tests for: 1) when a user has SYSTEM_SETTING selected (should use system default), 2) when a user has DEFAULT_THEME selected (should use default behavior), 3) when a user's saved theme is invalid or no longer available (the theme lookup returns nil), and 4) when a user has a theme setting but the theme value is blank.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +37
# If no setting or system setting is selected, use default behavior
return css_classes unless setting
return css_classes if setting.theme == ThemeChangerUserSetting::SYSTEM_SETTING

# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip

# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for handling DEFAULT_THEME may not work as intended. When setting.theme equals DEFAULT_THEME, the method returns the original css_classes without modifications. However, the original css_classes from super already contains a theme class based on the system default. This means DEFAULT_THEME behaves identically to SYSTEM_SETTING. If DEFAULT_THEME is meant to represent a different behavior (e.g., no theme or a specific default), the current logic may not implement that correctly. Clarify whether DEFAULT_THEME should have distinct behavior from SYSTEM_SETTING.

Suggested change
# If no setting or system setting is selected, use default behavior
return css_classes unless setting
return css_classes if setting.theme == ThemeChangerUserSetting::SYSTEM_SETTING
# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip
# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
# If no setting, system setting, or default theme is selected, use default behavior
return css_classes unless setting
return css_classes if setting.theme == ThemeChangerUserSetting::SYSTEM_SETTING ||
setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip
# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank?

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +40
# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip

# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
theme = Redmine::Themes.theme(setting.theme)
if theme
css_classes = "theme-#{theme.name.tr(' ', '_')} #{css_classes}"

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern /\btheme-\S+/ used to remove existing theme classes will match and remove any class starting with 'theme-' followed by non-whitespace characters. This could potentially match theme-related classes beyond just the main theme class (e.g., 'theme-switcher', 'theme-custom-class'). Consider using a more specific pattern that only matches the exact theme class format, or verify that no other classes in Redmine start with 'theme-' prefix to avoid unintended removals.

Suggested change
# Remove existing theme class from the CSS classes
css_classes = css_classes.gsub(/\btheme-\S+/, '').strip
# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
theme = Redmine::Themes.theme(setting.theme)
if theme
css_classes = "theme-#{theme.name.tr(' ', '_')} #{css_classes}"
# Remove existing theme class from the CSS classes.
# Only remove classes that correspond to actual Redmine themes.
css_class_tokens = css_classes.split(/\s+/)
css_class_tokens.reject! do |klass|
next false unless klass.start_with?('theme-')
theme_name = klass.sub(/\Atheme-/, '')
!theme_name.empty? && Redmine::Themes.theme(theme_name)
end
css_classes = css_class_tokens.join(' ')
# Add new theme class at the beginning if a specific theme is set
unless setting.theme.blank? || setting.theme == ThemeChangerUserSetting::DEFAULT_THEME
theme = Redmine::Themes.theme(setting.theme)
if theme
css_classes = "theme-#{theme.name.tr(' ', '_')} #{css_classes}".strip

Copilot uses AI. Check for mistakes.
Comment thread test/test_helper.rb
fixtures = Dir.glob("*.yml").map { |s| s.gsub(/.yml$/, "") }
end
ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + "/fixtures/", fixtures)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixture loading logic uses Dir.glob to load all .yml files from the fixtures directory, but this will only load fixtures from this plugin's test/fixtures directory. If the tests depend on Redmine core fixtures (such as users.yml), those need to be explicitly listed or already loaded by the parent test_helper. Verify that all required fixtures (especially 'users' referenced in line 22 of application_helper_patch_test.rb) are available when tests run.

Suggested change
# Ensure Redmine core fixtures needed by plugin tests are available (e.g. users.yml)
core_fixtures_path = File.expand_path(File.dirname(__FILE__) + "/../../../test/fixtures")
users_fixture_file = File.join(core_fixtures_path, "users.yml")
if File.exist?(users_fixture_file)
ActiveRecord::FixtureSet.create_fixtures(core_fixtures_path, ["users"])
end

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +41
assert_match(/\Atheme-Classic/, css_classes)
end

def test_body_css_classes_with_alternate_theme
User.current = User.find(2)
css_classes = body_css_classes
assert_match(/\Atheme-Alternate/, css_classes)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertion uses a regex pattern that only checks if the CSS classes start with 'theme-Classic', but doesn't verify the exact theme class or that it's properly placed in the string. Consider using a more specific assertion that verifies the exact theme class is present, such as checking for the complete class name or using assert_includes to verify 'theme-Classic' is in the space-separated list of classes.

Suggested change
assert_match(/\Atheme-Classic/, css_classes)
end
def test_body_css_classes_with_alternate_theme
User.current = User.find(2)
css_classes = body_css_classes
assert_match(/\Atheme-Alternate/, css_classes)
assert_includes css_classes.split(/\s+/), 'theme-Classic'
end
def test_body_css_classes_with_alternate_theme
User.current = User.find(2)
css_classes = body_css_classes
assert_includes css_classes.split(/\s+/), 'theme-Alternate'

Copilot uses AI. Check for mistakes.
def test_body_css_classes_with_alternate_theme
User.current = User.find(2)
css_classes = body_css_classes
assert_match(/\Atheme-Alternate/, css_classes)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertion uses a regex pattern that only checks if the CSS classes start with 'theme-Alternate', but doesn't verify the exact theme class or that it's properly placed in the string. Consider using a more specific assertion that verifies the exact theme class is present, such as checking for the complete class name or using assert_includes to verify 'theme-Alternate' is in the space-separated list of classes.

Suggested change
assert_match(/\Atheme-Alternate/, css_classes)
css_class_list = css_classes.to_s.split(/\s+/)
assert_includes css_class_list, 'theme-Alternate'

Copilot uses AI. Check for mistakes.
User.current = User.anonymous
with_settings :ui_theme => 'classic' do
css_classes = body_css_classes
assert_match(/\Atheme-/, css_classes)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only verifies that a theme class exists (starts with 'theme-') but doesn't check which theme is being used. This test would pass even if the wrong theme is applied. Consider asserting that the system default theme is being used by checking the actual value from Setting.ui_theme or verifying the absence of user-specific theme classes.

Copilot uses AI. Check for mistakes.
@haru haru merged commit 7130449 into develop Dec 23, 2025
132 of 134 checks passed
@haru haru deleted the bugfix/25-body-class-update branch December 23, 2025 13:17
@haru haru added the bug label Dec 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants