From dd092f37cd844cfa00a5e8768d3000993fe01dc5 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Wed, 6 May 2026 09:09:20 +0200 Subject: [PATCH] Add component specs --- .../quick_filter/boolean_component_spec.rb | 85 ++++++++++ .../quick_filter/segmented_component_spec.rb | 153 ++++++++++++++++++ .../support/shared/components/quick_filter.rb | 45 ++++++ 3 files changed, 283 insertions(+) create mode 100644 spec/components/quick_filter/boolean_component_spec.rb create mode 100644 spec/components/quick_filter/segmented_component_spec.rb create mode 100644 spec/support/shared/components/quick_filter.rb diff --git a/spec/components/quick_filter/boolean_component_spec.rb b/spec/components/quick_filter/boolean_component_spec.rb new file mode 100644 index 00000000000..9366c57f360 --- /dev/null +++ b/spec/components/quick_filter/boolean_component_spec.rb @@ -0,0 +1,85 @@ +# 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 "rails_helper" + +RSpec.describe QuickFilter::BooleanComponent, type: :component do + include QuickFilterHelpers + + let(:project) { build_stubbed(:project) } + let(:query) { build_meeting_query } + + subject(:component) do + described_class.new( + name: "Boolean filter", + query:, + filter_key: :type, + true_label: "Yes", + false_label: "No", + path_args: [project, :meetings] + ) + end + + context "when rendering" do + before { render_inline(component) } + + it "renders both items with the provided labels" do + expect(page).to have_text("Yes") + expect(page).to have_text("No") + end + + it "renders exactly two items" do + expect(page).to have_css("a", count: 2) + end + + it "uses 't' and 'f' as filter values" do + expect(filters_from_link(page.find("a", text: "Yes"))) + .to include({ "type" => { "operator" => "=", "values" => ["t"] } }) + expect(filters_from_link(page.find("a", text: "No"))) + .to include({ "type" => { "operator" => "=", "values" => ["f"] } }) + end + end + + context "when the true value is active" do + let(:query) { build_meeting_query.where("type", "=", ["t"]) } + + before do + render_inline(component) + end + + it "marks the true item as selected" do + expect(page).to have_css("[aria-current='true']", text: "Yes") + end + + it "does not mark the false item as selected" do + expect(page).to have_no_css("[aria-current='true']", text: "No") + end + end +end diff --git a/spec/components/quick_filter/segmented_component_spec.rb b/spec/components/quick_filter/segmented_component_spec.rb new file mode 100644 index 00000000000..a409157227d --- /dev/null +++ b/spec/components/quick_filter/segmented_component_spec.rb @@ -0,0 +1,153 @@ +# 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 "rails_helper" + +RSpec.describe QuickFilter::SegmentedComponent, type: :component do + include QuickFilterHelpers + + let(:project) { build_stubbed(:project) } + let(:query) { build_meeting_query } + let(:orders) { nil } + + subject(:component) do + described_class.new( + name: "Test filter", + query:, + filter_key: :time, + path_args: [project, :meetings], + orders: + ) + end + + context "when rendering with items" do + before do + render_inline(component) do |c| + c.with_item(label: "Upcoming", value: "future") + c.with_item(label: "Past", value: "past") + end + end + + it "renders all items" do + expect(page).to have_text("Upcoming") + expect(page).to have_text("Past") + end + + it "renders a segmented control" do + expect(page).to have_css("segmented-control [aria-label='Test filter']") + end + end + + context "when no items are given" do + before do + render_inline(component) + end + + it "does not render" do + expect(page).to have_no_css("segmented-control [aria-label='Test filter']") + end + end + + context "when an item matches the active filter value" do + let(:query) { build_meeting_query.where("time", "=", ["future"]) } + + before do + render_inline(component) do |c| + c.with_item(label: "Upcoming", value: "future") + c.with_item(label: "Past", value: "past") + end + end + + it "marks the matching item as selected" do + expect(page).to have_css("[aria-current='true']", text: "Upcoming") + end + + it "does not mark the other item as selected" do + expect(page).to have_no_css("[aria-current='true']", text: "Past") + end + end + + context "when no filter is active" do + before do + render_inline(component) do |c| + c.with_item(label: "All", value: nil) + c.with_item(label: "Upcoming", value: "future") + c.with_item(label: "Past", value: "past") + end + end + + it "marks the nil value item as selected" do + expect(page).to have_css("[aria-current='true']", text: "All") + end + end + + context "when other filters are active" do + let(:query) { build_meeting_query.where("time", "=", ["future"]) } + + subject(:component) do + described_class.new( + name: "Type filter", + query:, + filter_key: :type, + path_args: [project, :meetings] + ) + end + + before do + render_inline(component) do |c| + c.with_item(label: "All", value: nil) + c.with_item(label: "One-time", value: "f") + c.with_item(label: "Recurring", value: "t") + end + end + + it "excludes the target filter when value is nil, leaving others unchanged" do + filter_keys = filters_from_link(page.find("a", text: "All")).map { |f| f.keys.first } + + expect(filter_keys).to include("time") + expect(filter_keys).not_to include("type") + end + end + + context "with order overrides" do + let(:orders) { { "future" => { start_time: :asc }, "past" => { start_time: :desc } } } + + before do + render_inline(component) do |c| + c.with_item(label: "Upcoming", value: "future") + c.with_item(label: "Past", value: "past") + end + end + + it "uses the override sort for the matching value" do + expect(sort_from_link(page.find("a", text: "Past"))).to eq([["start_time", "desc"]]) + end + end +end diff --git a/spec/support/shared/components/quick_filter.rb b/spec/support/shared/components/quick_filter.rb new file mode 100644 index 00000000000..672850b36cf --- /dev/null +++ b/spec/support/shared/components/quick_filter.rb @@ -0,0 +1,45 @@ +# 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. +#++ + +module QuickFilterHelpers + def build_meeting_query(user: build_stubbed(:user)) + Queries::Meetings::MeetingQuery.new(user:) + end + + def filters_from_link(link) + json = CGI.unescape(link[:href].match(/filters=([^&]+)/)[1]) + JSON.parse(json) + end + + def sort_from_link(link) + json = CGI.unescape(link[:href].match(/sortBy=([^&]+)/)[1]) + JSON.parse(json) + end +end