diff --git a/app/components/queries/sort_by_field_component.html.erb b/app/components/queries/sort_by_field_component.html.erb index 302d95bdeadc..2e353bb75036 100644 --- a/app/components/queries/sort_by_field_component.html.erb +++ b/app/components/queries/sort_by_field_component.html.erb @@ -1,4 +1,4 @@ -<%= render(Primer::OpenProject::FlexLayout.new) do |flex| %> +<%= render(Primer::OpenProject::FlexLayout.new(test_selector: 'sort-by-field' })) do |flex| %> <% flex.with_column(flex: 1) do %> <%#- We are just using the classes of the primer component here, because when using the primer component, we cannot detach the input element from the form %> <%#- The form="none" adds the input to a nonexistant form (as we do not have one with the ID="none" and thus the fields to not get appended to the query string %> diff --git a/spec/features/projects/persisted_lists_spec.rb b/spec/features/projects/persisted_lists_spec.rb index 8acc53b360a7..e8976c70003b 100644 --- a/spec/features/projects/persisted_lists_spec.rb +++ b/spec/features/projects/persisted_lists_spec.rb @@ -353,7 +353,7 @@ projects_page.expect_no_sidebar_filter(another_users_projects_list.name) # Sorts ASC by name - projects_page.sort_by("Name") + projects_page.sort_by_via_table_header("Name") # Results should be filtered and ordered ASC by name and the user is still on the first page. # Column is kept. @@ -379,7 +379,7 @@ # Sorts DESC by name # Soon, a save icon should be displayed then. - projects_page.sort_by("Name") + projects_page.sort_by_via_table_header("Name") # The title is kept projects_page.expect_title(my_projects_list.name) diff --git a/spec/features/projects/projects_index_spec.rb b/spec/features/projects/projects_index_spec.rb index 5b3534a22dce..54b13a72b9be 100644 --- a/spec/features/projects/projects_index_spec.rb +++ b/spec/features/projects/projects_index_spec.rb @@ -28,10 +28,7 @@ require "spec_helper" -RSpec.describe "Projects index page", - :js, - :with_cuprite, - with_settings: { login_required?: false } do +RSpec.describe "Projects index page", :js, :with_cuprite, with_settings: { login_required?: false } do shared_let(:admin) { create(:admin) } shared_let(:manager) { create(:project_role, name: "Manager") } @@ -40,25 +37,13 @@ shared_let(:custom_field) { create(:text_project_custom_field) } shared_let(:invisible_custom_field) { create(:project_custom_field, visible: false) } - shared_let(:project) do - create(:project, - name: "Plain project", - identifier: "plain-project") - end + shared_let(:project) { create(:project, name: "Plain project", identifier: "plain-project") } shared_let(:public_project) do - project = create(:project, - name: "Public project", - identifier: "public-project", - public: true) - project.custom_field_values = { invisible_custom_field.id => "Secret CF" } - project.save - project - end - shared_let(:development_project) do - create(:project, - name: "Development project", - identifier: "development-project") + create(:project, name: "Public project", identifier: "public-project", public: true) do |project| + project.custom_field_values = { invisible_custom_field.id => "Secret CF" } + end end + shared_let(:development_project) { create(:project, name: "Development project", identifier: "development-project") } let(:news) { create(:news, project:) } let(:projects_page) { Pages::Projects::Index.new } @@ -370,7 +355,7 @@ def expect_projects_in_order(*projects) projects_page.expect_columns("Name") # Sorts ASC by name - projects_page.sort_by("Name") + projects_page.sort_by_via_table_header("Name") wait_for_reload # Results should be filtered and ordered ASC by name and only the selected columns should be present @@ -397,7 +382,7 @@ def expect_projects_in_order(*projects) projects_page.expect_total_pages(2) # Filters kept active, so there is no third page. # Sorts DESC by name - projects_page.sort_by("Name") + projects_page.sort_by_via_table_header("Name") wait_for_reload # Clicking on sorting resets the page to the first one @@ -949,7 +934,7 @@ def expect_projects_in_order(*projects) projects_page.activate_menu_of(parent_project) do |menu| expect(menu).to have_text("Add to favorites") - expect(menu).not_to have_text("Copy") + expect(menu).to have_no_text("Copy") end # For a project member with :copy_projects privilege the 'More' menu is visible. @@ -991,21 +976,12 @@ def expect_projects_in_order(*projects) shared_let(:integer_custom_field) { create(:integer_project_custom_field) } # order is important here as the implementation uses lft # first but then reorders in ruby - shared_let(:child_project_z) do - create(:project, - parent: project, - name: "Z Child") - end - shared_let(:child_project_m) do - create(:project, - parent: project, - name: "m Child") # intentionally written lowercase to test for case insensitive sorting - end - shared_let(:child_project_a) do - create(:project, - parent: project, - name: "A Child") - end + shared_let(:child_project_z) { create(:project, parent: project, name: "Z Child") } + + # intentionally written lowercase to test for case insensitive sorting + shared_let(:child_project_m) { create(:project, parent: project, name: "m Child") } + + shared_let(:child_project_a) { create(:project, parent: project, name: "A Child") } before do login_as(admin) @@ -1025,7 +1001,95 @@ def expect_projects_in_order(*projects) child_project_a.save! end - it "allows to alter the order in which projects are displayed" do + context "via the configure view dialog" do + before do + Setting.enabled_projects_columns += [integer_custom_field.column_name] + end + + it "allows to sort via multiple columns" do + projects_page.open_configure_view + projects_page.switch_configure_view_tab(I18n.t("label_sort")) + + # Initially we have the projects ordered by hierarchy + # When we sort by hierarchy, there is a special behavior that no other sorting is possible + # and the sort order is always ascending + projects_page.within_sort_row(0) do + projects_page.expect_sort_order(column_identifier: "lft", direction: "asc", direction_enabled: false) + end + projects_page.expect_number_of_sort_fields(1) + + # Switch sorting order to Name descending + # We now get a second sort field to add another sort order, but it has nothing selected + # in the second field, name is not available as an option + projects_page.within_sort_row(0) do + projects_page.change_sort_order(column_identifier: :name, direction: :desc) + end + projects_page.expect_number_of_sort_fields(2) + + projects_page.within_sort_row(1) do + projects_page.expect_sort_order(column_identifier: "", direction: "") + projects_page.expect_sort_option_is_disabled(column_identifier: :name) + end + + # Let's add another sorting, this time by a custom field + # This will add a third sorting field + projects_page.within_sort_row(1) do + projects_page.change_sort_order(column_identifier: integer_custom_field.column_name, direction: :asc) + end + + projects_page.expect_number_of_sort_fields(3) + projects_page.within_sort_row(2) do + projects_page.expect_sort_order(column_identifier: "", direction: "") + projects_page.expect_sort_option_is_disabled(column_identifier: :name) + projects_page.expect_sort_option_is_disabled(column_identifier: integer_custom_field.column_name) + end + + # And now let's select a third option + # it will not add a 4th sorting field + projects_page.within_sort_row(2) do + projects_page.change_sort_order(column_identifier: :public, direction: :asc) + end + projects_page.expect_number_of_sort_fields(3) + + # We unset the first sorting, this will move the 2nd sorting (custom field) to the first position and + # the 3rd sorting (public) to the second position and will add an empty option to the third position + projects_page.within_sort_row(0) do + projects_page.remove_sort_order + end + + projects_page.expect_number_of_sort_fields(3) + + projects_page.within_sort_row(0) do + projects_page.expect_sort_order(column_identifier: integer_custom_field.column_name, direction: :asc) + end + projects_page.within_sort_row(1) { projects_page.expect_sort_order(column_identifier: :public, direction: :asc) } + projects_page.within_sort_row(2) { projects_page.expect_sort_order(column_identifier: "", direction: "") } + + # To roll back, we now select hierarchy as the third option, this will remove all other options + projects_page.within_sort_row(2) do + projects_page.change_sort_order(column_identifier: :lft, direction: :asc) + end + + projects_page.within_sort_row(0) do + projects_page.expect_sort_order(column_identifier: "lft", direction: "asc", direction_enabled: false) + end + projects_page.expect_number_of_sort_fields(1) + end + + it "does not allow to sort via long text custom fields" do + long_text_custom_field = create(:text_project_custom_field) + Setting.enabled_projects_columns += [long_text_custom_field.column_name] + + projects_page.open_configure_view + projects_page.switch_configure_view_tab(I18n.t("label_sort")) + + projects_page.within_sort_row(0) do + projects_page.expect_sort_option_not_available(column_identifier: long_text_custom_field.column_name) + end + end + end + + it "allows to alter the order in which projects are displayed via the column headers" do Setting.enabled_projects_columns += [integer_custom_field.column_name] # initially, ordered by name asc on each hierarchical level diff --git a/spec/support/pages/projects/index.rb b/spec/support/pages/projects/index.rb index b4b05085145c..298029ae77fc 100644 --- a/spec/support/pages/projects/index.rb +++ b/spec/support/pages/projects/index.rb @@ -72,8 +72,7 @@ def expect_projects_not_listed(*projects) end def expect_title(name) - expect(page) - .to have_css('[data-test-selector="project-query-name"]', text: name) + expect(page).to have_css('[data-test-selector="project-query-name"]', text: name) end def expect_sidebar_filter(filter_name, selected: false) @@ -89,21 +88,17 @@ def expect_no_sidebar_filter(filter_name) end def expect_current_page_number(number) - expect(page) - .to have_css(".op-pagination--item_current", text: number) + expect(page).to have_css(".op-pagination--item_current", text: number) end def expect_total_pages(number) - expect(page) - .to have_css(".op-pagination--item", text: number) - - expect(page) - .to have_no_css(".op-pagination--item", text: number + 1) + expect(page).to have_css(".op-pagination--item", text: number) + expect(page).to have_no_css(".op-pagination--item", text: number + 1) end def set_sidebar_filter(filter_name) within "#main-menu" do - click_link text: filter_name + click_on text: filter_name end end @@ -116,13 +111,11 @@ def expect_filters_container_hidden end def expect_filter_set(filter_name) - expect(page).to have_css("li[filter-name='#{filter_name}']:not(.hidden)", - visible: :hidden) + expect(page).to have_css("li[filter-name='#{filter_name}']:not(.hidden)", visible: :hidden) end def expect_filter_count(count) - expect(page) - .to have_css('[data-test-selector="filters-button-counter"]', text: count) + expect(page).to have_css('[data-test-selector="filters-button-counter"]', text: count) end def expect_no_project_create_button @@ -166,39 +159,23 @@ def expect_no_menu_item(text, visible: true) end def filter_by_active(value) - set_filter("active", - "Active", - "is", - [value]) - - click_button "Apply" + set_filter("active", "Active", "is", [value]) + click_on "Apply" end def filter_by_public(value) - set_filter("public", - "Public", - "is", - [value]) - - click_button "Apply" + set_filter("public", "Public", "is", [value]) + click_on "Apply" end def filter_by_favored(value) - set_filter("favored", - "Favorite", - "is", - [value]) - - click_button "Apply" + set_filter("favored", "Favorite", "is", [value]) + click_on "Apply" end def filter_by_membership(value) - set_filter("member_of", - "I am member", - "is", - [value]) - - click_button "Apply" + set_filter("member_of", "I am member", "is", [value]) + click_on "Apply" end def set_filter(name, human_name, human_operator = nil, values = []) @@ -288,7 +265,7 @@ def toggle_filters_section end def set_columns(*columns) - click_more_menu_item(I18n.t(:"queries.configure_view.heading")) + open_configure_view # Assumption: there is always one item selected, the 'Name' column # That column can currently not be removed. @@ -318,13 +295,12 @@ def set_columns(*columns) def click_more_menu_item(item) page.find('[data-test-selector="project-more-dropdown-menu"]').click - page.find(".ActionListItem", text: item, exact_text: true).click end def click_menu_item_of(title, project) activate_menu_of(project) do - click_link title + click_on title end end @@ -366,7 +342,7 @@ def delete_query end end - def sort_by(column_name) + def sort_by_via_table_header(column_name) find(".generic-table--sort-header a", text: column_name.upcase).click end @@ -380,6 +356,62 @@ def go_to_page(page_number) end end + def open_configure_view + click_more_menu_item(I18n.t(:"queries.configure_view.heading")) + end + + def switch_configure_view_tab(tab_name) + within "tab-container" do + find('button[role="tab"]', text: tab_name).click + end + end + + def expect_sort_order(column_identifier:, direction:, direction_enabled: true) + select = find("select") + segmented_control = find("segmented-control") + + expect(select.value).to eq(column_identifier.to_s) + + if direction.present? + active_direction = segmented_control.find("button[aria-current='true']")["data-direction"] + expect(active_direction).to eq(direction.to_s) + else + expect(segmented_control).to have_no_button("[aria-current='true']") + end + + expect(segmented_control).to have_button(disabled: !direction_enabled, count: 2) + end + + def expect_number_of_sort_fields(number, visible: true) + expect(page).to have_css("[data-test-selector='sort-by-field']", count: number, visible:) + end + + def change_sort_order(column_identifier:, direction:) + find("select option[value='#{column_identifier}']").select_option + find("segmented-control button[data-direction='#{direction}']").click + end + + def remove_sort_order + find("select option[value='']").select_option + end + + def expect_sort_option_is_disabled(column_identifier:) + select = find("select") + + expect(select).to have_css("option[value='#{column_identifier}']:disabled") + end + + def expect_sort_option_not_available(column_identifier:) + select = find("select") + + expect(select).to have_no_css("option[value='#{column_identifier}']") + end + + def within_sort_row(index, &) + field_component = page.all("[data-test-selector='sort-by-field']")[index] + within(field_component, &) + end + def within_table(&) within "#project-table", & end