From 063b5312111ccea62f84fa9f68a2262dc1f66e64 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 4 Sep 2017 16:23:55 +0200 Subject: Add a separate finder for collecting children of groups --- spec/finders/group_children_finder_spec.rb | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 spec/finders/group_children_finder_spec.rb (limited to 'spec') diff --git a/spec/finders/group_children_finder_spec.rb b/spec/finders/group_children_finder_spec.rb new file mode 100644 index 00000000000..afd96e27a1d --- /dev/null +++ b/spec/finders/group_children_finder_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe GroupChildrenFinder do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:params) { {} } + subject(:finder) { described_class.new(user, parent_group: group, params: params) } + + before do + group.add_owner(user) + end + + describe '#execute' do + it 'includes projects' do + project = create(:project, namespace: group) + + expect(finder.execute).to contain_exactly(project) + end + + context 'with a filter' do + let(:params) { { filter: 'test' } } + + it 'includes only projects matching the filter' do + _other_project = create(:project, namespace: group) + matching_project = create(:project, namespace: group, name: 'testproject') + + expect(finder.execute).to contain_exactly(matching_project) + end + end + end + + context 'with nested groups', :nested_groups do + let!(:project) { create(:project, namespace: group) } + let!(:subgroup) { create(:group, parent: group) } + + describe '#execute' do + it 'contains projects and subgroups' do + expect(finder.execute).to contain_exactly(subgroup, project) + end + + context 'with a filter' do + let(:params) { { filter: 'test' } } + + it 'contains only matching projects and subgroups' do + matching_project = create(:project, namespace: group, name: 'Testproject') + matching_subgroup = create(:group, name: 'testgroup', parent: group) + + expect(finder.execute).to contain_exactly(matching_subgroup, matching_project) + end + end + end + + describe '#total_count' do + it 'counts the array children were already loaded' do + finder.instance_variable_set(:@children, [double]) + + expect(finder).not_to receive(:child_groups) + expect(finder).not_to receive(:projects) + + expect(finder.total_count).to eq(1) + end + + it 'performs a count without loading children when they are not loaded yet' do + expect(finder).to receive(:child_groups).and_call_original + expect(finder).to receive(:projects).and_call_original + + expect(finder.total_count).to eq(2) + end + end + end +end -- cgit v1.2.1 From 2eac1537ad907f2f7e628788cf980cb7e48d3f56 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 4 Sep 2017 20:01:58 +0200 Subject: Fetch children using new finder for the `show` of a group. --- spec/finders/group_children_finder_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/finders/group_children_finder_spec.rb b/spec/finders/group_children_finder_spec.rb index afd96e27a1d..a2a24b2a12e 100644 --- a/spec/finders/group_children_finder_spec.rb +++ b/spec/finders/group_children_finder_spec.rb @@ -52,16 +52,16 @@ describe GroupChildrenFinder do describe '#total_count' do it 'counts the array children were already loaded' do - finder.instance_variable_set(:@children, [double]) + finder.instance_variable_set(:@children, [build(:project)]) - expect(finder).not_to receive(:child_groups) + expect(finder).not_to receive(:subgroups) expect(finder).not_to receive(:projects) expect(finder.total_count).to eq(1) end it 'performs a count without loading children when they are not loaded yet' do - expect(finder).to receive(:child_groups).and_call_original + expect(finder).to receive(:subgroups).and_call_original expect(finder).to receive(:projects).and_call_original expect(finder.total_count).to eq(2) -- cgit v1.2.1 From 376a8c66c1ca8ee2a95255d21c9d55ce006ab655 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 5 Sep 2017 10:03:43 +0200 Subject: Remove the subgroups path on a group --- spec/controllers/groups_controller_spec.rb | 1 + spec/features/groups_spec.rb | 22 +++++++++------------- 2 files changed, 10 insertions(+), 13 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index b0564e27a68..83a2e82d78c 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -157,6 +157,7 @@ describe GroupsController do context 'as a user' do before do sign_in(user) + pending('spec the children path instead') end it 'shows all subgroups' do diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 4ec2e7e6012..493dd551d25 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -90,7 +90,10 @@ feature 'Group' do context 'as admin' do before do - visit subgroups_group_path(group) + visit group_path(group) + + pending('use the new subgroup button') + click_link 'New Subgroup' end @@ -111,7 +114,10 @@ feature 'Group' do sign_out(:user) sign_in(user) - visit subgroups_group_path(group) + visit group_path(group) + + pending('use the new subgroup button') + click_link 'New Subgroup' fill_in 'Group path', with: 'bar' click_button 'Create group' @@ -120,16 +126,6 @@ feature 'Group' do expect(page).to have_content("Group 'bar' was successfully created.") end end - - context 'when nested group feature is disabled' do - it 'renders 404' do - allow(Group).to receive(:supports_nested_groups?).and_return(false) - - visit subgroups_group_path(group) - - expect(page.status_code).to eq(404) - end - end end it 'checks permissions to avoid exposing groups by parent_id' do @@ -213,8 +209,8 @@ feature 'Group' do let!(:path) { group_path(group) } it 'has nested groups tab with nested groups inside' do + pending('the child should be visible on the show page') visit path - click_link 'Subgroups' expect(page).to have_content(nested_group.name) end -- cgit v1.2.1 From d33e15574b064e38ceadf8aafb47af985d1a7a7a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 5 Sep 2017 11:30:16 +0200 Subject: Add serializer for group children --- spec/serializers/group_child_entity_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 spec/serializers/group_child_entity_spec.rb (limited to 'spec') diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb new file mode 100644 index 00000000000..1c4cdc2a5fb --- /dev/null +++ b/spec/serializers/group_child_entity_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe GroupChildEntity do + let(:request) { double('request') } + let(:entity) { described_class.new(object, request: request) } + subject(:json) { entity.as_json } + + describe 'for a project' do + let(:object) { build_stubbed(:project) } + + it 'has the correct type' do + expect(json[:type]).to eq('project') + end + end +end -- cgit v1.2.1 From 648c082a23f51bdf7151b6c5f716e74c4fe6a5bd Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 5 Sep 2017 16:18:24 +0200 Subject: Render group children using the same entity --- spec/serializers/group_child_entity_spec.rb | 75 ++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 1c4cdc2a5fb..3a7d2205f53 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -1,15 +1,88 @@ require 'spec_helper' describe GroupChildEntity do + let(:user) { create(:user) } let(:request) { double('request') } let(:entity) { described_class.new(object, request: request) } subject(:json) { entity.as_json } + before do + allow(request).to receive(:current_user).and_return(user) + end + + shared_examples 'group child json' do + it 'renders json' do + is_expected.not_to be_nil + end + + %w[id + path + full_name + full_path + avatar_url + name + description + web_url + visibility + type + can_edit + visibility + edit_path + permission].each do |attribute| + it "includes #{attribute}" do + expect(json[attribute.to_sym]).to be_present + end + end + end + describe 'for a project' do - let(:object) { build_stubbed(:project) } + set(:object) do + create(:project, :with_avatar, + description: 'Awesomeness') + end + + before do + object.add_master(user) + end it 'has the correct type' do expect(json[:type]).to eq('project') end + + it 'includes the star count' do + expect(json[:star_count]).to be_present + end + + it_behaves_like 'group child json' + end + + describe 'for a group', :nested_groups do + set(:object) do + create(:group, :nested, :with_avatar, + description: 'Awesomeness') + end + + before do + object.add_owner(user) + end + + it 'has the correct type' do + expect(json[:type]).to eq('group') + end + + it 'counts projects and subgroups as children' do + create(:project, namespace: object) + create(:group, parent: object) + + expect(json[:children_count]).to eq(2) + end + + %w[children_count leave_path parent_id number_projects_with_delimiter number_users_with_delimiter project_count subgroup_count].each do |attribute| + it "includes #{attribute}" do + expect(json[attribute.to_sym]).to be_present + end + end + + it_behaves_like 'group child json' end end -- cgit v1.2.1 From 80780018a931ce41047ab62ed7dd6c5f1e28f08b Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 5 Sep 2017 16:57:46 +0200 Subject: Update `children` route to handle projects and groups --- spec/controllers/groups_controller_spec.rb | 82 ++++++++++++++++++++++-------- spec/lib/gitlab/path_regex_spec.rb | 14 ++--- 2 files changed, 68 insertions(+), 28 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 83a2e82d78c..c96a44d6186 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -150,39 +150,79 @@ describe GroupsController do end end - describe 'GET #subgroups', :nested_groups do - let!(:public_subgroup) { create(:group, :public, parent: group) } - let!(:private_subgroup) { create(:group, :private, parent: group) } + describe 'GET #children' do + context 'for projects' do + let!(:public_project) { create(:project, :public, namespace: group) } + let!(:private_project) { create(:project, :private, namespace: group) } - context 'as a user' do - before do - sign_in(user) - pending('spec the children path instead') + context 'as a user' do + before do + sign_in(user) + end + + it 'shows all children' do + get :children, id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_project, private_project) + end + + context 'being member of private subgroup' do + it 'shows public and private children the user is member of' do + group_member.destroy! + private_project.add_guest(user) + + get :children, id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_project, private_project) + end + end end - it 'shows all subgroups' do - get :subgroups, id: group.to_param + context 'as a guest' do + it 'shows the public children' do + get :children, id: group.to_param, format: :json - expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup) + expect(assigns(:children)).to contain_exactly(public_project) + end end + end - context 'being member of private subgroup' do - it 'shows public and private subgroups the user is member of' do - group_member.destroy! - private_subgroup.add_guest(user) + context 'for subgroups', :nested_groups do + let!(:public_subgroup) { create(:group, :public, parent: group) } + let!(:private_subgroup) { create(:group, :private, parent: group) } + let!(:public_project) { create(:project, :public, namespace: group) } + let!(:private_project) { create(:project, :private, namespace: group) } - get :subgroups, id: group.to_param + context 'as a user' do + before do + sign_in(user) + end - expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup) + it 'shows all children' do + get :children, id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project) + end + + context 'being member of private subgroup' do + it 'shows public and private children the user is member of' do + group_member.destroy! + private_subgroup.add_guest(user) + private_project.add_guest(user) + + get :children, id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project) + end end end - end - context 'as a guest' do - it 'shows the public subgroups' do - get :subgroups, id: group.to_param + context 'as a guest' do + it 'shows the public children' do + get :children, id: group.to_param, format: :json - expect(assigns(:nested_groups)).to contain_exactly(public_subgroup) + expect(assigns(:children)).to contain_exactly(public_subgroup, public_project) + end end end end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 1f1c48ee9b5..f1f188cbfb5 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -213,7 +213,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/') expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') + expect(subject).to match('labels/') end it 'is not case sensitive' do @@ -246,7 +246,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/') expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') + expect(subject).to match('labels/') end end @@ -268,7 +268,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/more/') expect(subject).to match('group_members/more/') - expect(subject).to match('subgroups/more/') + expect(subject).to match('labels/more/') end end end @@ -292,7 +292,7 @@ describe Gitlab::PathRegex do it 'rejects group routes' do expect(subject).not_to match('root/activity/') expect(subject).not_to match('root/group_members/') - expect(subject).not_to match('root/subgroups/') + expect(subject).not_to match('root/labels/') end end @@ -314,7 +314,7 @@ describe Gitlab::PathRegex do it 'rejects group routes' do expect(subject).not_to match('root/activity/more/') expect(subject).not_to match('root/group_members/more/') - expect(subject).not_to match('root/subgroups/more/') + expect(subject).not_to match('root/labels/more/') end end end @@ -349,7 +349,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('activity/') expect(subject).to match('group_members/') - expect(subject).to match('subgroups/') + expect(subject).to match('labels/') end it 'is not case sensitive' do @@ -382,7 +382,7 @@ describe Gitlab::PathRegex do it 'accepts group routes' do expect(subject).to match('root/activity/') expect(subject).to match('root/group_members/') - expect(subject).to match('root/subgroups/') + expect(subject).to match('root/labels/') end it 'is not case sensitive' do -- cgit v1.2.1 From 28c440045ed001ab6029131ab7c842b390f4e186 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 5 Sep 2017 17:27:55 +0200 Subject: Add pagination for children --- spec/controllers/groups_controller_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index c96a44d6186..9b4654dc3e4 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -466,6 +466,24 @@ describe GroupsController do end end end + + context 'pagination' do + let!(:other_subgroup) { create(:group, :public, parent: group) } + let!(:project) { create(:project, :public, namespace: group) } + let!(:first_page_subgroups) { create_list(:group, Kaminari.config.default_per_page, parent: group) } + + it 'contains all subgroups' do + get :children, id: group.to_param, sort: 'id', format: :json + + expect(assigns(:children)).to contain_exactly(*first_page_subgroups) + end + + it 'contains the project and group on the second page' do + get :children, id: group.to_param, sort: 'id', page: 2, format: :json + + expect(assigns(:children)).to contain_exactly(other_subgroup, project) + end + end end context 'for a POST request' do -- cgit v1.2.1 From 9f3995a0ca3d56fd8d219a2b01c3e6555fd91f27 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 6 Sep 2017 15:28:07 +0200 Subject: Find all children matching a query --- spec/finders/group_children_finder_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'spec') diff --git a/spec/finders/group_children_finder_spec.rb b/spec/finders/group_children_finder_spec.rb index a2a24b2a12e..8f83f88bb97 100644 --- a/spec/finders/group_children_finder_spec.rb +++ b/spec/finders/group_children_finder_spec.rb @@ -47,6 +47,20 @@ describe GroupChildrenFinder do expect(finder.execute).to contain_exactly(matching_subgroup, matching_project) end + + context 'with matching children' do + it 'includes a group that has a subgroup matching the query' do + matching_subgroup = create(:group, name: 'testgroup', parent: subgroup) + + expect(finder.execute).to contain_exactly(matching_subgroup) + end + + it 'includes a group that has a project matching the query' do + matching_project = create(:project, namespace: subgroup, name: 'Testproject') + + expect(finder.execute).to contain_exactly(matching_project) + end + end end end -- cgit v1.2.1 From 438a0773dc850d3fa690881ea0b022bc27435e1e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 6 Sep 2017 16:29:52 +0200 Subject: Add a concern to build hierarchies of groups --- spec/models/concerns/group_hierarchy_spec.rb | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 spec/models/concerns/group_hierarchy_spec.rb (limited to 'spec') diff --git a/spec/models/concerns/group_hierarchy_spec.rb b/spec/models/concerns/group_hierarchy_spec.rb new file mode 100644 index 00000000000..14ac910c90d --- /dev/null +++ b/spec/models/concerns/group_hierarchy_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe GroupHierarchy, :nested_groups do + let(:parent) { create(:group) } + let(:subgroup) { create(:group, parent: parent) } + let(:subsub_group) { create(:group, parent: subgroup) } + + context 'for a group' do + describe '#hierarchy' do + it 'builds a hierarchy for a group' do + expected_hierarchy = { parent => { subgroup => subsub_group } } + + expect(subsub_group.hierarchy).to eq(expected_hierarchy) + end + + it 'builds a hierarchy upto a specified parent' do + expected_hierarchy = { subgroup => subsub_group } + + expect(subsub_group.hierarchy(parent)).to eq(expected_hierarchy) + end + + it 'raises an error if specifying a base that is not part of the tree' do + expect { subsub_group.hierarchy(double) }.to raise_error('specified base is not part of the tree') + end + end + + describe '#parent' do + it 'returns the correct parent' do + expect(subsub_group.parent).to eq(subgroup) + end + end + end + + context 'for a project' do + let(:project) { create(:project, namespace: subsub_group) } + + describe '#hierarchy' do + it 'builds a hierarchy for a group' do + expected_hierarchy = { parent => { subgroup => { subsub_group => project } } } + + expect(project.hierarchy).to eq(expected_hierarchy) + end + + it 'builds a hierarchy upto a specified parent' do + expected_hierarchy = { subsub_group => project } + + expect(project.hierarchy(subgroup)).to eq(expected_hierarchy) + end + + it 'raises an error if specifying a base that is not part of the tree' do + expect { project.hierarchy(double) }.to raise_error('specified base is not part of the tree') + end + end + + describe '#parent' do + it 'returns the correct parent' do + expect(project.parent).to eq(subsub_group) + end + end + end +end -- cgit v1.2.1 From 530cf2a2669ea1ee3c41d48a15919f875babefa4 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 7 Sep 2017 11:46:58 +0200 Subject: Don't break when building unions on empty collections --- spec/lib/gitlab/sql/union_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'spec') diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 8026fba9f0a..fe6422c32b6 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -29,5 +29,12 @@ describe Gitlab::SQL::Union do expect(union.to_sql).to include('UNION ALL') end + + it 'returns `NULL` if all relations are empty' do + empty_relation = User.none + union = described_class.new([empty_relation, empty_relation]) + + expect(union.to_sql).to eq('NULL') + end end end -- cgit v1.2.1 From 518216c0627cb6c4b3db62f10877b44d0e912ddb Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 7 Sep 2017 19:08:56 +0200 Subject: Merge group hierarchies when parents are shared --- spec/models/concerns/group_hierarchy_spec.rb | 68 +++++++++++++++++++++++++ spec/serializers/group_child_serializer_spec.rb | 43 ++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 spec/serializers/group_child_serializer_spec.rb (limited to 'spec') diff --git a/spec/models/concerns/group_hierarchy_spec.rb b/spec/models/concerns/group_hierarchy_spec.rb index 14ac910c90d..1f4fab88781 100644 --- a/spec/models/concerns/group_hierarchy_spec.rb +++ b/spec/models/concerns/group_hierarchy_spec.rb @@ -29,6 +29,40 @@ describe GroupHierarchy, :nested_groups do expect(subsub_group.parent).to eq(subgroup) end end + + describe '#merge_hierarchy' do + it 'combines hierarchies' do + other_subgroup = create(:group, parent: parent) + + expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup] } + + expect(subsub_group.merge_hierarchy(other_subgroup)).to eq(expected_hierarchy) + end + end + + describe '.merge_hierarchies' do + it 'combines hierarchies until the top' do + other_subgroup = create(:group, parent: parent) + other_subsub_group = create(:group, parent: subgroup) + + groups = [other_subgroup, subsub_group, other_subsub_group] + + expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] } + + expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) + end + + it 'combines upto a given parent' do + other_subgroup = create(:group, parent: parent) + other_subsub_group = create(:group, parent: subgroup) + + groups = [other_subgroup, subsub_group, other_subsub_group] + + expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] + + expect(described_class.merge_hierarchies(groups, parent)).to eq(expected_hierarchy) + end + end end context 'for a project' do @@ -57,5 +91,39 @@ describe GroupHierarchy, :nested_groups do expect(project.parent).to eq(subsub_group) end end + + describe '#merge_hierarchy' do + it 'combines hierarchies' do + project = create(:project, namespace: parent) + + expected_hierarchy = { parent => [{ subgroup => subsub_group }, project] } + + expect(subsub_group.merge_hierarchy(project)).to eq(expected_hierarchy) + end + end + + describe '.merge_hierarchies' do + it 'combines hierarchies until the top' do + other_project = create(:project, namespace: parent) + other_subgroup_project = create(:project, namespace: subgroup) + + elements = [other_project, subsub_group, other_subgroup_project] + + expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] } + + expect(described_class.merge_hierarchies(elements)).to eq(expected_hierarchy) + end + + it 'combines upto a given parent' do + other_project = create(:project, namespace: parent) + other_subgroup_project = create(:project, namespace: subgroup) + + elements = [other_project, subsub_group, other_subgroup_project] + + expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }] + + expect(described_class.merge_hierarchies(elements, parent)).to eq(expected_hierarchy) + end + end end end diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb new file mode 100644 index 00000000000..967ed06d316 --- /dev/null +++ b/spec/serializers/group_child_serializer_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe GroupChildSerializer do + let(:request) { double('request') } + let(:user) { create(:user) } + subject(:serializer) { described_class.new(current_user: user) } + + describe '#represent' do + context 'for groups' do + it 'can render a single group' do + expect(serializer.represent(build(:group))).to be_kind_of(Hash) + end + + it 'can render a collection of groups' do + expect(serializer.represent(build_list(:group, 2))).to be_kind_of(Array) + end + end + + context 'with a hierarchy' do + let(:parent) { create(:group) } + + subject(:serializer) do + described_class.new(current_user: user).expand_hierarchy(parent) + end + + it 'expands the subgroups' do + subgroup = create(:group, parent: parent) + subsub_group = create(:group, parent: subgroup) + + json = serializer.represent(subsub_group) + subsub_group_json = json[:children].first + + expect(json[:id]).to eq(subgroup.id) + expect(subsub_group_json).not_to be_nil + expect(subsub_group_json[:id]).to eq(subsub_group.id) + end + + it 'can expand multiple trees' do + + end + end + end +end -- cgit v1.2.1 From 8f6dac4991ba7f5771a24175784f19dc1bbd4103 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 8 Sep 2017 19:22:33 +0200 Subject: Allow filtering children for a group When fetching children for a group with a filter, we will search all nested groups for results and render them in an expanded tree --- spec/controllers/groups_controller_spec.rb | 30 ++++++++++++++++ spec/serializers/group_child_serializer_spec.rb | 47 +++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 9b4654dc3e4..9cddb538a59 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -224,6 +224,36 @@ describe GroupsController do expect(assigns(:children)).to contain_exactly(public_subgroup, public_project) end end + + context 'filtering children' do + def get_filtered_list + get :children, id: group.to_param, filter: 'filter', format: :json + end + + it 'expands the tree for matching projects' do + project = create(:project, :public, namespace: public_subgroup, name: 'filterme') + + get_filtered_list + + group_json = json_response.first + project_json = group_json['children'].first + + expect(group_json['id']).to eq(public_subgroup.id) + expect(project_json['id']).to eq(project.id) + end + + it 'expands the tree for matching subgroups' do + matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') + + get_filtered_list + + group_json = json_response.first + matched_group_json = group_json['children'].first + + expect(group_json['id']).to eq(public_subgroup.id) + expect(matched_group_json['id']).to eq(matched_group.id) + end + end end end diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb index 967ed06d316..7a87c1adc8f 100644 --- a/spec/serializers/group_child_serializer_spec.rb +++ b/spec/serializers/group_child_serializer_spec.rb @@ -16,7 +16,7 @@ describe GroupChildSerializer do end end - context 'with a hierarchy' do + context 'with a hierarchy', :nested_groups do let(:parent) { create(:group) } subject(:serializer) do @@ -35,8 +35,51 @@ describe GroupChildSerializer do expect(subsub_group_json[:id]).to eq(subsub_group.id) end - it 'can expand multiple trees' do + it 'can render a nested tree' do + subgroup1 = create(:group, parent: parent) + subsub_group1 = create(:group, parent: subgroup1) + subgroup2 = create(:group, parent: parent) + subsub_group2 = create(:group, parent: subgroup2) + json = serializer.represent([subsub_group1, subsub_group2]) + subgroup1_json = json.first + subsub_group1_json = subgroup1_json[:children].first + + expect(json.size).to eq(2) + expect(subgroup1_json[:id]).to eq(subgroup1.id) + expect(subsub_group1_json[:id]).to eq(subsub_group1.id) + end + end + + context 'for projects' do + it 'can render a single project' do + expect(serializer.represent(build(:project))).to be_kind_of(Hash) + end + + it 'can render a collection of projects' do + expect(serializer.represent(build_list(:project, 2))).to be_kind_of(Array) + end + + context 'with a hierarchy', :nested_groups do + let(:parent) { create(:group) } + + subject(:serializer) do + described_class.new(current_user: user).expand_hierarchy(parent) + end + + it 'can render a nested tree' do + subgroup1 = create(:group, parent: parent) + project1 = create(:project, namespace: subgroup1) + subgroup2 = create(:group, parent: parent) + project2 = create(:project, namespace: subgroup2) + + json = serializer.represent([project1, project2]) + project1_json, project2_json = json.map { |group_json| group_json[:children].first } + + expect(json.size).to eq(2) + expect(project1_json[:id]).to eq(project1.id) + expect(project2_json[:id]).to eq(project2.id) + end end end end -- cgit v1.2.1 From bb5187bb2a71f87b3ff49388f70dbcb27dfe4c6a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 10 Sep 2017 17:20:27 +0200 Subject: Handle case where 2 matches in the same tree are found --- spec/models/concerns/group_hierarchy_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'spec') diff --git a/spec/models/concerns/group_hierarchy_spec.rb b/spec/models/concerns/group_hierarchy_spec.rb index 1f4fab88781..bb02983c776 100644 --- a/spec/models/concerns/group_hierarchy_spec.rb +++ b/spec/models/concerns/group_hierarchy_spec.rb @@ -124,6 +124,12 @@ describe GroupHierarchy, :nested_groups do expect(described_class.merge_hierarchies(elements, parent)).to eq(expected_hierarchy) end + + it 'merges to elements in the same hierarchy' do + expected_hierarchy = { parent => subgroup } + + expect(described_class.merge_hierarchies([parent, subgroup])).to eq(expected_hierarchy) + end end end end -- cgit v1.2.1 From 79cc3c8e3e7938ea53459e34ca585ae66791199e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 10 Sep 2017 18:43:15 +0200 Subject: Limit the amount of queries per row --- spec/controllers/groups_controller_spec.rb | 85 +++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 9cddb538a59..25778cc2b59 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -226,14 +226,10 @@ describe GroupsController do end context 'filtering children' do - def get_filtered_list - get :children, id: group.to_param, filter: 'filter', format: :json - end - it 'expands the tree for matching projects' do project = create(:project, :public, namespace: public_subgroup, name: 'filterme') - get_filtered_list + get :children, id: group.to_param, filter: 'filter', format: :json group_json = json_response.first project_json = group_json['children'].first @@ -245,7 +241,7 @@ describe GroupsController do it 'expands the tree for matching subgroups' do matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') - get_filtered_list + get :children, id: group.to_param, filter: 'filter', format: :json group_json = json_response.first matched_group_json = group_json['children'].first @@ -254,6 +250,83 @@ describe GroupsController do expect(matched_group_json['id']).to eq(matched_group.id) end end + + context 'queries per rendered element' do + # The expected extra queries for the rendered group are: + # 1. Count of memberships of the group + # 2. Count of visible projects in the element + # 3. Count of visible subgroups in the element + let(:expected_queries_per_group) { 3 } + let(:expected_queries_per_project) { 0 } + + def get_list + get :children, id: group.to_param, format: :json + end + + it 'queries the expected amount for a group row' do + control_count = ActiveRecord::QueryRecorder.new { get_list }.count + + _new_group = create(:group, :public, parent: group) + + expect { get_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + end + + it 'queries the expected amount for a project row' do + control_count = ActiveRecord::QueryRecorder.new { get_list }.count + + _new_project = create(:project, :public, namespace: group) + + expect { get_list }.not_to exceed_query_limit(control_count + expected_queries_per_project) + end + + context 'when rendering hierarchies' do + def get_filtered_list + get :children, id: group.to_param, filter: 'filter', format: :json + end + + it 'queries the expected amount when nested rows are rendered for a group' do + matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') + + control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + + nested_group = create(:group, :public, parent: public_subgroup) + matched_group.update!(parent: nested_group) + + expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + end + + it 'queries the expected amount when a new group match is added' do + create(:group, :public, parent: public_subgroup, name: 'filterme') + + control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + + create(:group, :public, parent: public_subgroup, name: 'filterme2') + + expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + end + + it 'queries the expected amount when nested rows are rendered for a project' do + matched_project = create(:project, :public, namespace: public_subgroup, name: 'filterme') + + control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + + nested_group = create(:group, :public, parent: public_subgroup) + matched_project.update!(namespace: nested_group) + + expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + end + + it 'queries the expected amount when a new project match is added' do + create(:project, :public, namespace: public_subgroup, name: 'filterme') + + control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + + create(:project, :public, namespace: public_subgroup, name: 'filterme2') + + expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_project) + end + end + end end end -- cgit v1.2.1 From 6388b8feec28b0f0ca2f9e6d9c5c7b4e404fed2f Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 10 Sep 2017 18:55:52 +0200 Subject: Don't include the parent in search results if it matches --- spec/finders/group_children_finder_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'spec') diff --git a/spec/finders/group_children_finder_spec.rb b/spec/finders/group_children_finder_spec.rb index 8f83f88bb97..3df153abc6a 100644 --- a/spec/finders/group_children_finder_spec.rb +++ b/spec/finders/group_children_finder_spec.rb @@ -60,6 +60,12 @@ describe GroupChildrenFinder do expect(finder.execute).to contain_exactly(matching_project) end + + it 'does not include the parent itself' do + group.update!(name: 'test') + + expect(finder.execute).not_to include(group) + end end end end -- cgit v1.2.1 From 5998157618eafb69f6980f41639a7b485fe2467f Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 12 Sep 2017 10:51:34 +0200 Subject: Include `can_leave` for a group --- spec/serializers/group_child_entity_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'spec') diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 3a7d2205f53..9ddaf8f6469 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -83,6 +83,12 @@ describe GroupChildEntity do end end + it 'allows an owner to leave when there is another one' do + object.add_owner(create(:user)) + + expect(json[:can_leave]).to be_truthy + end + it_behaves_like 'group child json' end end -- cgit v1.2.1 From 3e6dd7d88daaac2dbafc234753942086e0ba0403 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 12 Sep 2017 14:58:44 +0200 Subject: Use same response-body in groups-dashboard as we do for group-home --- .../dashboard/groups_controller_spec.rb | 29 ++++++++++++++++++++++ spec/serializers/group_child_serializer_spec.rb | 16 ++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 spec/controllers/dashboard/groups_controller_spec.rb (limited to 'spec') diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb new file mode 100644 index 00000000000..89a16c233d8 --- /dev/null +++ b/spec/controllers/dashboard/groups_controller_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Dashboard::GroupsController do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + + before do + group.add_owner(user) + sign_in(user) + end + + describe 'GET #index' do + it 'shows child groups as json' do + get :index, format: :json + + expect(json_response.first['id']).to eq(group.id) + end + + it 'filters groups' do + other_group = create(:group, name: 'filter') + other_group.add_owner(user) + + get :index, filter: 'filt', format: :json + all_ids = json_response.map { |group_json| group_json['id'] } + + expect(all_ids).to contain_exactly(other_group.id) + end + end +end diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb index 7a87c1adc8f..e5896f54dd7 100644 --- a/spec/serializers/group_child_serializer_spec.rb +++ b/spec/serializers/group_child_serializer_spec.rb @@ -49,6 +49,22 @@ describe GroupChildSerializer do expect(subgroup1_json[:id]).to eq(subgroup1.id) expect(subsub_group1_json[:id]).to eq(subsub_group1.id) end + + context 'without a specified parent' do + subject(:serializer) do + described_class.new(current_user: user).expand_hierarchy + end + + it 'can render a tree' do + subgroup = create(:group, parent: parent) + + json = serializer.represent([subgroup]) + parent_json = json.first + + expect(parent_json[:id]).to eq(parent.id) + expect(parent_json[:children].first[:id]).to eq(subgroup.id) + end + end end context 'for projects' do -- cgit v1.2.1 From 3299a970e09ceca7ecabb3d78a5693f58ef79d79 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 12 Sep 2017 19:03:12 +0200 Subject: Handle all cases for merging a hierarchy The possible cases are: - [Array, Array] - [Array, Hash] - [Array, GroupHierarchy] - [Hash,Hash] - [Hash, GroupHierarchy] - [GroupHierarchy, GroupHierarchy] --- spec/models/concerns/group_hierarchy_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'spec') diff --git a/spec/models/concerns/group_hierarchy_spec.rb b/spec/models/concerns/group_hierarchy_spec.rb index bb02983c776..fe30895f15e 100644 --- a/spec/models/concerns/group_hierarchy_spec.rb +++ b/spec/models/concerns/group_hierarchy_spec.rb @@ -62,6 +62,17 @@ describe GroupHierarchy, :nested_groups do expect(described_class.merge_hierarchies(groups, parent)).to eq(expected_hierarchy) end + + it 'handles building a tree out of order' do + other_subgroup = create(:group, parent: parent) + other_subgroup2 = create(:group, parent: parent) + other_subsub_group = create(:group, parent: other_subgroup) + + groups = [subsub_group, other_subgroup2, other_subsub_group] + expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup2, { other_subgroup => other_subsub_group }] } + + expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) + end end end -- cgit v1.2.1 From 4c8942f9b8cffe6bf513c4766e8f4260c0550b61 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 13 Sep 2017 11:11:09 +0200 Subject: Replace `full_path`, `path` & `web_url` with a single `relative_path` --- spec/serializers/group_child_entity_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'spec') diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 9ddaf8f6469..54a1b1846e1 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -16,19 +16,17 @@ describe GroupChildEntity do end %w[id - path full_name - full_path avatar_url name description - web_url visibility type can_edit visibility edit_path - permission].each do |attribute| + permission + relative_path].each do |attribute| it "includes #{attribute}" do expect(json[attribute.to_sym]).to be_present end -- cgit v1.2.1 From 3a4dc55f2924debcdbb37eb63d8ce57b1358df81 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 13 Sep 2017 17:16:30 +0200 Subject: Reuse the groups tree for explore and dashboard. --- spec/controllers/concerns/group_tree_spec.rb | 56 +++++++++ .../dashboard/groups_controller_spec.rb | 24 ++-- spec/controllers/explore/groups_controller_spec.rb | 24 ++++ spec/controllers/groups_controller_spec.rb | 139 ++++++++++++--------- spec/features/dashboard/groups_list_spec.rb | 4 + spec/features/explore/groups_list_spec.rb | 2 + 6 files changed, 174 insertions(+), 75 deletions(-) create mode 100644 spec/controllers/concerns/group_tree_spec.rb create mode 100644 spec/controllers/explore/groups_controller_spec.rb (limited to 'spec') diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb new file mode 100644 index 00000000000..19387f2d271 --- /dev/null +++ b/spec/controllers/concerns/group_tree_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe GroupTree do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + + controller(ApplicationController) do + include GroupTree # rubocop:disable Rspec/DescribedClass + + def index + render_group_tree Group.all + end + end + + before do + group.add_owner(user) + sign_in(user) + end + + describe 'GET #index' do + it 'filters groups' do + other_group = create(:group, name: 'filter') + other_group.add_owner(user) + + get :index, filter: 'filt', format: :json + + expect(assigns(:groups)).to contain_exactly(other_group) + end + + context 'for subgroups', :nested_groups do + it 'only renders root groups when no parent was given' do + create(:group, :public, parent: group) + + get :index, format: :json + + expect(assigns(:groups)).to contain_exactly(group) + end + + it 'contains only the subgroup when a parent was given' do + subgroup = create(:group, :public, parent: group) + + get :index, parent_id: group.id, format: :json + + expect(assigns(:groups)).to contain_exactly(subgroup) + end + end + + context 'json content' do + it 'shows groups as json' do + get :index, format: :json + + expect(json_response.first['id']).to eq(group.id) + end + end + end +end diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb index 89a16c233d8..fb9d3efbac0 100644 --- a/spec/controllers/dashboard/groups_controller_spec.rb +++ b/spec/controllers/dashboard/groups_controller_spec.rb @@ -1,29 +1,23 @@ require 'spec_helper' describe Dashboard::GroupsController do - let(:group) { create(:group, :public) } let(:user) { create(:user) } before do - group.add_owner(user) sign_in(user) end - describe 'GET #index' do - it 'shows child groups as json' do - get :index, format: :json - - expect(json_response.first['id']).to eq(group.id) - end + it 'renders group trees' do + expect(described_class).to include(GroupTree) + end - it 'filters groups' do - other_group = create(:group, name: 'filter') - other_group.add_owner(user) + it 'only includes projects the user is a member of' do + member_of_group = create(:group) + member_of_group.add_developer(user) + create(:group, :public) - get :index, filter: 'filt', format: :json - all_ids = json_response.map { |group_json| group_json['id'] } + get :index - expect(all_ids).to contain_exactly(other_group.id) - end + expect(assigns(:groups)).to contain_exactly(member_of_group) end end diff --git a/spec/controllers/explore/groups_controller_spec.rb b/spec/controllers/explore/groups_controller_spec.rb new file mode 100644 index 00000000000..1923d054e95 --- /dev/null +++ b/spec/controllers/explore/groups_controller_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Explore::GroupsController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'renders group trees' do + expect(described_class).to include(GroupTree) + end + + it 'includes public projects' do + member_of_group = create(:group) + member_of_group.add_developer(user) + public_group = create(:group, :public) + + get :index + + expect(assigns(:groups)).to contain_exactly(member_of_group, public_group) + end + +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 25778cc2b59..18f9d707e18 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' describe GroupsController do let(:user) { create(:user) } @@ -150,6 +150,45 @@ describe GroupsController do end end + describe 'GET #show' do + context 'pagination' do + context 'with only projects' do + let!(:other_project) { create(:project, :public, namespace: group) } + let!(:first_page_projects) { create_list(:project, Kaminari.config.default_per_page, :public, namespace: group ) } + + it 'has projects on the first page' do + get :show, id: group.to_param, sort: 'id_desc' + + expect(assigns(:children)).to contain_exactly(*first_page_projects) + end + + it 'has projects on the second page' do + get :show, id: group.to_param, sort: 'id_desc', page: 2 + + expect(assigns(:children)).to contain_exactly(other_project) + end + end + + context 'with subgroups and projects', :nested_groups do + let!(:other_subgroup) { create(:group, :public, parent: group) } + let!(:project) { create(:project, :public, namespace: group) } + let!(:first_page_subgroups) { create_list(:group, Kaminari.config.default_per_page, parent: group) } + + it 'contains all subgroups' do + get :children, id: group.to_param, sort: 'id_desc', format: :json + + expect(assigns(:children)).to contain_exactly(*first_page_subgroups) + end + + it 'contains the project and group on the second page' do + get :children, id: group.to_param, sort: 'id_desc', page: 2, format: :json + + expect(assigns(:children)).to contain_exactly(other_subgroup, project) + end + end + end + end + describe 'GET #children' do context 'for projects' do let!(:public_project) { create(:project, :public, namespace: group) } @@ -251,12 +290,14 @@ describe GroupsController do end end - context 'queries per rendered element' do + context 'queries per rendered element', :request_store do # The expected extra queries for the rendered group are: # 1. Count of memberships of the group # 2. Count of visible projects in the element # 3. Count of visible subgroups in the element - let(:expected_queries_per_group) { 3 } + # 4. Every parent + # 5. The route for a parent + let(:expected_queries_per_group) { 5 } let(:expected_queries_per_project) { 0 } def get_list @@ -265,7 +306,6 @@ describe GroupsController do it 'queries the expected amount for a group row' do control_count = ActiveRecord::QueryRecorder.new { get_list }.count - _new_group = create(:group, :public, parent: group) expect { get_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) @@ -273,7 +313,6 @@ describe GroupsController do it 'queries the expected amount for a project row' do control_count = ActiveRecord::QueryRecorder.new { get_list }.count - _new_project = create(:project, :public, namespace: group) expect { get_list }.not_to exceed_query_limit(control_count + expected_queries_per_project) @@ -288,7 +327,6 @@ describe GroupsController do matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count - nested_group = create(:group, :public, parent: public_subgroup) matched_group.update!(parent: nested_group) @@ -299,7 +337,6 @@ describe GroupsController do create(:group, :public, parent: public_subgroup, name: 'filterme') control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count - create(:group, :public, parent: public_subgroup, name: 'filterme2') expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) @@ -570,79 +607,61 @@ describe GroupsController do end end - context 'pagination' do - let!(:other_subgroup) { create(:group, :public, parent: group) } - let!(:project) { create(:project, :public, namespace: group) } - let!(:first_page_subgroups) { create_list(:group, Kaminari.config.default_per_page, parent: group) } - - it 'contains all subgroups' do - get :children, id: group.to_param, sort: 'id', format: :json - - expect(assigns(:children)).to contain_exactly(*first_page_subgroups) - end - - it 'contains the project and group on the second page' do - get :children, id: group.to_param, sort: 'id', page: 2, format: :json - - expect(assigns(:children)).to contain_exactly(other_subgroup, project) - end - end - end - - context 'for a POST request' do - context 'when requesting the canonical path with different casing' do - it 'does not 404' do - post :update, id: group.to_param.upcase, group: { path: 'new_path' } + context 'for a POST request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + post :update, id: group.to_param.upcase, group: { path: 'new_path' } - expect(response).not_to have_http_status(404) - end + expect(response).not_to have_http_status(404) + end - it 'does not redirect to the correct casing' do - post :update, id: group.to_param.upcase, group: { path: 'new_path' } + it 'does not redirect to the correct casing' do + post :update, id: group.to_param.upcase, group: { path: 'new_path' } - expect(response).not_to have_http_status(301) + expect(response).not_to have_http_status(301) + end end - end - context 'when requesting a redirected path' do - let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } - it 'returns not found' do - post :update, id: redirect_route.path, group: { path: 'new_path' } + it 'returns not found' do + post :update, id: redirect_route.path, group: { path: 'new_path' } - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end end - end - context 'for a DELETE request' do - context 'when requesting the canonical path with different casing' do - it 'does not 404' do - delete :destroy, id: group.to_param.upcase + context 'for a DELETE request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + delete :destroy, id: group.to_param.upcase - expect(response).not_to have_http_status(404) - end + expect(response).not_to have_http_status(404) + end - it 'does not redirect to the correct casing' do - delete :destroy, id: group.to_param.upcase + it 'does not redirect to the correct casing' do + delete :destroy, id: group.to_param.upcase - expect(response).not_to have_http_status(301) + expect(response).not_to have_http_status(301) + end end - end - context 'when requesting a redirected path' do - let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } - it 'returns not found' do - delete :destroy, id: redirect_route.path + it 'returns not found' do + delete :destroy, id: redirect_route.path - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end end end - end - def group_moved_message(redirect_route, group) - "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path." + def group_moved_message(redirect_route, group) + "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path." + end end end diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index 533df7a325c..227550e62be 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -6,6 +6,10 @@ feature 'Dashboard Groups page', :js do let!(:nested_group) { create(:group, :nested) } let!(:another_group) { create(:group) } + before do + pending('Update for new group tree') + end + it 'shows groups user is member of' do group.add_owner(user) nested_group.add_owner(user) diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index b5325301968..fa3d8f97a09 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -8,6 +8,8 @@ describe 'Explore Groups page', :js do let!(:empty_project) { create(:project, group: public_group) } before do + pending('Update for new group tree') + group.add_owner(user) sign_in(user) -- cgit v1.2.1 From ea4e17e2aec525f249430b0a22dc6a8450648837 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 14 Sep 2017 09:01:29 +0200 Subject: Search subgroups on dashboard and explore views --- spec/controllers/concerns/group_tree_spec.rb | 24 +++++++++++++++++++++- spec/controllers/explore/groups_controller_spec.rb | 1 - 2 files changed, 23 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb index 19387f2d271..5d4fb66b492 100644 --- a/spec/controllers/concerns/group_tree_spec.rb +++ b/spec/controllers/concerns/group_tree_spec.rb @@ -5,7 +5,8 @@ describe GroupTree do let(:user) { create(:user) } controller(ApplicationController) do - include GroupTree # rubocop:disable Rspec/DescribedClass + # `described_class` is not available in this context + include GroupTree # rubocop:disable RSpec/DescribedClass def index render_group_tree Group.all @@ -43,6 +44,14 @@ describe GroupTree do expect(assigns(:groups)).to contain_exactly(subgroup) end + + it 'allows filtering for subgroups' do + subgroup = create(:group, :public, parent: group, name: 'filter') + + get :index, filter: 'filt', format: :json + + expect(assigns(:groups)).to contain_exactly(subgroup) + end end context 'json content' do @@ -51,6 +60,19 @@ describe GroupTree do expect(json_response.first['id']).to eq(group.id) end + + context 'nested groups', :nested_groups do + it 'expands the tree when filtering' do + subgroup = create(:group, :public, parent: group, name: 'filter') + + get :index, filter: 'filt', format: :json + + children_response = json_response.first['children'] + + expect(json_response.first['id']).to eq(group.id) + expect(children_response.first['id']).to eq(subgroup.id) + end + end end end end diff --git a/spec/controllers/explore/groups_controller_spec.rb b/spec/controllers/explore/groups_controller_spec.rb index 1923d054e95..9e0ad9ea86f 100644 --- a/spec/controllers/explore/groups_controller_spec.rb +++ b/spec/controllers/explore/groups_controller_spec.rb @@ -20,5 +20,4 @@ describe Explore::GroupsController do expect(assigns(:groups)).to contain_exactly(member_of_group, public_group) end - end -- cgit v1.2.1 From 20a08965bc949ea233cdde4e777698222fcabff2 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 14 Sep 2017 11:53:55 +0200 Subject: [WIP] improve number of queries when rendering a hierarchy --- spec/controllers/groups_controller_spec.rb | 31 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 18f9d707e18..18791a01035 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -300,22 +300,28 @@ describe GroupsController do let(:expected_queries_per_group) { 5 } let(:expected_queries_per_project) { 0 } + before do + # Create the group before anything so it doesn't get tracked by the + # query recorder + group + end + def get_list get :children, id: group.to_param, format: :json end it 'queries the expected amount for a group row' do - control_count = ActiveRecord::QueryRecorder.new { get_list }.count + control = ActiveRecord::QueryRecorder.new { get_list } _new_group = create(:group, :public, parent: group) - expect { get_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) end it 'queries the expected amount for a project row' do - control_count = ActiveRecord::QueryRecorder.new { get_list }.count + control = ActiveRecord::QueryRecorder.new { get_list } _new_project = create(:project, :public, namespace: group) - expect { get_list }.not_to exceed_query_limit(control_count + expected_queries_per_project) + expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project) end context 'when rendering hierarchies' do @@ -326,41 +332,42 @@ describe GroupsController do it 'queries the expected amount when nested rows are rendered for a group' do matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') - control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + control = ActiveRecord::QueryRecorder.new { get_filtered_list } nested_group = create(:group, :public, parent: public_subgroup) matched_group.update!(parent: nested_group) - expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) end it 'queries the expected amount when a new group match is added' do create(:group, :public, parent: public_subgroup, name: 'filterme') - control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + control = ActiveRecord::QueryRecorder.new { get_filtered_list } + create(:group, :public, parent: public_subgroup, name: 'filterme2') - expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) end it 'queries the expected amount when nested rows are rendered for a project' do matched_project = create(:project, :public, namespace: public_subgroup, name: 'filterme') - control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + control = ActiveRecord::QueryRecorder.new { get_filtered_list } nested_group = create(:group, :public, parent: public_subgroup) matched_project.update!(namespace: nested_group) - expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_group) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) end it 'queries the expected amount when a new project match is added' do create(:project, :public, namespace: public_subgroup, name: 'filterme') - control_count = ActiveRecord::QueryRecorder.new { get_filtered_list }.count + control = ActiveRecord::QueryRecorder.new { get_filtered_list } create(:project, :public, namespace: public_subgroup, name: 'filterme2') - expect { get_filtered_list }.not_to exceed_query_limit(control_count + expected_queries_per_project) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project) end end end -- cgit v1.2.1 From 9781ac552d4ae41983b2d95768e0fb06817e0ef9 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 15 Sep 2017 12:28:21 +0200 Subject: Include pagination when rendering expanded hierarchies --- spec/controllers/groups_controller_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 18791a01035..21d5433a970 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -152,6 +152,10 @@ describe GroupsController do describe 'GET #show' do context 'pagination' do + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(2) + end + context 'with only projects' do let!(:other_project) { create(:project, :public, namespace: group) } let!(:first_page_projects) { create_list(:project, Kaminari.config.default_per_page, :public, namespace: group ) } @@ -288,6 +292,14 @@ describe GroupsController do expect(group_json['id']).to eq(public_subgroup.id) expect(matched_group_json['id']).to eq(matched_group.id) end + + it 'includes pagination headers' do + 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } + + get :children, id: group.to_param, filter: 'filter', per_page: 1, format: :json + + expect(response).to include_pagination_headers + end end context 'queries per rendered element', :request_store do -- cgit v1.2.1 From 31f775689396722e38de20157b46a75b1fe40582 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 19 Sep 2017 11:15:57 +0200 Subject: `current_user:` as a keyword argument --- spec/finders/group_children_finder_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/finders/group_children_finder_spec.rb b/spec/finders/group_children_finder_spec.rb index 3df153abc6a..8257e158b06 100644 --- a/spec/finders/group_children_finder_spec.rb +++ b/spec/finders/group_children_finder_spec.rb @@ -4,7 +4,9 @@ describe GroupChildrenFinder do let(:user) { create(:user) } let(:group) { create(:group) } let(:params) { {} } - subject(:finder) { described_class.new(user, parent_group: group, params: params) } + subject(:finder) do + described_class.new(current_user: user, parent_group: group, params: params) + end before do group.add_owner(user) -- cgit v1.2.1 From 22aa034427b9392b44d9ecba0a51bb1b6c6616d7 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 19 Sep 2017 13:11:09 +0200 Subject: Rename `GroupHierarchy` to `GroupDescendant` --- spec/finders/group_children_finder_spec.rb | 93 -------------- spec/finders/group_descendants_finder_spec.rb | 93 ++++++++++++++ spec/models/concerns/group_descendant_spec.rb | 173 ++++++++++++++++++++++++++ spec/models/concerns/group_hierarchy_spec.rb | 146 ---------------------- 4 files changed, 266 insertions(+), 239 deletions(-) delete mode 100644 spec/finders/group_children_finder_spec.rb create mode 100644 spec/finders/group_descendants_finder_spec.rb create mode 100644 spec/models/concerns/group_descendant_spec.rb delete mode 100644 spec/models/concerns/group_hierarchy_spec.rb (limited to 'spec') diff --git a/spec/finders/group_children_finder_spec.rb b/spec/finders/group_children_finder_spec.rb deleted file mode 100644 index 8257e158b06..00000000000 --- a/spec/finders/group_children_finder_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'spec_helper' - -describe GroupChildrenFinder do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:params) { {} } - subject(:finder) do - described_class.new(current_user: user, parent_group: group, params: params) - end - - before do - group.add_owner(user) - end - - describe '#execute' do - it 'includes projects' do - project = create(:project, namespace: group) - - expect(finder.execute).to contain_exactly(project) - end - - context 'with a filter' do - let(:params) { { filter: 'test' } } - - it 'includes only projects matching the filter' do - _other_project = create(:project, namespace: group) - matching_project = create(:project, namespace: group, name: 'testproject') - - expect(finder.execute).to contain_exactly(matching_project) - end - end - end - - context 'with nested groups', :nested_groups do - let!(:project) { create(:project, namespace: group) } - let!(:subgroup) { create(:group, parent: group) } - - describe '#execute' do - it 'contains projects and subgroups' do - expect(finder.execute).to contain_exactly(subgroup, project) - end - - context 'with a filter' do - let(:params) { { filter: 'test' } } - - it 'contains only matching projects and subgroups' do - matching_project = create(:project, namespace: group, name: 'Testproject') - matching_subgroup = create(:group, name: 'testgroup', parent: group) - - expect(finder.execute).to contain_exactly(matching_subgroup, matching_project) - end - - context 'with matching children' do - it 'includes a group that has a subgroup matching the query' do - matching_subgroup = create(:group, name: 'testgroup', parent: subgroup) - - expect(finder.execute).to contain_exactly(matching_subgroup) - end - - it 'includes a group that has a project matching the query' do - matching_project = create(:project, namespace: subgroup, name: 'Testproject') - - expect(finder.execute).to contain_exactly(matching_project) - end - - it 'does not include the parent itself' do - group.update!(name: 'test') - - expect(finder.execute).not_to include(group) - end - end - end - end - - describe '#total_count' do - it 'counts the array children were already loaded' do - finder.instance_variable_set(:@children, [build(:project)]) - - expect(finder).not_to receive(:subgroups) - expect(finder).not_to receive(:projects) - - expect(finder.total_count).to eq(1) - end - - it 'performs a count without loading children when they are not loaded yet' do - expect(finder).to receive(:subgroups).and_call_original - expect(finder).to receive(:projects).and_call_original - - expect(finder.total_count).to eq(2) - end - end - end -end diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb new file mode 100644 index 00000000000..c1268a486cf --- /dev/null +++ b/spec/finders/group_descendants_finder_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe GroupDescendantsFinder do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:params) { {} } + subject(:finder) do + described_class.new(current_user: user, parent_group: group, params: params) + end + + before do + group.add_owner(user) + end + + describe '#execute' do + it 'includes projects' do + project = create(:project, namespace: group) + + expect(finder.execute).to contain_exactly(project) + end + + context 'with a filter' do + let(:params) { { filter: 'test' } } + + it 'includes only projects matching the filter' do + _other_project = create(:project, namespace: group) + matching_project = create(:project, namespace: group, name: 'testproject') + + expect(finder.execute).to contain_exactly(matching_project) + end + end + end + + context 'with nested groups', :nested_groups do + let!(:project) { create(:project, namespace: group) } + let!(:subgroup) { create(:group, parent: group) } + + describe '#execute' do + it 'contains projects and subgroups' do + expect(finder.execute).to contain_exactly(subgroup, project) + end + + context 'with a filter' do + let(:params) { { filter: 'test' } } + + it 'contains only matching projects and subgroups' do + matching_project = create(:project, namespace: group, name: 'Testproject') + matching_subgroup = create(:group, name: 'testgroup', parent: group) + + expect(finder.execute).to contain_exactly(matching_subgroup, matching_project) + end + + context 'with matching children' do + it 'includes a group that has a subgroup matching the query' do + matching_subgroup = create(:group, name: 'testgroup', parent: subgroup) + + expect(finder.execute).to contain_exactly(matching_subgroup) + end + + it 'includes a group that has a project matching the query' do + matching_project = create(:project, namespace: subgroup, name: 'Testproject') + + expect(finder.execute).to contain_exactly(matching_project) + end + + it 'does not include the parent itself' do + group.update!(name: 'test') + + expect(finder.execute).not_to include(group) + end + end + end + end + + describe '#total_count' do + it 'counts the array children were already loaded' do + finder.instance_variable_set(:@children, [build(:project)]) + + expect(finder).not_to receive(:subgroups) + expect(finder).not_to receive(:projects) + + expect(finder.total_count).to eq(1) + end + + it 'performs a count without loading children when they are not loaded yet' do + expect(finder).to receive(:subgroups).and_call_original + expect(finder).to receive(:projects).and_call_original + + expect(finder.total_count).to eq(2) + end + end + end +end diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb new file mode 100644 index 00000000000..87eee515cde --- /dev/null +++ b/spec/models/concerns/group_descendant_spec.rb @@ -0,0 +1,173 @@ +require 'spec_helper' + +describe GroupDescendant, :nested_groups do + let(:parent) { create(:group) } + let(:subgroup) { create(:group, parent: parent) } + let(:subsub_group) { create(:group, parent: subgroup) } + + context 'for a group' do + describe '#hierarchy' do + it 'builds a hierarchy for a group' do + expected_hierarchy = { parent => { subgroup => subsub_group } } + + expect(subsub_group.hierarchy).to eq(expected_hierarchy) + end + + it 'builds a hierarchy upto a specified parent' do + expected_hierarchy = { subgroup => subsub_group } + + expect(subsub_group.hierarchy(parent)).to eq(expected_hierarchy) + end + + it 'raises an error if specifying a base that is not part of the tree' do + expect { subsub_group.hierarchy(double) }.to raise_error('specified base is not part of the tree') + end + end + + describe '#parent' do + it 'returns the correct parent' do + expect(subsub_group.parent).to eq(subgroup) + end + end + + describe '#merge_hierarchy' do + it 'combines hierarchies' do + other_subgroup = create(:group, parent: parent) + + expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup] } + + expect(subsub_group.merge_hierarchy(other_subgroup)).to eq(expected_hierarchy) + end + end + + describe '.merge_hierarchies' do + it 'combines hierarchies until the top' do + other_subgroup = create(:group, parent: parent) + other_subsub_group = create(:group, parent: subgroup) + + groups = [other_subgroup, subsub_group, other_subsub_group] + + expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] } + + expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) + end + + it 'combines upto a given parent' do + other_subgroup = create(:group, parent: parent) + other_subsub_group = create(:group, parent: subgroup) + + groups = [other_subgroup, subsub_group, other_subsub_group] + + expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] + + expect(described_class.merge_hierarchies(groups, parent)).to eq(expected_hierarchy) + end + + it 'handles building a tree out of order' do + other_subgroup = create(:group, parent: parent) + other_subgroup2 = create(:group, parent: parent) + other_subsub_group = create(:group, parent: other_subgroup) + + groups = [subsub_group, other_subgroup2, other_subsub_group] + expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup2, { other_subgroup => other_subsub_group }] } + + expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) + end + end + end + + context 'for a project' do + let(:project) { create(:project, namespace: subsub_group) } + + describe '#hierarchy' do + it 'builds a hierarchy for a group' do + expected_hierarchy = { parent => { subgroup => { subsub_group => project } } } + + expect(project.hierarchy).to eq(expected_hierarchy) + end + + it 'builds a hierarchy upto a specified parent' do + expected_hierarchy = { subsub_group => project } + + expect(project.hierarchy(subgroup)).to eq(expected_hierarchy) + end + + it 'raises an error if specifying a base that is not part of the tree' do + expect { project.hierarchy(double) }.to raise_error('specified base is not part of the tree') + end + end + + describe '#parent' do + it 'returns the correct parent' do + expect(project.parent).to eq(subsub_group) + end + end + + describe '#merge_hierarchy' do + it 'combines hierarchies' do + project = create(:project, namespace: parent) + + expected_hierarchy = { parent => [{ subgroup => subsub_group }, project] } + + expect(subsub_group.merge_hierarchy(project)).to eq(expected_hierarchy) + end + end + + describe '.merge_hierarchies' do + it 'combines hierarchies until the top' do + other_project = create(:project, namespace: parent) + other_subgroup_project = create(:project, namespace: subgroup) + + elements = [other_project, subsub_group, other_subgroup_project] + + expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] } + + expect(described_class.merge_hierarchies(elements)).to eq(expected_hierarchy) + end + + it 'combines upto a given parent' do + other_project = create(:project, namespace: parent) + other_subgroup_project = create(:project, namespace: subgroup) + + elements = [other_project, subsub_group, other_subgroup_project] + + expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }] + + expect(described_class.merge_hierarchies(elements, parent)).to eq(expected_hierarchy) + end + + it 'merges to elements in the same hierarchy' do + expected_hierarchy = { parent => subgroup } + + expect(described_class.merge_hierarchies([parent, subgroup])).to eq(expected_hierarchy) + end + + it 'merges complex hierarchies' do + project = create(:project, namespace: parent) + sub_project = create(:project, namespace: subgroup) + subsubsub_group = create(:group, parent: subsub_group) + subsub_project = create(:project, namespace: subsub_group) + subsubsub_project = create(:project, namespace: subsubsub_group) + other_subgroup = create(:group, parent: parent) + other_subproject = create(:project, namespace: other_subgroup) + + projects = [project, subsubsub_project, sub_project, other_subproject, subsub_project] + + expected_hierarchy = [ + project, + { + subgroup => [ + { subsub_group => [{ subsubsub_group => subsubsub_project }, subsub_project] }, + sub_project + ] + }, + { other_subgroup => other_subproject } + ] + + actual_hierarchy = described_class.merge_hierarchies(projects, parent) + + expect(actual_hierarchy).to eq(expected_hierarchy) + end + end + end +end diff --git a/spec/models/concerns/group_hierarchy_spec.rb b/spec/models/concerns/group_hierarchy_spec.rb deleted file mode 100644 index fe30895f15e..00000000000 --- a/spec/models/concerns/group_hierarchy_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'spec_helper' - -describe GroupHierarchy, :nested_groups do - let(:parent) { create(:group) } - let(:subgroup) { create(:group, parent: parent) } - let(:subsub_group) { create(:group, parent: subgroup) } - - context 'for a group' do - describe '#hierarchy' do - it 'builds a hierarchy for a group' do - expected_hierarchy = { parent => { subgroup => subsub_group } } - - expect(subsub_group.hierarchy).to eq(expected_hierarchy) - end - - it 'builds a hierarchy upto a specified parent' do - expected_hierarchy = { subgroup => subsub_group } - - expect(subsub_group.hierarchy(parent)).to eq(expected_hierarchy) - end - - it 'raises an error if specifying a base that is not part of the tree' do - expect { subsub_group.hierarchy(double) }.to raise_error('specified base is not part of the tree') - end - end - - describe '#parent' do - it 'returns the correct parent' do - expect(subsub_group.parent).to eq(subgroup) - end - end - - describe '#merge_hierarchy' do - it 'combines hierarchies' do - other_subgroup = create(:group, parent: parent) - - expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup] } - - expect(subsub_group.merge_hierarchy(other_subgroup)).to eq(expected_hierarchy) - end - end - - describe '.merge_hierarchies' do - it 'combines hierarchies until the top' do - other_subgroup = create(:group, parent: parent) - other_subsub_group = create(:group, parent: subgroup) - - groups = [other_subgroup, subsub_group, other_subsub_group] - - expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] } - - expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) - end - - it 'combines upto a given parent' do - other_subgroup = create(:group, parent: parent) - other_subsub_group = create(:group, parent: subgroup) - - groups = [other_subgroup, subsub_group, other_subsub_group] - - expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] - - expect(described_class.merge_hierarchies(groups, parent)).to eq(expected_hierarchy) - end - - it 'handles building a tree out of order' do - other_subgroup = create(:group, parent: parent) - other_subgroup2 = create(:group, parent: parent) - other_subsub_group = create(:group, parent: other_subgroup) - - groups = [subsub_group, other_subgroup2, other_subsub_group] - expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup2, { other_subgroup => other_subsub_group }] } - - expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) - end - end - end - - context 'for a project' do - let(:project) { create(:project, namespace: subsub_group) } - - describe '#hierarchy' do - it 'builds a hierarchy for a group' do - expected_hierarchy = { parent => { subgroup => { subsub_group => project } } } - - expect(project.hierarchy).to eq(expected_hierarchy) - end - - it 'builds a hierarchy upto a specified parent' do - expected_hierarchy = { subsub_group => project } - - expect(project.hierarchy(subgroup)).to eq(expected_hierarchy) - end - - it 'raises an error if specifying a base that is not part of the tree' do - expect { project.hierarchy(double) }.to raise_error('specified base is not part of the tree') - end - end - - describe '#parent' do - it 'returns the correct parent' do - expect(project.parent).to eq(subsub_group) - end - end - - describe '#merge_hierarchy' do - it 'combines hierarchies' do - project = create(:project, namespace: parent) - - expected_hierarchy = { parent => [{ subgroup => subsub_group }, project] } - - expect(subsub_group.merge_hierarchy(project)).to eq(expected_hierarchy) - end - end - - describe '.merge_hierarchies' do - it 'combines hierarchies until the top' do - other_project = create(:project, namespace: parent) - other_subgroup_project = create(:project, namespace: subgroup) - - elements = [other_project, subsub_group, other_subgroup_project] - - expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] } - - expect(described_class.merge_hierarchies(elements)).to eq(expected_hierarchy) - end - - it 'combines upto a given parent' do - other_project = create(:project, namespace: parent) - other_subgroup_project = create(:project, namespace: subgroup) - - elements = [other_project, subsub_group, other_subgroup_project] - - expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }] - - expect(described_class.merge_hierarchies(elements, parent)).to eq(expected_hierarchy) - end - - it 'merges to elements in the same hierarchy' do - expected_hierarchy = { parent => subgroup } - - expect(described_class.merge_hierarchies([parent, subgroup])).to eq(expected_hierarchy) - end - end - end -end -- cgit v1.2.1 From 29df1ce84198801863fd1890b14099d13c6ec7fb Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 19 Sep 2017 17:24:57 +0200 Subject: Improve number of queries And document what extra queries are still being performed. --- spec/controllers/groups_controller_spec.rb | 50 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 21d5433a970..befd346596f 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -304,26 +304,18 @@ describe GroupsController do context 'queries per rendered element', :request_store do # The expected extra queries for the rendered group are: - # 1. Count of memberships of the group - # 2. Count of visible projects in the element - # 3. Count of visible subgroups in the element - # 4. Every parent - # 5. The route for a parent - let(:expected_queries_per_group) { 5 } + # 1. Count of visible projects in the element + # 2. Count of visible subgroups in the element + let(:expected_queries_per_group) { 2 } let(:expected_queries_per_project) { 0 } - before do - # Create the group before anything so it doesn't get tracked by the - # query recorder - group - end - def get_list get :children, id: group.to_param, format: :json end it 'queries the expected amount for a group row' do control = ActiveRecord::QueryRecorder.new { get_list } + _new_group = create(:group, :public, parent: group) expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) @@ -337,18 +329,26 @@ describe GroupsController do end context 'when rendering hierarchies' do + # Extra queries per group when rendering a hierarchy: + # The route and the namespace are `included` for all matched elements + # But the parent's above those are not, so there's 2 extra queries per + # nested level: + # 1. Loading the parent that wasn't loaded yet + # 2. Loading the route for that parent. + let(:extra_queries_per_nested_level) { expected_queries_per_group + 2 } + def get_filtered_list get :children, id: group.to_param, filter: 'filter', format: :json end - it 'queries the expected amount when nested rows are rendered for a group' do - matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') + it 'queries the expected amount when nested rows are increased for a group' do + matched_group = create(:group, :public, parent: group, name: 'filterme') control = ActiveRecord::QueryRecorder.new { get_filtered_list } - nested_group = create(:group, :public, parent: public_subgroup) - matched_group.update!(parent: nested_group) - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) + matched_group.update!(parent: public_subgroup) + + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) end it 'queries the expected amount when a new group match is added' do @@ -358,18 +358,17 @@ describe GroupsController do create(:group, :public, parent: public_subgroup, name: 'filterme2') - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) end - it 'queries the expected amount when nested rows are rendered for a project' do - matched_project = create(:project, :public, namespace: public_subgroup, name: 'filterme') + it 'queries the expected amount when nested rows are increased for a project' do + matched_project = create(:project, :public, namespace: group, name: 'filterme') control = ActiveRecord::QueryRecorder.new { get_filtered_list } - nested_group = create(:group, :public, parent: public_subgroup) - matched_project.update!(namespace: nested_group) + matched_project.update!(namespace: public_subgroup) - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) end it 'queries the expected amount when a new project match is added' do @@ -377,9 +376,10 @@ describe GroupsController do control = ActiveRecord::QueryRecorder.new { get_filtered_list } - create(:project, :public, namespace: public_subgroup, name: 'filterme2') + nested_group = create(:group, :public, parent: group) + create(:project, :public, namespace: nested_group, name: 'filterme2') - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) end end end -- cgit v1.2.1 From e13753fcaa4901a840f6b33bf9e1a06185c3ba10 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 21 Sep 2017 09:20:37 +0200 Subject: Only take unarchived projects into account When finding children for a group --- spec/finders/group_descendants_finder_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'spec') diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index c1268a486cf..77401ba09a2 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -19,6 +19,12 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(project) end + it 'does not include archived projects' do + _archived_project = create(:project, :archived, namespace: group) + + expect(finder.execute).to be_empty + end + context 'with a filter' do let(:params) { { filter: 'test' } } -- cgit v1.2.1 From cd85c22faa7092edabf252fa157125ea571ed054 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 22 Sep 2017 17:36:39 +0200 Subject: Rename hierarchies to descendants where applicable --- spec/models/concerns/group_descendant_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'spec') diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb index 87eee515cde..b1578fc593e 100644 --- a/spec/models/concerns/group_descendant_spec.rb +++ b/spec/models/concerns/group_descendant_spec.rb @@ -40,7 +40,7 @@ describe GroupDescendant, :nested_groups do end end - describe '.merge_hierarchies' do + describe '.build_hierarchy' do it 'combines hierarchies until the top' do other_subgroup = create(:group, parent: parent) other_subsub_group = create(:group, parent: subgroup) @@ -49,7 +49,7 @@ describe GroupDescendant, :nested_groups do expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] } - expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) + expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy) end it 'combines upto a given parent' do @@ -60,7 +60,7 @@ describe GroupDescendant, :nested_groups do expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] - expect(described_class.merge_hierarchies(groups, parent)).to eq(expected_hierarchy) + expect(described_class.build_hierarchy(groups, parent)).to eq(expected_hierarchy) end it 'handles building a tree out of order' do @@ -71,7 +71,7 @@ describe GroupDescendant, :nested_groups do groups = [subsub_group, other_subgroup2, other_subsub_group] expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup2, { other_subgroup => other_subsub_group }] } - expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy) + expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy) end end end @@ -113,7 +113,7 @@ describe GroupDescendant, :nested_groups do end end - describe '.merge_hierarchies' do + describe '.build_hierarchy' do it 'combines hierarchies until the top' do other_project = create(:project, namespace: parent) other_subgroup_project = create(:project, namespace: subgroup) @@ -122,7 +122,7 @@ describe GroupDescendant, :nested_groups do expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] } - expect(described_class.merge_hierarchies(elements)).to eq(expected_hierarchy) + expect(described_class.build_hierarchy(elements)).to eq(expected_hierarchy) end it 'combines upto a given parent' do @@ -133,13 +133,13 @@ describe GroupDescendant, :nested_groups do expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }] - expect(described_class.merge_hierarchies(elements, parent)).to eq(expected_hierarchy) + expect(described_class.build_hierarchy(elements, parent)).to eq(expected_hierarchy) end it 'merges to elements in the same hierarchy' do expected_hierarchy = { parent => subgroup } - expect(described_class.merge_hierarchies([parent, subgroup])).to eq(expected_hierarchy) + expect(described_class.build_hierarchy([parent, subgroup])).to eq(expected_hierarchy) end it 'merges complex hierarchies' do @@ -164,7 +164,7 @@ describe GroupDescendant, :nested_groups do { other_subgroup => other_subproject } ] - actual_hierarchy = described_class.merge_hierarchies(projects, parent) + actual_hierarchy = described_class.build_hierarchy(projects, parent) expect(actual_hierarchy).to eq(expected_hierarchy) end -- cgit v1.2.1 From ac0b104ae4968adaed7b94db76c0ac86badb6d6b Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 26 Sep 2017 11:22:52 +0200 Subject: Minimize the number of queries by preloading counts and ancestors By preloading the count of members, projects and subgroups of a group, we don't need to query them later. We also preload the entire hierarchy for a search result and include the counts so we don't need to query for them again --- spec/controllers/groups_controller_spec.rb | 34 ++++++++---------------- spec/finders/group_descendants_finder_spec.rb | 38 +++++++++++---------------- 2 files changed, 27 insertions(+), 45 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index befd346596f..84207144036 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -303,10 +303,12 @@ describe GroupsController do end context 'queries per rendered element', :request_store do - # The expected extra queries for the rendered group are: + # We need to make sure the following counts are preloaded + # otherwise they will cause an extra query # 1. Count of visible projects in the element # 2. Count of visible subgroups in the element - let(:expected_queries_per_group) { 2 } + # 3. Count of members of a group + let(:expected_queries_per_group) { 0 } let(:expected_queries_per_project) { 0 } def get_list @@ -329,13 +331,9 @@ describe GroupsController do end context 'when rendering hierarchies' do - # Extra queries per group when rendering a hierarchy: - # The route and the namespace are `included` for all matched elements - # But the parent's above those are not, so there's 2 extra queries per - # nested level: - # 1. Loading the parent that wasn't loaded yet - # 2. Loading the route for that parent. - let(:extra_queries_per_nested_level) { expected_queries_per_group + 2 } + # When loading hierarchies we load the all the ancestors for matched projects + # in 1 separate query + let(:extra_queries_for_hierarchies) { 1 } def get_filtered_list get :children, id: group.to_param, filter: 'filter', format: :json @@ -348,7 +346,7 @@ describe GroupsController do matched_group.update!(parent: public_subgroup) - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) end it 'queries the expected amount when a new group match is added' do @@ -357,8 +355,9 @@ describe GroupsController do control = ActiveRecord::QueryRecorder.new { get_filtered_list } create(:group, :public, parent: public_subgroup, name: 'filterme2') + create(:group, :public, parent: public_subgroup, name: 'filterme3') - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) end it 'queries the expected amount when nested rows are increased for a project' do @@ -368,18 +367,7 @@ describe GroupsController do matched_project.update!(namespace: public_subgroup) - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) - end - - it 'queries the expected amount when a new project match is added' do - create(:project, :public, namespace: public_subgroup, name: 'filterme') - - control = ActiveRecord::QueryRecorder.new { get_filtered_list } - - nested_group = create(:group, :public, parent: group) - create(:project, :public, namespace: nested_group, name: 'filterme2') - - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_per_nested_level) + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) end end end diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 77401ba09a2..09a773aaf68 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -46,6 +46,18 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(subgroup, project) end + it 'includes the preloaded counts for groups' do + create(:group, parent: subgroup) + create(:project, namespace: subgroup) + subgroup.add_developer(create(:user)) + + found_group = finder.execute.detect { |child| child.is_a?(Group) } + + expect(found_group.preloaded_project_count).to eq(1) + expect(found_group.preloaded_subgroup_count).to eq(1) + expect(found_group.preloaded_member_count).to eq(1) + end + context 'with a filter' do let(:params) { { filter: 'test' } } @@ -57,16 +69,16 @@ describe GroupDescendantsFinder do end context 'with matching children' do - it 'includes a group that has a subgroup matching the query' do + it 'includes a group that has a subgroup matching the query and its parent' do matching_subgroup = create(:group, name: 'testgroup', parent: subgroup) - expect(finder.execute).to contain_exactly(matching_subgroup) + expect(finder.execute).to contain_exactly(subgroup, matching_subgroup) end - it 'includes a group that has a project matching the query' do + it 'includes the parent of a matching project' do matching_project = create(:project, namespace: subgroup, name: 'Testproject') - expect(finder.execute).to contain_exactly(matching_project) + expect(finder.execute).to contain_exactly(subgroup, matching_project) end it 'does not include the parent itself' do @@ -77,23 +89,5 @@ describe GroupDescendantsFinder do end end end - - describe '#total_count' do - it 'counts the array children were already loaded' do - finder.instance_variable_set(:@children, [build(:project)]) - - expect(finder).not_to receive(:subgroups) - expect(finder).not_to receive(:projects) - - expect(finder.total_count).to eq(1) - end - - it 'performs a count without loading children when they are not loaded yet' do - expect(finder).to receive(:subgroups).and_call_original - expect(finder).to receive(:projects).and_call_original - - expect(finder.total_count).to eq(2) - end - end end end -- cgit v1.2.1 From b92e7103fcced2d62000ed382848219016484f7b Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 26 Sep 2017 14:12:12 +0200 Subject: Fix nesting bug when rendering children of a shared subgroup --- spec/controllers/groups_controller_spec.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 84207144036..ff76eaee25f 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -293,6 +293,32 @@ describe GroupsController do expect(matched_group_json['id']).to eq(matched_group.id) end + it 'merges the trees correctly' do + shared_subgroup = create(:group, :public, parent: group, path: 'hardware') + matched_project_1 = create(:project, :public, namespace: shared_subgroup, name: 'mobile-soc') + + l2_subgroup = create(:group, :public, parent: shared_subgroup, path: 'broadcom') + l3_subgroup = create(:group, :public, parent: l2_subgroup, path: 'wifi-group') + matched_project_2 = create(:project, :public, namespace: l3_subgroup, name: 'mobile') + + get :children, id: group.to_param, filter: 'mobile', format: :json + + shared_group_json = json_response.first + expect(shared_group_json['id']).to eq(shared_subgroup.id) + + matched_project_1_json = shared_group_json['children'].detect { |child| child['type'] == 'project' } + expect(matched_project_1_json['id']).to eq(matched_project_1.id) + + l2_subgroup_json = shared_group_json['children'].detect { |child| child['type'] == 'group' } + expect(l2_subgroup_json['id']).to eq(l2_subgroup.id) + + l3_subgroup_json = l2_subgroup_json['children'].first + expect(l3_subgroup_json['id']).to eq(l3_subgroup.id) + + matched_project_2_json = l3_subgroup_json['children'].first + expect(matched_project_2_json['id']).to eq(matched_project_2.id) + end + it 'includes pagination headers' do 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } -- cgit v1.2.1 From 7a3ba8e9845b89c9f3f37d43e8edfeaa9093cfdf Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 26 Sep 2017 20:06:08 +0200 Subject: Make sure the user only sees groups he's allowed to see --- spec/finders/group_descendants_finder_spec.rb | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'spec') diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 09a773aaf68..7b9dfcbfad0 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -58,6 +58,19 @@ describe GroupDescendantsFinder do expect(found_group.preloaded_member_count).to eq(1) end + it 'does not include subgroups the user does not have access to' do + subgroup.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + + public_subgroup = create(:group, :public, parent: group, path: 'public-group') + other_subgroup = create(:group, :private, parent: group, path: 'visible-private-group') + other_user = create(:user) + other_subgroup.add_developer(other_user) + + finder = described_class.new(current_user: other_user, parent_group: group) + + expect(finder.execute).to contain_exactly(public_subgroup, other_subgroup) + end + context 'with a filter' do let(:params) { { filter: 'test' } } @@ -68,6 +81,21 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(matching_subgroup, matching_project) end + it 'does not include subgroups the user does not have access to' do + _invisible_subgroup = create(:group, :private, parent: group, name: 'test1') + other_subgroup = create(:group, :private, parent: group, name: 'test2') + public_subgroup = create(:group, :public, parent: group, name: 'test3') + other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4') + other_user = create(:user) + other_subgroup.add_developer(other_user) + + finder = described_class.new(current_user: other_user, + parent_group: group, + params: params) + + expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup) + end + context 'with matching children' do it 'includes a group that has a subgroup matching the query and its parent' do matching_subgroup = create(:group, name: 'testgroup', parent: subgroup) -- cgit v1.2.1 From ab5d5b7ecea5d68c00171d0750b1b2f62ffbe55d Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 26 Sep 2017 21:31:32 +0200 Subject: Make sure all queries are limited to the page size And fix some pagination bugs --- spec/controllers/groups_controller_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index ff76eaee25f..575f70440e1 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -174,18 +174,18 @@ describe GroupsController do end context 'with subgroups and projects', :nested_groups do + let!(:first_page_subgroups) { create_list(:group, Kaminari.config.default_per_page, parent: group) } let!(:other_subgroup) { create(:group, :public, parent: group) } let!(:project) { create(:project, :public, namespace: group) } - let!(:first_page_subgroups) { create_list(:group, Kaminari.config.default_per_page, parent: group) } it 'contains all subgroups' do - get :children, id: group.to_param, sort: 'id_desc', format: :json + get :children, id: group.to_param, sort: 'id_asc', format: :json expect(assigns(:children)).to contain_exactly(*first_page_subgroups) end it 'contains the project and group on the second page' do - get :children, id: group.to_param, sort: 'id_desc', page: 2, format: :json + get :children, id: group.to_param, sort: 'id_asc', page: 2, format: :json expect(assigns(:children)).to contain_exactly(other_subgroup, project) end -- cgit v1.2.1 From af0b8e0558f529cd79a9dd061dc54ae3bfa9d1dd Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 2 Oct 2017 07:55:44 +0200 Subject: Only preload ancestors for search results in the specified parent When filtering we want all to preload all the ancestors upto the specified parent group. - root - subgroup - nested-group - project So when searching 'project', on the 'subgroup' page we want to preload 'nested-group' but not 'subgroup' or 'root' --- spec/controllers/groups_controller_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 575f70440e1..d33251f2641 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -319,6 +319,16 @@ describe GroupsController do expect(matched_project_2_json['id']).to eq(matched_project_2.id) end + it 'expands the tree upto a specified parent' do + subgroup = create(:group, :public, parent: group) + l2_subgroup = create(:group, :public, parent: subgroup) + create(:project, :public, namespace: l2_subgroup, name: 'test') + + get :children, id: subgroup.to_param, filter: 'test', format: :json + + expect(response).to have_http_status(200) + end + it 'includes pagination headers' do 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } -- cgit v1.2.1 From 8a685ca8562a288f0a14f1b5864b97e90fe8c709 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 2 Oct 2017 12:54:12 +0200 Subject: Fix bug with project pagination When projects were listed after groups, the projects that would also have been listed on the last page containing groups would be repeated. --- spec/controllers/groups_controller_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index d33251f2641..00a6fa885bf 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -152,13 +152,15 @@ describe GroupsController do describe 'GET #show' do context 'pagination' do + let(:per_page) { 3 } + before do - allow(Kaminari.config).to receive(:default_per_page).and_return(2) + allow(Kaminari.config).to receive(:default_per_page).and_return(per_page) end context 'with only projects' do let!(:other_project) { create(:project, :public, namespace: group) } - let!(:first_page_projects) { create_list(:project, Kaminari.config.default_per_page, :public, namespace: group ) } + let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) } it 'has projects on the first page' do get :show, id: group.to_param, sort: 'id_desc' @@ -174,9 +176,9 @@ describe GroupsController do end context 'with subgroups and projects', :nested_groups do - let!(:first_page_subgroups) { create_list(:group, Kaminari.config.default_per_page, parent: group) } + let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } let!(:other_subgroup) { create(:group, :public, parent: group) } - let!(:project) { create(:project, :public, namespace: group) } + let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) } it 'contains all subgroups' do get :children, id: group.to_param, sort: 'id_asc', format: :json @@ -187,7 +189,7 @@ describe GroupsController do it 'contains the project and group on the second page' do get :children, id: group.to_param, sort: 'id_asc', page: 2, format: :json - expect(assigns(:children)).to contain_exactly(other_subgroup, project) + expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) end end end -- cgit v1.2.1 From ef043063f9d6f9f9482707d78214709b09620a78 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 2 Oct 2017 14:23:36 +0200 Subject: Clean up public/private api of `GroupDescendant` So only methods that are used elsewhere are public. --- spec/models/concerns/group_descendant_spec.rb | 32 --------------------------- 1 file changed, 32 deletions(-) (limited to 'spec') diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb index b1578fc593e..f3a0c342d35 100644 --- a/spec/models/concerns/group_descendant_spec.rb +++ b/spec/models/concerns/group_descendant_spec.rb @@ -24,22 +24,6 @@ describe GroupDescendant, :nested_groups do end end - describe '#parent' do - it 'returns the correct parent' do - expect(subsub_group.parent).to eq(subgroup) - end - end - - describe '#merge_hierarchy' do - it 'combines hierarchies' do - other_subgroup = create(:group, parent: parent) - - expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup] } - - expect(subsub_group.merge_hierarchy(other_subgroup)).to eq(expected_hierarchy) - end - end - describe '.build_hierarchy' do it 'combines hierarchies until the top' do other_subgroup = create(:group, parent: parent) @@ -97,22 +81,6 @@ describe GroupDescendant, :nested_groups do end end - describe '#parent' do - it 'returns the correct parent' do - expect(project.parent).to eq(subsub_group) - end - end - - describe '#merge_hierarchy' do - it 'combines hierarchies' do - project = create(:project, namespace: parent) - - expected_hierarchy = { parent => [{ subgroup => subsub_group }, project] } - - expect(subsub_group.merge_hierarchy(project)).to eq(expected_hierarchy) - end - end - describe '.build_hierarchy' do it 'combines hierarchies until the top' do other_project = create(:project, namespace: parent) -- cgit v1.2.1 From 167fd71348d145c6fee953004bf77ceebf6efb1e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 2 Oct 2017 18:21:18 +0200 Subject: Always preload all elements in a hierarchy to avoid extra queries. --- spec/models/concerns/group_descendant_spec.rb | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb index f3a0c342d35..c17c8f2abec 100644 --- a/spec/models/concerns/group_descendant_spec.rb +++ b/spec/models/concerns/group_descendant_spec.rb @@ -7,6 +7,23 @@ describe GroupDescendant, :nested_groups do context 'for a group' do describe '#hierarchy' do + it 'only queries once for the ancestors' do + # make sure the subsub_group does not have anything cached + test_group = create(:group, parent: subsub_group).reload + + query_count = ActiveRecord::QueryRecorder.new { test_group.hierarchy }.count + + expect(query_count).to eq(1) + end + + it 'only queries once for the ancestors when a top is given' do + test_group = create(:group, parent: subsub_group).reload + + query_count = ActiveRecord::QueryRecorder.new { test_group.hierarchy(subgroup) }.count + + expect(query_count).to eq(1) + end + it 'builds a hierarchy for a group' do expected_hierarchy = { parent => { subgroup => subsub_group } } @@ -20,7 +37,7 @@ describe GroupDescendant, :nested_groups do end it 'raises an error if specifying a base that is not part of the tree' do - expect { subsub_group.hierarchy(double) }.to raise_error('specified base is not part of the tree') + expect { subsub_group.hierarchy(build_stubbed(:group)) }.to raise_error('specified top is not part of the tree') end end @@ -77,7 +94,7 @@ describe GroupDescendant, :nested_groups do end it 'raises an error if specifying a base that is not part of the tree' do - expect { project.hierarchy(double) }.to raise_error('specified base is not part of the tree') + expect { project.hierarchy(build_stubbed(:group)) }.to raise_error('specified top is not part of the tree') end end -- cgit v1.2.1 From 67815272dceb971c03bea3490ec26529b48a52b4 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 3 Oct 2017 15:32:32 +0200 Subject: Return an empty array when no matches are found --- spec/controllers/groups_controller_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 00a6fa885bf..8582f31f059 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -331,6 +331,16 @@ describe GroupsController do expect(response).to have_http_status(200) end + it 'returns an empty array when there are no search results' do + subgroup = create(:group, :public, parent: group) + l2_subgroup = create(:group, :public, parent: subgroup) + create(:project, :public, namespace: l2_subgroup, name: 'no-match') + + get :children, id: subgroup.to_param, filter: 'test', format: :json + + expect(json_response).to eq([]) + end + it 'includes pagination headers' do 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } -- cgit v1.2.1 From de55396134e9e3de429c5c6df55ff06efb8ba329 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 4 Oct 2017 14:10:24 +0000 Subject: Groups tree enhancements for Groups Dashboard and Group Homepage --- spec/features/dashboard/groups_list_spec.rb | 7 +- spec/features/explore/groups_list_spec.rb | 13 +- spec/javascripts/groups/components/app_spec.js | 440 +++++++++++++++++++ .../groups/components/group_folder_spec.js | 66 +++ .../groups/components/group_item_spec.js | 177 ++++++++ spec/javascripts/groups/components/groups_spec.js | 70 +++ .../groups/components/item_actions_spec.js | 110 +++++ .../groups/components/item_caret_spec.js | 40 ++ .../groups/components/item_stats_spec.js | 159 +++++++ .../groups/components/item_type_icon_spec.js | 54 +++ spec/javascripts/groups/group_item_spec.js | 102 ----- spec/javascripts/groups/groups_spec.js | 99 ----- spec/javascripts/groups/mock_data.js | 470 ++++++++++++++++----- .../groups/service/groups_service_spec.js | 41 ++ spec/javascripts/groups/store/groups_store_spec.js | 110 +++++ 15 files changed, 1646 insertions(+), 312 deletions(-) create mode 100644 spec/javascripts/groups/components/app_spec.js create mode 100644 spec/javascripts/groups/components/group_folder_spec.js create mode 100644 spec/javascripts/groups/components/group_item_spec.js create mode 100644 spec/javascripts/groups/components/groups_spec.js create mode 100644 spec/javascripts/groups/components/item_actions_spec.js create mode 100644 spec/javascripts/groups/components/item_caret_spec.js create mode 100644 spec/javascripts/groups/components/item_stats_spec.js create mode 100644 spec/javascripts/groups/components/item_type_icon_spec.js delete mode 100644 spec/javascripts/groups/group_item_spec.js delete mode 100644 spec/javascripts/groups/groups_spec.js create mode 100644 spec/javascripts/groups/service/groups_service_spec.js create mode 100644 spec/javascripts/groups/store/groups_store_spec.js (limited to 'spec') diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index 227550e62be..9cfef46d346 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -16,6 +16,7 @@ feature 'Dashboard Groups page', :js do sign_in(user) visit dashboard_groups_path + wait_for_requests expect(page).to have_content(group.full_name) expect(page).to have_content(nested_group.full_name) @@ -33,7 +34,7 @@ feature 'Dashboard Groups page', :js do end it 'filters groups' do - fill_in 'filter_groups', with: group.name + fill_in 'filter', with: group.name wait_for_requests expect(page).to have_content(group.full_name) @@ -42,10 +43,10 @@ feature 'Dashboard Groups page', :js do end it 'resets search when user cleans the input' do - fill_in 'filter_groups', with: group.name + fill_in 'filter', with: group.name wait_for_requests - fill_in 'filter_groups', with: '' + fill_in 'filter', with: '' wait_for_requests expect(page).to have_content(group.full_name) diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index fa3d8f97a09..41778542e23 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -15,6 +15,7 @@ describe 'Explore Groups page', :js do sign_in(user) visit explore_groups_path + wait_for_requests end it 'shows groups user is member of' do @@ -24,7 +25,7 @@ describe 'Explore Groups page', :js do end it 'filters groups' do - fill_in 'filter_groups', with: group.name + fill_in 'filter', with: group.name wait_for_requests expect(page).to have_content(group.full_name) @@ -33,10 +34,10 @@ describe 'Explore Groups page', :js do end it 'resets search when user cleans the input' do - fill_in 'filter_groups', with: group.name + fill_in 'filter', with: group.name wait_for_requests - fill_in 'filter_groups', with: "" + fill_in 'filter', with: "" wait_for_requests expect(page).to have_content(group.full_name) @@ -47,21 +48,21 @@ describe 'Explore Groups page', :js do it 'shows non-archived projects count' do # Initially project is not archived - expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + expect(find('.js-groups-list-holder .content-list li:first-child .stats .number-projects')).to have_text("1") # Archive project empty_project.archive! visit explore_groups_path # Check project count - expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0") + expect(find('.js-groups-list-holder .content-list li:first-child .stats .number-projects')).to have_text("0") # Unarchive project empty_project.unarchive! visit explore_groups_path # Check project count - expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + expect(find('.js-groups-list-holder .content-list li:first-child .stats .number-projects')).to have_text("1") end describe 'landing component' do diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js new file mode 100644 index 00000000000..8472c726b08 --- /dev/null +++ b/spec/javascripts/groups/components/app_spec.js @@ -0,0 +1,440 @@ +import Vue from 'vue'; + +import appComponent from '~/groups/components/app.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; + +import eventHub from '~/groups/event_hub'; +import GroupsStore from '~/groups/store/groups_store'; +import GroupsService from '~/groups/service/groups_service'; + +import { + mockEndpoint, mockGroups, mockSearchedGroups, + mockRawPageInfo, mockParentGroupItem, mockRawChildren, + mockChildren, mockPageInfo, +} from '../mock_data'; + +const createComponent = (hideProjects = false) => { + const Component = Vue.extend(appComponent); + const store = new GroupsStore(false); + const service = new GroupsService(mockEndpoint); + + return new Component({ + propsData: { + store, + service, + hideProjects, + }, + }); +}; + +const returnServicePromise = (data, failed) => new Promise((resolve, reject) => { + if (failed) { + reject(data); + } else { + resolve({ + json() { + return data; + }, + }); + } +}); + +describe('AppComponent', () => { + let vm; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + + Vue.nextTick(() => { + done(); + }); + }); + + describe('computed', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('groups', () => { + it('should return list of groups from store', () => { + spyOn(vm.store, 'getGroups'); + + const groups = vm.groups; + expect(vm.store.getGroups).toHaveBeenCalled(); + expect(groups).not.toBeDefined(); + }); + }); + + describe('pageInfo', () => { + it('should return pagination info from store', () => { + spyOn(vm.store, 'getPaginationInfo'); + + const pageInfo = vm.pageInfo; + expect(vm.store.getPaginationInfo).toHaveBeenCalled(); + expect(pageInfo).not.toBeDefined(); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('fetchGroups', () => { + it('should call `getGroups` with all the params provided', (done) => { + spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(mockGroups)); + + vm.fetchGroups({ + parentId: 1, + page: 2, + filterGroupsBy: 'git', + sortBy: 'created_desc', + }); + setTimeout(() => { + expect(vm.service.getGroups).toHaveBeenCalledWith(1, 2, 'git', 'created_desc'); + done(); + }, 0); + }); + + it('should set headers to store for building pagination info when called with `updatePagination`', (done) => { + spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise({ headers: mockRawPageInfo })); + spyOn(vm, 'updatePagination'); + + vm.fetchGroups({ updatePagination: true }); + setTimeout(() => { + expect(vm.service.getGroups).toHaveBeenCalled(); + expect(vm.updatePagination).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should show flash error when request fails', (done) => { + spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(null, true)); + spyOn($, 'scrollTo'); + spyOn(window, 'Flash'); + + vm.fetchGroups({}); + setTimeout(() => { + expect(vm.isLoading).toBeFalsy(); + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.'); + done(); + }, 0); + }); + }); + + describe('fetchAllGroups', () => { + it('should fetch default set of groups', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); + spyOn(vm, 'updatePagination').and.callThrough(); + spyOn(vm, 'updateGroups').and.callThrough(); + + vm.fetchAllGroups(); + expect(vm.isLoading).toBeTruthy(); + expect(vm.fetchGroups).toHaveBeenCalled(); + setTimeout(() => { + expect(vm.isLoading).toBeFalsy(); + expect(vm.updateGroups).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should fetch matching set of groups when app is loaded with search query', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockSearchedGroups)); + spyOn(vm, 'updateGroups').and.callThrough(); + + vm.fetchAllGroups(); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + page: null, + filterGroupsBy: null, + sortBy: null, + updatePagination: true, + }); + setTimeout(() => { + expect(vm.updateGroups).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('fetchPage', () => { + it('should fetch groups for provided page details and update window state', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); + spyOn(vm, 'updateGroups').and.callThrough(); + spyOn(gl.utils, 'mergeUrlParams').and.callThrough(); + spyOn(window.history, 'replaceState'); + spyOn($, 'scrollTo'); + + vm.fetchPage(2, null, null); + expect(vm.isLoading).toBeTruthy(); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + page: 2, + filterGroupsBy: null, + sortBy: null, + updatePagination: true, + }); + setTimeout(() => { + expect(vm.isLoading).toBeFalsy(); + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); + expect(window.history.replaceState).toHaveBeenCalledWith({ + page: jasmine.any(String), + }, jasmine.any(String), jasmine.any(String)); + expect(vm.updateGroups).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('toggleChildren', () => { + let groupItem; + + beforeEach(() => { + groupItem = Object.assign({}, mockParentGroupItem); + groupItem.isOpen = false; + groupItem.isChildrenLoading = false; + }); + + it('should fetch children of given group and expand it if group is collapsed and children are not loaded', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockRawChildren)); + spyOn(vm.store, 'setGroupChildren'); + + vm.toggleChildren(groupItem); + expect(groupItem.isChildrenLoading).toBeTruthy(); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + parentId: groupItem.id, + }); + setTimeout(() => { + expect(vm.store.setGroupChildren).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should skip network request while expanding group if children are already loaded', () => { + spyOn(vm, 'fetchGroups'); + groupItem.children = mockRawChildren; + + vm.toggleChildren(groupItem); + expect(vm.fetchGroups).not.toHaveBeenCalled(); + expect(groupItem.isOpen).toBeTruthy(); + }); + + it('should collapse group if it is already expanded', () => { + spyOn(vm, 'fetchGroups'); + groupItem.isOpen = true; + + vm.toggleChildren(groupItem); + expect(vm.fetchGroups).not.toHaveBeenCalled(); + expect(groupItem.isOpen).toBeFalsy(); + }); + + it('should set `isChildrenLoading` back to `false` if load request fails', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true)); + + vm.toggleChildren(groupItem); + expect(groupItem.isChildrenLoading).toBeTruthy(); + setTimeout(() => { + expect(groupItem.isChildrenLoading).toBeFalsy(); + done(); + }, 0); + }); + }); + + describe('leaveGroup', () => { + let groupItem; + let childGroupItem; + + beforeEach(() => { + groupItem = Object.assign({}, mockParentGroupItem); + groupItem.children = mockChildren; + childGroupItem = groupItem.children[0]; + groupItem.isChildrenLoading = false; + }); + + it('should leave group and remove group item from tree', (done) => { + const notice = `You left the "${childGroupItem.fullName}" group.`; + spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); + spyOn(vm.store, 'removeGroup').and.callThrough(); + spyOn(window, 'Flash'); + spyOn($, 'scrollTo'); + + vm.leaveGroup(childGroupItem, groupItem); + expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + setTimeout(() => { + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem); + expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); + done(); + }, 0); + }); + + it('should show error flash message if request failed to leave group', (done) => { + const message = 'An error occurred. Please try again.'; + spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 500 }, true)); + spyOn(vm.store, 'removeGroup').and.callThrough(); + spyOn(window, 'Flash'); + + vm.leaveGroup(childGroupItem, groupItem); + expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + setTimeout(() => { + expect(vm.store.removeGroup).not.toHaveBeenCalled(); + expect(window.Flash).toHaveBeenCalledWith(message); + expect(childGroupItem.isBeingRemoved).toBeFalsy(); + done(); + }, 0); + }); + + it('should show appropriate error flash message if request forbids to leave group', (done) => { + const message = 'Failed to leave the group. Please make sure you are not the only owner.'; + spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 403 }, true)); + spyOn(vm.store, 'removeGroup').and.callThrough(); + spyOn(window, 'Flash'); + + vm.leaveGroup(childGroupItem, groupItem); + expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + setTimeout(() => { + expect(vm.store.removeGroup).not.toHaveBeenCalled(); + expect(window.Flash).toHaveBeenCalledWith(message); + expect(childGroupItem.isBeingRemoved).toBeFalsy(); + done(); + }, 0); + }); + }); + + describe('updatePagination', () => { + it('should set pagination info to store from provided headers', () => { + spyOn(vm.store, 'setPaginationInfo'); + + vm.updatePagination(mockRawPageInfo); + expect(vm.store.setPaginationInfo).toHaveBeenCalledWith(mockRawPageInfo); + }); + }); + + describe('updateGroups', () => { + it('should call setGroups on store if method was called directly', () => { + spyOn(vm.store, 'setGroups'); + + vm.updateGroups(mockGroups); + expect(vm.store.setGroups).toHaveBeenCalledWith(mockGroups); + }); + + it('should call setSearchedGroups on store if method was called with fromSearch param', () => { + spyOn(vm.store, 'setSearchedGroups'); + + vm.updateGroups(mockGroups, true); + expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups); + }); + + it('should set `isSearchEmpty` prop based on groups count', () => { + vm.updateGroups(mockGroups); + expect(vm.isSearchEmpty).toBeFalsy(); + + vm.updateGroups([]); + expect(vm.isSearchEmpty).toBeTruthy(); + }); + }); + }); + + describe('created', () => { + it('should bind event listeners on eventHub', (done) => { + spyOn(eventHub, '$on'); + + const newVm = createComponent(); + newVm.$mount(); + + Vue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); + newVm.$destroy(); + done(); + }); + }); + + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', (done) => { + const newVm = createComponent(); + newVm.$mount(); + Vue.nextTick(() => { + expect(newVm.searchEmptyMessage).toBe('Sorry, no groups or projects matched your search'); + newVm.$destroy(); + done(); + }); + }); + + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', (done) => { + const newVm = createComponent(true); + newVm.$mount(); + Vue.nextTick(() => { + expect(newVm.searchEmptyMessage).toBe('Sorry, no groups matched your search'); + newVm.$destroy(); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', (done) => { + spyOn(eventHub, '$off'); + + const newVm = createComponent(); + newVm.$mount(); + newVm.$destroy(); + + Vue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('template', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render loading icon', (done) => { + vm.isLoading = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); + expect(vm.$el.querySelector('i.fa').getAttribute('aria-label')).toBe('Loading groups'); + done(); + }); + }); + + it('should render groups tree', (done) => { + vm.groups = [mockParentGroupItem]; + vm.isLoading = false; + vm.pageInfo = mockPageInfo; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/groups/components/group_folder_spec.js b/spec/javascripts/groups/components/group_folder_spec.js new file mode 100644 index 00000000000..4eb198595fb --- /dev/null +++ b/spec/javascripts/groups/components/group_folder_spec.js @@ -0,0 +1,66 @@ +import Vue from 'vue'; + +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import { mockGroups, mockParentGroupItem } from '../mock_data'; + +const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem) => { + const Component = Vue.extend(groupFolderComponent); + + return new Component({ + propsData: { + groups, + parentGroup, + }, + }); +}; + +describe('GroupFolderComponent', () => { + let vm; + + beforeEach((done) => { + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + vm.$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hasMoreChildren', () => { + it('should return false when childrenCount of group is less than MAX_CHILDREN_COUNT', () => { + expect(vm.hasMoreChildren).toBeFalsy(); + }); + }); + + describe('moreChildrenStats', () => { + it('should return message with count of excess children over MAX_CHILDREN_COUNT limit', () => { + expect(vm.moreChildrenStats).toBe('3 more items'); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + expect(vm.$el.classList.contains('group-list-tree')).toBeTruthy(); + expect(vm.$el.querySelectorAll('li.group-row').length).toBe(7); + }); + + it('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => { + const parentGroup = Object.assign({}, mockParentGroupItem); + parentGroup.childrenCount = 21; + + const newVm = createComponent(mockGroups, parentGroup); + newVm.$mount(); + expect(newVm.$el.querySelector('li.group-row a.has-more-items')).toBeDefined(); + newVm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js new file mode 100644 index 00000000000..0f4fbdae445 --- /dev/null +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -0,0 +1,177 @@ +import Vue from 'vue'; + +import groupItemComponent from '~/groups/components/group_item.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import eventHub from '~/groups/event_hub'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { + const Component = Vue.extend(groupItemComponent); + + return mountComponent(Component, { + group, + parentGroup, + }); +}; + +describe('GroupItemComponent', () => { + let vm; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + + vm = createComponent(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('groupDomId', () => { + it('should return ID string suffixed with group ID', () => { + expect(vm.groupDomId).toBe('group-55'); + }); + }); + + describe('rowClass', () => { + it('should return map of classes based on group details', () => { + const classes = ['is-open', 'has-children', 'has-description', 'being-removed']; + const rowClass = vm.rowClass; + + expect(Object.keys(rowClass).length).toBe(classes.length); + Object.keys(rowClass).forEach((className) => { + expect(classes.indexOf(className) > -1).toBeTruthy(); + }); + }); + }); + + describe('hasChildren', () => { + it('should return boolean value representing if group has any children present', () => { + let newVm; + const group = Object.assign({}, mockParentGroupItem); + + group.childrenCount = 5; + newVm = createComponent(group); + expect(newVm.hasChildren).toBeTruthy(); + newVm.$destroy(); + + group.childrenCount = 0; + newVm = createComponent(group); + expect(newVm.hasChildren).toBeFalsy(); + newVm.$destroy(); + }); + }); + + describe('hasAvatar', () => { + it('should return boolean value representing if group has any avatar present', () => { + let newVm; + const group = Object.assign({}, mockParentGroupItem); + + group.avatarUrl = null; + newVm = createComponent(group); + expect(newVm.hasAvatar).toBeFalsy(); + newVm.$destroy(); + + group.avatarUrl = '/uploads/group_avatar.png'; + newVm = createComponent(group); + expect(newVm.hasAvatar).toBeTruthy(); + newVm.$destroy(); + }); + }); + + describe('isGroup', () => { + it('should return boolean value representing if group item is of type `group` or not', () => { + let newVm; + const group = Object.assign({}, mockParentGroupItem); + + group.type = 'group'; + newVm = createComponent(group); + expect(newVm.isGroup).toBeTruthy(); + newVm.$destroy(); + + group.type = 'project'; + newVm = createComponent(group); + expect(newVm.isGroup).toBeFalsy(); + newVm.$destroy(); + }); + }); + }); + + describe('methods', () => { + describe('onClickRowGroup', () => { + let event; + + beforeEach(() => { + const classList = { + contains() { + return false; + }, + }; + + event = { + target: { + classList, + parentElement: { + classList, + }, + }, + }; + }); + + it('should emit `toggleChildren` event when expand is clicked on a group and it has children present', () => { + spyOn(eventHub, '$emit'); + + vm.onClickRowGroup(event); + expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', vm.group); + }); + + it('should navigate page to group homepage if group does not have any children present', (done) => { + const group = Object.assign({}, mockParentGroupItem); + group.childrenCount = 0; + const newVm = createComponent(group); + spyOn(gl.utils, 'visitUrl').and.stub(); + spyOn(eventHub, '$emit'); + + newVm.onClickRowGroup(event); + setTimeout(() => { + expect(eventHub.$emit).not.toHaveBeenCalled(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); + done(); + }, 0); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + expect(vm.$el.getAttribute('id')).toBe('group-55'); + expect(vm.$el.classList.contains('group-row')).toBeTruthy(); + + expect(vm.$el.querySelector('.group-row-contents')).toBeDefined(); + expect(vm.$el.querySelector('.group-row-contents .controls')).toBeDefined(); + expect(vm.$el.querySelector('.group-row-contents .stats')).toBeDefined(); + + expect(vm.$el.querySelector('.folder-toggle-wrap')).toBeDefined(); + expect(vm.$el.querySelector('.folder-toggle-wrap .folder-caret')).toBeDefined(); + expect(vm.$el.querySelector('.folder-toggle-wrap .item-type-icon')).toBeDefined(); + + expect(vm.$el.querySelector('.avatar-container')).toBeDefined(); + expect(vm.$el.querySelector('.avatar-container a.no-expand')).toBeDefined(); + expect(vm.$el.querySelector('.avatar-container .avatar')).toBeDefined(); + + expect(vm.$el.querySelector('.title')).toBeDefined(); + expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined(); + expect(vm.$el.querySelector('.access-type')).toBeDefined(); + expect(vm.$el.querySelector('.description')).toBeDefined(); + + expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js new file mode 100644 index 00000000000..280376d4903 --- /dev/null +++ b/spec/javascripts/groups/components/groups_spec.js @@ -0,0 +1,70 @@ +import Vue from 'vue'; + +import groupsComponent from '~/groups/components/groups.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import eventHub from '~/groups/event_hub'; +import { mockGroups, mockPageInfo } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (searchEmpty = false) => { + const Component = Vue.extend(groupsComponent); + + return mountComponent(Component, { + groups: mockGroups, + pageInfo: mockPageInfo, + searchEmptyMessage: 'No matching results', + searchEmpty, + }); +}; + +describe('GroupsComponent', () => { + let vm; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('change', () => { + it('should emit `fetchPage` event when page is changed via pagination', () => { + spyOn(eventHub, '$emit').and.stub(); + + vm.change(2); + expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', 2, jasmine.any(Object), jasmine.any(Object)); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); + expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + expect(vm.$el.querySelectorAll('.has-no-search-results').length === 0).toBeTruthy(); + done(); + }); + }); + + it('should render empty search message when `searchEmpty` is `true`', (done) => { + vm.searchEmpty = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js new file mode 100644 index 00000000000..2ce1a749a96 --- /dev/null +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -0,0 +1,110 @@ +import Vue from 'vue'; + +import itemActionsComponent from '~/groups/components/item_actions.vue'; +import eventHub from '~/groups/event_hub'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { + const Component = Vue.extend(itemActionsComponent); + + return mountComponent(Component, { + group, + parentGroup, + }); +}; + +describe('ItemActionsComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('leaveConfirmationMessage', () => { + it('should return appropriate string for leave group confirmation', () => { + expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?'); + }); + }); + }); + + describe('methods', () => { + describe('onLeaveGroup', () => { + it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => { + expect(vm.dialogStatus).toBeFalsy(); + vm.onLeaveGroup(); + expect(vm.dialogStatus).toBeTruthy(); + }); + }); + + describe('leaveGroup', () => { + it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { + spyOn(eventHub, '$emit'); + vm.dialogStatus = true; + vm.leaveGroup(true); + expect(vm.dialogStatus).toBeFalsy(); + expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); + }); + + it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { + spyOn(eventHub, '$emit'); + vm.dialogStatus = true; + vm.leaveGroup(false); + expect(vm.dialogStatus).toBeFalsy(); + expect(eventHub.$emit).not.toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + expect(vm.$el.classList.contains('controls')).toBeTruthy(); + }); + + it('should render Edit Group button with correct attribute values', () => { + const group = Object.assign({}, mockParentGroupItem); + group.canEdit = true; + const newVm = createComponent(group); + + const editBtn = newVm.$el.querySelector('a.edit-group'); + expect(editBtn).toBeDefined(); + expect(editBtn.classList.contains('no-expand')).toBeTruthy(); + expect(editBtn.getAttribute('href')).toBe(group.editPath); + expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); + expect(editBtn.dataset.originalTitle).toBe('Edit group'); + expect(editBtn.querySelector('i.fa.fa-cogs')).toBeDefined(); + + newVm.$destroy(); + }); + + it('should render Leave Group button with correct attribute values', () => { + const group = Object.assign({}, mockParentGroupItem); + group.canLeave = true; + const newVm = createComponent(group); + + const leaveBtn = newVm.$el.querySelector('a.leave-group'); + expect(leaveBtn).toBeDefined(); + expect(leaveBtn.classList.contains('no-expand')).toBeTruthy(); + expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); + expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); + expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); + expect(leaveBtn.querySelector('i.fa.fa-sign-out')).toBeDefined(); + + newVm.$destroy(); + }); + + it('should show modal dialog when `dialogStatus` is set to `true`', () => { + vm.dialogStatus = true; + const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog'); + expect(modalDialogEl).toBeDefined(); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js new file mode 100644 index 00000000000..4310a07e6e6 --- /dev/null +++ b/spec/javascripts/groups/components/item_caret_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; + +import itemCaretComponent from '~/groups/components/item_caret.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (isGroupOpen = false) => { + const Component = Vue.extend(itemCaretComponent); + + return mountComponent(Component, { + isGroupOpen, + }); +}; + +describe('ItemCaretComponent', () => { + describe('template', () => { + it('should render component template correctly', () => { + const vm = createComponent(); + vm.$mount(); + expect(vm.$el.classList.contains('folder-caret')).toBeTruthy(); + vm.$destroy(); + }); + + it('should render caret down icon if `isGroupOpen` prop is `true`', () => { + const vm = createComponent(true); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(1); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(0); + vm.$destroy(); + }); + + it('should render caret right icon if `isGroupOpen` prop is `false`', () => { + const vm = createComponent(); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(0); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(1); + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js new file mode 100644 index 00000000000..e200f9f08bd --- /dev/null +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -0,0 +1,159 @@ +import Vue from 'vue'; + +import itemStatsComponent from '~/groups/components/item_stats.vue'; +import { + mockParentGroupItem, + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, +} from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (item = mockParentGroupItem) => { + const Component = Vue.extend(itemStatsComponent); + + return mountComponent(Component, { + item, + }); +}; + +describe('ItemStatsComponent', () => { + describe('computed', () => { + describe('visibilityIcon', () => { + it('should return icon class based on `item.visibility` value', () => { + Object.keys(VISIBILITY_TYPE_ICON).forEach((visibility) => { + const item = Object.assign({}, mockParentGroupItem, { visibility }); + const vm = createComponent(item); + vm.$mount(); + expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]); + vm.$destroy(); + }); + }); + }); + + describe('visibilityTooltip', () => { + it('should return tooltip string for Group based on `item.visibility` value', () => { + Object.keys(GROUP_VISIBILITY_TYPE).forEach((visibility) => { + const item = Object.assign({}, mockParentGroupItem, { + visibility, + type: ITEM_TYPE.GROUP, + }); + const vm = createComponent(item); + vm.$mount(); + expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]); + vm.$destroy(); + }); + }); + + it('should return tooltip string for Project based on `item.visibility` value', () => { + Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibility) => { + const item = Object.assign({}, mockParentGroupItem, { + visibility, + type: ITEM_TYPE.PROJECT, + }); + const vm = createComponent(item); + vm.$mount(); + expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]); + vm.$destroy(); + }); + }); + }); + + describe('isProject', () => { + it('should return boolean value representing whether `item.type` is Project or not', () => { + let item; + let vm; + + item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isProject).toBeTruthy(); + vm.$destroy(); + + item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isProject).toBeFalsy(); + vm.$destroy(); + }); + }); + + describe('isGroup', () => { + it('should return boolean value representing whether `item.type` is Group or not', () => { + let item; + let vm; + + item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isGroup).toBeTruthy(); + vm.$destroy(); + + item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isGroup).toBeFalsy(); + vm.$destroy(); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + const vm = createComponent(); + vm.$mount(); + + const visibilityIconEl = vm.$el.querySelector('.item-visibility'); + expect(vm.$el.classList.contains('.stats')).toBeDefined(); + expect(visibilityIconEl).toBeDefined(); + expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); + expect(visibilityIconEl.querySelector('i.fa')).toBeDefined(); + + vm.$destroy(); + }); + + it('should render stat icons if `item.type` is Group', () => { + const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + const vm = createComponent(item); + vm.$mount(); + + const subgroupIconEl = vm.$el.querySelector('span.number-subgroups'); + expect(subgroupIconEl).toBeDefined(); + expect(subgroupIconEl.dataset.originalTitle).toBe('Subgroups'); + expect(subgroupIconEl.querySelector('i.fa.fa-folder')).toBeDefined(); + expect(subgroupIconEl.innerText.trim()).toBe(`${vm.item.subgroupCount}`); + + const projectsIconEl = vm.$el.querySelector('span.number-projects'); + expect(projectsIconEl).toBeDefined(); + expect(projectsIconEl.dataset.originalTitle).toBe('Projects'); + expect(projectsIconEl.querySelector('i.fa.fa-bookmark')).toBeDefined(); + expect(projectsIconEl.innerText.trim()).toBe(`${vm.item.projectCount}`); + + const membersIconEl = vm.$el.querySelector('span.number-users'); + expect(membersIconEl).toBeDefined(); + expect(membersIconEl.dataset.originalTitle).toBe('Members'); + expect(membersIconEl.querySelector('i.fa.fa-users')).toBeDefined(); + expect(membersIconEl.innerText.trim()).toBe(`${vm.item.memberCount}`); + + vm.$destroy(); + }); + + it('should render stat icons if `item.type` is Project', () => { + const item = Object.assign({}, mockParentGroupItem, { + type: ITEM_TYPE.PROJECT, + starCount: 4, + }); + const vm = createComponent(item); + vm.$mount(); + + const projectStarIconEl = vm.$el.querySelector('.project-stars'); + expect(projectStarIconEl).toBeDefined(); + expect(projectStarIconEl.querySelector('i.fa.fa-star')).toBeDefined(); + expect(projectStarIconEl.innerText.trim()).toBe(`${vm.item.starCount}`); + + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js new file mode 100644 index 00000000000..528e6ed1b4c --- /dev/null +++ b/spec/javascripts/groups/components/item_type_icon_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; + +import itemTypeIconComponent from '~/groups/components/item_type_icon.vue'; +import { ITEM_TYPE } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => { + const Component = Vue.extend(itemTypeIconComponent); + + return mountComponent(Component, { + itemType, + isGroupOpen, + }); +}; + +describe('ItemTypeIconComponent', () => { + describe('template', () => { + it('should render component template correctly', () => { + const vm = createComponent(); + vm.$mount(); + expect(vm.$el.classList.contains('item-type-icon')).toBeTruthy(); + vm.$destroy(); + }); + + it('should render folder open or close icon based `isGroupOpen` prop value', () => { + let vm; + + vm = createComponent(ITEM_TYPE.GROUP, true); + vm.$mount(); + expect(vm.$el.querySelector('i.fa.fa-folder-open')).toBeDefined(); + vm.$destroy(); + + vm = createComponent(ITEM_TYPE.GROUP); + vm.$mount(); + expect(vm.$el.querySelector('i.fa.fa-folder')).toBeDefined(); + vm.$destroy(); + }); + + it('should render bookmark icon based on `isProject` prop value', () => { + let vm; + + vm = createComponent(ITEM_TYPE.PROJECT); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(1); + vm.$destroy(); + + vm = createComponent(ITEM_TYPE.GROUP); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(0); + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js deleted file mode 100644 index 25e10552d95..00000000000 --- a/spec/javascripts/groups/group_item_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import Vue from 'vue'; -import groupItemComponent from '~/groups/components/group_item.vue'; -import GroupsStore from '~/groups/stores/groups_store'; -import { group1 } from './mock_data'; - -describe('Groups Component', () => { - let GroupItemComponent; - let component; - let store; - let group; - - describe('group with default data', () => { - beforeEach((done) => { - GroupItemComponent = Vue.extend(groupItemComponent); - store = new GroupsStore(); - group = store.decorateGroup(group1); - - component = new GroupItemComponent({ - propsData: { - group, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should render the group item correctly', () => { - expect(component.$el.classList.contains('group-row')).toBe(true); - expect(component.$el.classList.contains('.no-description')).toBe(false); - expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects); - expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers); - expect(component.$el.querySelector('.group-visibility')).toBeDefined(); - expect(component.$el.querySelector('.avatar-container')).toBeDefined(); - expect(component.$el.querySelector('.title').textContent).toContain(group.name); - expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess); - expect(component.$el.querySelector('.description').textContent).toContain(group.description); - expect(component.$el.querySelector('.edit-group')).toBeDefined(); - expect(component.$el.querySelector('.leave-group')).toBeDefined(); - }); - }); - - describe('group without description', () => { - beforeEach((done) => { - GroupItemComponent = Vue.extend(groupItemComponent); - store = new GroupsStore(); - group1.description = ''; - group = store.decorateGroup(group1); - - component = new GroupItemComponent({ - propsData: { - group, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should render group item correctly', () => { - expect(component.$el.querySelector('.description').textContent).toBe(''); - expect(component.$el.classList.contains('.no-description')).toBe(false); - }); - }); - - describe('user has not access to group', () => { - beforeEach((done) => { - GroupItemComponent = Vue.extend(groupItemComponent); - store = new GroupsStore(); - group1.permissions.human_group_access = null; - group = store.decorateGroup(group1); - - component = new GroupItemComponent({ - propsData: { - group, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should not display access type', () => { - expect(component.$el.querySelector('.access-type')).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js deleted file mode 100644 index b14153dbbfa..00000000000 --- a/spec/javascripts/groups/groups_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/groups/event_hub'; -import groupFolderComponent from '~/groups/components/group_folder.vue'; -import groupItemComponent from '~/groups/components/group_item.vue'; -import groupsComponent from '~/groups/components/groups.vue'; -import GroupsStore from '~/groups/stores/groups_store'; -import { groupsData } from './mock_data'; - -describe('Groups Component', () => { - let GroupsComponent; - let store; - let component; - let groups; - - beforeEach((done) => { - Vue.component('group-folder', groupFolderComponent); - Vue.component('group-item', groupItemComponent); - - store = new GroupsStore(); - groups = store.setGroups(groupsData.groups); - - store.storePagination(groupsData.pagination); - - GroupsComponent = Vue.extend(groupsComponent); - - component = new GroupsComponent({ - propsData: { - groups: store.state.groups, - pageInfo: store.state.pageInfo, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - describe('with data', () => { - it('should render a list of groups', () => { - expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true); - expect(component.$el.querySelector('#group-12')).toBeDefined(); - expect(component.$el.querySelector('#group-1119')).toBeDefined(); - expect(component.$el.querySelector('#group-1120')).toBeDefined(); - }); - - it('should respect the order of groups', () => { - const wrap = component.$el.querySelector('.groups-list-tree-container > .group-list-tree'); - expect(wrap.querySelector('.group-row:nth-child(1)').id).toBe('group-12'); - expect(wrap.querySelector('.group-row:nth-child(2)').id).toBe('group-1119'); - }); - - it('should render group and its subgroup', () => { - const lists = component.$el.querySelectorAll('.group-list-tree'); - - expect(lists.length).toBe(3); // one parent and two subgroups - - expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true); - expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true); - - expect(lists[2].querySelector('#group-1120').textContent).toContain(groups.id1119.subGroups.id1120.name); - }); - - it('should render group identicon when group avatar is not present', () => { - const avatar = component.$el.querySelector('#group-12 .avatar-container .avatar'); - expect(avatar.nodeName).toBe('DIV'); - expect(avatar.classList.contains('identicon')).toBeTruthy(); - expect(avatar.getAttribute('style').indexOf('background-color') > -1).toBeTruthy(); - }); - - it('should render group avatar when group avatar is present', () => { - const avatar = component.$el.querySelector('#group-1120 .avatar-container .avatar'); - expect(avatar.nodeName).toBe('IMG'); - expect(avatar.classList.contains('identicon')).toBeFalsy(); - }); - - it('should remove prefix of parent group', () => { - expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4'); - }); - - it('should remove the group after leaving the group', (done) => { - spyOn(window, 'confirm').and.returnValue(true); - - eventHub.$on('leaveGroup', (group, collection) => { - store.removeGroup(group, collection); - }); - - component.$el.querySelector('#group-12 .leave-group').click(); - - Vue.nextTick(() => { - expect(component.$el.querySelector('#group-12')).toBeNull(); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index 5bb84b591f4..6184d671790 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -1,114 +1,380 @@ -const group1 = { - id: 12, - name: 'level1', - path: 'level1', - description: 'foo', - visibility: 'public', - avatar_url: null, - web_url: 'http://localhost:3000/groups/level1', - group_path: '/level1', - full_name: 'level1', - full_path: 'level1', - parent_id: null, - created_at: '2017-05-15T19:01:23.670Z', - updated_at: '2017-05-15T19:01:23.670Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, +export const mockEndpoint = '/dashboard/groups.json'; + +export const ITEM_TYPE = { + PROJECT: 'project', + GROUP: 'group', }; -// This group has no direct parent, should be placed as subgroup of group1 -const group14 = { - id: 1128, - name: 'level4', - path: 'level4', - description: 'foo', - visibility: 'public', - avatar_url: null, - web_url: 'http://localhost:3000/groups/level1/level2/level3/level4', - group_path: '/level1/level2/level3/level4', - full_name: 'level1 / level2 / level3 / level4', - full_path: 'level1/level2/level3/level4', - parent_id: 1127, - created_at: '2017-05-15T19:02:01.645Z', - updated_at: '2017-05-15T19:02:01.645Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, +export const GROUP_VISIBILITY_TYPE = { + public: 'Public - The group and any public projects can be viewed without any authentication.', + internal: 'Internal - The group and any internal projects can be viewed by any logged in user.', + private: 'Private - The group and its projects can only be viewed by members.', }; -const group2 = { - id: 1119, - name: 'devops', - path: 'devops', - description: 'foo', - visibility: 'public', - avatar_url: null, - web_url: 'http://localhost:3000/groups/devops', - group_path: '/devops', - full_name: 'devops', - full_path: 'devops', - parent_id: null, - created_at: '2017-05-11T19:35:09.635Z', - updated_at: '2017-05-11T19:35:09.635Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, +export const PROJECT_VISIBILITY_TYPE = { + public: 'Public - The project can be accessed without any authentication.', + internal: 'Internal - The project can be accessed by any logged in user.', + private: 'Private - Project access must be granted explicitly to each user.', +}; + +export const VISIBILITY_TYPE_ICON = { + public: 'fa-globe', + internal: 'fa-shield', + private: 'fa-lock', }; -const group21 = { - id: 1120, - name: 'chef', - path: 'chef', - description: 'foo', +export const mockParentGroupItem = { + id: 55, + name: 'hardware', + description: '', visibility: 'public', - avatar_url: '/uploads/-/system/group/avatar/2/GitLab.png', - web_url: 'http://localhost:3000/groups/devops/chef', - group_path: '/devops/chef', - full_name: 'devops / chef', - full_path: 'devops/chef', - parent_id: 1119, - created_at: '2017-05-11T19:51:04.060Z', - updated_at: '2017-05-11T19:51:04.060Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, + fullName: 'platform / hardware', + relativePath: '/platform/hardware', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/edit', + childrenCount: 3, + leavePath: '/groups/platform/hardware/group_members/leave', + parentId: 54, + memberCount: '1', + projectCount: 1, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, }; -const groupsData = { - groups: [group1, group14, group2, group21], - pagination: { - Date: 'Mon, 22 May 2017 22:31:52 GMT', - 'X-Prev-Page': '1', - 'X-Content-Type-Options': 'nosniff', - 'X-Total': '31', - 'Transfer-Encoding': 'chunked', - 'X-Runtime': '0.611144', - 'X-Xss-Protection': '1; mode=block', - 'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09', - 'X-Ua-Compatible': 'IE=edge', - 'X-Per-Page': '20', - Link: '; rel="prev", ; rel="first", ; rel="last"', - 'X-Next-Page': '', - Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"', - 'X-Frame-Options': 'DENY', - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'max-age=0, private, must-revalidate', - 'X-Total-Pages': '2', - 'X-Page': '2', +export const mockRawChildren = [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp', + relative_path: '/platform/hardware/bsp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/edit', + children_count: 6, + leave_path: '/groups/platform/hardware/bsp/group_members/leave', + parent_id: 55, + number_users_with_delimiter: '1', + project_count: 4, + subgroup_count: 2, + can_leave: false, + children: [], + }, +]; + +export const mockChildren = [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + fullName: 'platform / hardware / bsp', + relativePath: '/platform/hardware/bsp', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/bsp/edit', + childrenCount: 6, + leavePath: '/groups/platform/hardware/bsp/group_members/leave', + parentId: 55, + memberCount: '1', + projectCount: 4, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, }, +]; + +export const mockGroups = [ + { + id: 75, + name: 'test-group', + description: '', + visibility: 'public', + full_name: 'test-group', + relative_path: '/test-group', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/test-group/edit', + children_count: 2, + leave_path: '/groups/test-group/group_members/leave', + parent_id: null, + number_users_with_delimiter: '1', + project_count: 2, + subgroup_count: 0, + can_leave: false, + }, + { + id: 67, + name: 'open-source', + description: '', + visibility: 'private', + full_name: 'open-source', + relative_path: '/open-source', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/open-source/edit', + children_count: 0, + leave_path: '/groups/open-source/group_members/leave', + parent_id: null, + number_users_with_delimiter: '1', + project_count: 0, + subgroup_count: 0, + can_leave: false, + }, + { + id: 54, + name: 'platform', + description: '', + visibility: 'public', + full_name: 'platform', + relative_path: '/platform', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/edit', + children_count: 1, + leave_path: '/groups/platform/group_members/leave', + parent_id: null, + number_users_with_delimiter: '1', + project_count: 0, + subgroup_count: 1, + can_leave: false, + }, + { + id: 5, + name: 'H5bp', + description: 'Minus dolor consequuntur qui nam recusandae quam incidunt.', + visibility: 'public', + full_name: 'H5bp', + relative_path: '/h5bp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/h5bp/edit', + children_count: 1, + leave_path: '/groups/h5bp/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 1, + subgroup_count: 0, + can_leave: false, + }, + { + id: 4, + name: 'Twitter', + description: 'Deserunt hic nostrum placeat veniam.', + visibility: 'public', + full_name: 'Twitter', + relative_path: '/twitter', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/twitter/edit', + children_count: 2, + leave_path: '/groups/twitter/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 2, + subgroup_count: 0, + can_leave: false, + }, + { + id: 3, + name: 'Documentcloud', + description: 'Consequatur saepe totam ea pariatur maxime.', + visibility: 'public', + full_name: 'Documentcloud', + relative_path: '/documentcloud', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/documentcloud/edit', + children_count: 1, + leave_path: '/groups/documentcloud/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 1, + subgroup_count: 0, + can_leave: false, + }, + { + id: 2, + name: 'Gitlab Org', + description: 'Debitis ea quas aperiam velit doloremque ab.', + visibility: 'public', + full_name: 'Gitlab Org', + relative_path: '/gitlab-org', + can_edit: true, + type: 'group', + avatar_url: '/uploads/-/system/group/avatar/2/GitLab.png', + permission: 'Owner', + edit_path: '/groups/gitlab-org/edit', + children_count: 4, + leave_path: '/groups/gitlab-org/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 4, + subgroup_count: 0, + can_leave: false, + }, +]; + +export const mockSearchedGroups = [ + { + id: 55, + name: 'hardware', + description: '', + visibility: 'public', + full_name: 'platform / hardware', + relative_path: '/platform/hardware', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/edit', + children_count: 3, + leave_path: '/groups/platform/hardware/group_members/leave', + parent_id: 54, + number_users_with_delimiter: '1', + project_count: 1, + subgroup_count: 2, + can_leave: false, + children: [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp', + relative_path: '/platform/hardware/bsp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/edit', + children_count: 6, + leave_path: '/groups/platform/hardware/bsp/group_members/leave', + parent_id: 55, + number_users_with_delimiter: '1', + project_count: 4, + subgroup_count: 2, + can_leave: false, + children: [ + { + id: 60, + name: 'kernel', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel', + relative_path: '/platform/hardware/bsp/kernel', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/kernel/edit', + children_count: 1, + leave_path: '/groups/platform/hardware/bsp/kernel/group_members/leave', + parent_id: 57, + number_users_with_delimiter: '1', + project_count: 0, + subgroup_count: 1, + can_leave: false, + children: [ + { + id: 61, + name: 'common', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel / common', + relative_path: '/platform/hardware/bsp/kernel/common', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/kernel/common/edit', + children_count: 2, + leave_path: '/groups/platform/hardware/bsp/kernel/common/group_members/leave', + parent_id: 60, + number_users_with_delimiter: '1', + project_count: 2, + subgroup_count: 0, + can_leave: false, + children: [ + { + id: 17, + name: 'v4.4', + description: 'Voluptatem qui ea error aperiam veritatis doloremque consequatur temporibus.', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel / common / v4.4', + relative_path: '/platform/hardware/bsp/kernel/common/v4.4', + can_edit: true, + type: 'project', + avatar_url: null, + permission: null, + edit_path: '/platform/hardware/bsp/kernel/common/v4.4/edit', + star_count: 0, + }, + { + id: 16, + name: 'v4.1', + description: 'Rerum expedita voluptatem doloribus neque ducimus ut hic.', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel / common / v4.1', + relative_path: '/platform/hardware/bsp/kernel/common/v4.1', + can_edit: true, + type: 'project', + avatar_url: null, + permission: null, + edit_path: '/platform/hardware/bsp/kernel/common/v4.1/edit', + star_count: 0, + }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export const mockRawPageInfo = { + 'x-per-page': 10, + 'x-page': 10, + 'x-total': 10, + 'x-total-pages': 10, + 'x-next-page': 10, + 'x-prev-page': 10, }; -export { groupsData, group1 }; +export const mockPageInfo = { + perPage: 10, + page: 10, + total: 10, + totalPages: 10, + nextPage: 10, + prevPage: 10, +}; diff --git a/spec/javascripts/groups/service/groups_service_spec.js b/spec/javascripts/groups/service/groups_service_spec.js new file mode 100644 index 00000000000..222e75d24a4 --- /dev/null +++ b/spec/javascripts/groups/service/groups_service_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +import GroupsService from '~/groups/service/groups_service'; +import { mockEndpoint, mockParentGroupItem } from '../mock_data'; + +Vue.use(VueResource); + +describe('GroupsService', () => { + let service; + + beforeEach(() => { + service = new GroupsService(mockEndpoint); + }); + + describe('getGroups', () => { + it('should return promise for `GET` request on provided endpoint', () => { + spyOn(service.groups, 'get').and.stub(); + const queryParams = { + page: 2, + filter: 'git', + sort: 'created_asc', + }; + + service.getGroups(55, 2, 'git', 'created_asc'); + expect(service.groups.get).toHaveBeenCalledWith({ parent_id: 55 }); + + service.getGroups(null, 2, 'git', 'created_asc'); + expect(service.groups.get).toHaveBeenCalledWith(queryParams); + }); + }); + + describe('leaveGroup', () => { + it('should return promise for `DELETE` request on provided endpoint', () => { + spyOn(Vue.http, 'delete').and.stub(); + + service.leaveGroup(mockParentGroupItem.leavePath); + expect(Vue.http.delete).toHaveBeenCalledWith(mockParentGroupItem.leavePath); + }); + }); +}); diff --git a/spec/javascripts/groups/store/groups_store_spec.js b/spec/javascripts/groups/store/groups_store_spec.js new file mode 100644 index 00000000000..d74f38f476e --- /dev/null +++ b/spec/javascripts/groups/store/groups_store_spec.js @@ -0,0 +1,110 @@ +import GroupsStore from '~/groups/store/groups_store'; +import { + mockGroups, mockSearchedGroups, + mockParentGroupItem, mockRawChildren, + mockRawPageInfo, +} from '../mock_data'; + +describe('ProjectsStore', () => { + describe('constructor', () => { + it('should initialize default state', () => { + let store; + + store = new GroupsStore(); + expect(Object.keys(store.state).length).toBe(2); + expect(Array.isArray(store.state.groups)).toBeTruthy(); + expect(Object.keys(store.state.pageInfo).length).toBe(0); + expect(store.hideProjects).not.toBeDefined(); + + store = new GroupsStore(true); + expect(store.hideProjects).toBeTruthy(); + }); + }); + + describe('setGroups', () => { + it('should set groups to state', () => { + const store = new GroupsStore(); + spyOn(store, 'formatGroupItem').and.callThrough(); + + store.setGroups(mockGroups); + expect(store.state.groups.length).toBe(mockGroups.length); + expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); + expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy(); + }); + }); + + describe('setSearchedGroups', () => { + it('should set searched groups to state', () => { + const store = new GroupsStore(); + spyOn(store, 'formatGroupItem').and.callThrough(); + + store.setSearchedGroups(mockSearchedGroups); + expect(store.state.groups.length).toBe(mockSearchedGroups.length); + expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); + expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName') > -1).toBeTruthy(); + }); + }); + + describe('setGroupChildren', () => { + it('should set children to group item in state', () => { + const store = new GroupsStore(); + spyOn(store, 'formatGroupItem').and.callThrough(); + + store.setGroupChildren(mockParentGroupItem, mockRawChildren); + expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); + expect(mockParentGroupItem.children.length).toBe(1); + expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(mockParentGroupItem.isOpen).toBeTruthy(); + expect(mockParentGroupItem.isChildrenLoading).toBeFalsy(); + }); + }); + + describe('setPaginationInfo', () => { + it('should parse and set pagination info in state', () => { + const store = new GroupsStore(); + + store.setPaginationInfo(mockRawPageInfo); + expect(store.state.pageInfo.perPage).toBe(10); + expect(store.state.pageInfo.page).toBe(10); + expect(store.state.pageInfo.total).toBe(10); + expect(store.state.pageInfo.totalPages).toBe(10); + expect(store.state.pageInfo.nextPage).toBe(10); + expect(store.state.pageInfo.previousPage).toBe(10); + }); + }); + + describe('formatGroupItem', () => { + it('should parse group item object and return updated object', () => { + let store; + let updatedGroupItem; + + store = new GroupsStore(); + updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); + expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy(); + expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count); + expect(updatedGroupItem.isChildrenLoading).toBe(false); + expect(updatedGroupItem.isBeingRemoved).toBe(false); + + store = new GroupsStore(true); + updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); + expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy(); + expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count); + }); + }); + + describe('removeGroup', () => { + it('should remove children from group item in state', () => { + const store = new GroupsStore(); + const rawParentGroup = Object.assign({}, mockGroups[0]); + const rawChildGroup = Object.assign({}, mockGroups[1]); + + store.setGroups([rawParentGroup]); + store.setGroupChildren(store.state.groups[0], [rawChildGroup]); + const childItem = store.state.groups[0].children[0]; + + store.removeGroup(childItem, store.state.groups[0]); + expect(store.state.groups[0].children.length).toBe(0); + }); + }); +}); -- cgit v1.2.1 From 08383fd2e32b88bba1429cf9b03b493dfc6b9b3e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 4 Oct 2017 16:13:16 +0200 Subject: Make it possible to limit ancestors in a `GroupHierarchy` Passing a parent_id will limit ancestors upto the specified parent if it is found. Using `ancestors` and `descendants` the `base` relation will not be included --- spec/lib/gitlab/group_hierarchy_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'spec') diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 8dc83a6db7f..30686634af4 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -18,6 +18,12 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1) end + it 'can find ancestors upto a certain level' do + relation = described_class.new(Group.where(id: child2)).base_and_ancestors(upto: child1) + + expect(relation).to contain_exactly(child2) + end + it 'uses ancestors_base #initialize argument' do relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors @@ -55,6 +61,28 @@ describe Gitlab::GroupHierarchy, :postgresql do end end + describe '#descendants' do + it 'includes only the descendants' do + relation = described_class.new(Group.where(id: parent)).descendants + + expect(relation).to contain_exactly(child1, child2) + end + end + + describe '#ancestors' do + it 'includes only the ancestors' do + relation = described_class.new(Group.where(id: child2)).ancestors + + expect(relation).to contain_exactly(child1, parent) + end + + it 'can find ancestors upto a certain level' do + relation = described_class.new(Group.where(id: child2)).ancestors(upto: child1) + + expect(relation).to be_empty + end + end + describe '#all_groups' do let(:relation) do described_class.new(Group.where(id: child1.id)).all_groups -- cgit v1.2.1 From 06e00913f505268e0a45f4f1516a93a84600c242 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 4 Oct 2017 16:56:42 +0200 Subject: Move merging of Hashes out of the `GroupDescendant` concern Since it can technically merge any hash with objects that respond to `==` --- spec/lib/gitlab/utils/merge_hash_spec.rb | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 spec/lib/gitlab/utils/merge_hash_spec.rb (limited to 'spec') diff --git a/spec/lib/gitlab/utils/merge_hash_spec.rb b/spec/lib/gitlab/utils/merge_hash_spec.rb new file mode 100644 index 00000000000..4fa7bb31301 --- /dev/null +++ b/spec/lib/gitlab/utils/merge_hash_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +describe Gitlab::Utils::MergeHash do + describe '.crush' do + it 'can flatten a hash to each element' do + input = { hello: "world", this: { crushes: ["an entire", "hash"] } } + expected_result = [:hello, "world", :this, :crushes, "an entire", "hash"] + + expect(described_class.crush(input)).to eq(expected_result) + end + end + + describe '.elements' do + it 'deep merges an array of elements' do + input = [{ hello: ["world"] }, + { hello: "Everyone" }, + { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzień dobry'] } }, + "Goodbye", "Hallo"] + expected_output = [ + { + hello: + [ + "world", + "Everyone", + { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzień dobry'] } + ] + }, + "Goodbye" + ] + + expect(described_class.merge(input)).to eq(expected_output) + end + end +end -- cgit v1.2.1 From 57bd3bb34a19bf812fd6a74f394a69c491b05dd0 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 4 Oct 2017 16:57:33 +0200 Subject: Force parents to be preloaded for building a hierarchy --- spec/models/concerns/group_descendant_spec.rb | 38 +++++++++++++++---------- spec/serializers/group_child_serializer_spec.rb | 8 +++--- 2 files changed, 27 insertions(+), 19 deletions(-) (limited to 'spec') diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb index c17c8f2abec..c163fb01a81 100644 --- a/spec/models/concerns/group_descendant_spec.rb +++ b/spec/models/concerns/group_descendant_spec.rb @@ -5,6 +5,10 @@ describe GroupDescendant, :nested_groups do let(:subgroup) { create(:group, parent: parent) } let(:subsub_group) { create(:group, parent: subgroup) } + def all_preloaded_groups(*groups) + groups + [parent, subgroup, subsub_group] + end + context 'for a group' do describe '#hierarchy' do it 'only queries once for the ancestors' do @@ -19,9 +23,8 @@ describe GroupDescendant, :nested_groups do it 'only queries once for the ancestors when a top is given' do test_group = create(:group, parent: subsub_group).reload - query_count = ActiveRecord::QueryRecorder.new { test_group.hierarchy(subgroup) }.count - - expect(query_count).to eq(1) + recorder = ActiveRecord::QueryRecorder.new { test_group.hierarchy(subgroup) } + expect(recorder.count).to eq(1) end it 'builds a hierarchy for a group' do @@ -37,7 +40,8 @@ describe GroupDescendant, :nested_groups do end it 'raises an error if specifying a base that is not part of the tree' do - expect { subsub_group.hierarchy(build_stubbed(:group)) }.to raise_error('specified top is not part of the tree') + expect { subsub_group.hierarchy(build_stubbed(:group)) } + .to raise_error('specified top is not part of the tree') end end @@ -46,7 +50,7 @@ describe GroupDescendant, :nested_groups do other_subgroup = create(:group, parent: parent) other_subsub_group = create(:group, parent: subgroup) - groups = [other_subgroup, subsub_group, other_subsub_group] + groups = all_preloaded_groups(other_subgroup, subsub_group, other_subsub_group) expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] } @@ -58,9 +62,9 @@ describe GroupDescendant, :nested_groups do other_subsub_group = create(:group, parent: subgroup) groups = [other_subgroup, subsub_group, other_subsub_group] + groups << subgroup # Add the parent as if it was preloaded expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] - expect(described_class.build_hierarchy(groups, parent)).to eq(expected_hierarchy) end @@ -69,11 +73,16 @@ describe GroupDescendant, :nested_groups do other_subgroup2 = create(:group, parent: parent) other_subsub_group = create(:group, parent: other_subgroup) - groups = [subsub_group, other_subgroup2, other_subsub_group] + groups = all_preloaded_groups(subsub_group, other_subgroup2, other_subsub_group, other_subgroup) expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup2, { other_subgroup => other_subsub_group }] } expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy) end + + it 'raises an error if not all elements were preloaded' do + expect { described_class.build_hierarchy([subsub_group]) } + .to raise_error('parent was not preloaded') + end end end @@ -81,7 +90,7 @@ describe GroupDescendant, :nested_groups do let(:project) { create(:project, namespace: subsub_group) } describe '#hierarchy' do - it 'builds a hierarchy for a group' do + it 'builds a hierarchy for a project' do expected_hierarchy = { parent => { subgroup => { subsub_group => project } } } expect(project.hierarchy).to eq(expected_hierarchy) @@ -92,10 +101,6 @@ describe GroupDescendant, :nested_groups do expect(project.hierarchy(subgroup)).to eq(expected_hierarchy) end - - it 'raises an error if specifying a base that is not part of the tree' do - expect { project.hierarchy(build_stubbed(:group)) }.to raise_error('specified top is not part of the tree') - end end describe '.build_hierarchy' do @@ -103,7 +108,7 @@ describe GroupDescendant, :nested_groups do other_project = create(:project, namespace: parent) other_subgroup_project = create(:project, namespace: subgroup) - elements = [other_project, subsub_group, other_subgroup_project] + elements = all_preloaded_groups(other_project, subsub_group, other_subgroup_project) expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] } @@ -115,6 +120,7 @@ describe GroupDescendant, :nested_groups do other_subgroup_project = create(:project, namespace: subgroup) elements = [other_project, subsub_group, other_subgroup_project] + elements << subgroup # Added as if it was preloaded expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }] @@ -136,7 +142,9 @@ describe GroupDescendant, :nested_groups do other_subgroup = create(:group, parent: parent) other_subproject = create(:project, namespace: other_subgroup) - projects = [project, subsubsub_project, sub_project, other_subproject, subsub_project] + elements = [project, subsubsub_project, sub_project, other_subproject, subsub_project] + # Add parent groups as if they were preloaded + elements += [other_subgroup, subsubsub_group, subsub_group, subgroup] expected_hierarchy = [ project, @@ -149,7 +157,7 @@ describe GroupDescendant, :nested_groups do { other_subgroup => other_subproject } ] - actual_hierarchy = described_class.build_hierarchy(projects, parent) + actual_hierarchy = described_class.build_hierarchy(elements, parent) expect(actual_hierarchy).to eq(expected_hierarchy) end diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb index e5896f54dd7..30333386058 100644 --- a/spec/serializers/group_child_serializer_spec.rb +++ b/spec/serializers/group_child_serializer_spec.rb @@ -27,7 +27,7 @@ describe GroupChildSerializer do subgroup = create(:group, parent: parent) subsub_group = create(:group, parent: subgroup) - json = serializer.represent(subsub_group) + json = serializer.represent([subgroup, subsub_group]).first subsub_group_json = json[:children].first expect(json[:id]).to eq(subgroup.id) @@ -41,7 +41,7 @@ describe GroupChildSerializer do subgroup2 = create(:group, parent: parent) subsub_group2 = create(:group, parent: subgroup2) - json = serializer.represent([subsub_group1, subsub_group2]) + json = serializer.represent([subgroup1, subsub_group1, subgroup1, subgroup2]) subgroup1_json = json.first subsub_group1_json = subgroup1_json[:children].first @@ -58,7 +58,7 @@ describe GroupChildSerializer do it 'can render a tree' do subgroup = create(:group, parent: parent) - json = serializer.represent([subgroup]) + json = serializer.represent([parent, subgroup]) parent_json = json.first expect(parent_json[:id]).to eq(parent.id) @@ -89,7 +89,7 @@ describe GroupChildSerializer do subgroup2 = create(:group, parent: parent) project2 = create(:project, namespace: subgroup2) - json = serializer.represent([project1, project2]) + json = serializer.represent([project1, project2, subgroup1, subgroup2]) project1_json, project2_json = json.map { |group_json| group_json[:children].first } expect(json.size).to eq(2) -- cgit v1.2.1 From dda023d66d09b8a3a43a5599bde42ac52eb6fd06 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 4 Oct 2017 22:26:51 +0200 Subject: Optimize queries and pagination in `GroupDescendantsFinder` --- spec/lib/gitlab/multi_collection_paginator_spec.rb | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/lib/gitlab/multi_collection_paginator_spec.rb (limited to 'spec') diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb new file mode 100644 index 00000000000..385cfa63dda --- /dev/null +++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::MultiCollectionPaginator do + subject(:paginator) { described_class.new(Project.all, Group.all, per_page: 3) } + + it 'combines both collections' do + project = create(:project) + group = create(:group) + + expect(paginator.paginate(1)).to eq([project, group]) + end + + it 'includes elements second collection if first collection is empty' do + group = create(:group) + + expect(paginator.paginate(1)).to eq([group]) + end + + context 'with a full first page' do + let!(:all_groups) { create_list(:group, 4) } + let!(:all_projects) { create_list(:project, 4) } + + it 'knows the total count of the collection' do + expect(paginator.total_count).to eq(8) + end + + it 'fills the first page with elements of the first collection' do + expect(paginator.paginate(1)).to eq(all_projects.take(3)) + end + + it 'fils the second page with a mixture of of the first & second collection' do + first_collection_element = all_projects.last + second_collection_elements = all_groups.take(2) + + expected_collection = [first_collection_element] + second_collection_elements + + expect(paginator.paginate(2)).to eq(expected_collection) + end + + it 'fils the last page with elements from the second collection' do + expected_collection = all_groups[-2..-1] + + expect(paginator.paginate(3)).to eq(expected_collection) + end + end +end -- cgit v1.2.1 From ec8a7a36c09f44c44a21444f632389e7d08166cf Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 4 Oct 2017 23:04:47 +0200 Subject: Make sure all ancestors are loaded when searching groups --- spec/controllers/concerns/group_tree_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb index 5d4fb66b492..2fe041a5ecc 100644 --- a/spec/controllers/concerns/group_tree_spec.rb +++ b/spec/controllers/concerns/group_tree_spec.rb @@ -45,12 +45,12 @@ describe GroupTree do expect(assigns(:groups)).to contain_exactly(subgroup) end - it 'allows filtering for subgroups' do + it 'allows filtering for subgroups and includes the parents for rendering' do subgroup = create(:group, :public, parent: group, name: 'filter') get :index, filter: 'filt', format: :json - expect(assigns(:groups)).to contain_exactly(subgroup) + expect(assigns(:groups)).to contain_exactly(group, subgroup) end end -- cgit v1.2.1 From 951abe2b2efc3a208ceea46d9c1c47d3d253ff63 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 5 Oct 2017 10:32:52 +0200 Subject: Load counts everywhere we render a group tree --- spec/finders/group_descendants_finder_spec.rb | 12 ---------- spec/models/concerns/loaded_in_group_list_spec.rb | 28 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 spec/models/concerns/loaded_in_group_list_spec.rb (limited to 'spec') diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 7b9dfcbfad0..86a7a793457 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -46,18 +46,6 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(subgroup, project) end - it 'includes the preloaded counts for groups' do - create(:group, parent: subgroup) - create(:project, namespace: subgroup) - subgroup.add_developer(create(:user)) - - found_group = finder.execute.detect { |child| child.is_a?(Group) } - - expect(found_group.preloaded_project_count).to eq(1) - expect(found_group.preloaded_subgroup_count).to eq(1) - expect(found_group.preloaded_member_count).to eq(1) - end - it 'does not include subgroups the user does not have access to' do subgroup.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb new file mode 100644 index 00000000000..d64b288aa0c --- /dev/null +++ b/spec/models/concerns/loaded_in_group_list_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe LoadedInGroupList do + let(:parent) { create(:group) } + subject(:found_group) { Group.with_selects_for_list.find_by(id: parent.id) } + + before do + create(:group, parent: parent) + create(:project, namespace: parent) + parent.add_developer(create(:user)) + end + + describe '.with_selects_for_list' do + it 'includes the preloaded counts for groups' do + found_group = Group.with_selects_for_list.find_by(id: parent.id) + + expect(found_group.preloaded_project_count).to eq(1) + expect(found_group.preloaded_subgroup_count).to eq(1) + expect(found_group.preloaded_member_count).to eq(1) + end + end + + describe '#children_count' do + it 'counts groups and projects' do + expect(found_group.children_count).to eq(2) + end + end +end -- cgit v1.2.1 From e013d39875bbf5f6e11fda627a8dab045023d59e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 5 Oct 2017 10:38:05 +0200 Subject: Optimize finding a membership for a user to avoid extra queries --- spec/serializers/group_child_entity_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 54a1b1846e1..64000385781 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -34,7 +34,7 @@ describe GroupChildEntity do end describe 'for a project' do - set(:object) do + let(:object) do create(:project, :with_avatar, description: 'Awesomeness') end @@ -55,7 +55,7 @@ describe GroupChildEntity do end describe 'for a group', :nested_groups do - set(:object) do + let(:object) do create(:group, :nested, :with_avatar, description: 'Awesomeness') end -- cgit v1.2.1 From b3acd5459c08f61b82799821ae09f17f4dcfec10 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 5 Oct 2017 10:47:32 +0200 Subject: Use `alias_attribute` & `alias_method` to define parent-methods --- spec/models/project_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'spec') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 176bb568cbe..def2e0983ac 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2095,6 +2095,13 @@ describe Project do it { expect(project.parent).to eq(project.namespace) } end + describe '#parent_id' do + let(:project) { create(:project) } + + it { expect(project.parent_id).to eq(project.namespace_id) } + end + + describe '#parent_changed?' do let(:project) { create(:project) } -- cgit v1.2.1 From 17dccc35ca4754405b87c87f81daec34e02ea7a1 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 5 Oct 2017 13:13:24 +0200 Subject: Update feature specs for updated group lists --- spec/features/dashboard/groups_list_spec.rb | 65 ++++++++++++++++------------- spec/features/explore/groups_list_spec.rb | 2 - spec/features/groups_spec.rb | 17 +++----- 3 files changed, 43 insertions(+), 41 deletions(-) (limited to 'spec') diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index 9cfef46d346..c9d9371f5ab 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -6,8 +6,11 @@ feature 'Dashboard Groups page', :js do let!(:nested_group) { create(:group, :nested) } let!(:another_group) { create(:group) } - before do - pending('Update for new group tree') + def click_group_caret(group) + within("#group-#{group.id}") do + first('.folder-caret').click + end + wait_for_requests end it 'shows groups user is member of' do @@ -18,9 +21,13 @@ feature 'Dashboard Groups page', :js do visit dashboard_groups_path wait_for_requests - expect(page).to have_content(group.full_name) - expect(page).to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) + expect(page).to have_content(group.name) + expect(page).to have_content(nested_group.parent.name) + + click_group_caret(nested_group.parent) + expect(page).to have_content(nested_group.name) + + expect(page).not_to have_content(another_group.name) end describe 'when filtering groups' do @@ -33,13 +40,14 @@ feature 'Dashboard Groups page', :js do visit dashboard_groups_path end - it 'filters groups' do - fill_in 'filter', with: group.name + it 'expands when filtering groups' do + fill_in 'filter', with: nested_group.name wait_for_requests - expect(page).to have_content(group.full_name) - expect(page).not_to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) + expect(page).not_to have_content(group.name) + expect(page).to have_content(nested_group.parent.name) + expect(page).to have_content(nested_group.name) + expect(page).not_to have_content(another_group.name) end it 'resets search when user cleans the input' do @@ -49,9 +57,9 @@ feature 'Dashboard Groups page', :js do fill_in 'filter', with: '' wait_for_requests - expect(page).to have_content(group.full_name) - expect(page).to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) + expect(page).to have_content(group.name) + expect(page).to have_content(nested_group.parent.name) + expect(page).not_to have_content(another_group.name) expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 end end @@ -69,28 +77,29 @@ feature 'Dashboard Groups page', :js do end it 'shows subgroups inside of its parent group' do - expect(page).to have_selector('.groups-list-tree-container .group-list-tree', count: 2) - expect(page).to have_selector(".groups-list-tree-container #group-#{group.id} #group-#{subgroup.id}", count: 1) + expect(page).to have_selector("#group-#{group.id}") + click_group_caret(group) + expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") end it 'can toggle parent group' do - # Expanded by default - expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1) - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right") + # Collapsed by default + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1) + expect(page).to have_selector("#group-#{group.id} .fa-caret-right") - # Collapse - find("#group-#{group.id}").trigger('click') + # expand + click_group_caret(group) - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down") - expect(page).to have_selector("#group-#{group.id} .fa-caret-right", count: 1) - expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}") + expect(page).to have_selector("#group-#{group.id} .fa-caret-down") + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right", count: 1) + expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") - # Expand - find("#group-#{group.id}").trigger('click') + # collapse + click_group_caret(group) - expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1) - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right") - expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1) + expect(page).to have_selector("#group-#{group.id} .fa-caret-right") + expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}") end end diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 41778542e23..801a33979ff 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -8,8 +8,6 @@ describe 'Explore Groups page', :js do let!(:empty_project) { create(:project, group: public_group) } before do - pending('Update for new group tree') - group.add_owner(user) sign_in(user) diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 493dd551d25..da2edd8b980 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -90,11 +90,7 @@ feature 'Group' do context 'as admin' do before do - visit group_path(group) - - pending('use the new subgroup button') - - click_link 'New Subgroup' + visit new_group_path(group, parent_id: group.id) end it 'creates a nested group' do @@ -114,11 +110,8 @@ feature 'Group' do sign_out(:user) sign_in(user) - visit group_path(group) - - pending('use the new subgroup button') + visit new_group_path(group, parent_id: group.id) - click_link 'New Subgroup' fill_in 'Group path', with: 'bar' click_button 'Create group' @@ -206,13 +199,15 @@ feature 'Group' do describe 'group page with nested groups', :nested_groups, js: true do let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } + let!(:project) { create(:project, namespace: group) } let!(:path) { group_path(group) } - it 'has nested groups tab with nested groups inside' do - pending('the child should be visible on the show page') + it 'it renders projects and groups on the page' do visit path + wait_for_requests expect(page).to have_content(nested_group.name) + expect(page).to have_content(project.name) end end -- cgit v1.2.1 From 4e88ca12060df0c6e73a1eed74d6a7f6e42d7b58 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 5 Oct 2017 14:23:01 +0200 Subject: Hide "New subgroup" links when subgroups are not supported --- spec/features/groups/show_spec.rb | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'spec') diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 303013e59d5..501086fce80 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -24,4 +24,34 @@ feature 'Group show page' do it_behaves_like "an autodiscoverable RSS feed without an RSS token" end + + context 'subgroup support' do + let(:user) { create(:user) } + + before do + group.add_owner(user) + sign_in(user) + end + context 'when subgroups are supported', :js do + before do + allow(Group).to receive(:supports_nested_groups?) { true } + visit path + end + + it 'allows creating subgroups' do + expect(page).to have_css("li[data-text='New subgroup']", visible: false) + end + end + + context 'when subgroups are not supported' do + before do + allow(Group).to receive(:supports_nested_groups?) { false } + visit path + end + + it 'allows creating subgroups' do + expect(page).not_to have_selector("li[data-text='New subgroup']", visible: false) + end + end + end end -- cgit v1.2.1 From 524f65152fde2591a52d4c58d14c643ce379ec5b Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 9 Oct 2017 12:02:40 +0200 Subject: Only expand ancestors when searching Not all_groups, since that would expose groups the user does not have access to --- spec/controllers/concerns/group_tree_spec.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb index 2fe041a5ecc..ba84fbf8564 100644 --- a/spec/controllers/concerns/group_tree_spec.rb +++ b/spec/controllers/concerns/group_tree_spec.rb @@ -9,7 +9,7 @@ describe GroupTree do include GroupTree # rubocop:disable RSpec/DescribedClass def index - render_group_tree Group.all + render_group_tree GroupsFinder.new(current_user).execute end end @@ -52,6 +52,17 @@ describe GroupTree do expect(assigns(:groups)).to contain_exactly(group, subgroup) end + + it 'does not include groups the user does not have access to' do + parent = create(:group, :private) + subgroup = create(:group, :private, parent: parent, name: 'filter') + subgroup.add_developer(user) + _other_subgroup = create(:group, :private, parent: parent, name: 'filte') + + get :index, filter: 'filt', format: :json + + expect(assigns(:groups)).to contain_exactly(parent, subgroup) + end end context 'json content' do -- cgit v1.2.1 From deb45634ae841d64d1756c4cc0dc3c442e171ba9 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 10 Oct 2017 13:33:02 +0200 Subject: Use `EXISTS` instead of `WHERE id IN (...)` for authorized groups --- spec/finders/group_descendants_finder_spec.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 86a7a793457..1aef49613ee 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -39,7 +39,7 @@ describe GroupDescendantsFinder do context 'with nested groups', :nested_groups do let!(:project) { create(:project, namespace: group) } - let!(:subgroup) { create(:group, parent: group) } + let!(:subgroup) { create(:group, :private, parent: group) } describe '#execute' do it 'contains projects and subgroups' do @@ -59,6 +59,15 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(public_subgroup, other_subgroup) end + it 'only includes public groups when no user is given' do + public_subgroup = create(:group, :public, parent: group) + _private_subgroup = create(:group, :private, parent: group) + + finder = described_class.new(current_user: nil, parent_group: group) + + expect(finder.execute).to contain_exactly(public_subgroup) + end + context 'with a filter' do let(:params) { { filter: 'test' } } @@ -86,7 +95,7 @@ describe GroupDescendantsFinder do context 'with matching children' do it 'includes a group that has a subgroup matching the query and its parent' do - matching_subgroup = create(:group, name: 'testgroup', parent: subgroup) + matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup) expect(finder.execute).to contain_exactly(subgroup, matching_subgroup) end -- cgit v1.2.1 From aee5691db3ec411c242e050aaa11ebb44f07f164 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 10 Oct 2017 14:11:55 +0200 Subject: Don't load unneeded elements in GroupsController#show --- spec/controllers/groups_controller_spec.rb | 89 +++++++++++++-------------- spec/finders/group_descendants_finder_spec.rb | 16 +++++ 2 files changed, 60 insertions(+), 45 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 8582f31f059..f914fd6f20a 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -150,51 +150,6 @@ describe GroupsController do end end - describe 'GET #show' do - context 'pagination' do - let(:per_page) { 3 } - - before do - allow(Kaminari.config).to receive(:default_per_page).and_return(per_page) - end - - context 'with only projects' do - let!(:other_project) { create(:project, :public, namespace: group) } - let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) } - - it 'has projects on the first page' do - get :show, id: group.to_param, sort: 'id_desc' - - expect(assigns(:children)).to contain_exactly(*first_page_projects) - end - - it 'has projects on the second page' do - get :show, id: group.to_param, sort: 'id_desc', page: 2 - - expect(assigns(:children)).to contain_exactly(other_project) - end - end - - context 'with subgroups and projects', :nested_groups do - let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } - let!(:other_subgroup) { create(:group, :public, parent: group) } - let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) } - - it 'contains all subgroups' do - get :children, id: group.to_param, sort: 'id_asc', format: :json - - expect(assigns(:children)).to contain_exactly(*first_page_subgroups) - end - - it 'contains the project and group on the second page' do - get :children, id: group.to_param, sort: 'id_asc', page: 2, format: :json - - expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) - end - end - end - end - describe 'GET #children' do context 'for projects' do let!(:public_project) { create(:project, :public, namespace: group) } @@ -420,6 +375,50 @@ describe GroupsController do end end end + + context 'pagination' do + let(:per_page) { 3 } + + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(per_page) + end + + context 'with only projects' do + let!(:other_project) { create(:project, :public, namespace: group) } + let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) } + + it 'has projects on the first page' do + get :children, id: group.to_param, sort: 'id_desc', format: :json + + expect(assigns(:children)).to contain_exactly(*first_page_projects) + end + + it 'has projects on the second page' do + get :children, id: group.to_param, sort: 'id_desc', page: 2, format: :json + + expect(assigns(:children)).to contain_exactly(other_project) + end + end + + context 'with subgroups and projects', :nested_groups do + let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } + let!(:other_subgroup) { create(:group, :public, parent: group) } + let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) } + + it 'contains all subgroups' do + get :children, id: group.to_param, sort: 'id_asc', format: :json + + expect(assigns(:children)).to contain_exactly(*first_page_subgroups) + end + + it 'contains the project and group on the second page' do + get :children, id: group.to_param, sort: 'id_asc', page: 2, format: :json + + expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) + end + end + end + end describe 'GET #issues' do diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 1aef49613ee..4a5bdd84508 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -12,6 +12,22 @@ describe GroupDescendantsFinder do group.add_owner(user) end + describe '#has_children?' do + it 'is true when there are projects' do + create(:project, namespace: group) + + expect(finder.has_children?).to be_truthy + end + + context 'when there are subgroups', :nested_groups do + it 'is true when there are projects' do + create(:group, parent: group) + + expect(finder.has_children?).to be_truthy + end + end + end + describe '#execute' do it 'includes projects' do project = create(:project, namespace: group) -- cgit v1.2.1 From 9d1348d66838b4c5e25ba133d486239482973fca Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 10 Oct 2017 15:45:35 +0200 Subject: Move the `ancestors_upto` to `Project` and `Namespace` --- spec/models/namespace_spec.rb | 14 ++++++++++++++ spec/models/project_spec.rb | 15 +++++++++++++++ 2 files changed, 29 insertions(+) (limited to 'spec') diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 81d5ab7a6d3..4ce9f1b02e3 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -151,6 +151,20 @@ describe Namespace do end end + describe '#ancestors_upto', :nested_groups do + let(:parent) { create(:group) } + let(:child) { create(:group, parent: parent) } + let(:child2) { create(:group, parent: child) } + + it 'returns all ancestors when no namespace is given' do + expect(child2.ancestors_upto).to contain_exactly(child, parent) + end + + it 'includes ancestors upto but excluding the given ancestor' do + expect(child2.ancestors_upto(parent)).to contain_exactly(child) + end + end + describe '#move_dir' do before do @namespace = create :namespace diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index def2e0983ac..dedf3008994 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1731,6 +1731,21 @@ describe Project do it { expect(project.gitea_import?).to be true } end + describe '#ancestors_upto', :nested_groups do + let(:parent) { create(:group) } + let(:child) { create(:group, parent: parent) } + let(:child2) { create(:group, parent: child) } + let(:project) { create(:project, namespace: child2) } + + it 'returns all ancestors when no namespace is given' do + expect(project.ancestors_upto).to contain_exactly(child2, child, parent) + end + + it 'includes ancestors upto but excluding the given ancestor' do + expect(project.ancestors_upto(parent)).to contain_exactly(child2, child) + end + end + describe '#lfs_enabled?' do let(:project) { create(:project) } -- cgit v1.2.1 From 5a903149e75465e4025f154977597aeef94b618c Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 11 Oct 2017 10:17:24 +0200 Subject: Handle archived projects in the `GroupDescendantsFinder` --- spec/finders/group_descendants_finder_spec.rb | 32 ++++++++++++++++++++++ spec/models/concerns/loaded_in_group_list_spec.rb | 33 ++++++++++++++++++----- 2 files changed, 59 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 4a5bdd84508..074914420a1 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -35,6 +35,28 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(project) end + context 'when archived is `true`' do + let(:params) { { archived: 'true' } } + + it 'includes archived projects' do + archived_project = create(:project, namespace: group, archived: true) + project = create(:project, namespace: group) + + expect(finder.execute).to contain_exactly(archived_project, project) + end + end + + context 'when archived is `only`' do + let(:params) { { archived: 'only' } } + + it 'includes only archived projects' do + archived_project = create(:project, namespace: group, archived: true) + _project = create(:project, namespace: group) + + expect(finder.execute).to contain_exactly(archived_project) + end + end + it 'does not include archived projects' do _archived_project = create(:project, :archived, namespace: group) @@ -84,6 +106,16 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(public_subgroup) end + context 'when archived is `true`' do + let(:params) { { archived: 'true' } } + + it 'includes archived projects in the count of subgroups' do + create(:project, namespace: subgroup, archived: true) + + expect(finder.execute.first.preloaded_project_count).to eq(1) + end + end + context 'with a filter' do let(:params) { { filter: 'test' } } diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb index d64b288aa0c..bf5bfaa76de 100644 --- a/spec/models/concerns/loaded_in_group_list_spec.rb +++ b/spec/models/concerns/loaded_in_group_list_spec.rb @@ -4,24 +4,45 @@ describe LoadedInGroupList do let(:parent) { create(:group) } subject(:found_group) { Group.with_selects_for_list.find_by(id: parent.id) } - before do - create(:group, parent: parent) - create(:project, namespace: parent) - parent.add_developer(create(:user)) - end - describe '.with_selects_for_list' do it 'includes the preloaded counts for groups' do + create(:group, parent: parent) + create(:project, namespace: parent) + parent.add_developer(create(:user)) + found_group = Group.with_selects_for_list.find_by(id: parent.id) expect(found_group.preloaded_project_count).to eq(1) expect(found_group.preloaded_subgroup_count).to eq(1) expect(found_group.preloaded_member_count).to eq(1) end + + context 'with archived projects' do + it 'counts including archived projects when `true` is passed' do + create(:project, namespace: parent, archived: true) + create(:project, namespace: parent) + + found_group = Group.with_selects_for_list('true').find_by(id: parent.id) + + expect(found_group.preloaded_project_count).to eq(2) + end + + it 'counts only archived projects when `only` is passed' do + create_list(:project, 2, namespace: parent, archived: true) + create(:project, namespace: parent) + + found_group = Group.with_selects_for_list('only').find_by(id: parent.id) + + expect(found_group.preloaded_project_count).to eq(2) + end + end end describe '#children_count' do it 'counts groups and projects' do + create(:group, parent: parent) + create(:project, namespace: parent) + expect(found_group.children_count).to eq(2) end end -- cgit v1.2.1 From 2c6c2ed6faded64277565b4c15455fe813753b8a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 11 Oct 2017 10:26:05 +0200 Subject: Always use the same order specs for `MultiCollectionPaginator` --- spec/lib/gitlab/multi_collection_paginator_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb index 385cfa63dda..68bd4f93159 100644 --- a/spec/lib/gitlab/multi_collection_paginator_spec.rb +++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::MultiCollectionPaginator do - subject(:paginator) { described_class.new(Project.all, Group.all, per_page: 3) } + subject(:paginator) { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 3) } it 'combines both collections' do project = create(:project) -- cgit v1.2.1 From bd8943f5adfc377491bedb2a794d8c39b2b4c45e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 11 Oct 2017 11:22:49 +0200 Subject: Fix spinach features And several other failures --- spec/controllers/groups_controller_spec.rb | 1 - spec/models/project_spec.rb | 1 - spec/serializers/group_child_serializer_spec.rb | 1 - 3 files changed, 3 deletions(-) (limited to 'spec') diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index f914fd6f20a..827c4cd3d19 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -418,7 +418,6 @@ describe GroupsController do end end end - end describe 'GET #issues' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 984832b6959..74eba7e33f6 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2199,7 +2199,6 @@ describe Project do it { expect(project.parent_id).to eq(project.namespace_id) } end - describe '#parent_changed?' do let(:project) { create(:project) } diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb index 30333386058..566b235769e 100644 --- a/spec/serializers/group_child_serializer_spec.rb +++ b/spec/serializers/group_child_serializer_spec.rb @@ -39,7 +39,6 @@ describe GroupChildSerializer do subgroup1 = create(:group, parent: parent) subsub_group1 = create(:group, parent: subgroup1) subgroup2 = create(:group, parent: parent) - subsub_group2 = create(:group, parent: subgroup2) json = serializer.represent([subgroup1, subsub_group1, subgroup1, subgroup2]) subgroup1_json = json.first -- cgit v1.2.1 From 8cde1e3285c870c85bee3a9a9ff4b8e5f53cff86 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 11 Oct 2017 14:39:23 +0200 Subject: Use polymorphism for common attributes in `GroupChildEntity` --- spec/serializers/group_child_entity_spec.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index 64000385781..452754d7a79 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe GroupChildEntity do + include Gitlab::Routing.url_helpers + let(:user) { create(:user) } let(:request) { double('request') } let(:entity) { described_class.new(object, request: request) } @@ -24,7 +26,6 @@ describe GroupChildEntity do type can_edit visibility - edit_path permission relative_path].each do |attribute| it "includes #{attribute}" do @@ -51,6 +52,10 @@ describe GroupChildEntity do expect(json[:star_count]).to be_present end + it 'has the correct edit path' do + expect(json[:edit_path]).to eq(edit_project_path(object)) + end + it_behaves_like 'group child json' end @@ -87,6 +92,10 @@ describe GroupChildEntity do expect(json[:can_leave]).to be_truthy end + it 'has the correct edit path' do + expect(json[:edit_path]).to eq(edit_group_path(object)) + end + it_behaves_like 'group child json' end end -- cgit v1.2.1 From 18907efbc9209ee39d8348eef5b3c7321b9aca26 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 11 Oct 2017 17:05:39 +0200 Subject: Pass `archived:` as a keyword argument --- spec/models/concerns/loaded_in_group_list_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb index bf5bfaa76de..7a279547a3a 100644 --- a/spec/models/concerns/loaded_in_group_list_spec.rb +++ b/spec/models/concerns/loaded_in_group_list_spec.rb @@ -22,7 +22,7 @@ describe LoadedInGroupList do create(:project, namespace: parent, archived: true) create(:project, namespace: parent) - found_group = Group.with_selects_for_list('true').find_by(id: parent.id) + found_group = Group.with_selects_for_list(archived: 'true').find_by(id: parent.id) expect(found_group.preloaded_project_count).to eq(2) end @@ -31,7 +31,7 @@ describe LoadedInGroupList do create_list(:project, 2, namespace: parent, archived: true) create(:project, namespace: parent) - found_group = Group.with_selects_for_list('only').find_by(id: parent.id) + found_group = Group.with_selects_for_list(archived: 'only').find_by(id: parent.id) expect(found_group.preloaded_project_count).to eq(2) end -- cgit v1.2.1 From 2c25a7ae3453e72ad6cab504255e327c17df0a95 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 11 Oct 2017 18:27:53 +0200 Subject: Nest the group_children_path inside the canonical group path --- .../controllers/groups/children_controller_spec.rb | 277 +++++++++++++++++++++ spec/controllers/groups_controller_spec.rb | 270 -------------------- 2 files changed, 277 insertions(+), 270 deletions(-) create mode 100644 spec/controllers/groups/children_controller_spec.rb (limited to 'spec') diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb new file mode 100644 index 00000000000..f15a12ef7fd --- /dev/null +++ b/spec/controllers/groups/children_controller_spec.rb @@ -0,0 +1,277 @@ +require 'spec_helper' + +describe Groups::ChildrenController do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + let!(:group_member) { create(:group_member, group: group, user: user) } + + describe 'GET #index' do + context 'for projects' do + let!(:public_project) { create(:project, :public, namespace: group) } + let!(:private_project) { create(:project, :private, namespace: group) } + + context 'as a user' do + before do + sign_in(user) + end + + it 'shows all children' do + get :index, group_id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_project, private_project) + end + + context 'being member of private subgroup' do + it 'shows public and private children the user is member of' do + group_member.destroy! + private_project.add_guest(user) + + get :index, group_id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_project, private_project) + end + end + end + + context 'as a guest' do + it 'shows the public children' do + get :index, group_id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_project) + end + end + end + + context 'for subgroups', :nested_groups do + let!(:public_subgroup) { create(:group, :public, parent: group) } + let!(:private_subgroup) { create(:group, :private, parent: group) } + let!(:public_project) { create(:project, :public, namespace: group) } + let!(:private_project) { create(:project, :private, namespace: group) } + + context 'as a user' do + before do + sign_in(user) + end + + it 'shows all children' do + get :index, group_id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project) + end + + context 'being member of private subgroup' do + it 'shows public and private children the user is member of' do + group_member.destroy! + private_subgroup.add_guest(user) + private_project.add_guest(user) + + get :index, group_id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project) + end + end + end + + context 'as a guest' do + it 'shows the public children' do + get :index, group_id: group.to_param, format: :json + + expect(assigns(:children)).to contain_exactly(public_subgroup, public_project) + end + end + + context 'filtering children' do + it 'expands the tree for matching projects' do + project = create(:project, :public, namespace: public_subgroup, name: 'filterme') + + get :index, group_id: group.to_param, filter: 'filter', format: :json + + group_json = json_response.first + project_json = group_json['children'].first + + expect(group_json['id']).to eq(public_subgroup.id) + expect(project_json['id']).to eq(project.id) + end + + it 'expands the tree for matching subgroups' do + matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') + + get :index, group_id: group.to_param, filter: 'filter', format: :json + + group_json = json_response.first + matched_group_json = group_json['children'].first + + expect(group_json['id']).to eq(public_subgroup.id) + expect(matched_group_json['id']).to eq(matched_group.id) + end + + it 'merges the trees correctly' do + shared_subgroup = create(:group, :public, parent: group, path: 'hardware') + matched_project_1 = create(:project, :public, namespace: shared_subgroup, name: 'mobile-soc') + + l2_subgroup = create(:group, :public, parent: shared_subgroup, path: 'broadcom') + l3_subgroup = create(:group, :public, parent: l2_subgroup, path: 'wifi-group') + matched_project_2 = create(:project, :public, namespace: l3_subgroup, name: 'mobile') + + get :index, group_id: group.to_param, filter: 'mobile', format: :json + + shared_group_json = json_response.first + expect(shared_group_json['id']).to eq(shared_subgroup.id) + + matched_project_1_json = shared_group_json['children'].detect { |child| child['type'] == 'project' } + expect(matched_project_1_json['id']).to eq(matched_project_1.id) + + l2_subgroup_json = shared_group_json['children'].detect { |child| child['type'] == 'group' } + expect(l2_subgroup_json['id']).to eq(l2_subgroup.id) + + l3_subgroup_json = l2_subgroup_json['children'].first + expect(l3_subgroup_json['id']).to eq(l3_subgroup.id) + + matched_project_2_json = l3_subgroup_json['children'].first + expect(matched_project_2_json['id']).to eq(matched_project_2.id) + end + + it 'expands the tree upto a specified parent' do + subgroup = create(:group, :public, parent: group) + l2_subgroup = create(:group, :public, parent: subgroup) + create(:project, :public, namespace: l2_subgroup, name: 'test') + + get :index, group_id: subgroup.to_param, filter: 'test', format: :json + + expect(response).to have_http_status(200) + end + + it 'returns an empty array when there are no search results' do + subgroup = create(:group, :public, parent: group) + l2_subgroup = create(:group, :public, parent: subgroup) + create(:project, :public, namespace: l2_subgroup, name: 'no-match') + + get :index, group_id: subgroup.to_param, filter: 'test', format: :json + + expect(json_response).to eq([]) + end + + it 'includes pagination headers' do + 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } + + get :index, group_id: group.to_param, filter: 'filter', per_page: 1, format: :json + + expect(response).to include_pagination_headers + end + end + + context 'queries per rendered element', :request_store do + # We need to make sure the following counts are preloaded + # otherwise they will cause an extra query + # 1. Count of visible projects in the element + # 2. Count of visible subgroups in the element + # 3. Count of members of a group + let(:expected_queries_per_group) { 0 } + let(:expected_queries_per_project) { 0 } + + def get_list + get :index, group_id: group.to_param, format: :json + end + + it 'queries the expected amount for a group row' do + control = ActiveRecord::QueryRecorder.new { get_list } + + _new_group = create(:group, :public, parent: group) + + expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) + end + + it 'queries the expected amount for a project row' do + control = ActiveRecord::QueryRecorder.new { get_list } + _new_project = create(:project, :public, namespace: group) + + expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project) + end + + context 'when rendering hierarchies' do + # When loading hierarchies we load the all the ancestors for matched projects + # in 1 separate query + let(:extra_queries_for_hierarchies) { 1 } + + def get_filtered_list + get :index, group_id: group.to_param, filter: 'filter', format: :json + end + + it 'queries the expected amount when nested rows are increased for a group' do + matched_group = create(:group, :public, parent: group, name: 'filterme') + + control = ActiveRecord::QueryRecorder.new { get_filtered_list } + + matched_group.update!(parent: public_subgroup) + + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) + end + + it 'queries the expected amount when a new group match is added' do + create(:group, :public, parent: public_subgroup, name: 'filterme') + + control = ActiveRecord::QueryRecorder.new { get_filtered_list } + + create(:group, :public, parent: public_subgroup, name: 'filterme2') + create(:group, :public, parent: public_subgroup, name: 'filterme3') + + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) + end + + it 'queries the expected amount when nested rows are increased for a project' do + matched_project = create(:project, :public, namespace: group, name: 'filterme') + + control = ActiveRecord::QueryRecorder.new { get_filtered_list } + + matched_project.update!(namespace: public_subgroup) + + expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) + end + end + end + end + + context 'pagination' do + let(:per_page) { 3 } + + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(per_page) + end + + context 'with only projects' do + let!(:other_project) { create(:project, :public, namespace: group) } + let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) } + + it 'has projects on the first page' do + get :index, group_id: group.to_param, sort: 'id_desc', format: :json + + expect(assigns(:children)).to contain_exactly(*first_page_projects) + end + + it 'has projects on the second page' do + get :index, group_id: group.to_param, sort: 'id_desc', page: 2, format: :json + + expect(assigns(:children)).to contain_exactly(other_project) + end + end + + context 'with subgroups and projects', :nested_groups do + let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } + let!(:other_subgroup) { create(:group, :public, parent: group) } + let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) } + + it 'contains all subgroups' do + get :index, group_id: group.to_param, sort: 'id_asc', format: :json + + expect(assigns(:children)).to contain_exactly(*first_page_subgroups) + end + + it 'contains the project and group on the second page' do + get :index, group_id: group.to_param, sort: 'id_asc', page: 2, format: :json + + expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) + end + end + end + end +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 827c4cd3d19..e7631d4d709 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -150,276 +150,6 @@ describe GroupsController do end end - describe 'GET #children' do - context 'for projects' do - let!(:public_project) { create(:project, :public, namespace: group) } - let!(:private_project) { create(:project, :private, namespace: group) } - - context 'as a user' do - before do - sign_in(user) - end - - it 'shows all children' do - get :children, id: group.to_param, format: :json - - expect(assigns(:children)).to contain_exactly(public_project, private_project) - end - - context 'being member of private subgroup' do - it 'shows public and private children the user is member of' do - group_member.destroy! - private_project.add_guest(user) - - get :children, id: group.to_param, format: :json - - expect(assigns(:children)).to contain_exactly(public_project, private_project) - end - end - end - - context 'as a guest' do - it 'shows the public children' do - get :children, id: group.to_param, format: :json - - expect(assigns(:children)).to contain_exactly(public_project) - end - end - end - - context 'for subgroups', :nested_groups do - let!(:public_subgroup) { create(:group, :public, parent: group) } - let!(:private_subgroup) { create(:group, :private, parent: group) } - let!(:public_project) { create(:project, :public, namespace: group) } - let!(:private_project) { create(:project, :private, namespace: group) } - - context 'as a user' do - before do - sign_in(user) - end - - it 'shows all children' do - get :children, id: group.to_param, format: :json - - expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project) - end - - context 'being member of private subgroup' do - it 'shows public and private children the user is member of' do - group_member.destroy! - private_subgroup.add_guest(user) - private_project.add_guest(user) - - get :children, id: group.to_param, format: :json - - expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project) - end - end - end - - context 'as a guest' do - it 'shows the public children' do - get :children, id: group.to_param, format: :json - - expect(assigns(:children)).to contain_exactly(public_subgroup, public_project) - end - end - - context 'filtering children' do - it 'expands the tree for matching projects' do - project = create(:project, :public, namespace: public_subgroup, name: 'filterme') - - get :children, id: group.to_param, filter: 'filter', format: :json - - group_json = json_response.first - project_json = group_json['children'].first - - expect(group_json['id']).to eq(public_subgroup.id) - expect(project_json['id']).to eq(project.id) - end - - it 'expands the tree for matching subgroups' do - matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme') - - get :children, id: group.to_param, filter: 'filter', format: :json - - group_json = json_response.first - matched_group_json = group_json['children'].first - - expect(group_json['id']).to eq(public_subgroup.id) - expect(matched_group_json['id']).to eq(matched_group.id) - end - - it 'merges the trees correctly' do - shared_subgroup = create(:group, :public, parent: group, path: 'hardware') - matched_project_1 = create(:project, :public, namespace: shared_subgroup, name: 'mobile-soc') - - l2_subgroup = create(:group, :public, parent: shared_subgroup, path: 'broadcom') - l3_subgroup = create(:group, :public, parent: l2_subgroup, path: 'wifi-group') - matched_project_2 = create(:project, :public, namespace: l3_subgroup, name: 'mobile') - - get :children, id: group.to_param, filter: 'mobile', format: :json - - shared_group_json = json_response.first - expect(shared_group_json['id']).to eq(shared_subgroup.id) - - matched_project_1_json = shared_group_json['children'].detect { |child| child['type'] == 'project' } - expect(matched_project_1_json['id']).to eq(matched_project_1.id) - - l2_subgroup_json = shared_group_json['children'].detect { |child| child['type'] == 'group' } - expect(l2_subgroup_json['id']).to eq(l2_subgroup.id) - - l3_subgroup_json = l2_subgroup_json['children'].first - expect(l3_subgroup_json['id']).to eq(l3_subgroup.id) - - matched_project_2_json = l3_subgroup_json['children'].first - expect(matched_project_2_json['id']).to eq(matched_project_2.id) - end - - it 'expands the tree upto a specified parent' do - subgroup = create(:group, :public, parent: group) - l2_subgroup = create(:group, :public, parent: subgroup) - create(:project, :public, namespace: l2_subgroup, name: 'test') - - get :children, id: subgroup.to_param, filter: 'test', format: :json - - expect(response).to have_http_status(200) - end - - it 'returns an empty array when there are no search results' do - subgroup = create(:group, :public, parent: group) - l2_subgroup = create(:group, :public, parent: subgroup) - create(:project, :public, namespace: l2_subgroup, name: 'no-match') - - get :children, id: subgroup.to_param, filter: 'test', format: :json - - expect(json_response).to eq([]) - end - - it 'includes pagination headers' do - 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") } - - get :children, id: group.to_param, filter: 'filter', per_page: 1, format: :json - - expect(response).to include_pagination_headers - end - end - - context 'queries per rendered element', :request_store do - # We need to make sure the following counts are preloaded - # otherwise they will cause an extra query - # 1. Count of visible projects in the element - # 2. Count of visible subgroups in the element - # 3. Count of members of a group - let(:expected_queries_per_group) { 0 } - let(:expected_queries_per_project) { 0 } - - def get_list - get :children, id: group.to_param, format: :json - end - - it 'queries the expected amount for a group row' do - control = ActiveRecord::QueryRecorder.new { get_list } - - _new_group = create(:group, :public, parent: group) - - expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group) - end - - it 'queries the expected amount for a project row' do - control = ActiveRecord::QueryRecorder.new { get_list } - _new_project = create(:project, :public, namespace: group) - - expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project) - end - - context 'when rendering hierarchies' do - # When loading hierarchies we load the all the ancestors for matched projects - # in 1 separate query - let(:extra_queries_for_hierarchies) { 1 } - - def get_filtered_list - get :children, id: group.to_param, filter: 'filter', format: :json - end - - it 'queries the expected amount when nested rows are increased for a group' do - matched_group = create(:group, :public, parent: group, name: 'filterme') - - control = ActiveRecord::QueryRecorder.new { get_filtered_list } - - matched_group.update!(parent: public_subgroup) - - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) - end - - it 'queries the expected amount when a new group match is added' do - create(:group, :public, parent: public_subgroup, name: 'filterme') - - control = ActiveRecord::QueryRecorder.new { get_filtered_list } - - create(:group, :public, parent: public_subgroup, name: 'filterme2') - create(:group, :public, parent: public_subgroup, name: 'filterme3') - - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) - end - - it 'queries the expected amount when nested rows are increased for a project' do - matched_project = create(:project, :public, namespace: group, name: 'filterme') - - control = ActiveRecord::QueryRecorder.new { get_filtered_list } - - matched_project.update!(namespace: public_subgroup) - - expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies) - end - end - end - end - - context 'pagination' do - let(:per_page) { 3 } - - before do - allow(Kaminari.config).to receive(:default_per_page).and_return(per_page) - end - - context 'with only projects' do - let!(:other_project) { create(:project, :public, namespace: group) } - let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) } - - it 'has projects on the first page' do - get :children, id: group.to_param, sort: 'id_desc', format: :json - - expect(assigns(:children)).to contain_exactly(*first_page_projects) - end - - it 'has projects on the second page' do - get :children, id: group.to_param, sort: 'id_desc', page: 2, format: :json - - expect(assigns(:children)).to contain_exactly(other_project) - end - end - - context 'with subgroups and projects', :nested_groups do - let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } - let!(:other_subgroup) { create(:group, :public, parent: group) } - let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) } - - it 'contains all subgroups' do - get :children, id: group.to_param, sort: 'id_asc', format: :json - - expect(assigns(:children)).to contain_exactly(*first_page_subgroups) - end - - it 'contains the project and group on the second page' do - get :children, id: group.to_param, sort: 'id_asc', page: 2, format: :json - - expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) - end - end - end - end - describe 'GET #issues' do let(:issue_1) { create(:issue, project: project) } let(:issue_2) { create(:issue, project: project) } -- cgit v1.2.1 From d2a9d95a2237aa3d6a93be5df012838180b704b7 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 12 Oct 2017 09:44:53 +0200 Subject: Skip some nested group specs when using MySQL --- spec/features/dashboard/groups_list_spec.rb | 19 ++++++++++++++----- spec/features/groups/show_spec.rb | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index c9d9371f5ab..3f68e63797d 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -22,15 +22,24 @@ feature 'Dashboard Groups page', :js do wait_for_requests expect(page).to have_content(group.name) - expect(page).to have_content(nested_group.parent.name) + expect(page).not_to have_content(another_group.name) + end + + it 'shows subgroups the user is member of', :nested_groups do + group.add_owner(user) + nested_group.add_owner(user) + + sign_in(user) + visit dashboard_groups_path + wait_for_requests + + expect(page).to have_content(nested_group.parent.name) click_group_caret(nested_group.parent) expect(page).to have_content(nested_group.name) - - expect(page).not_to have_content(another_group.name) end - describe 'when filtering groups' do + describe 'when filtering groups', :nested_groups do before do group.add_owner(user) nested_group.add_owner(user) @@ -64,7 +73,7 @@ feature 'Dashboard Groups page', :js do end end - describe 'group with subgroups' do + describe 'group with subgroups', :nested_groups do let!(:subgroup) { create(:group, :public, parent: group) } before do diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 501086fce80..7fc2b383749 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -32,7 +32,8 @@ feature 'Group show page' do group.add_owner(user) sign_in(user) end - context 'when subgroups are supported', :js do + + context 'when subgroups are supported', :js, :nested_groups do before do allow(Group).to receive(:supports_nested_groups?) { true } visit path -- cgit v1.2.1 From 13b299f4ff06a045269937f9e5fce5991bfca2d8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 12 Oct 2017 12:43:00 -0500 Subject: Add ability to pass class name to spriteIcon helper --- spec/javascripts/lib/utils/common_utils_spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index f86f2f260c3..6613b7dee6b 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -474,7 +474,11 @@ describe('common_utils', () => { }); it('should return the svg for a linked icon', () => { - expect(gl.utils.spriteIcon('test')).toEqual(''); + expect(gl.utils.spriteIcon('test')).toEqual(''); + }); + + it('should set svg className when passed', () => { + expect(gl.utils.spriteIcon('test', 'fa fa-test')).toEqual(''); }); }); }); -- cgit v1.2.1 From 771b777ab57d1c8d323ecc08a9e2cdc4f6a01e0b Mon Sep 17 00:00:00 2001 From: Guilherme Vieira Date: Sat, 23 Sep 2017 10:21:32 -0300 Subject: Adds requirements that supports anything in sha params --- spec/requests/api/v3/repositories_spec.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'spec') diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb index 1a55e2a71cd..67624a0bbea 100644 --- a/spec/requests/api/v3/repositories_spec.rb +++ b/spec/requests/api/v3/repositories_spec.rb @@ -97,10 +97,11 @@ describe API::V3::Repositories do end end - { - 'blobs/:sha' => 'blobs/master', - 'commits/:sha/blob' => 'commits/master/blob' - }.each do |desc_path, example_path| + [ + ['blobs/:sha', 'blobs/master'], + ['blobs/:sha', 'blobs/v1.1.0'], + ['commits/:sha/blob', 'commits/master/blob'] + ].each do |desc_path, example_path| describe "GET /projects/:id/repository/#{desc_path}" do let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } shared_examples_for 'repository blob' do @@ -110,7 +111,7 @@ describe API::V3::Repositories do end context 'when sha does not exist' do it_behaves_like '404 response' do - let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:request) { get v3_api("/projects/#{project.id}/repository/#{desc_path.sub(':sha', 'invalid_branch_name')}?filepath=README.md", current_user) } let(:message) { '404 Commit Not Found' } end end -- cgit v1.2.1 From d72b95cfb7a538c9385f373747e3675de8acb980 Mon Sep 17 00:00:00 2001 From: kushalpandya Date: Fri, 13 Oct 2017 14:06:25 +0530 Subject: Add support for `archived` param --- spec/javascripts/groups/components/app_spec.js | 7 +++++-- spec/javascripts/groups/components/groups_spec.js | 2 +- spec/javascripts/groups/service/groups_service_spec.js | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 8472c726b08..cd19a0fae1e 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -102,9 +102,10 @@ describe('AppComponent', () => { page: 2, filterGroupsBy: 'git', sortBy: 'created_desc', + archived: true, }); setTimeout(() => { - expect(vm.service.getGroups).toHaveBeenCalledWith(1, 2, 'git', 'created_desc'); + expect(vm.service.getGroups).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true); done(); }, 0); }); @@ -162,6 +163,7 @@ describe('AppComponent', () => { filterGroupsBy: null, sortBy: null, updatePagination: true, + archived: null, }); setTimeout(() => { expect(vm.updateGroups).toHaveBeenCalled(); @@ -178,13 +180,14 @@ describe('AppComponent', () => { spyOn(window.history, 'replaceState'); spyOn($, 'scrollTo'); - vm.fetchPage(2, null, null); + vm.fetchPage(2, null, null, true); expect(vm.isLoading).toBeTruthy(); expect(vm.fetchGroups).toHaveBeenCalledWith({ page: 2, filterGroupsBy: null, sortBy: null, updatePagination: true, + archived: true, }); setTimeout(() => { expect(vm.isLoading).toBeFalsy(); diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js index 280376d4903..90e818c1545 100644 --- a/spec/javascripts/groups/components/groups_spec.js +++ b/spec/javascripts/groups/components/groups_spec.js @@ -43,7 +43,7 @@ describe('GroupsComponent', () => { spyOn(eventHub, '$emit').and.stub(); vm.change(2); - expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', 2, jasmine.any(Object), jasmine.any(Object)); + expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', 2, jasmine.any(Object), jasmine.any(Object), jasmine.any(Object)); }); }); }); diff --git a/spec/javascripts/groups/service/groups_service_spec.js b/spec/javascripts/groups/service/groups_service_spec.js index 222e75d24a4..20bb63687f7 100644 --- a/spec/javascripts/groups/service/groups_service_spec.js +++ b/spec/javascripts/groups/service/groups_service_spec.js @@ -20,12 +20,13 @@ describe('GroupsService', () => { page: 2, filter: 'git', sort: 'created_asc', + archived: true, }; - service.getGroups(55, 2, 'git', 'created_asc'); + service.getGroups(55, 2, 'git', 'created_asc', true); expect(service.groups.get).toHaveBeenCalledWith({ parent_id: 55 }); - service.getGroups(null, 2, 'git', 'created_asc'); + service.getGroups(null, 2, 'git', 'created_asc', true); expect(service.groups.get).toHaveBeenCalledWith(queryParams); }); }); -- cgit v1.2.1 From 2622a6de59bc87a7eb82ec42ffafec8c361815f3 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 13 Oct 2017 11:36:00 +0200 Subject: Add Performance category to the changelog Resolves gitlab-org/gitlab-ce#36417 --- spec/bin/changelog_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 6d8b9865dcb..fc1bf67d7b9 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -84,7 +84,7 @@ describe 'bin/changelog' do expect do expect do expect { described_class.read_type }.to raise_error(SystemExit) - end.to output("Invalid category index, please select an index between 1 and 7\n").to_stderr + end.to output("Invalid category index, please select an index between 1 and 8\n").to_stderr end.to output.to_stdout end end -- cgit v1.2.1 From 4d79159973d7b51230ceb9efd33bb1cbb191621b Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 13 Oct 2017 15:13:52 +0200 Subject: Make sure we always return an array of hierarchies Even when we pass an array of only a single object --- spec/controllers/groups/children_controller_spec.rb | 9 +++++++++ spec/serializers/group_child_serializer_spec.rb | 9 +++++++++ 2 files changed, 18 insertions(+) (limited to 'spec') diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index f15a12ef7fd..4262d474e59 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -141,6 +141,15 @@ describe Groups::ChildrenController do expect(response).to have_http_status(200) end + it 'returns an array with one element when only one result is matched' do + create(:project, :public, namespace: group, name: 'match') + + get :index, group_id: group.to_param, filter: 'match', format: :json + + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq(1) + end + it 'returns an empty array when there are no search results' do subgroup = create(:group, :public, parent: group) l2_subgroup = create(:group, :public, parent: subgroup) diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb index 566b235769e..5541ada3750 100644 --- a/spec/serializers/group_child_serializer_spec.rb +++ b/spec/serializers/group_child_serializer_spec.rb @@ -95,6 +95,15 @@ describe GroupChildSerializer do expect(project1_json[:id]).to eq(project1.id) expect(project2_json[:id]).to eq(project2.id) end + + it 'returns an array when an array of a single instance was given' do + project = create(:project, namespace: parent) + + json = serializer.represent([project]) + + expect(json).to be_kind_of(Array) + expect(json.size).to eq(1) + end end end end -- cgit v1.2.1 From c909b6aa6d62a4c556a866166d0a98c952d2ef62 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 5 Oct 2017 21:56:23 +0200 Subject: Prevent creating multiple ApplicationSetting by forcing it to always have id=1 --- spec/models/application_setting_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'spec') diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 78cacf9ff5d..eff84c308b5 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -209,6 +209,16 @@ describe ApplicationSetting do end end + context 'restrict creating duplicates' do + before do + described_class.create_from_defaults + end + + it 'raises an record creation violation if already created' do + expect { described_class.create_from_defaults }.to raise_error(ActiveRecord::RecordNotUnique) + end + end + context 'restricted signup domains' do it 'sets single domain' do setting.domain_whitelist_raw = 'example.com' -- cgit v1.2.1 From b1b91aa0658d81107327884ca56f579cf6146078 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 13 Oct 2017 10:08:10 +0100 Subject: Refactored multi-file data structure This moves away from storing in a single array just to render the table. It now stores in a multi-dimensional array/object type where each entry in the array can have its own tree. This makes storing the data for future feature a little easier as there is only one way to store the data. Previously to insert a directory the code had to insert the directory & then the file at the right point in the array. Now the directory can be inserted anywhere & then a file can be quickly added into this directory. The rendering is still done with a single array, but this is handled through underscore. Underscore takes the array & then goes through each item to flatten it into one. It is done this way to save changing the markup away from table, keeping it as a table keeps it semantically correct. --- .../repo/components/repo_file_options_spec.js | 33 ---------------------- 1 file changed, 33 deletions(-) delete mode 100644 spec/javascripts/repo/components/repo_file_options_spec.js (limited to 'spec') diff --git a/spec/javascripts/repo/components/repo_file_options_spec.js b/spec/javascripts/repo/components/repo_file_options_spec.js deleted file mode 100644 index 9759b4bf12d..00000000000 --- a/spec/javascripts/repo/components/repo_file_options_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import repoFileOptions from '~/repo/components/repo_file_options.vue'; - -describe('RepoFileOptions', () => { - const projectName = 'projectName'; - - function createComponent(propsData) { - const RepoFileOptions = Vue.extend(repoFileOptions); - - return new RepoFileOptions({ - propsData, - }).$mount(); - } - - it('renders the title and new file/folder buttons if isMini is true', () => { - const vm = createComponent({ - isMini: true, - projectName, - }); - - expect(vm.$el.classList.contains('repo-file-options')).toBeTruthy(); - expect(vm.$el.querySelector('.title').textContent).toEqual(projectName); - }); - - it('does not render if isMini is false', () => { - const vm = createComponent({ - isMini: false, - projectName, - }); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); -}); -- cgit v1.2.1 From 5f80d04271a15c3065513e6417891f3949c7a530 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 13 Oct 2017 16:01:54 +0100 Subject: Spec updates Updated as best as possible, a lot of tests still change the store and never reset the state back which can cause some issues with other tests. --- .../repo/components/repo_commit_section_spec.js | 1 + .../repo/components/repo_edit_button_spec.js | 12 +-- .../repo/components/repo_editor_spec.js | 5 + .../repo/components/repo_file_buttons_spec.js | 4 + spec/javascripts/repo/components/repo_file_spec.js | 103 +++++++------------- .../repo/components/repo_loading_file_spec.js | 29 ++---- .../repo/components/repo_prev_directory_spec.js | 16 ++-- .../repo/components/repo_sidebar_spec.js | 105 +++++++++------------ spec/javascripts/repo/components/repo_tab_spec.js | 40 +++++--- spec/javascripts/repo/components/repo_tabs_spec.js | 18 +--- spec/javascripts/repo/mock_data.js | 12 +++ 11 files changed, 150 insertions(+), 195 deletions(-) create mode 100644 spec/javascripts/repo/mock_data.js (limited to 'spec') diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index 0635de4b30b..e09d593f04c 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -134,6 +134,7 @@ describe('RepoCommitSection', () => { afterEach(() => { vm.$destroy(); el.remove(); + RepoStore.openedFiles = []; }); it('shows commit message', () => { diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js index 411514009dc..dff2fac191d 100644 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ b/spec/javascripts/repo/components/repo_edit_button_spec.js @@ -9,6 +9,10 @@ describe('RepoEditButton', () => { return new RepoEditButton().$mount(); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders an edit button that toggles the view state', (done) => { RepoStore.isCommitable = true; RepoStore.changedFiles = []; @@ -38,12 +42,4 @@ describe('RepoEditButton', () => { expect(vm.$el.innerHTML).toBeUndefined(); }); - - describe('methods', () => { - describe('editCancelClicked', () => { - it('sets dialog to open when there are changedFiles'); - - it('toggles editMode and calls toggleBlobView'); - }); - }); }); diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index 85d55d171f9..a25a600b3be 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import RepoStore from '~/repo/stores/repo_store'; import repoEditor from '~/repo/components/repo_editor.vue'; describe('RepoEditor', () => { @@ -8,6 +9,10 @@ describe('RepoEditor', () => { this.vm = new RepoEditor().$mount(); }); + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders an ide container', (done) => { this.vm.openedFiles = ['idiidid']; this.vm.binary = false; diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js index dfab51710c3..701c260224f 100644 --- a/spec/javascripts/repo/components/repo_file_buttons_spec.js +++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js @@ -9,6 +9,10 @@ describe('RepoFileButtons', () => { return new RepoFileButtons().$mount(); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders Raw, Blame, History, Permalink and Preview toggle', () => { const activeFile = { extension: 'md', diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 620b604f404..da8c850bc78 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -1,21 +1,11 @@ import Vue from 'vue'; import repoFile from '~/repo/components/repo_file.vue'; import RepoStore from '~/repo/stores/repo_store'; +import eventHub from '~/repo/event_hub'; +import { file } from '../mock_data'; describe('RepoFile', () => { const updated = 'updated'; - const file = { - icon: 'icon', - url: 'url', - name: 'name', - lastCommitMessage: 'message', - lastCommitUpdate: Date.now(), - level: 10, - }; - const activeFile = { - pageTitle: 'pageTitle', - url: 'url', - }; const otherFile = { html: '

html

', pageTitle: 'otherpageTitle', @@ -29,12 +19,15 @@ describe('RepoFile', () => { }).$mount(); } + beforeEach(() => { + RepoStore.openedFiles = []; + }); + it('renders link, icon, name and last commit details', () => { const RepoFile = Vue.extend(repoFile); const vm = new RepoFile({ propsData: { - file, - activeFile, + file: file(), }, }); spyOn(vm, 'timeFormated').and.returnValue(updated); @@ -43,28 +36,20 @@ describe('RepoFile', () => { const name = vm.$el.querySelector('.repo-file-name'); const fileIcon = vm.$el.querySelector('.file-icon'); - expect(vm.$el.classList.contains('active')).toBeTruthy(); - expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px'); - expect(name.title).toEqual(file.url); - expect(name.href).toMatch(`/${file.url}`); - expect(name.textContent.trim()).toEqual(file.name); - expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage); + expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px'); + expect(name.href).toMatch(`/${vm.file.url}`); + expect(name.textContent.trim()).toEqual(vm.file.name); + expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(vm.file.lastCommit.message); expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated); - expect(fileIcon.classList.contains(file.icon)).toBeTruthy(); - expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`); + expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy(); + expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`); }); it('does render if hasFiles is true and is loading tree', () => { const vm = createComponent({ - file, - activeFile, - loading: { - tree: true, - }, - hasFiles: true, + file: file(), }); - expect(vm.$el.innerHTML).toBeTruthy(); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); }); @@ -75,75 +60,51 @@ describe('RepoFile', () => { }); it('renders a spinner if the file is loading', () => { - file.loading = true; - const vm = createComponent({ - file, - activeFile, - loading: { - tree: true, - }, - hasFiles: true, - }); - - expect(vm.$el.innerHTML).toBeTruthy(); - expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${file.level * 10}px`); - }); - - it('does not render if loading tree', () => { + const f = file(); + f.loading = true; const vm = createComponent({ - file, - activeFile, - loading: { - tree: true, - }, + file: f, }); - expect(vm.$el.innerHTML).toBeFalsy(); + expect(vm.$el.querySelector('.fa-spin.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`); }); it('does not render commit message and datetime if mini', () => { + RepoStore.openedFiles.push(file()); + const vm = createComponent({ - file, - activeFile, - isMini: true, + file: file(), }); expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); }); - it('does not set active class if file is active file', () => { - const vm = createComponent({ - file, - activeFile: {}, - }); - - expect(vm.$el.classList.contains('active')).toBeFalsy(); - }); - it('fires linkClicked when the link is clicked', () => { const vm = createComponent({ - file, - activeFile, + file: file(), }); spyOn(vm, 'linkClicked'); - vm.$el.querySelector('.repo-file-name').click(); + vm.$el.click(); - expect(vm.linkClicked).toHaveBeenCalledWith(file); + expect(vm.linkClicked).toHaveBeenCalledWith(vm.file); }); describe('methods', () => { describe('linkClicked', () => { - const vm = jasmine.createSpyObj('vm', ['$emit']); - it('$emits linkclicked with file obj', () => { - const theFile = {}; + spyOn(eventHub, '$emit'); + + const vm = createComponent({ + file: file(), + }); - repoFile.methods.linkClicked.call(vm, theFile); + vm.linkClicked(vm.file); - expect(vm.$emit).toHaveBeenCalledWith('linkclicked', theFile); + expect(eventHub.$emit).toHaveBeenCalledWith('linkclicked', vm.file); }); }); }); diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js index a030314d749..e9f95a02028 100644 --- a/spec/javascripts/repo/components/repo_loading_file_spec.js +++ b/spec/javascripts/repo/components/repo_loading_file_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import RepoStore from '~/repo/stores/repo_store'; import repoLoadingFile from '~/repo/components/repo_loading_file.vue'; describe('RepoLoadingFile', () => { @@ -28,6 +29,10 @@ describe('RepoLoadingFile', () => { }); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders 3 columns of animated LoC', () => { const vm = createComponent({ loading: { @@ -42,38 +47,16 @@ describe('RepoLoadingFile', () => { }); it('renders 1 column of animated LoC if isMini', () => { + RepoStore.openedFiles = new Array(1); const vm = createComponent({ loading: { tree: true, }, hasFiles: false, - isMini: true, }); const columns = [...vm.$el.querySelectorAll('td')]; expect(columns.length).toEqual(1); assertColumns(columns); }); - - it('does not render if tree is not loading', () => { - const vm = createComponent({ - loading: { - tree: false, - }, - hasFiles: false, - }); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); - - it('does not render if hasFiles is true', () => { - const vm = createComponent({ - loading: { - tree: true, - }, - hasFiles: true, - }); - - expect(vm.$el.innerHTML).toBeFalsy(); - }); }); diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js index 34dde545e6a..4c064f21084 100644 --- a/spec/javascripts/repo/components/repo_prev_directory_spec.js +++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue'; +import eventHub from '~/repo/event_hub'; describe('RepoPrevDirectory', () => { function createComponent(propsData) { @@ -20,7 +21,7 @@ describe('RepoPrevDirectory', () => { spyOn(vm, 'linkClicked'); expect(link.href).toMatch(`/${prevUrl}`); - expect(link.textContent).toEqual('..'); + expect(link.textContent).toEqual('...'); link.click(); @@ -29,14 +30,17 @@ describe('RepoPrevDirectory', () => { describe('methods', () => { describe('linkClicked', () => { - const vm = jasmine.createSpyObj('vm', ['$emit']); + it('$emits linkclicked with prevUrl', () => { + const prevUrl = 'prevUrl'; + const vm = createComponent({ + prevUrl, + }); - it('$emits linkclicked with file obj', () => { - const file = {}; + spyOn(eventHub, '$emit'); - repoPrevDirectory.methods.linkClicked.call(vm, file); + vm.linkClicked(prevUrl); - expect(vm.$emit).toHaveBeenCalledWith('linkclicked', file); + expect(eventHub.$emit).toHaveBeenCalledWith('goToPreviousDirectoryClicked', prevUrl); }); }); }); diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 35d2b37ac2a..f53e035bbcf 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -3,6 +3,7 @@ import Helper from '~/repo/helpers/repo_helper'; import RepoService from '~/repo/services/repo_service'; import RepoStore from '~/repo/stores/repo_store'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; +import { file } from '../mock_data'; describe('RepoSidebar', () => { let vm; @@ -15,14 +16,15 @@ describe('RepoSidebar', () => { afterEach(() => { vm.$destroy(); + + RepoStore.files = []; + RepoStore.openedFiles = []; }); it('renders a sidebar', () => { - RepoStore.files = [{ - id: 0, - }]; + RepoStore.files = [file()]; RepoStore.openedFiles = []; - RepoStore.isRoot = false; + RepoStore.isRoot = true; vm = createComponent(); const thead = vm.$el.querySelector('thead'); @@ -30,9 +32,9 @@ describe('RepoSidebar', () => { expect(vm.$el.id).toEqual('sidebar'); expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); - expect(thead.querySelector('.name').textContent).toEqual('Name'); - expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit'); - expect(thead.querySelector('.last-update').textContent).toEqual('Last update'); + expect(thead.querySelector('.name').textContent.trim()).toEqual('Name'); + expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit'); + expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update'); expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); expect(tbody.querySelector('.prev-directory')).toBeFalsy(); expect(tbody.querySelector('.loading-file')).toBeFalsy(); @@ -46,25 +48,21 @@ describe('RepoSidebar', () => { vm = createComponent(); expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); - expect(vm.$el.querySelector('thead')).toBeFalsy(); - expect(vm.$el.querySelector('tbody .repo-file-options')).toBeTruthy(); + expect(vm.$el.querySelector('thead')).toBeTruthy(); + expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy(); }); it('renders 5 loading files if tree is loading and not hasFiles', () => { - RepoStore.loading = { - tree: true, - }; + RepoStore.loading.tree = true; RepoStore.files = []; vm = createComponent(); expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); }); - it('renders a prev directory if isRoot', () => { - RepoStore.files = [{ - id: 0, - }]; - RepoStore.isRoot = true; + it('renders a prev directory if is not root', () => { + RepoStore.files = [file()]; + RepoStore.isRoot = false; vm = createComponent(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); @@ -74,48 +72,36 @@ describe('RepoSidebar', () => { describe('fileClicked', () => { it('should fetch data for new file', () => { spyOn(Helper, 'getContent').and.callThrough(); - const file1 = { - id: 0, - url: '', - }; - RepoStore.files = [file1]; + RepoStore.files = [file()]; RepoStore.isRoot = true; vm = createComponent(); - vm.fileClicked(file1); + vm.fileClicked(RepoStore.files[0]); - expect(Helper.getContent).toHaveBeenCalledWith(file1); + expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[0]); }); it('should not fetch data for already opened files', () => { - const file = { - id: 42, - url: 'foo', - }; - - spyOn(Helper, 'getFileFromPath').and.returnValue(file); + const f = file(); + spyOn(Helper, 'getFileFromPath').and.returnValue(f); spyOn(RepoStore, 'setActiveFiles'); vm = createComponent(); - vm.fileClicked(file); + vm.fileClicked(f); - expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file); + expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(f); }); it('should hide files in directory if already open', () => { - spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); - const file1 = { - id: 0, - type: 'tree', - url: '', - opened: true, - }; - RepoStore.files = [file1]; - RepoStore.isRoot = true; + spyOn(Helper, 'setDirectoryToClosed').and.callThrough(); + const f = file(); + f.opened = true; + f.type = 'tree'; + RepoStore.files = [f]; vm = createComponent(); - vm.fileClicked(file1); + vm.fileClicked(RepoStore.files[0]); - expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1); + expect(Helper.setDirectoryToClosed).toHaveBeenCalledWith(RepoStore.files[0]); }); }); @@ -131,36 +117,31 @@ describe('RepoSidebar', () => { }); describe('back button', () => { - const file1 = { - id: 1, - url: 'file1', - }; - const file2 = { - id: 2, - url: 'file2', - }; - RepoStore.files = [file1, file2]; - RepoStore.openedFiles = [file1, file2]; - RepoStore.isRoot = true; - - vm = createComponent(); - vm.fileClicked(file1); + beforeEach(() => { + const f = file(); + const file2 = Object.assign({}, file()); + file2.url = 'test'; + RepoStore.files = [f, file2]; + RepoStore.openedFiles = []; + RepoStore.isRoot = true; + + vm = createComponent(); + }); it('render previous file when using back button', () => { spyOn(Helper, 'getContent').and.callThrough(); - vm.fileClicked(file2); - expect(Helper.getContent).toHaveBeenCalledWith(file2); - Helper.getContent.calls.reset(); + vm.fileClicked(RepoStore.files[1]); + expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[1]); history.pushState({ key: Math.random(), - }, '', file1.url); + }, '', RepoStore.files[1].url); const popEvent = document.createEvent('Event'); popEvent.initEvent('popstate', true, true); window.dispatchEvent(popEvent); - expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url); + expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(RepoStore.files[1].url); window.history.pushState({}, null, '/'); }); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index d2a790ad73a..37e297437f0 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import repoTab from '~/repo/components/repo_tab.vue'; +import RepoStore from '~/repo/stores/repo_store'; describe('RepoTab', () => { function createComponent(propsData) { @@ -18,7 +19,7 @@ describe('RepoTab', () => { const vm = createComponent({ tab, }); - const close = vm.$el.querySelector('.close'); + const close = vm.$el.querySelector('.close-btn'); const name = vm.$el.querySelector(`a[title="${tab.url}"]`); spyOn(vm, 'closeTab'); @@ -44,26 +45,43 @@ describe('RepoTab', () => { tab, }); - expect(vm.$el.querySelector('.close .fa-circle')).toBeTruthy(); + expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy(); }); describe('methods', () => { describe('closeTab', () => { - const vm = jasmine.createSpyObj('vm', ['$emit']); - it('returns undefined and does not $emit if file is changed', () => { - const file = { changed: true }; - const returnVal = repoTab.methods.closeTab.call(vm, file); + const tab = { + url: 'url', + name: 'name', + changed: true, + }; + const vm = createComponent({ + tab, + }); + + spyOn(RepoStore, 'removeFromOpenedFiles'); + + vm.$el.querySelector('.close-btn').click(); - expect(returnVal).toBeUndefined(); - expect(vm.$emit).not.toHaveBeenCalled(); + expect(RepoStore.removeFromOpenedFiles).not.toHaveBeenCalled(); }); it('$emits tabclosed event with file obj', () => { - const file = { changed: false }; - repoTab.methods.closeTab.call(vm, file); + const tab = { + url: 'url', + name: 'name', + changed: false, + }; + const vm = createComponent({ + tab, + }); + + spyOn(RepoStore, 'removeFromOpenedFiles'); + + vm.$el.querySelector('.close-btn').click(); - expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file); + expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(tab); }); }); }); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index a02b54efafc..431129bc866 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -16,6 +16,10 @@ describe('RepoTabs', () => { return new RepoTabs().$mount(); } + afterEach(() => { + RepoStore.openedFiles = []; + }); + it('renders a list of tabs', () => { RepoStore.openedFiles = openedFiles; @@ -28,18 +32,4 @@ describe('RepoTabs', () => { expect(tabs[1].classList.contains('active')).toBeFalsy(); expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); }); - - describe('methods', () => { - describe('tabClosed', () => { - it('calls removeFromOpenedFiles with file obj', () => { - const file = {}; - - spyOn(RepoStore, 'removeFromOpenedFiles'); - - repoTabs.methods.tabClosed(file); - - expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file); - }); - }); - }); }); diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js new file mode 100644 index 00000000000..097d5c88d1e --- /dev/null +++ b/spec/javascripts/repo/mock_data.js @@ -0,0 +1,12 @@ +import RepoHelper from '~/repo/helpers/repo_helper'; + +export const file = () => RepoHelper.serializeBlob({ + icon: 'icon', + url: 'url', + name: 'name', + last_commit: { + id: '123', + message: 'test', + committed_date: '', + }, +}); -- cgit v1.2.1 From fa9e729aba58ddb00e09282e63a497d9d8e99dea Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 Oct 2017 11:28:56 +0100 Subject: fixed rendering bugs fixed eslint --- spec/javascripts/repo/mock_data.js | 1 + 1 file changed, 1 insertion(+) (limited to 'spec') diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js index 097d5c88d1e..6342b3539b5 100644 --- a/spec/javascripts/repo/mock_data.js +++ b/spec/javascripts/repo/mock_data.js @@ -1,5 +1,6 @@ import RepoHelper from '~/repo/helpers/repo_helper'; +// eslint-disable-next-line import/prefer-default-export export const file = () => RepoHelper.serializeBlob({ icon: 'icon', url: 'url', -- cgit v1.2.1 From 61f4d08b7bf734c9af579e54fa8c74ac1bde4b39 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 12 Oct 2017 10:39:11 +0200 Subject: Allow testing on Gitaly call count Previous efforts were aimed at detecting N + 1 queries, general regressions are hard to find and mitigate. --- .../projects/pipelines_controller_spec.rb | 32 ++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) (limited to 'spec') diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 167e80ed9cd..67b53d2acce 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -3,32 +3,36 @@ require 'spec_helper' describe Projects::PipelinesController do include ApiHelpers - let(:user) { create(:user) } - let(:project) { create(:project, :public) } + set(:user) { create(:user) } + set(:project) { create(:project, :public, :repository) } let(:feature) { ProjectFeature::DISABLED } before do stub_not_protect_default_branch project.add_developer(user) - project.project_feature.update( - builds_access_level: feature) + project.project_feature.update(builds_access_level: feature) sign_in(user) end describe 'GET index.json' do before do - create(:ci_empty_pipeline, status: 'pending', project: project) - create(:ci_empty_pipeline, status: 'running', project: project) - create(:ci_empty_pipeline, status: 'created', project: project) - create(:ci_empty_pipeline, status: 'success', project: project) + branch_head = project.commit + parent = branch_head.parent - get :index, namespace_id: project.namespace, - project_id: project, - format: :json + create(:ci_empty_pipeline, status: 'pending', project: project, sha: branch_head.id) + create(:ci_empty_pipeline, status: 'running', project: project, sha: branch_head.id) + create(:ci_empty_pipeline, status: 'created', project: project, sha: parent.id) + create(:ci_empty_pipeline, status: 'success', project: project, sha: parent.id) + end + + subject do + get :index, namespace_id: project.namespace, project_id: project, format: :json end it 'returns JSON with serialized pipelines' do + subject + expect(response).to have_http_status(:ok) expect(response).to match_response_schema('pipeline') @@ -39,6 +43,12 @@ describe Projects::PipelinesController do expect(json_response['count']['pending']).to eq 1 expect(json_response['count']['finished']).to eq 1 end + + context 'when performing gitaly calls', :request_store do + it 'limits the Gitaly requests' do + expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10) + end + end end describe 'GET show JSON' do -- cgit v1.2.1 From a11410a82d2323cb4834ac5ed405a92245788dac Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 Oct 2017 15:13:43 +0100 Subject: fixed karma build because of a removed method --- spec/javascripts/repo/mock_data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js index 6342b3539b5..46c68c00357 100644 --- a/spec/javascripts/repo/mock_data.js +++ b/spec/javascripts/repo/mock_data.js @@ -1,7 +1,7 @@ import RepoHelper from '~/repo/helpers/repo_helper'; // eslint-disable-next-line import/prefer-default-export -export const file = () => RepoHelper.serializeBlob({ +export const file = () => RepoHelper.serializeRepoEntity('blob', { icon: 'icon', url: 'url', name: 'name', -- cgit v1.2.1 From 5e5e9c19284f8a15e0ecd72b1c8639839535dcc5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 Oct 2017 15:27:23 +0100 Subject: fixed karma spec with prev directory button --- spec/javascripts/repo/components/repo_sidebar_spec.js | 1 + 1 file changed, 1 insertion(+) (limited to 'spec') diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index f53e035bbcf..c61631bb230 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -63,6 +63,7 @@ describe('RepoSidebar', () => { it('renders a prev directory if is not root', () => { RepoStore.files = [file()]; RepoStore.isRoot = false; + RepoStore.loading.tree = false; vm = createComponent(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); -- cgit v1.2.1 From a906015cb1ad724b2f0726a40ac4a013443b1d6a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 16 Oct 2017 16:04:17 -0500 Subject: Move user avatar specs to folder to mimic javascript directory --- .../user_avatar/user_avatar_image_spec.js | 54 ++++++++++++++++++++++ .../user_avatar/user_avatar_link_spec.js | 50 ++++++++++++++++++++ .../components/user_avatar/user_avatar_svg_spec.js | 29 ++++++++++++ .../components/user_avatar_image_spec.js | 54 ---------------------- .../vue_shared/components/user_avatar_link_spec.js | 50 -------------------- .../vue_shared/components/user_avatar_svg_spec.js | 29 ------------ 6 files changed, 133 insertions(+), 133 deletions(-) create mode 100644 spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js create mode 100644 spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js create mode 100644 spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js delete mode 100644 spec/javascripts/vue_shared/components/user_avatar_image_spec.js delete mode 100644 spec/javascripts/vue_shared/components/user_avatar_link_spec.js delete mode 100644 spec/javascripts/vue_shared/components/user_avatar_svg_spec.js (limited to 'spec') diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js new file mode 100644 index 00000000000..8daa7610274 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; + +const UserAvatarImageComponent = Vue.extend(UserAvatarImage); + +describe('User Avatar Image Component', function () { + describe('Initialization', function () { + beforeEach(function () { + this.propsData = { + size: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + }; + + this.userAvatarImage = new UserAvatarImageComponent({ + propsData: this.propsData, + }).$mount(); + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should have as a child element', function () { + expect(this.userAvatarImage.$el.tagName).toBe('IMG'); + }); + + it('should properly compute tooltipContainer', function () { + expect(this.userAvatarImage.tooltipContainer).toBe('body'); + }); + + it('should properly render tooltipContainer', function () { + expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body'); + }); + + it('should properly compute avatarSizeClass', function () { + expect(this.userAvatarImage.avatarSizeClass).toBe('s99'); + }); + + it('should properly render img css', function () { + const classList = this.userAvatarImage.$el.classList; + const containsAvatar = classList.contains('avatar'); + const containsSizeClass = classList.contains('s99'); + const containsCustomClass = classList.contains('myextraavatarclass'); + + expect(containsAvatar).toBe(true); + expect(containsSizeClass).toBe(true); + expect(containsCustomClass).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js new file mode 100644 index 00000000000..52e450e9ba5 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +describe('User Avatar Link Component', function () { + beforeEach(function () { + this.propsData = { + linkHref: 'myavatarurl.com', + imgSize: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + imgCssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + }; + + const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); + + this.userAvatarLink = new UserAvatarLinkComponent({ + propsData: this.propsData, + }).$mount(); + + this.userAvatarImage = this.userAvatarLink.$children[0]; + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarLink).toBeDefined(); + }); + + it('should have user-avatar-image registered as child component', function () { + expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined(); + }); + + it('user-avatar-link should have user-avatar-image as child component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should render as a child element', function () { + expect(this.userAvatarLink.$el.tagName).toBe('A'); + }); + + it('should have as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); + }); + + it('should return neccessary props as defined', function () { + _.each(this.propsData, (val, key) => { + expect(this.userAvatarLink[key]).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js new file mode 100644 index 00000000000..b8d639ffbec --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; +import avatarSvg from 'icons/_icon_random.svg'; + +const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg); + +describe('User Avatar Svg Component', function () { + describe('Initialization', function () { + beforeEach(function () { + this.propsData = { + size: 99, + svg: avatarSvg, + }; + + this.userAvatarSvg = new UserAvatarSvgComponent({ + propsData: this.propsData, + }).$mount(); + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarSvg).toBeDefined(); + }); + + it('should have as a child element', function () { + expect(this.userAvatarSvg.$el.tagName).toEqual('svg'); + expect(this.userAvatarSvg.$el.innerHTML).toContain(' as a child element', function () { - expect(this.userAvatarImage.$el.tagName).toBe('IMG'); - }); - - it('should properly compute tooltipContainer', function () { - expect(this.userAvatarImage.tooltipContainer).toBe('body'); - }); - - it('should properly render tooltipContainer', function () { - expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body'); - }); - - it('should properly compute avatarSizeClass', function () { - expect(this.userAvatarImage.avatarSizeClass).toBe('s99'); - }); - - it('should properly render img css', function () { - const classList = this.userAvatarImage.$el.classList; - const containsAvatar = classList.contains('avatar'); - const containsSizeClass = classList.contains('s99'); - const containsCustomClass = classList.contains('myextraavatarclass'); - - expect(containsAvatar).toBe(true); - expect(containsSizeClass).toBe(true); - expect(containsCustomClass).toBe(true); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js deleted file mode 100644 index 52e450e9ba5..00000000000 --- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue from 'vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; - -describe('User Avatar Link Component', function () { - beforeEach(function () { - this.propsData = { - linkHref: 'myavatarurl.com', - imgSize: 99, - imgSrc: 'myavatarurl.com', - imgAlt: 'mydisplayname', - imgCssClasses: 'myextraavatarclass', - tooltipText: 'tooltip text', - tooltipPlacement: 'bottom', - }; - - const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); - - this.userAvatarLink = new UserAvatarLinkComponent({ - propsData: this.propsData, - }).$mount(); - - this.userAvatarImage = this.userAvatarLink.$children[0]; - }); - - it('should return a defined Vue component', function () { - expect(this.userAvatarLink).toBeDefined(); - }); - - it('should have user-avatar-image registered as child component', function () { - expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined(); - }); - - it('user-avatar-link should have user-avatar-image as child component', function () { - expect(this.userAvatarImage).toBeDefined(); - }); - - it('should render as a child element', function () { - expect(this.userAvatarLink.$el.tagName).toBe('A'); - }); - - it('should have as a child element', function () { - expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); - }); - - it('should return neccessary props as defined', function () { - _.each(this.propsData, (val, key) => { - expect(this.userAvatarLink[key]).toBeDefined(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js deleted file mode 100644 index b8d639ffbec..00000000000 --- a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import Vue from 'vue'; -import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; -import avatarSvg from 'icons/_icon_random.svg'; - -const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg); - -describe('User Avatar Svg Component', function () { - describe('Initialization', function () { - beforeEach(function () { - this.propsData = { - size: 99, - svg: avatarSvg, - }; - - this.userAvatarSvg = new UserAvatarSvgComponent({ - propsData: this.propsData, - }).$mount(); - }); - - it('should return a defined Vue component', function () { - expect(this.userAvatarSvg).toBeDefined(); - }); - - it('should have as a child element', function () { - expect(this.userAvatarSvg.$el.tagName).toEqual('svg'); - expect(this.userAvatarSvg.$el.innerHTML).toContain(' Date: Thu, 12 Oct 2017 14:13:59 +0200 Subject: Read circuitbreaker settings from `Gitlab::CurrentSettings` Instead of from the configuration file --- spec/features/admin/admin_health_check_spec.rb | 4 +- spec/initializers/settings_spec.rb | 20 -------- .../lib/gitlab/git/storage/circuit_breaker_spec.rb | 54 ++++++++++++++-------- .../git/storage/null_circuit_breaker_spec.rb | 4 ++ spec/support/stub_configuration.rb | 4 -- 5 files changed, 43 insertions(+), 43 deletions(-) (limited to 'spec') diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 09e6965849a..4430fc15501 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -65,9 +65,11 @@ feature "Admin Health Check", :feature, :broken_storage do it 'shows storage failure information' do hostname = Gitlab::Environment.hostname + maximum_failures = Gitlab::CurrentSettings.current_application_settings + .circuitbreaker_failure_count_threshold expect(page).to have_content('broken: failed storage access attempt on host:') - expect(page).to have_content("#{hostname}: 1 of 10 failures.") + expect(page).to have_content("#{hostname}: 1 of #{maximum_failures} failures.") end it 'allows resetting storage failures' do diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb index 9a974e70e8c..a11824d0ac5 100644 --- a/spec/initializers/settings_spec.rb +++ b/spec/initializers/settings_spec.rb @@ -18,26 +18,6 @@ describe Settings do end end - describe '#repositories' do - it 'assigns the default failure attributes' do - repository_settings = Gitlab.config.repositories.storages['broken'] - - expect(repository_settings['failure_count_threshold']).to eq(10) - expect(repository_settings['failure_wait_time']).to eq(30) - expect(repository_settings['failure_reset_time']).to eq(1800) - expect(repository_settings['storage_timeout']).to eq(5) - end - - it 'can be accessed with dot syntax all the way down' do - expect(Gitlab.config.repositories.storages.broken.failure_count_threshold).to eq(10) - end - - it 'can be accessed in a very specific way that breaks without reassigning each element with Settingslogic' do - storage_settings = Gitlab.config.repositories.storages['broken'] - expect(storage_settings.failure_count_threshold).to eq(10) - end - end - describe '#host_without_www' do context 'URL with protocol' do it 'returns the host' do diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index 98cf7966dad..68eb93eb9f9 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -10,18 +10,10 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: # Override test-settings for the circuitbreaker with something more realistic # for these specs. stub_storage_settings('default' => { - 'path' => TestEnv.repos_path, - 'failure_count_threshold' => 10, - 'failure_wait_time' => 30, - 'failure_reset_time' => 1800, - 'storage_timeout' => 5 + 'path' => TestEnv.repos_path }, 'broken' => { - 'path' => 'tmp/tests/non-existent-repositories', - 'failure_count_threshold' => 10, - 'failure_wait_time' => 30, - 'failure_reset_time' => 1800, - 'storage_timeout' => 5 + 'path' => 'tmp/tests/non-existent-repositories' }, 'nopath' => { 'path' => nil } ) @@ -75,10 +67,39 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(circuit_breaker.hostname).to eq(hostname) expect(circuit_breaker.storage).to eq('default') expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path) - expect(circuit_breaker.failure_count_threshold).to eq(10) - expect(circuit_breaker.failure_wait_time).to eq(30) - expect(circuit_breaker.failure_reset_time).to eq(1800) - expect(circuit_breaker.storage_timeout).to eq(5) + end + end + + context 'circuitbreaker settings' do + before do + stub_application_setting(circuitbreaker_failure_count_threshold: 0, + circuitbreaker_failure_wait_time: 1, + circuitbreaker_failure_reset_time: 2, + circuitbreaker_storage_timeout: 3) + end + + describe '#failure_count_threshold' do + it 'reads the value from settings' do + expect(circuit_breaker.failure_count_threshold).to eq(0) + end + end + + describe '#failure_wait_time' do + it 'reads the value from settings' do + expect(circuit_breaker.failure_wait_time).to eq(1) + end + end + + describe '#failure_reset_time' do + it 'reads the value from settings' do + expect(circuit_breaker.failure_reset_time).to eq(2) + end + end + + describe '#storage_timeout' do + it 'reads the value from settings' do + expect(circuit_breaker.storage_timeout).to eq(3) + end end end @@ -151,10 +172,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: context 'the `failure_wait_time` is set to 0' do before do - stub_storage_settings('default' => { - 'failure_wait_time' => 0, - 'path' => TestEnv.repos_path - }) + stub_application_setting(circuitbreaker_failure_wait_time: 0) end it 'is working even when there is a recent failure' do diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb index 0e645008c88..7ee6d2f3709 100644 --- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb @@ -54,6 +54,10 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do end describe '#failure_count_threshold' do + before do + stub_application_setting(circuitbreaker_failure_count_threshold: 1) + end + it { expect(breaker.failure_count_threshold).to eq(1) } end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 2dfb4d4a07f..4d448a55978 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -43,10 +43,6 @@ module StubConfiguration messages['default'] ||= Gitlab.config.repositories.storages.default messages.each do |storage_name, storage_settings| storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path') - storage_settings['failure_count_threshold'] ||= 10 - storage_settings['failure_wait_time'] ||= 30 - storage_settings['failure_reset_time'] ||= 1800 - storage_settings['storage_timeout'] ||= 5 end allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages)) -- cgit v1.2.1 From 38af7c1613e75561b405b15d6b8db1724da59ef6 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 12 Oct 2017 16:35:59 +0200 Subject: Allow configuring the circuitbreaker through the API and UI --- spec/models/application_setting_spec.rb | 13 +++++++++++++ spec/requests/api/settings_spec.rb | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 78cacf9ff5d..cf192691507 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -114,6 +114,19 @@ describe ApplicationSetting do it { expect(setting.repository_storages).to eq(['default']) } end + context 'circuitbreaker settings' do + [:circuitbreaker_failure_count_threshold, + :circuitbreaker_failure_wait_time, + :circuitbreaker_failure_reset_time, + :circuitbreaker_storage_timeout].each do |field| + it "Validates #{field} as number" do + is_expected.to validate_numericality_of(field) + .only_integer + .is_greater_than_or_equal_to(0) + end + end + end + context 'repository storages' do before do storages = { diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 0b9a4b5c3db..c24de58ee9d 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -23,6 +23,7 @@ describe API::Settings, 'Settings' do expect(json_response['dsa_key_restriction']).to eq(0) expect(json_response['ecdsa_key_restriction']).to eq(0) expect(json_response['ed25519_key_restriction']).to eq(0) + expect(json_response['circuitbreaker_failure_count_threshold']).not_to be_nil end end @@ -52,7 +53,8 @@ describe API::Settings, 'Settings' do rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE, dsa_key_restriction: 2048, ecdsa_key_restriction: 384, - ed25519_key_restriction: 256 + ed25519_key_restriction: 256, + circuitbreaker_failure_wait_time: 2 expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) @@ -73,6 +75,7 @@ describe API::Settings, 'Settings' do expect(json_response['dsa_key_restriction']).to eq(2048) expect(json_response['ecdsa_key_restriction']).to eq(384) expect(json_response['ed25519_key_restriction']).to eq(256) + expect(json_response['circuitbreaker_failure_wait_time']).to eq(2) end end -- cgit v1.2.1 From c3dec96e837d6118b2a3d354ec1f142894da48a1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 17 Oct 2017 11:03:12 +0100 Subject: Fixed bug when clicking file link causing user to navigate away Adds a test for flattenedFiles Changes the data method to not be an arrow method Various other review fixes --- spec/javascripts/repo/components/repo_sidebar_spec.js | 13 +++++++++++++ spec/javascripts/repo/mock_data.js | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index c61631bb230..61283da8257 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -69,6 +69,19 @@ describe('RepoSidebar', () => { expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); }); + describe('flattendFiles', () => { + it('returns a flattend array of files', () => { + const f = file(); + f.files.push(file('testing 123')); + const files = [f, file()]; + vm = createComponent(); + vm.files = files; + + expect(vm.flattendFiles.length).toBe(3); + expect(vm.flattendFiles[1].name).toBe('testing 123'); + }); + }); + describe('methods', () => { describe('fileClicked', () => { it('should fetch data for new file', () => { diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js index 46c68c00357..836b867205e 100644 --- a/spec/javascripts/repo/mock_data.js +++ b/spec/javascripts/repo/mock_data.js @@ -1,10 +1,10 @@ import RepoHelper from '~/repo/helpers/repo_helper'; // eslint-disable-next-line import/prefer-default-export -export const file = () => RepoHelper.serializeRepoEntity('blob', { +export const file = (name = 'name') => RepoHelper.serializeRepoEntity('blob', { icon: 'icon', url: 'url', - name: 'name', + name, last_commit: { id: '123', message: 'test', -- cgit v1.2.1 From bdbcf58ac085e4c96a6eb0bc2a2be767e3b74b9d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 17 Oct 2017 11:46:00 +0100 Subject: fixed Karam test because of event name change --- spec/javascripts/repo/components/repo_file_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index da8c850bc78..334bf0997ca 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -95,7 +95,7 @@ describe('RepoFile', () => { describe('methods', () => { describe('linkClicked', () => { - it('$emits linkclicked with file obj', () => { + it('$emits fileNameClicked with file obj', () => { spyOn(eventHub, '$emit'); const vm = createComponent({ @@ -104,7 +104,7 @@ describe('RepoFile', () => { vm.linkClicked(vm.file); - expect(eventHub.$emit).toHaveBeenCalledWith('linkclicked', vm.file); + expect(eventHub.$emit).toHaveBeenCalledWith('fileNameClicked', vm.file); }); }); }); -- cgit v1.2.1 From c365dea887ae0f139cf99e36701a2e4ca08ec0b9 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 16 Oct 2017 11:38:01 +0200 Subject: Don't use `Redis#keys` in the circuitbreaker --- .../lib/gitlab/git/storage/circuit_breaker_spec.rb | 4 +++ spec/lib/gitlab/git/storage/health_spec.rb | 30 ---------------------- spec/support/redis_without_keys.rb | 8 ++++++ 3 files changed, 12 insertions(+), 30 deletions(-) create mode 100644 spec/support/redis_without_keys.rb (limited to 'spec') diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index 98cf7966dad..4edd360d11d 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -49,6 +49,10 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(key_exists).to be_falsey end + + it 'does not break when there are no keys in redis' do + expect { described_class.reset_all! }.not_to raise_error + end end describe '.for_storage' do diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb index 2d3af387971..4a14a5201d1 100644 --- a/spec/lib/gitlab/git/storage/health_spec.rb +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -20,36 +20,6 @@ describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, br end end - describe '.load_for_keys' do - let(:subject) do - results = Gitlab::Git::Storage.redis.with do |redis| - fake_future = double - allow(fake_future).to receive(:value).and_return([host1_key]) - described_class.load_for_keys({ 'broken' => fake_future }, redis) - end - - # Make sure the `Redis#future is loaded - results.inject({}) do |result, (name, info)| - info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } - - result[name] = info - - result - end - end - - it 'loads when there is no info in redis' do - expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }]) - end - - it 'reads the correct values for a storage from redis' do - set_in_redis(host1_key, 5) - set_in_redis(host2_key, 7) - - expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }]) - end - end - describe '.for_all_storages' do it 'loads health status for all configured storages' do healths = described_class.for_all_storages diff --git a/spec/support/redis_without_keys.rb b/spec/support/redis_without_keys.rb new file mode 100644 index 00000000000..6220167dee6 --- /dev/null +++ b/spec/support/redis_without_keys.rb @@ -0,0 +1,8 @@ +class Redis + ForbiddenCommand = Class.new(StandardError) + + def keys(*args) + raise ForbiddenCommand.new("Don't use `Redis#keys` as it iterates over all "\ + "keys in redis. Use `Redis#scan_each` instead.") + end +end -- cgit v1.2.1 From 9245bfc2936209eb52c1f34451b2ecd401626c60 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 17 Oct 2017 12:15:40 +0100 Subject: Handle null serialised commits in background migration This is already handled for diffs, but not commits. --- ...erialize_merge_request_diffs_and_commits_spec.rb | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index d2e7243ee05..4d3fdbd9554 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -31,8 +31,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t end it 'creates correct entries in the merge_request_diff_commits table' do - expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(commits.count) - expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(commits) + expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(expected_commits.count) + expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(expected_commits) end it 'creates correct entries in the merge_request_diff_files table' do @@ -199,6 +199,16 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diff has valid commits and diffs' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } + let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } + let(:expected_diffs) { diffs } + + include_examples 'updated MR diff' + end + + context 'when the merge request diff has diffs but no commits' do + let(:commits) { nil } + let(:expected_commits) { [] } let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:expected_diffs) { diffs } @@ -207,6 +217,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs do not have too_large set' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:diffs) do @@ -218,6 +229,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs do not have a_mode and b_mode set' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } let(:diffs) do @@ -229,6 +241,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs have binary content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:expected_diffs) { diffs } # The start of a PDF created by Illustrator @@ -257,6 +270,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diff has commits, but no diffs' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:diffs) { [] } let(:expected_diffs) { diffs } @@ -265,6 +279,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs have invalid content' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_commits) { commits } let(:diffs) { ['--broken-diff'] } let(:expected_diffs) { [] } @@ -274,6 +289,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs are Rugged::Patch instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:expected_commits) { commits } let(:diffs) { first_commit.rugged_diff_from_parent.patches } let(:expected_diffs) { [] } @@ -283,6 +299,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t context 'when the merge request diffs are Rugged::Diff::Delta instances' do let(:commits) { merge_request_diff.commits.map(&:to_hash) } let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:expected_commits) { commits } let(:diffs) { first_commit.rugged_diff_from_parent.deltas } let(:expected_diffs) { [] } -- cgit v1.2.1 From 2c0b677604e438b9bf608e6ea17b47e5640b4700 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 17 Oct 2017 16:33:30 +0200 Subject: Fix errors when deleting a forked project The problem would occur when the `ForkedProjectLink` was deleted, but the `ForkNetworkMember` was not. The delete would be rolled back and retried. But the error would not be saved because `Project#forked?` would still be true, because the `ForkNetworkMember` exists. But the `Project#forked_project_link` would be `nil`. So the validation for the visibility level would fail. --- spec/services/projects/destroy_service_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'spec') diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index c90bad46295..0bec2054f50 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::DestroyService do + include ProjectForksHelper + let!(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace) } let!(:path) { project.repository.path_to_repo } @@ -212,6 +214,21 @@ describe Projects::DestroyService do end end + context 'for a forked project with LFS objects' do + let(:forked_project) { fork_project(project, user) } + + before do + project.lfs_objects << create(:lfs_object) + forked_project.forked_project_link.destroy + forked_project.reload + end + + it 'destroys the fork' do + expect { destroy_project(forked_project, user) } + .not_to raise_error + end + end + context 'as the root of a fork network' do let!(:fork_network) { create(:fork_network, root_project: project) } -- cgit v1.2.1 From 22a120a24d2523b5fca91b5fe08a57a7be75d72e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 17 Oct 2017 16:19:40 +0000 Subject: Improve sprite icon spec --- spec/javascripts/lib/utils/common_utils_spec.js | 28 ++++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 6613b7dee6b..a5298be5669 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -467,19 +467,27 @@ describe('common_utils', () => { commonUtils.ajaxPost(requestURL, data); expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); }); + }); - describe('gl.utils.spriteIcon', () => { - beforeEach(() => { - window.gon.sprite_icons = 'icons.svg'; - }); + describe('spriteIcon', () => { + let beforeGon; - it('should return the svg for a linked icon', () => { - expect(gl.utils.spriteIcon('test')).toEqual(''); - }); + beforeEach(() => { + window.gon = window.gon || {}; + beforeGon = Object.assign({}, window.gon); + window.gon.sprite_icons = 'icons.svg'; + }); - it('should set svg className when passed', () => { - expect(gl.utils.spriteIcon('test', 'fa fa-test')).toEqual(''); - }); + afterEach(() => { + window.gon = beforeGon; + }); + + it('should return the svg for a linked icon', () => { + expect(commonUtils.spriteIcon('test')).toEqual(''); + }); + + it('should set svg className when passed', () => { + expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual(''); }); }); }); -- cgit v1.2.1 From fc68a3aeaff713de13e2f709588c82fdb8b48cfe Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 17 Oct 2017 16:44:34 +0000 Subject: Change project deletion message from alert to notice --- spec/features/projects_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 3bc7ec3123f..3b01ed442bf 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -139,7 +139,7 @@ feature 'Project' do it 'removes a project' do expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) - expect(page).to have_content "Project 'test / project1' will be deleted." + expect(page).to have_content "Project 'test / project1' is in the process of being deleted." expect(Project.all.count).to be_zero expect(project.issues).to be_empty expect(project.merge_requests).to be_empty -- cgit v1.2.1 From baf07e914d744f9c6b4daa6f84b96d506f1ffe46 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 17 Oct 2017 17:04:33 +0000 Subject: Add inline edit button to issue_show app --- spec/javascripts/issue_show/components/app_spec.js | 11 ++++++ .../issue_show/components/title_spec.js | 39 ++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 583a3a74d77..2ea290108a4 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -332,4 +332,15 @@ describe('Issuable output', () => { .catch(done.fail); }); }); + + describe('show inline edit button', () => { + it('should not render by default', () => { + expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + }); + + it('should render if showInlineEditButton', () => { + vm.showInlineEditButton = true; + expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined(); + }); + }); }); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index a2d90a9b9f5..c1edc785d0f 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import Store from '~/issue_show/stores'; import titleComponent from '~/issue_show/components/title.vue'; +import eventHub from '~/issue_show/event_hub'; describe('Title component', () => { let vm; @@ -25,7 +26,7 @@ describe('Title component', () => { it('renders title HTML', () => { expect( - vm.$el.innerHTML.trim(), + vm.$el.querySelector('.title').innerHTML.trim(), ).toBe('Testing '); }); @@ -47,12 +48,12 @@ describe('Title component', () => { Vue.nextTick(() => { expect( - vm.$el.classList.contains('issue-realtime-pre-pulse'), + vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'), ).toBeTruthy(); setTimeout(() => { expect( - vm.$el.classList.contains('issue-realtime-trigger-pulse'), + vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'), ).toBeTruthy(); done(); @@ -72,4 +73,36 @@ describe('Title component', () => { done(); }); }); + + describe('inline edit button', () => { + beforeEach(() => { + spyOn(eventHub, '$emit'); + }); + + it('should not show by default', () => { + expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + }); + + it('should not show if canUpdate is false', () => { + vm.showInlineEditButton = true; + vm.canUpdate = false; + expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + }); + + it('should show if showInlineEditButton and canUpdate', () => { + vm.showInlineEditButton = true; + vm.canUpdate = true; + expect(vm.$el.querySelector('.note-action-button')).toBeDefined(); + }); + + it('should trigger open.form event when clicked', () => { + vm.showInlineEditButton = true; + vm.canUpdate = true; + + Vue.nextTick(() => { + vm.$el.querySelector('.note-action-button').click(); + expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); + }); + }); + }); }); -- cgit v1.2.1