Fix body element theme class not updating when user changes theme#26
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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
ThemeChangerApplicationHelperPatchmodule to overridebody_css_classesand 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 |
| # 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 |
There was a problem hiding this comment.
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.
| # 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 |
| 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 |
There was a problem hiding this comment.
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.
| # 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 |
There was a problem hiding this comment.
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.
| # 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? |
| # 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}" |
There was a problem hiding this comment.
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.
| # 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 |
| fixtures = Dir.glob("*.yml").map { |s| s.gsub(/.yml$/, "") } | ||
| end | ||
| ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + "/fixtures/", fixtures) | ||
|
|
There was a problem hiding this comment.
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.
| # 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 |
| 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) |
There was a problem hiding this comment.
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.
| 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' |
| def test_body_css_classes_with_alternate_theme | ||
| User.current = User.find(2) | ||
| css_classes = body_css_classes | ||
| assert_match(/\Atheme-Alternate/, css_classes) |
There was a problem hiding this comment.
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.
| assert_match(/\Atheme-Alternate/, css_classes) | |
| css_class_list = css_classes.to_s.split(/\s+/) | |
| assert_includes css_class_list, 'theme-Alternate' |
| User.current = User.anonymous | ||
| with_settings :ui_theme => 'classic' do | ||
| css_classes = body_css_classes | ||
| assert_match(/\Atheme-/, css_classes) |
There was a problem hiding this comment.
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.
fix #25