diff --git a/Gemfile b/Gemfile index d279a87b8..53b8bded4 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,7 @@ gem 'turbo-rails' # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem 'stimulus-rails' gem 'public_activity' +gem 'view_component' group :development do gem 'better_errors' diff --git a/Gemfile.lock b/Gemfile.lock index c50679c92..e7c6dc5f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -584,6 +584,10 @@ GEM uri (1.1.1) useragent (0.16.11) version_gem (1.1.3) + view_component (4.5.0) + actionview (>= 7.1.0) + activesupport (>= 7.1.0) + concurrent-ruby (~> 1) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -694,6 +698,7 @@ DEPENDENCIES timecop (~> 0.9.10) turbo-rails tzinfo-data + view_component web-console (>= 4.1.0) webmock diff --git a/app/components/chapters_sidebar_component.html.erb b/app/components/chapters_sidebar_component.html.erb new file mode 100644 index 000000000..df5116ca4 --- /dev/null +++ b/app/components/chapters_sidebar_component.html.erb @@ -0,0 +1,12 @@ +<% cache "chapters-sidebar" do %> + <% if title %> +

<%= title %>

+ <% end %> + +<% end %> diff --git a/app/components/chapters_sidebar_component.rb b/app/components/chapters_sidebar_component.rb new file mode 100644 index 000000000..c4e2b8faa --- /dev/null +++ b/app/components/chapters_sidebar_component.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ChaptersSidebarComponent < ViewComponent::Base + # ViewComponent::Base does not define initialize, so super is not needed + def initialize(chapters:, title: nil) # rubocop:disable Lint/MissingSuper + @chapters = chapters + @title = title + end + + private + + attr_reader :chapters, :title +end diff --git a/app/models/chapter.rb b/app/models/chapter.rb index fa98ae7b3..ae0f9d9bd 100644 --- a/app/models/chapter.rb +++ b/app/models/chapter.rb @@ -16,6 +16,9 @@ class Chapter < ApplicationRecord has_many :feedbacks, through: :workshops before_save :set_slug + after_update_commit :expire_chapters_sidebar_cache + after_create_commit :expire_chapters_sidebar_cache + after_destroy_commit :expire_chapters_sidebar_cache scope :active, -> { where(active: true) } @@ -47,6 +50,10 @@ def coaches private + def expire_chapters_sidebar_cache + Rails.cache.delete('chapters-sidebar') + end + def time_zone_exists return unless time_zone && ActiveSupport::TimeZone[time_zone].nil? diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index a4c19c2a1..53c43ba8a 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -77,12 +77,7 @@ = link_to 'Explore all events →', upcoming_events_path, class: 'btn btn-outline-primary mt-3' .col-lg-4.pl-lg-5 - %h3 - = t('homepage.chapters.title') - %ul.list-unstyled.ms-0 - - @chapters.each do |chapter| - %li - = link_to chapter.name, chapter_path(chapter.slug) + = render ChaptersSidebarComponent.new(chapters: @chapters, title: t('homepage.chapters.title')) - if @testimonials.any? .py-4.py-lg-5.bg-light diff --git a/spec/components/chapters_sidebar_component_spec.rb b/spec/components/chapters_sidebar_component_spec.rb new file mode 100644 index 000000000..ee4483b99 --- /dev/null +++ b/spec/components/chapters_sidebar_component_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' +require 'view_component/test_helpers' + +RSpec.describe ChaptersSidebarComponent do + include ViewComponent::TestHelpers + include Rails.application.routes.url_helpers + + let(:chapters) { Fabricate.times(3, :chapter) } + + it 'renders chapter names as links' do + render_inline ChaptersSidebarComponent.new(chapters: chapters) + + chapters.each do |chapter| + expect(page).to have_link(chapter.name, href: chapter_path(chapter.slug)) + end + end + + it 'renders nothing when no chapters' do + render_inline ChaptersSidebarComponent.new(chapters: []) + + expect(page).to have_css('ul.list-unstyled.ms-0', visible: true) + expect(page).to have_no_css('li') + end +end diff --git a/spec/models/chapter_spec.rb b/spec/models/chapter_spec.rb index 55db73d56..575685425 100644 --- a/spec/models/chapter_spec.rb +++ b/spec/models/chapter_spec.rb @@ -41,4 +41,28 @@ end end end + + context 'cache expiration' do + let(:cache_key) { 'chapters-sidebar' } + + it 'expires cache when chapter is created' do + Rails.cache.write(cache_key, 'cached content') + Fabricate(:chapter) + expect(Rails.cache.read(cache_key)).to be_nil + end + + it 'expires cache when chapter is updated' do + Rails.cache.write(cache_key, 'cached content') + chapter = Fabricate(:chapter) + chapter.update!(name: 'Updated Name') + expect(Rails.cache.read(cache_key)).to be_nil + end + + it 'expires cache when chapter is destroyed' do + Rails.cache.write(cache_key, 'cached content') + chapter = Fabricate(:chapter) + chapter.destroy + expect(Rails.cache.read(cache_key)).to be_nil + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 000000000..069b8d2d4 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,45 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../config/environment', __dir__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_helper.rb` can +# be required explicitly. +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migration and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.check_all_pending! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = ["#{::Rails.root}/spec/fixtures"] + + # If you're not using ActiveRecord, or you'd prefer not to use each of your + # test frameworks (like Capybara or Selenium) for test + # see https://relishapp.com/rspec/rspec-rails/docs/configuration + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours based on the + # file location of the spec. This line needs to be present for ViewComponent + # to work with controller specs + config.infer_spec_type_from_file_location! + + # Connect ViewComponent to RSpec, adding RSpec metadata type: :component. + # This extends Rails' own built-in (e.g. "type: :controller") metadata system. + RSpec::Rails::DIRECTORY_MAPPINGS[:component] = %w[spec/components] + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered from backtraces + # this line is optional and should be present if your application depends + # on external gems, but this is not needed for ViewComponent tests +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3ee48ccc4..3ca44fcb8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -71,19 +71,6 @@ def self.branch_coverage? # See https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example config.before(:suite) do - if config.use_transactional_fixtures? - raise(<<-MSG) - Delete line `config.use_transactional_fixtures = true` from spec_helper.rb - (or set it to false) to prevent uncommitted transactions being used in - JavaScript-dependent specs. - - During testing, the app-under-test that the browser driver connects to - uses a different database connection to the database connection used by - the spec. The app's database connection would not be able to access - uncommitted transaction data setup over the spec's database connection. - MSG - end - DatabaseCleaner.clean_with(:truncation) DatabaseCleaner.strategy = :deletion end diff --git a/spec/views/dashboard/show.html.haml_spec.rb b/spec/views/dashboard/show.html.haml_spec.rb new file mode 100644 index 000000000..64d02fa40 --- /dev/null +++ b/spec/views/dashboard/show.html.haml_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe 'dashboard/show.html.haml', type: :view do + let(:chapters) { Fabricate.times(2, :chapter) } + let(:upcoming_workshops) { {} } + let(:testimonials) { [] } + + before do + assign(:chapters, chapters) + assign(:upcoming_workshops, upcoming_workshops) + assign(:has_more_events, false) + assign(:testimonials, testimonials) + render + end + + it 'renders the chapters sidebar component' do + expect(rendered).to have_selector('.col-lg-4.pl-lg-5') + chapters.each do |chapter| + expect(rendered).to have_link(chapter.name, href: chapter_path(chapter.slug)) + end + end +end