|
|
|
#-- copyright
|
|
|
|
# OpenProject is an open source project management software.
|
|
|
|
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
|
|
|
#++
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe 'Projects index page',
|
|
|
|
type: :feature,
|
|
|
|
clear_cache: true,
|
|
|
|
js: true,
|
|
|
|
with_settings: { login_required?: false } do
|
|
|
|
shared_let(:admin) { FactoryBot.create :admin }
|
|
|
|
|
|
|
|
shared_let(:manager) { FactoryBot.create :role, name: 'Manager' }
|
|
|
|
shared_let(:developer) { FactoryBot.create :role, name: 'Developer' }
|
|
|
|
|
|
|
|
shared_let(:custom_field) { FactoryBot.create :project_custom_field }
|
|
|
|
shared_let(:invisible_custom_field) { FactoryBot.create :project_custom_field, visible: false }
|
|
|
|
|
|
|
|
shared_let(:project) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Plain project',
|
|
|
|
identifier: 'plain-project')
|
|
|
|
end
|
|
|
|
shared_let(:public_project) do
|
|
|
|
project = FactoryBot.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
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Development project',
|
|
|
|
identifier: 'development-project')
|
|
|
|
end
|
|
|
|
let(:news) { FactoryBot.create(:news, project: project) }
|
|
|
|
let(:projects_page) { Pages::Projects::Index.new }
|
|
|
|
|
|
|
|
def load_and_open_filters(user)
|
|
|
|
login_as(user)
|
|
|
|
projects_page.visit!
|
|
|
|
projects_page.open_filters
|
|
|
|
end
|
|
|
|
|
|
|
|
def allow_enterprise_edition
|
|
|
|
with_enterprise_token(:custom_fields_in_projects_list)
|
|
|
|
end
|
|
|
|
|
|
|
|
def remove_filter(name)
|
|
|
|
page.find("li[filter-name='#{name}'] .filter_rem").click
|
|
|
|
end
|
|
|
|
|
|
|
|
def expect_project_at_place(project, place)
|
|
|
|
expect(page)
|
|
|
|
.to have_selector("#project-table .project:nth-of-type(#{place}) td.name",
|
|
|
|
text: project.name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def expect_projects_in_order(*projects)
|
|
|
|
projects.each_with_index do |project, index|
|
|
|
|
expect_project_at_place(project, index + 1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'restricts project visibility' do
|
|
|
|
feature 'for a anonymous user' do
|
|
|
|
scenario 'only public projects shall be visible' do
|
|
|
|
Role.anonymous
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
expect(page).to_not have_text(project.name)
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
|
|
|
|
# Test that the 'More' menu stays invisible on hover
|
|
|
|
expect(page).to_not have_selector('.icon-show-more-horizontal')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'for project members' do
|
|
|
|
shared_let(:user) do
|
|
|
|
FactoryBot.create(:user,
|
|
|
|
member_in_project: development_project,
|
|
|
|
member_through_role: developer,
|
|
|
|
login: 'nerd',
|
|
|
|
firstname: 'Alan',
|
|
|
|
lastname: 'Turing')
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
allow_enterprise_edition
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'only public project or those the user is member of shall be visible' do
|
|
|
|
Role.non_member
|
|
|
|
login_as(user)
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
expect(page).to have_text(development_project.name)
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
expect(page).to_not have_text(project.name)
|
|
|
|
|
|
|
|
# Non-admin users shall not see invisible CFs.
|
|
|
|
expect(page).to_not have_text(invisible_custom_field.name.upcase)
|
|
|
|
expect(page).to_not have_select('add_filter_select', with_options: [invisible_custom_field.name])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'for admins' do
|
|
|
|
before do
|
|
|
|
project.update(created_at: 7.days.ago)
|
|
|
|
|
|
|
|
news
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'test that all projects are visible' do
|
|
|
|
login_as(admin)
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
expect(page).to have_text(project.name)
|
|
|
|
|
|
|
|
# Test visibility of 'more' menu list items
|
|
|
|
item = page.first('tbody tr .icon-show-more-horizontal', visible: :all)
|
|
|
|
item.hover
|
|
|
|
item.click
|
|
|
|
|
|
|
|
menu = page.first('tbody tr .project-actions')
|
|
|
|
expect(menu).to have_text('Copy')
|
|
|
|
expect(menu).to have_text('Project settings')
|
|
|
|
expect(menu).to have_text('New subproject')
|
|
|
|
expect(menu).to have_text('Delete')
|
|
|
|
expect(menu).to have_text('Archive')
|
|
|
|
|
|
|
|
# Test visibility of admin only properties
|
|
|
|
within('#project-table') do
|
|
|
|
expect(page)
|
|
|
|
.to have_selector('th', text: 'REQUIRED DISK STORAGE')
|
|
|
|
expect(page)
|
|
|
|
.to have_selector('th', text: 'CREATED ON')
|
|
|
|
expect(page)
|
|
|
|
.to have_selector('td', text: project.created_at.strftime('%m/%d/%Y'))
|
|
|
|
expect(page)
|
|
|
|
.to have_selector('th', text: 'LATEST ACTIVITY AT')
|
|
|
|
expect(page)
|
|
|
|
.to have_selector('td', text: news.created_at.strftime('%m/%d/%Y'))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'test that flash sortBy is being escaped' do
|
|
|
|
login_as(admin)
|
|
|
|
visit projects_path(sortBy: "[[\"><script src='/foobar.js'></script>\",\"\"]]")
|
|
|
|
|
|
|
|
error_text = "Orders ><script src='/foobar js'></script> is not set to one of the allowed values. and does not exist."
|
|
|
|
error_html = "Orders ><script src='/foobar js'></script> is not set to one of the allowed values. and does not exist."
|
|
|
|
expect(page).to have_selector('.flash.error', text: error_text)
|
|
|
|
|
|
|
|
error_container = page.find('.flash.error')
|
|
|
|
expect(error_container['innerHTML']).to include error_html
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'without valid Enterprise token' do
|
|
|
|
scenario 'CF columns and filters are not visible' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
# CF's columns are not present:
|
|
|
|
expect(page).to_not have_text(custom_field.name.upcase)
|
|
|
|
# CF's filters are not present:
|
|
|
|
expect(page).to_not have_select('add_filter_select', with_options: [custom_field.name])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'with valid Enterprise token' do
|
|
|
|
before do
|
|
|
|
allow_enterprise_edition
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'CF columns and filters are not visible by default' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
# CF's columns are not shown due to setting
|
|
|
|
expect(page).to_not have_text(custom_field.name.upcase)
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'CF columns and filters are visible when added to settings' do
|
|
|
|
Setting.enabled_projects_columns += ["cf_#{custom_field.id}", "cf_#{invisible_custom_field.id}"]
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
# CF's column is present:
|
|
|
|
expect(page).to have_text(custom_field.name.upcase)
|
|
|
|
# CF's filter is present:
|
|
|
|
expect(page).to have_select('add_filter_select', with_options: [custom_field.name])
|
|
|
|
|
|
|
|
# Admins shall be the only ones to see invisible CFs
|
|
|
|
expect(page).to have_text(invisible_custom_field.name.upcase)
|
|
|
|
expect(page).to have_select('add_filter_select', with_options: [invisible_custom_field.name])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'with a filter set' do
|
|
|
|
scenario 'it should only show the matching projects and filters' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
projects_page.set_filter('name_and_identifier',
|
|
|
|
'Name or identifier',
|
|
|
|
'contains',
|
|
|
|
['Plain'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
# Filter is applied: Only the project that contains the the word "Plain" gets listed
|
|
|
|
expect(page).to_not have_text(public_project.name)
|
|
|
|
expect(page).to have_text(project.name)
|
|
|
|
# Filter form is visible and the filter is still set.
|
|
|
|
expect(page).to have_selector('li[filter-name="name_and_identifier"]')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'when paginating' do
|
|
|
|
before do
|
|
|
|
allow(Setting).to receive(:per_page_options_array).and_return([1, 5])
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'it keeps applying filters and order' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.set_filter('name_and_identifier',
|
|
|
|
'Name or identifier',
|
|
|
|
'doesn\'t contain',
|
|
|
|
['Plain'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
# Sorts ASC by name
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_on 'Sort by "Name"'
|
|
|
|
|
|
|
|
# Results should be filtered and ordered ASC by name
|
|
|
|
expect(page).to have_text(development_project.name)
|
|
|
|
expect(page).to have_no_text(project.name) # as it filtered away
|
|
|
|
expect(page).to have_text('Next') # as the result set is larger than 1
|
|
|
|
expect(page).to have_no_text(public_project.name) # as it is on the second page
|
|
|
|
|
|
|
|
# Changing the page size to 5 and back to 1 should not change the filters (which we test later on the second page)
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
find('.pagination--options .pagination--item', text: '5').click # click page size '5'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
find('.pagination--options .pagination--item', text: '1').click # return back to page size '1'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_on '2' # Go to pagination page 2
|
|
|
|
|
|
|
|
# On page 2 you should see the second page of the filtered set ordered ASC by name
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
expect(page).to have_no_text(project.name) # Filtered away
|
|
|
|
expect(page).to have_no_text('Next') # Filters kept active, so there is no third page.
|
|
|
|
expect(page).to have_no_text(development_project.name) # That one should be on page 1
|
|
|
|
|
|
|
|
# Sorts DESC by name
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_on 'Ascending sorted by "Name"'
|
|
|
|
|
|
|
|
# On page 2 the same filters should still be intact but the order should be DESC on name
|
|
|
|
expect(page).to have_text(development_project.name)
|
|
|
|
expect(page).to have_no_text(project.name) # Filtered away
|
|
|
|
expect(page).to have_no_text('Next') # Filters kept active, so there is no third page.
|
|
|
|
expect(page).to have_no_text(public_project.name) # That one should be on page 1
|
|
|
|
|
|
|
|
# Sending the filter form again what implies to compose the request freshly
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
# We should see page 1, resetting pagination, as it is a new filter, but keeping the DESC order on the project
|
|
|
|
# name
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
expect(page).to have_no_text(development_project.name) # as it is on the second page
|
|
|
|
expect(page).to have_no_text(project.name) # as it filtered away
|
|
|
|
expect(page).to have_text('Next') # as the result set is larger than 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'when filter of type' do
|
|
|
|
scenario 'Name and identifier gives results in both, name and identifier' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
# Filter on model attribute 'name'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.set_filter('name_and_identifier',
|
|
|
|
'Name or identifier',
|
|
|
|
'doesn\'t contain',
|
|
|
|
['Plain'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(development_project.name)
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
expect(page).to have_no_text(project.name)
|
|
|
|
|
|
|
|
# Filter on model attribute 'identifier'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('name_and_identifier')
|
|
|
|
|
|
|
|
projects_page.set_filter('name_and_identifier',
|
|
|
|
'Name or identifier',
|
|
|
|
'is',
|
|
|
|
['plain-project'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project.name)
|
|
|
|
expect(page).to have_no_text(development_project.name)
|
|
|
|
expect(page).to have_no_text(public_project.name)
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'Active or archived' do
|
|
|
|
shared_let(:parent_project) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Parent project',
|
|
|
|
identifier: 'parent-project')
|
|
|
|
end
|
|
|
|
shared_let(:child_project) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Child project',
|
|
|
|
identifier: 'child-project',
|
|
|
|
parent: parent_project)
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'filter on "status", archive and unarchive' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
# value selection defaults to "active"'
|
|
|
|
expect(page).to have_selector('li[filter-name="active"]')
|
|
|
|
|
|
|
|
expect(page).to have_text(parent_project.name)
|
|
|
|
expect(page).to have_text(child_project.name)
|
|
|
|
expect(page).to have_text('Plain project')
|
|
|
|
expect(page).to have_text('Development project')
|
|
|
|
expect(page).to have_text('Public project')
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.click_menu_item_of('Archive', parent_project)
|
|
|
|
projects_page.accept_alert_dialog!
|
|
|
|
|
|
|
|
expect(page).to have_no_text(parent_project.name)
|
|
|
|
# The child project gets archived automatically
|
|
|
|
expect(page).to have_no_text(child_project.name)
|
|
|
|
expect(page).to have_text('Plain project')
|
|
|
|
expect(page).to have_text('Development project')
|
|
|
|
expect(page).to have_text('Public project')
|
|
|
|
|
|
|
|
visit project_overview_path(parent_project)
|
|
|
|
expect(page).to have_text("The project you're trying to access has been archived.")
|
|
|
|
|
|
|
|
# The child project gets archived automatically
|
|
|
|
visit project_overview_path(child_project)
|
|
|
|
expect(page).to have_text("The project you're trying to access has been archived.")
|
|
|
|
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.filter_by_active('no')
|
|
|
|
|
|
|
|
expect(page).to have_text("ARCHIVED #{parent_project.name}")
|
|
|
|
expect(page).to have_text("ARCHIVED #{child_project.name}")
|
|
|
|
|
|
|
|
# Test visibility of 'more' menu list items
|
|
|
|
projects_page.activate_menu_of(parent_project) do |menu|
|
|
|
|
expect(menu).to have_text('Unarchive')
|
|
|
|
expect(menu).to have_text('Delete')
|
|
|
|
expect(menu).to_not have_text('Archive')
|
|
|
|
expect(menu).to_not have_text('Copy')
|
|
|
|
expect(menu).to_not have_text('Settings')
|
|
|
|
expect(menu).to_not have_text('New subproject')
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link('Unarchive')
|
|
|
|
end
|
|
|
|
|
|
|
|
# The child project does not get unarchived automatically
|
|
|
|
visit project_path(child_project)
|
|
|
|
expect(page).to have_text("The project you're trying to access has been archived.")
|
|
|
|
|
|
|
|
visit project_path(parent_project)
|
|
|
|
expect(page).to have_text(parent_project.name)
|
|
|
|
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.filter_by_active('yes')
|
|
|
|
|
|
|
|
expect(page).to have_text(parent_project.name)
|
|
|
|
expect(page).to have_no_text(child_project.name)
|
|
|
|
expect(page).to have_text('Plain project')
|
|
|
|
expect(page).to have_text('Development project')
|
|
|
|
expect(page).to have_text('Public project')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'project status filter' do
|
|
|
|
shared_let(:no_status_project) do
|
|
|
|
# A project that never had project status associated.
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'No status project')
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_let(:green_project) do
|
|
|
|
# A project that has a project status associated.
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Green project',
|
|
|
|
status: FactoryBot.create(:project_status))
|
|
|
|
end
|
|
|
|
shared_let(:gray_project) do
|
|
|
|
# A project that once had a project status associated, that was later unset.
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Gray project',
|
|
|
|
status: FactoryBot.create(:project_status, code: nil))
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'sort and filter on project status' do
|
|
|
|
login_as(admin)
|
|
|
|
projects_page.visit!
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link('Sort by "Status"')
|
|
|
|
|
|
|
|
expect_project_at_place(green_project, 1)
|
|
|
|
expect(page).to have_text('(1 - 8/8)')
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link('Ascending sorted by "Status"')
|
|
|
|
|
|
|
|
expect_project_at_place(green_project, 8)
|
|
|
|
expect(page).to have_text('(1 - 8/8)')
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.open_filters
|
|
|
|
|
|
|
|
projects_page.set_filter('project_status_code',
|
|
|
|
'Project status',
|
|
|
|
'is',
|
|
|
|
['On track'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(green_project.name)
|
|
|
|
expect(page).to_not have_text(gray_project.name)
|
|
|
|
expect(page).to_not have_text(no_status_project.name)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.set_filter('project_status_code',
|
|
|
|
'Project status',
|
|
|
|
'all',
|
|
|
|
[])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(green_project.name)
|
|
|
|
expect(page).to_not have_text(gray_project.name)
|
|
|
|
expect(page).to_not have_text(no_status_project.name)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.set_filter('project_status_code',
|
|
|
|
'Project status',
|
|
|
|
'none',
|
|
|
|
[])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to_not have_text(green_project.name)
|
|
|
|
expect(page).to have_text(gray_project.name)
|
|
|
|
expect(page).to have_text(no_status_project.name)
|
|
|
|
|
|
|
|
projects_page.set_filter('project_status_code',
|
|
|
|
'Project status',
|
|
|
|
'is not',
|
|
|
|
['On track'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to_not have_text(green_project.name)
|
|
|
|
expect(page).to have_text(gray_project.name)
|
|
|
|
expect(page).to have_text(no_status_project.name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'other filter types' do
|
|
|
|
shared_let(:list_custom_field) { FactoryBot.create :list_project_custom_field }
|
|
|
|
shared_let(:date_custom_field) { FactoryBot.create :date_project_custom_field }
|
|
|
|
shared_let(:datetime_of_this_week) do
|
|
|
|
today = Date.today
|
|
|
|
# Ensure that the date is not today but still in the middle of the week to not run into week-start-issues here.
|
|
|
|
date_of_this_week = today + ((today.wday % 7) > 2 ? -1 : 1)
|
|
|
|
DateTime.parse(date_of_this_week.to_s + 'T11:11:11+00:00')
|
|
|
|
end
|
|
|
|
shared_let(:fixed_datetime) { DateTime.parse('2017-11-11T11:11:11+00:00') }
|
|
|
|
|
|
|
|
shared_let(:project_created_on_today) do
|
|
|
|
project = FactoryBot.create(:project,
|
|
|
|
name: 'Created today project',
|
|
|
|
created_at: DateTime.now)
|
|
|
|
project.custom_field_values = { list_custom_field.id => list_custom_field.possible_values[2],
|
|
|
|
date_custom_field.id => '2011-11-11' }
|
|
|
|
project.save!
|
|
|
|
project
|
|
|
|
end
|
|
|
|
shared_let(:project_created_on_this_week) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Created on this week project',
|
|
|
|
created_at: datetime_of_this_week)
|
|
|
|
end
|
|
|
|
shared_let(:project_created_on_six_days_ago) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Created on six days ago project',
|
|
|
|
created_at: DateTime.now - 6.days)
|
|
|
|
end
|
|
|
|
shared_let(:project_created_on_fixed_date) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Created on fixed date project',
|
|
|
|
created_at: fixed_datetime)
|
|
|
|
end
|
|
|
|
shared_let(:todays_wp) do
|
|
|
|
# This WP should trigger a change to the project's 'latest activity at' DateTime
|
|
|
|
FactoryBot.create(:work_package,
|
|
|
|
updated_at: DateTime.now,
|
|
|
|
project: project_created_on_today)
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
allow_enterprise_edition
|
|
|
|
project_created_on_today
|
|
|
|
load_and_open_filters admin
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'selecting operator' do
|
|
|
|
# created on 'today' shows projects that were created today
|
|
|
|
projects_page.set_filter('created_at',
|
|
|
|
'Created on',
|
|
|
|
'today')
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_today.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_this_week.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_fixed_date.name)
|
|
|
|
|
|
|
|
# created on 'this week' shows projects that were created within the last seven days
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('created_at')
|
|
|
|
|
|
|
|
projects_page.set_filter('created_at',
|
|
|
|
'Created on',
|
|
|
|
'this week')
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_today.name)
|
|
|
|
expect(page).to have_text(project_created_on_this_week.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_fixed_date.name)
|
|
|
|
|
|
|
|
# created on 'on' shows projects that were created within the last seven days
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('created_at')
|
|
|
|
|
|
|
|
projects_page.set_filter('created_at',
|
|
|
|
'Created on',
|
|
|
|
'on',
|
|
|
|
['2017-11-11'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_fixed_date.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_today.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_this_week.name)
|
|
|
|
|
|
|
|
# created on 'less than days ago'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('created_at')
|
|
|
|
|
|
|
|
projects_page.set_filter('created_at',
|
|
|
|
'Created on',
|
|
|
|
'less than days ago',
|
|
|
|
['1'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_today.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_fixed_date.name)
|
|
|
|
|
|
|
|
# created on 'more than days ago'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('created_at')
|
|
|
|
|
|
|
|
projects_page.set_filter('created_at',
|
|
|
|
'Created on',
|
|
|
|
'more than days ago',
|
|
|
|
['1'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_fixed_date.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_today.name)
|
|
|
|
|
|
|
|
# created on 'between'
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('created_at')
|
|
|
|
|
|
|
|
projects_page.set_filter('created_at',
|
|
|
|
'Created on',
|
|
|
|
'between',
|
|
|
|
['2017-11-10', '2017-11-12'])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_fixed_date.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_today.name)
|
|
|
|
|
|
|
|
# Latest activity at 'today'. This spot check would fail if the data does not get collected from multiple tables
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('created_at')
|
|
|
|
|
|
|
|
projects_page.set_filter('latest_activity_at',
|
|
|
|
'Latest activity at',
|
|
|
|
'today')
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_today.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_fixed_date.name)
|
|
|
|
|
|
|
|
# CF List
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter('latest_activity_at')
|
|
|
|
|
|
|
|
projects_page.set_filter("cf_#{list_custom_field.id}",
|
|
|
|
list_custom_field.name,
|
|
|
|
'is',
|
|
|
|
[list_custom_field.possible_values[2].value])
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_today.name)
|
|
|
|
expect(page).to_not have_text(project_created_on_fixed_date.name)
|
|
|
|
|
|
|
|
# switching to multiselect keeps the current selection
|
|
|
|
cf_filter = page.find("li[filter-name='cf_#{list_custom_field.id}']")
|
|
|
|
within(cf_filter) do
|
|
|
|
# Initial filter is a 'single select'
|
|
|
|
expect(cf_filter.find(:select, 'value')).not_to be_multiple
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_on 'Toggle multiselect'
|
|
|
|
# switching to multiselect keeps the current selection
|
|
|
|
expect(cf_filter.find(:select, 'value')).to be_multiple
|
|
|
|
expect(cf_filter).to have_select('value', selected: list_custom_field.possible_values[2].value)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
select list_custom_field.possible_values[3].value, from: 'value'
|
|
|
|
end
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
cf_filter = page.find("li[filter-name='cf_#{list_custom_field.id}']")
|
|
|
|
within(cf_filter) do
|
|
|
|
# Query has two values for that filter, so it should show a 'multi select'.
|
|
|
|
expect(cf_filter.find(:select, 'value')).to be_multiple
|
|
|
|
expect(cf_filter)
|
|
|
|
.to have_select('value',
|
|
|
|
selected: [list_custom_field.possible_values[2].value,
|
|
|
|
list_custom_field.possible_values[3].value])
|
|
|
|
|
|
|
|
# switching to single select keeps the first selection
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
select list_custom_field.possible_values[1].value, from: 'value'
|
|
|
|
unselect list_custom_field.possible_values[2].value, from: 'value'
|
|
|
|
|
|
|
|
click_on 'Toggle multiselect'
|
|
|
|
expect(cf_filter.find(:select, 'value')).not_to be_multiple
|
|
|
|
expect(cf_filter).to have_select('value', selected: list_custom_field.possible_values[1].value)
|
|
|
|
expect(cf_filter).to_not have_select('value', selected: list_custom_field.possible_values[3].value)
|
|
|
|
end
|
|
|
|
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
cf_filter = page.find("li[filter-name='cf_#{list_custom_field.id}']")
|
|
|
|
within(cf_filter) do
|
|
|
|
# Query has one value for that filter, so it should show a 'single select'.
|
|
|
|
expect(cf_filter.find(:select, 'value')).not_to be_multiple
|
|
|
|
end
|
|
|
|
|
|
|
|
# CF date filter work (at least for one operator)
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
remove_filter("cf_#{list_custom_field.id}")
|
|
|
|
|
|
|
|
projects_page.set_filter("cf_#{date_custom_field.id}",
|
|
|
|
date_custom_field.name,
|
|
|
|
'on',
|
|
|
|
['2011-11-11'])
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_on 'Apply'
|
|
|
|
|
|
|
|
expect(page).to have_text(project_created_on_today.name)
|
|
|
|
expect(page).to have_no_text(project_created_on_fixed_date.name)
|
|
|
|
end
|
|
|
|
|
|
|
|
pending "NOT WORKING YET: Date vs. DateTime issue: Selecting same date for from and to value shows projects of that date"
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'public filter' do
|
|
|
|
scenario 'filter on "public" status' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
expect(page).to have_text(project.name)
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.filter_by_public('no')
|
|
|
|
|
|
|
|
expect(page).to have_text(project.name)
|
|
|
|
expect(page).to have_no_text(public_project.name)
|
|
|
|
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
projects_page.filter_by_public('yes')
|
|
|
|
|
|
|
|
expect(page).to have_text(public_project.name)
|
|
|
|
expect(page).to have_no_text(project.name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'Non-admins with role with permission' do
|
|
|
|
shared_let(:can_copy_projects_role) do
|
|
|
|
FactoryBot.create :role, name: 'Can Copy Projects Role', permissions: [:copy_projects]
|
|
|
|
end
|
|
|
|
shared_let(:can_add_subprojects_role) do
|
|
|
|
FactoryBot.create :role, name: 'Can Add Subprojects Role', permissions: [:add_subprojects]
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_let(:parent_project) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
name: 'Parent project',
|
|
|
|
identifier: 'parent-project')
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_let(:can_copy_projects_manager) do
|
|
|
|
FactoryBot.create(:user,
|
|
|
|
member_in_project: parent_project,
|
|
|
|
member_through_role: can_copy_projects_role)
|
|
|
|
end
|
|
|
|
shared_let(:can_add_subprojects_manager) do
|
|
|
|
FactoryBot.create(:user,
|
|
|
|
member_in_project: parent_project,
|
|
|
|
member_through_role: can_add_subprojects_role)
|
|
|
|
end
|
|
|
|
let(:simple_member) do
|
|
|
|
FactoryBot.create(:user,
|
|
|
|
member_in_project: parent_project,
|
|
|
|
member_through_role: developer)
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
# We are not admin so we need to force the built-in roles to have them.
|
|
|
|
Role.non_member
|
|
|
|
|
|
|
|
# Remove public projects from the default list for these scenarios.
|
|
|
|
public_project.update(active: false)
|
|
|
|
|
|
|
|
project.update(created_at: 7.days.ago)
|
|
|
|
|
|
|
|
news
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'can see the "More" menu' do
|
|
|
|
# For a simple project member the 'More' menu is not visible.
|
|
|
|
login_as(simple_member)
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
expect(page).to have_text(parent_project.name)
|
|
|
|
|
|
|
|
# 'More' does not become visible on hover
|
|
|
|
page.find('tbody tr').hover
|
|
|
|
expect(page).not_to have_selector('.icon-show-more-horizontal')
|
|
|
|
|
|
|
|
# For a project member with :copy_projects privilege the 'More' menu is visible.
|
|
|
|
login_as(can_copy_projects_manager)
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
expect(page).to have_text(parent_project.name)
|
|
|
|
|
|
|
|
# 'More' becomes visible on hover
|
|
|
|
# because we use css opacity we can not test for the visibility changes
|
|
|
|
page.find('tbody tr').hover
|
|
|
|
expect(page).to have_selector('.icon-show-more-horizontal')
|
|
|
|
|
|
|
|
# Test visibility of 'more' menu list items
|
|
|
|
page.find('tbody tr .icon-show-more-horizontal').click
|
|
|
|
menu = page.find('tbody tr .project-actions')
|
|
|
|
expect(menu).to have_text('Copy')
|
|
|
|
expect(menu).to_not have_text('New subproject')
|
|
|
|
expect(menu).to_not have_text('Delete')
|
|
|
|
expect(menu).to_not have_text('Archive')
|
|
|
|
expect(menu).to_not have_text('Unarchive')
|
|
|
|
|
|
|
|
# For a project member with :add_subprojects privilege the 'More' menu is visible.
|
|
|
|
login_as(can_add_subprojects_manager)
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
# 'More' becomes visible on hover
|
|
|
|
# because we use css opacity we can not test for the visibility changes
|
|
|
|
page.find('tbody tr').hover
|
|
|
|
expect(page).to have_selector('.icon-show-more-horizontal')
|
|
|
|
|
|
|
|
# Test visibility of 'more' menu list items
|
|
|
|
page.find('tbody tr .icon-show-more-horizontal').click
|
|
|
|
menu = page.find('tbody tr .project-actions')
|
|
|
|
expect(menu).to have_text('New subproject')
|
|
|
|
expect(menu).to_not have_text('Copy')
|
|
|
|
expect(menu).to_not have_text('Delete')
|
|
|
|
expect(menu).to_not have_text('Archive')
|
|
|
|
expect(menu).to_not have_text('Unrchive')
|
|
|
|
|
|
|
|
# Test admin only properties are invisible
|
|
|
|
within('#project-table') do
|
|
|
|
expect(page)
|
|
|
|
.to have_no_selector('th', text: 'REQUIRED DISK STORAGE')
|
|
|
|
expect(page)
|
|
|
|
.to have_no_selector('th', text: 'CREATED ON')
|
|
|
|
expect(page)
|
|
|
|
.to have_no_selector('td', text: project.created_at.strftime('%m/%d/%Y'))
|
|
|
|
expect(page)
|
|
|
|
.to have_no_selector('th', text: 'LATEST ACTIVITY AT')
|
|
|
|
expect(page)
|
|
|
|
.to have_no_selector('td', text: news.created_at.strftime('%m/%d/%Y'))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'order' do
|
|
|
|
shared_let(:integer_custom_field) { FactoryBot.create(:int_project_custom_field) }
|
|
|
|
# order is important here as the implementation uses lft
|
|
|
|
# first but then reorders in ruby
|
|
|
|
shared_let(:child_project_z) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
parent: project,
|
|
|
|
name: "Z Child")
|
|
|
|
end
|
|
|
|
shared_let(:child_project_m) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
parent: project,
|
|
|
|
name: "m Child") # intentionally written lowercase to test for case insensitive sorting
|
|
|
|
end
|
|
|
|
shared_let(:child_project_a) do
|
|
|
|
FactoryBot.create(:project,
|
|
|
|
parent: project,
|
|
|
|
name: "A Child")
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
allow_enterprise_edition
|
|
|
|
login_as(admin)
|
|
|
|
visit projects_path
|
|
|
|
|
|
|
|
project.custom_field_values = { integer_custom_field.id => 1 }
|
|
|
|
project.save!
|
|
|
|
development_project.custom_field_values = { integer_custom_field.id => 2 }
|
|
|
|
development_project.save!
|
|
|
|
public_project.custom_field_values = { integer_custom_field.id => 3 }
|
|
|
|
public_project.save!
|
|
|
|
child_project_z.custom_field_values = { integer_custom_field.id => 4 }
|
|
|
|
child_project_z.save!
|
|
|
|
child_project_m.custom_field_values = { integer_custom_field.id => 4 }
|
|
|
|
child_project_m.save!
|
|
|
|
child_project_a.custom_field_values = { integer_custom_field.id => 4 }
|
|
|
|
child_project_a.save!
|
|
|
|
end
|
|
|
|
|
|
|
|
scenario 'allows to alter the order in which projects are displayed' do
|
|
|
|
Setting.enabled_projects_columns += ["cf_#{integer_custom_field.id}"]
|
|
|
|
|
|
|
|
# initially, ordered by name asc on each hierarchical level
|
|
|
|
expect_projects_in_order(development_project,
|
|
|
|
project,
|
|
|
|
child_project_a,
|
|
|
|
child_project_m,
|
|
|
|
child_project_z,
|
|
|
|
public_project)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link('Name')
|
|
|
|
|
|
|
|
# Projects ordered by name asc
|
|
|
|
expect_projects_in_order(child_project_a,
|
|
|
|
development_project,
|
|
|
|
child_project_m,
|
|
|
|
project,
|
|
|
|
public_project,
|
|
|
|
child_project_z)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link('Name')
|
|
|
|
|
|
|
|
# Projects ordered by name desc
|
|
|
|
expect_projects_in_order(child_project_z,
|
|
|
|
public_project,
|
|
|
|
project,
|
|
|
|
child_project_m,
|
|
|
|
development_project,
|
|
|
|
child_project_a)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link(integer_custom_field.name)
|
|
|
|
|
|
|
|
# Projects ordered by cf asc first then project name desc
|
|
|
|
expect_projects_in_order(project,
|
|
|
|
development_project,
|
|
|
|
public_project,
|
|
|
|
child_project_z,
|
|
|
|
child_project_m,
|
|
|
|
child_project_a)
|
|
|
|
|
|
|
|
SeleniumHubWaiter.wait
|
|
|
|
click_link('Sort by "Project hierarchy"')
|
|
|
|
|
|
|
|
# again ordered by name asc on each hierarchical level
|
|
|
|
expect_projects_in_order(development_project,
|
|
|
|
project,
|
|
|
|
child_project_a,
|
|
|
|
child_project_m,
|
|
|
|
child_project_z,
|
|
|
|
public_project)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
feature 'blacklisted filters' do
|
|
|
|
scenario 'are not visible' do
|
|
|
|
load_and_open_filters admin
|
|
|
|
|
|
|
|
expect(page).to_not have_select('add_filter_select', with_options: ["Principal"])
|
|
|
|
expect(page).to_not have_select('add_filter_select', with_options: ["ID"])
|
|
|
|
expect(page).to_not have_select('add_filter_select', with_options: ["Subproject of"])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with a multi-value custom field' do
|
|
|
|
let!(:list_custom_field) { FactoryBot.create(:list_project_custom_field, multi_value: true) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
project.custom_values << CustomValue.new(custom_field: list_custom_field, value: list_custom_field.value_of('A'))
|
|
|
|
project.custom_values << CustomValue.new(custom_field: list_custom_field, value: list_custom_field.value_of('B'))
|
|
|
|
|
|
|
|
project.save!
|
|
|
|
|
|
|
|
allow_enterprise_edition
|
|
|
|
allow(Setting)
|
|
|
|
.to receive(:enabled_projects_columns) #<< "cf_#{list_custom_field.id}"
|
|
|
|
.and_return ["cf_#{list_custom_field.id}"]
|
|
|
|
|
|
|
|
login_as(admin)
|
|
|
|
visit projects_path
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'shows the multi selection' do
|
|
|
|
expected_sort = list_custom_field
|
|
|
|
.custom_options
|
|
|
|
.where(value: %w[A B])
|
|
|
|
.reorder(:id)
|
|
|
|
.pluck(:value)
|
|
|
|
expect(page).to have_selector(".cf_#{list_custom_field.id}.format-list", text: expected_sort.join(", "))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|