diff options
Diffstat (limited to 'spec')
147 files changed, 4063 insertions, 2016 deletions
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 5dd8f66343f..2565622f8df 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -3,12 +3,49 @@ require 'spec_helper' describe Admin::ApplicationSettingsController do include StubENV + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } let(:admin) { create(:admin) } + let(:user) { create(:user)} before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end + describe 'GET #usage_data with no access' do + before do + sign_in(user) + end + + it 'returns 404' do + get :usage_data, format: :html + + expect(response.status).to eq(404) + end + end + + describe 'GET #usage_data' do + before do + sign_in(admin) + end + + it 'returns HTML data' do + get :usage_data, format: :html + + expect(response.body).to start_with('<span') + expect(response.status).to eq(200) + end + + it 'returns JSON data' do + get :usage_data, format: :json + + body = JSON.parse(response.body) + expect(body["version"]).to eq(Gitlab::VERSION) + expect(body).to include('counts') + expect(response.status).to eq(200) + end + end + describe 'PUT #update' do before do sign_in(admin) diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index 84db26a958a..c29b2fe8946 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -22,4 +22,28 @@ describe Admin::GroupsController do expect(response).to redirect_to(admin_groups_path) end end + + describe 'PUT #members_update' do + let(:group_user) { create(:user) } + + it 'adds user to members' do + put :members_update, id: group, + user_ids: group_user.id, + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'Users were successfully added.' + expect(response).to redirect_to(admin_group_path(group)) + expect(group.users).to include group_user + end + + it 'adds no user to members' do + put :members_update, id: group, + user_ids: '', + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'No users specified.' + expect(response).to redirect_to(admin_group_path(group)) + expect(group.users).not_to include group_user + end + end end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 3e9f272a0d8..0fd09d156c4 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -106,7 +106,7 @@ describe Projects::BlobController do namespace_id: project.namespace, project_id: project, id: 'master/CHANGELOG', - target_branch: 'master', + branch_name: 'master', content: 'Added changes', commit_message: 'Update CHANGELOG' } @@ -178,7 +178,7 @@ describe Projects::BlobController do context 'when editing on the original repository' do it "redirects to forked project new merge request" do - default_params[:target_branch] = "fork-test-1" + default_params[:branch_name] = "fork-test-1" default_params[:create_merge_request] = 1 put :update, default_params diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb index 13208d21918..faf3770f5e9 100644 --- a/spec/controllers/projects/builds_controller_spec.rb +++ b/spec/controllers/projects/builds_controller_spec.rb @@ -60,7 +60,7 @@ describe Projects::BuildsController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to eq status.favicon + expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" end end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 6a6e9bf378a..05999431d8f 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -127,7 +127,7 @@ describe Projects::LabelsController do context 'group owner' do before do - GroupMember.add_users_to_group(group, [user], :owner) + GroupMember.add_users(group, [user], :owner) end it 'gives access' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 1739d40ab88..cc393bd24f2 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1208,7 +1208,7 @@ describe Projects::MergeRequestsController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to eq status.favicon + expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index d8f9bfd0d37..d9192177a06 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -86,7 +86,7 @@ describe Projects::PipelinesController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to eq status.favicon + expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" end end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 416eaa0037e..a4b4392d7cc 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -55,7 +55,7 @@ describe Projects::ProjectMembersController do user_ids: '', access_level: Gitlab::Access::GUEST - expect(response).to set_flash.to 'No users or groups specified.' + expect(response).to set_flash.to 'No users specified.' expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project)) end end @@ -225,7 +225,7 @@ describe Projects::ProjectMembersController do id: member expect(response).to redirect_to( - namespace_project_project_members_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) expect(project.members).to include member end diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index ab94e292e48..a43dad5756d 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -97,29 +97,29 @@ describe Projects::TreeController do project_id: project, id: 'master', dir_name: path, - target_branch: target_branch, + branch_name: branch_name, commit_message: 'Test commit message') end context 'successful creation' do let(:path) { 'files/new_dir'} - let(:target_branch) { 'master-test'} + let(:branch_name) { 'master-test'} it 'redirects to the new directory' do expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/#{target_branch}/#{path}") + to redirect_to("/#{project.path_with_namespace}/tree/#{branch_name}/#{path}") expect(flash[:notice]).to eq('The directory has been successfully created.') end end context 'unsuccessful creation' do let(:path) { 'README.md' } - let(:target_branch) { 'master'} + let(:branch_name) { 'master'} it 'does not allow overwriting of existing files' do expect(subject). to redirect_to("/#{project.path_with_namespace}/tree/master") - expect(flash[:alert]).to eq('Directory already exists as a file') + expect(flash[:alert]).to eq('A file with this name already exists') end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 9c16a7bc08b..038132cffe0 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -16,7 +16,9 @@ describe SessionsController do end end - context 'when using valid password' do + context 'when using valid password', :redis do + include UserActivitiesHelpers + let(:user) { create(:user) } it 'authenticates user correctly' do @@ -37,6 +39,12 @@ describe SessionsController do subject.sign_out user end end + + it 'updates the user activity' do + expect do + post(:create, user: { login: user.username, password: user.password }) + end.to change { user_activity(user) } + end end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 361f9dac191..253a025af48 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -40,6 +40,10 @@ FactoryGirl.define do state :closed end + trait :opened do + state :opened + end + trait :reopened do state :reopened end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 87a8f62687a..9d205104ebe 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -109,7 +109,7 @@ describe "Admin::Projects", feature: true do expect(page).to have_content('Developer') end - find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-remove').click expect(page).not_to have_selector(:css, '.content-list') end diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb index 14c193f7450..543879bd21d 100644 --- a/spec/features/groups/members/list_spec.rb +++ b/spec/features/groups/members/list_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Groups members list', feature: true do + include Select2Helper + let(:user1) { create(:user, name: 'John Doe') } let(:user2) { create(:user, name: 'Mary Jane') } let(:group) { create(:group) } @@ -30,7 +32,7 @@ feature 'Groups members list', feature: true do expect(second_row).to be_blank end - it 'updates user to owner level', :js do + scenario 'update user to owner level', :js do group.add_owner(user1) group.add_developer(user2) @@ -38,13 +40,52 @@ feature 'Groups members list', feature: true do page.within(second_row) do click_button('Developer') - click_link('Owner') expect(page).to have_button('Owner') end end + scenario 'add user to group', :js do + group.add_owner(user1) + + visit group_group_members_path(group) + + add_user(user2.id, 'Reporter') + + page.within(second_row) do + expect(page).to have_content(user2.name) + expect(page).to have_button('Reporter') + end + end + + scenario 'add yourself to group when already an owner', :js do + group.add_owner(user1) + + visit group_group_members_path(group) + + add_user(user1.id, 'Reporter') + + page.within(first_row) do + expect(page).to have_content(user1.name) + expect(page).to have_content('Owner') + end + end + + scenario 'invite user to group', :js do + group.add_owner(user1) + + visit group_group_members_path(group) + + add_user('test@example.com', 'Reporter') + + page.within(second_row) do + expect(page).to have_content('test@example.com') + expect(page).to have_content('Invited') + expect(page).to have_button('Reporter') + end + end + def first_row page.all('ul.content-list > li')[0] end @@ -52,4 +93,13 @@ feature 'Groups members list', feature: true do def second_row page.all('ul.content-list > li')[1] end + + def add_user(id, role) + page.within ".users-group-form" do + select2(id, from: "#user_ids", multiple: true) + select(role, from: "access_level") + end + + click_button "Add to group" + end end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb new file mode 100644 index 00000000000..daa2c6afd63 --- /dev/null +++ b/spec/features/groups/milestone_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +feature 'Group milestones', :feature, :js do + let(:group) { create(:group) } + let!(:project) { create(:project_empty_repo, group: group) } + let(:user) { create(:group_member, :master, user: create(:user), group: group ).user } + + before do + Timecop.freeze + + login_as(user) + end + + after do + Timecop.return + end + + context 'create a milestone' do + before do + visit new_group_milestone_path(group) + end + + it 'creates milestone with start date' do + fill_in 'Title', with: 'testing' + find('#milestone_start_date').click + + page.within(find('.pika-single')) do + click_button '1' + end + + click_button 'Create milestone' + + expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y')) + end + end +end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 7b9d4534ada..85585587fb1 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -120,6 +120,20 @@ feature 'Issue Sidebar', feature: true do end end + context 'as a allowed mobile user', js: true do + before do + project.team << [user, :developer] + resize_screen_xs + visit_issue(project, issue) + end + + context 'mobile sidebar' do + it 'collapses the sidebar for small screens' do + expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') + end + end + end + context 'as a guest' do before do project.team << [user, :guest] diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index f5cfe2d666e..378f6de1a78 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -1,17 +1,15 @@ require 'spec_helper' -feature 'Issue notes polling' do - let!(:project) { create(:project, :public) } - let!(:issue) { create(:issue, project: project) } +feature 'Issue notes polling', :feature, :js do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project) } - background do + before do visit namespace_project_issue_path(project.namespace, project, issue) end - scenario 'Another user adds a comment to an issue', js: true do - note = create(:note, noteable: issue, project: project, - note: 'Looks good!') - + it 'should display the new comment' do + note = create(:note, noteable: issue, project: project, note: 'Looks good!') page.execute_script('notes.refresh();') expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index e3213d24f6a..55eca187f6c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -414,7 +414,8 @@ describe 'Issues', feature: true do it 'will not send ajax request when no data is changed' do page.within '.labels' do click_link 'Edit' - first('.dropdown-menu-close').click + + find('.dropdown-menu-close', match: :first).click expect(page).not_to have_selector('.block-loading') end @@ -601,10 +602,10 @@ describe 'Issues', feature: true do expect(page.find_field("issue_description").value).to have_content 'banana_sample' end - it 'adds double newline to end of attachment markdown' do + it "doesn't add double newline to end of a single attachment markdown" do dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') - expect(page.find_field("issue_description").value).to match /\n\n$/ + expect(page.find_field("issue_description").value).not_to match /\n\n$/ end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 3a4ec07b2b0..16b09933bda 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -20,13 +20,13 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_content('Target branch') first('.js-source-branch').click - first('.dropdown-source-branch .dropdown-content a', text: 'v1.1.0').click + find('.dropdown-source-branch .dropdown-content a', match: :first).click expect(page).to have_content "b83d6e3" end it 'selects the target branch sha when a tag with the same name exists' do - visit namespace_project_merge_requests_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) click_link 'New merge request' @@ -46,8 +46,8 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_content('Source branch') expect(page).to have_content('Target branch') - first('.js-source-branch').click - first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click + find('.js-source-branch', match: :first).click + find('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch', match: :first).click click_button "Compare branches" click_link "Changes" diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index 88d28b649a4..0e23c3a8849 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -198,6 +198,8 @@ feature 'Diff notes resolve', feature: true, js: true do it 'does not mark discussion as resolved when resolving single note' do page.first '.diff-content .note' do first('.line-resolve-btn').click + + expect(page).to have_selector('.note-action-button .loading') expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") end diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb deleted file mode 100644 index 06fad1007e8..00000000000 --- a/spec/features/merge_requests/diff_notes_spec.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'spec_helper' - -feature 'Diff notes', js: true, feature: true do - include WaitForAjax - - before do - login_as :admin - @merge_request = create(:merge_request) - @project = @merge_request.source_project - end - - context 'merge request diffs' do - let(:comment_button_class) { '.add-diff-note' } - let(:notes_holder_input_class) { 'js-temp-notes-holder' } - let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } - let(:test_note_comment) { 'this is a test note!' } - - context 'when hovering over a parallel view diff file' do - before(:each) do - visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel') - end - - context 'with an old line on the left and no line on the right' do - it 'should allow commenting on the left side' do - should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left') - end - - it 'should not allow commenting on the right side' do - should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right') - end - end - - context 'with no line on the left and a new line on the right' do - it 'should not allow commenting on the left side' do - should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left') - end - - it 'should allow commenting on the right side' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right') - end - end - - context 'with an old line on the left and a new line on the right' do - it 'should allow commenting on the left side' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left') - end - - it 'should allow commenting on the right side' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right') - end - end - - context 'with an unchanged line on the left and an unchanged line on the right' do - it 'should allow commenting on the left side' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left') - end - - it 'should allow commenting on the right side' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right') - end - end - - context 'with a match line' do - it 'should not allow commenting on the left side' do - should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left') - end - - it 'should not allow commenting on the right side' do - should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right') - end - end - - context 'with an unfolded line' do - before(:each) do - find('.js-unfold', match: :first).click - wait_for_ajax - end - - # The first `.js-unfold` unfolds upwards, therefore the first - # `.line_holder` will be an unfolded line. - let(:line_holder) { first('.line_holder[id="1"]') } - - it 'should not allow commenting on the left side' do - should_not_allow_commenting(line_holder, 'left') - end - - it 'should not allow commenting on the right side' do - should_not_allow_commenting(line_holder, 'right') - end - end - end - - context 'when hovering over an inline view diff file' do - before do - visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') - end - - context 'with a new line' do - it 'should allow commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) - end - end - - context 'with an old line' do - it 'should allow commenting' do - should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) - end - end - - context 'with an unchanged line' do - it 'should allow commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) - end - end - - context 'with a match line' do - it 'should not allow commenting' do - should_not_allow_commenting(find('.match', match: :first)) - end - end - - context 'with an unfolded line' do - before(:each) do - find('.js-unfold', match: :first).click - wait_for_ajax - end - - # The first `.js-unfold` unfolds upwards, therefore the first - # `.line_holder` will be an unfolded line. - let(:line_holder) { first('.line_holder[id="1"]') } - - it 'should not allow commenting' do - should_not_allow_commenting line_holder - end - end - - context 'when hovering over a diff discussion' do - before do - visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) - visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - end - - it 'should not allow commenting' do - should_not_allow_commenting(find('.line_holder', match: :first)) - end - end - end - - context 'when the MR only supports legacy diff notes' do - before do - @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) - visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') - end - - context 'with a new line' do - it 'should allow commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) - end - end - - context 'with an old line' do - it 'should allow commenting' do - should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) - end - end - - context 'with an unchanged line' do - it 'should allow commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) - end - end - - context 'with a match line' do - it 'should not allow commenting' do - should_not_allow_commenting(find('.match', match: :first)) - end - end - end - - def should_allow_commenting(line_holder, diff_side = nil) - line = get_line_components(line_holder, diff_side) - line[:content].hover - expect(line[:num]).to have_css comment_button_class - - comment_on_line(line_holder, line) - - assert_comment_persistence(line_holder) - end - - def should_not_allow_commenting(line_holder, diff_side = nil) - line = get_line_components(line_holder, diff_side) - line[:content].hover - expect(line[:num]).not_to have_css comment_button_class - end - - def get_line_components(line_holder, diff_side = nil) - if diff_side.nil? - get_inline_line_components(line_holder) - else - get_parallel_line_components(line_holder, diff_side) - end - end - - def get_inline_line_components(line_holder) - { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) } - end - - def get_parallel_line_components(line_holder, diff_side = nil) - side_index = diff_side == 'left' ? 0 : 1 - # Wait for `.line_content` - line_holder.find('.line_content', match: :first) - # Wait for `.diff-line-num` - line_holder.find('.diff-line-num', match: :first) - { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] } - end - - def comment_on_line(line_holder, line) - line[:num].find(comment_button_class).trigger 'click' - line_holder.find(:xpath, notes_holder_input_xpath) - - notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath) - expect(notes_holder_input[:class]).to include(notes_holder_input_class) - - notes_holder_input.fill_in 'note[note]', with: test_note_comment - click_button 'Comment' - wait_for_ajax - end - - def assert_comment_persistence(line_holder) - expect(line_holder).to have_xpath notes_holder_input_xpath - - notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath) - expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class) - expect(notes_holder_saved).to have_content test_note_comment - end - end -end diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb new file mode 100644 index 00000000000..7756202e3f5 --- /dev/null +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -0,0 +1,294 @@ +require 'spec_helper' + +feature 'Merge requests > User posts diff notes', :js do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.source_project } + + before do + project.add_developer(user) + login_as(user) + end + + let(:comment_button_class) { '.add-diff-note' } + let(:notes_holder_input_class) { 'js-temp-notes-holder' } + let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } + let(:test_note_comment) { 'this is a test note!' } + + context 'when hovering over a parallel view diff file' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'parallel') + end + + context 'with an old line on the left and no line on the right' do + it 'allows commenting on the left side' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left') + end + + it 'does not allow commenting on the right side' do + should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right') + end + end + + context 'with no line on the left and a new line on the right' do + it 'does not allow commenting on the left side' do + should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left') + end + + it 'allows commenting on the right side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right') + end + end + + context 'with an old line on the left and a new line on the right' do + it 'allows commenting on the left side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left') + end + + it 'allows commenting on the right side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right') + end + end + + context 'with an unchanged line on the left and an unchanged line on the right' do + it 'allows commenting on the left side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left') + end + + it 'allows commenting on the right side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right') + end + end + + context 'with a match line' do + it 'does not allow commenting on the left side' do + should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left') + end + + it 'does not allow commenting on the right side' do + should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right') + end + end + + context 'with an unfolded line' do + before(:each) do + find('.js-unfold', match: :first).click + wait_for_ajax + end + + # The first `.js-unfold` unfolds upwards, therefore the first + # `.line_holder` will be an unfolded line. + let(:line_holder) { first('.line_holder[id="1"]') } + + it 'does not allow commenting on the left side' do + should_not_allow_commenting(line_holder, 'left') + end + + it 'does not allow commenting on the right side' do + should_not_allow_commenting(line_holder, 'right') + end + end + end + + context 'when hovering over an inline view diff file' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline') + end + + context 'with a new line' do + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'allows commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'does not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + + context 'with an unfolded line' do + before(:each) do + find('.js-unfold', match: :first).click + wait_for_ajax + end + + # The first `.js-unfold` unfolds upwards, therefore the first + # `.line_holder` will be an unfolded line. + let(:line_holder) { first('.line_holder[id="1"]') } + + it 'does not allow commenting' do + should_not_allow_commenting line_holder + end + end + + context 'when hovering over a diff discussion' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline') + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not allow commenting' do + should_not_allow_commenting(find('.line_holder', match: :first)) + end + end + end + + context 'when cancelling the comment addition' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline') + end + + context 'with a new line' do + it 'allows dismissing a comment' do + should_allow_dismissing_a_comment(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + end + + describe 'with muliple note forms' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline') + click_diff_line(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + click_diff_line(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + + describe 'posting a note' do + it 'adds as discussion' do + expect(page).to have_css('.js-temp-notes-holder', count: 2) + + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false) + expect(page).to have_css('.notes_holder .note', count: 1) + expect(page).to have_css('.js-temp-notes-holder', count: 1) + expect(page).to have_button('Reply...') + end + end + end + + context 'when the MR only supports legacy diff notes' do + before do + merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline') + end + + context 'with a new line' do + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'allows commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'does not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + end + + def should_allow_commenting(line_holder, diff_side = nil, asset_form_reset: true) + write_comment_on_line(line_holder, diff_side) + + click_button 'Comment' + wait_for_ajax + + assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset) + end + + def should_allow_dismissing_a_comment(line_holder, diff_side = nil) + write_comment_on_line(line_holder, diff_side) + + find('.js-close-discussion-note-form').trigger('click') + + assert_comment_dismissal(line_holder) + end + + def should_not_allow_commenting(line_holder, diff_side = nil) + line = get_line_components(line_holder, diff_side) + line[:content].hover + expect(line[:num]).not_to have_css comment_button_class + end + + def get_line_components(line_holder, diff_side = nil) + if diff_side.nil? + get_inline_line_components(line_holder) + else + get_parallel_line_components(line_holder, diff_side) + end + end + + def get_inline_line_components(line_holder) + { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) } + end + + def get_parallel_line_components(line_holder, diff_side = nil) + side_index = diff_side == 'left' ? 0 : 1 + # Wait for `.line_content` + line_holder.find('.line_content', match: :first) + # Wait for `.diff-line-num` + line_holder.find('.diff-line-num', match: :first) + { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] } + end + + def click_diff_line(line_holder, diff_side = nil) + line = get_line_components(line_holder, diff_side) + line[:content].hover + + expect(line[:num]).to have_css comment_button_class + + line[:num].find(comment_button_class).trigger 'click' + end + + def write_comment_on_line(line_holder, diff_side) + click_diff_line(line_holder, diff_side) + + notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath) + + expect(notes_holder_input[:class]).to include(notes_holder_input_class) + + notes_holder_input.fill_in 'note[note]', with: test_note_comment + end + + def assert_comment_persistence(line_holder, asset_form_reset:) + notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath) + + expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class) + expect(notes_holder_saved).to have_content test_note_comment + + assert_form_is_reset if asset_form_reset + end + + def assert_comment_dismissal(line_holder) + expect(line_holder).not_to have_xpath notes_holder_input_xpath + expect(page).not_to have_content test_note_comment + + assert_form_is_reset + end + + def assert_form_is_reset + expect(page).to have_no_css('.js-temp-notes-holder') + end +end diff --git a/spec/features/merge_requests/user_posts_notes.rb b/spec/features/merge_requests/user_posts_notes.rb new file mode 100644 index 00000000000..c7cc4d6bc72 --- /dev/null +++ b/spec/features/merge_requests/user_posts_notes.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe 'Merge requests > User posts notes', :js do + let(:project) { create(:project) } + let(:merge_request) do + create(:merge_request, source_project: project, target_project: project) + end + let!(:note) do + create(:note_on_merge_request, :with_attachment, noteable: merge_request, + project: project) + end + + before do + login_as :admin + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + subject { page } + + describe 'the note form' do + it 'is valid' do + is_expected.to have_css('.js-main-target-form', visible: true, count: 1) + expect(find('.js-main-target-form .js-comment-button').value). + to eq('Comment') + page.within('.js-main-target-form') do + expect(page).not_to have_link('Cancel') + end + end + + describe 'with text' do + before do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: 'This is awesome' + end + end + + it 'has enable submit button and preview button' do + page.within('.js-main-target-form') do + expect(page).not_to have_css('.js-comment-button[disabled]') + expect(page).to have_css('.js-md-preview-button', visible: true) + end + end + end + end + + describe 'when posting a note' do + before do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: 'This is awesome!' + find('.js-md-preview-button').click + click_button 'Comment' + end + end + + it 'is added and form reset' do + is_expected.to have_content('This is awesome!') + page.within('.js-main-target-form') do + expect(page).to have_no_field('note[note]', with: 'This is awesome!') + expect(page).to have_css('.js-md-preview', visible: :hidden) + end + page.within('.js-main-target-form') do + is_expected.to have_css('.js-note-text', visible: true) + end + end + end + + describe 'when editing a note' do + it 'there should be a hidden edit form' do + is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1) + is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1) + end + + describe 'editing the note' do + before do + find('.note').hover + find('.js-note-edit').click + end + + it 'shows the note edit form and hide the note body' do + page.within("#note_#{note.id}") do + expect(find('.current-note-edit-form', visible: true)).to be_visible + expect(find('.note-edit-form', visible: true)).to be_visible + expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible + end + end + + it 'resets the edit note form textarea with the original content of the note if cancelled' do + within('.current-note-edit-form') do + fill_in 'note[note]', with: 'Some new content' + find('.btn-cancel').click + expect(find('.js-note-text', visible: false).text).to eq '' + end + end + + it 'allows using markdown buttons after saving a note and then trying to edit it again' do + page.within('.current-note-edit-form') do + fill_in 'note[note]', with: 'This is the new content' + find('.btn-save').click + end + + find('.note').hover + find('.js-note-edit').click + + page.within('.current-note-edit-form') do + expect(find('#note_note').value).to eq('This is the new content') + find('.js-md:first-child').click + expect(find('#note_note').value).to eq('This is the new content****') + end + end + + it 'appends the edited at time to the note' do + page.within('.current-note-edit-form') do + fill_in 'note[note]', with: 'Some new content' + find('.btn-save').click + end + + page.within("#note_#{note.id}") do + is_expected.to have_css('.note_edited_ago') + expect(find('.note_edited_ago').text). + to match(/less than a minute ago/) + end + end + end + + describe 'deleting an attachment' do + before do + find('.note').hover + find('.js-note-edit').click + end + + it 'shows the delete link' do + page.within('.note-attachment') do + is_expected.to have_css('.js-note-attachment-delete') + end + end + + it 'removes the attachment div and resets the edit form' do + find('.js-note-attachment-delete').click + is_expected.not_to have_css('.note-attachment') + is_expected.not_to have_css('.current-note-edit-form') + wait_for_ajax + end + end + end +end diff --git a/spec/features/merge_requests/user_sees_system_notes_spec.rb b/spec/features/merge_requests/user_sees_system_notes_spec.rb new file mode 100644 index 00000000000..55d0f9d728c --- /dev/null +++ b/spec/features/merge_requests/user_sees_system_notes_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +feature 'Merge requests > User sees system notes' do + let(:public_project) { create(:project, :public) } + let(:private_project) { create(:project, :private) } + let(:issue) { create(:issue, project: private_project) } + let(:merge_request) { create(:merge_request, source_project: public_project, source_branch: 'markdown') } + let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: public_project, note: "mentioned in #{issue.to_reference(public_project)}") } + + context 'when logged-in as a member of the private project' do + before do + user = create(:user) + private_project.add_developer(user) + login_as(user) + end + + it 'shows the system note' do + visit namespace_project_merge_request_path(public_project.namespace, public_project, merge_request) + + expect(page).to have_css('.system-note') + end + end + + context 'when not logged-in' do + it 'hides the system note' do + visit namespace_project_merge_request_path(public_project.namespace, public_project, merge_request) + + expect(page).not_to have_css('.system-note') + end + end +end diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb index 68a68f5d3f3..7a2da623c58 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_requests/versions_spec.rb @@ -107,14 +107,13 @@ feature 'Merge Request versions', js: true, feature: true do it 'should have 0 chages between versions' do page.within '.mr-version-compare-dropdown' do - expect(page).to have_content 'version 1' + expect(find('.dropdown-toggle')).to have_content 'version 1' end page.within '.mr-version-dropdown' do find('.btn-default').click - find(:link, 'version 1').trigger('click') + click_link 'version 1' end - expect(page).to have_content '0 changed files' end end @@ -129,12 +128,12 @@ feature 'Merge Request versions', js: true, feature: true do it 'should set the compared versions to be the same' do page.within '.mr-version-compare-dropdown' do - expect(page).to have_content 'version 2' + expect(find('.dropdown-toggle')).to have_content 'version 2' end page.within '.mr-version-dropdown' do find('.btn-default').click - find(:link, 'version 1').trigger('click') + click_link 'version 1' end page.within '.mr-version-compare-dropdown' do diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index a62c5435748..4e128cd4a7d 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -141,6 +141,27 @@ describe 'Merge request', :feature, :js do end end + context 'view merge request with MWPS enabled but automatically merge fails' do + before do + merge_request.update( + merge_when_pipeline_succeeds: true, + merge_user: merge_request.author, + merge_error: 'Something went wrong' + ) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'shows information about the merge error' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + + page.within('.mr-widget-body') do + expect(page).to have_content('Something went wrong') + end + end + end + context 'merge error' do before do allow_any_instance_of(Repository).to receive(:merge).and_return(false) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb deleted file mode 100644 index 783f2e93909..00000000000 --- a/spec/features/notes_on_merge_requests_spec.rb +++ /dev/null @@ -1,285 +0,0 @@ -require 'spec_helper' - -describe 'Comments', feature: true do - include RepoHelpers - include WaitForAjax - - describe 'On a merge request', js: true, feature: true do - let!(:project) { create(:project) } - let!(:merge_request) do - create(:merge_request, source_project: project, target_project: project) - end - - let!(:note) do - create(:note_on_merge_request, :with_attachment, noteable: merge_request, - project: project) - end - - before do - login_as :admin - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - end - - subject { page } - - describe 'the note form' do - it 'is valid' do - is_expected.to have_css('.js-main-target-form', visible: true, count: 1) - expect(find('.js-main-target-form .js-comment-button').value). - to eq('Comment') - page.within('.js-main-target-form') do - expect(page).not_to have_link('Cancel') - end - end - - describe 'with text' do - before do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: 'This is awesome' - end - end - - it 'has enable submit button and preview button' do - page.within('.js-main-target-form') do - expect(page).not_to have_css('.js-comment-button[disabled]') - expect(page).to have_css('.js-md-preview-button', visible: true) - end - end - end - end - - describe 'when posting a note' do - before do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: 'This is awsome!' - find('.js-md-preview-button').click - click_button 'Comment' - end - end - - it 'is added and form reset' do - is_expected.to have_content('This is awsome!') - page.within('.js-main-target-form') do - expect(page).to have_no_field('note[note]', with: 'This is awesome!') - expect(page).to have_css('.js-md-preview', visible: :hidden) - end - page.within('.js-main-target-form') do - is_expected.to have_css('.js-note-text', visible: true) - end - end - end - - describe 'when editing a note', js: true do - it 'there should be a hidden edit form' do - is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1) - is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1) - end - - describe 'editing the note' do - before do - find('.note').hover - find('.js-note-edit').click - end - - it 'shows the note edit form and hide the note body' do - page.within("#note_#{note.id}") do - expect(find('.current-note-edit-form', visible: true)).to be_visible - expect(find('.note-edit-form', visible: true)).to be_visible - expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible - end - end - - it 'resets the edit note form textarea with the original content of the note if cancelled' do - within('.current-note-edit-form') do - fill_in 'note[note]', with: 'Some new content' - find('.btn-cancel').click - expect(find('.js-note-text', visible: false).text).to eq '' - end - end - - it 'allows using markdown buttons after saving a note and then trying to edit it again' do - page.within('.current-note-edit-form') do - fill_in 'note[note]', with: 'This is the new content' - find('.btn-save').click - end - - find('.note').hover - find('.js-note-edit').click - - page.within('.current-note-edit-form') do - expect(find('#note_note').value).to eq('This is the new content') - find('.js-md:first-child').click - expect(find('#note_note').value).to eq('This is the new content****') - end - end - - it 'appends the edited at time to the note' do - page.within('.current-note-edit-form') do - fill_in 'note[note]', with: 'Some new content' - find('.btn-save').click - end - - page.within("#note_#{note.id}") do - is_expected.to have_css('.note_edited_ago') - expect(find('.note_edited_ago').text). - to match(/less than a minute ago/) - end - end - end - - describe 'deleting an attachment' do - before do - find('.note').hover - find('.js-note-edit').click - end - - it 'shows the delete link' do - page.within('.note-attachment') do - is_expected.to have_css('.js-note-attachment-delete') - end - end - - it 'removes the attachment div and resets the edit form' do - find('.js-note-attachment-delete').click - is_expected.not_to have_css('.note-attachment') - is_expected.not_to have_css('.current-note-edit-form') - wait_for_ajax - end - end - end - end - - describe 'Handles cross-project system notes', js: true, feature: true do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - let(:project2) { create(:project, :private) } - let(:issue) { create(:issue, project: project2) } - let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') } - let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") } - - it 'shows the system note' do - login_as :admin - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - - expect(page).to have_css('.system-note') - end - - it 'hides redacted system note' do - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - - expect(page).not_to have_css('.system-note') - end - end - - describe 'On a merge request diff', js: true, feature: true do - let(:merge_request) { create(:merge_request) } - let(:project) { merge_request.source_project } - - before do - login_as :admin - visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) - end - - subject { page } - - describe 'when adding a note' do - before do - click_diff_line - end - - describe 'the notes holder' do - it { is_expected.to have_css('.js-temp-notes-holder') } - - it 'has .new_note css class' do - page.within('.js-temp-notes-holder') do - expect(subject).to have_css('.new-note') - end - end - end - - describe 'the note form' do - it "does not add a second form for same row" do - click_diff_line - - is_expected. - to have_css("form[data-line-code='#{line_code}']", - count: 1) - end - - it 'is removed when canceled' do - is_expected.to have_css('.js-temp-notes-holder') - - page.within("form[data-line-code='#{line_code}']") do - find('.js-close-discussion-note-form').trigger('click') - end - - is_expected.to have_no_css('.js-temp-notes-holder') - end - end - end - - describe 'with muliple note forms' do - before do - click_diff_line - click_diff_line(line_code_2) - end - - it { is_expected.to have_css('.js-temp-notes-holder', count: 2) } - - describe 'previewing them separately' do - before do - # add two separate texts and trigger previews on both - page.within("tr[id='#{line_code}'] + .js-temp-notes-holder") do - fill_in 'note[note]', with: 'One comment on line 7' - find('.js-md-preview-button').click - end - page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do - fill_in 'note[note]', with: 'Another comment on line 10' - find('.js-md-preview-button').click - end - end - end - - describe 'posting a note' do - before do - page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do - fill_in 'note[note]', with: 'Another comment on line 10' - click_button('Comment') - end - end - - it 'adds as discussion' do - is_expected.to have_content('Another comment on line 10') - is_expected.to have_css('.notes_holder') - is_expected.to have_css('.notes_holder .note', count: 1) - is_expected.to have_button('Reply...') - end - - it 'adds code to discussion' do - click_button 'Reply...' - - page.within(first('.js-discussion-note-form')) do - fill_in 'note[note]', with: '```{{ test }}```' - - click_button('Comment') - end - - expect(page).to have_content('{{ test }}') - end - end - end - end - - def line_code - sample_compare.changes.first[:line_code] - end - - def line_code_2 - sample_compare.changes.last[:line_code] - end - - def click_diff_line(data = line_code) - find(".line_holder[id='#{data}'] td.line_content").hover - find(".line_holder[id='#{data}'] button").trigger('click') - end -end diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb index fa1a753afcb..6ea149956fe 100644 --- a/spec/features/projects/blobs/user_create_spec.rb +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -77,7 +77,7 @@ feature 'New blob creation', feature: true, js: true do project, user, start_branch: 'master', - target_branch: 'master', + branch_name: 'master', commit_message: 'Create file', file_path: 'feature.rb', file_content: content @@ -87,7 +87,7 @@ feature 'New blob creation', feature: true, js: true do end scenario 'shows error message' do - expect(page).to have_content('Your changes could not be committed because a file with the same name already exists') + expect(page).to have_content('A file with this name already exists') expect(page).to have_content('New file') expect(page).to have_content('NextFeature') end diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb index 5d7bd3dc4ce..de6905f2b58 100644 --- a/spec/features/projects/files/creating_a_file_spec.rb +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -29,16 +29,16 @@ feature 'User wants to create a file', feature: true do scenario 'directory name contains Chinese characters' do submit_new_file(file_name: 'ä¸æ–‡/测试.md') - expect(page).to have_content 'The file has been successfully created.' + expect(page).to have_content 'The file has been successfully created' end scenario 'file name contains invalid characters' do submit_new_file(file_name: '\\') - expect(page).to have_content 'Your changes could not be committed, because the file name can contain only' + expect(page).to have_content 'Path can contain only' end scenario 'file name contains directory traversal' do submit_new_file(file_name: '../README.md') - expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.' + expect(page).to have_content 'Path cannot include directory traversal' end end diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index 3e544316f28..4da34108b46 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -8,7 +8,7 @@ feature 'User wants to edit a file', feature: true do let(:commit_params) do { start_branch: project.default_branch, - target_branch: project.default_branch, + branch_name: project.default_branch, commit_message: "Committing First Update", file_path: ".gitignore", file_content: "First Update", diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 62d0aedda48..6cdca0f114b 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -163,12 +163,14 @@ feature 'issuable templates', feature: true, js: true do end def select_template(name) - first('.js-issuable-selector').click - first('.js-issuable-selector-wrap .dropdown-content a', text: name).click + find('.js-issuable-selector').click + + find('.js-issuable-selector-wrap .dropdown-content a', text: name, match: :first).click end def select_option(name) - first('.js-issuable-selector').click - first('.js-issuable-selector-wrap .dropdown-footer-list a', text: name).click + find('.js-issuable-selector').click + + find('.js-issuable-selector-wrap .dropdown-footer-list a', text: name, match: :first).click end end diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb new file mode 100644 index 00000000000..deea34214fb --- /dev/null +++ b/spec/features/projects/members/list_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +feature 'Project members list', feature: true do + include Select2Helper + + let(:user1) { create(:user, name: 'John Doe') } + let(:user2) { create(:user, name: 'Mary Jane') } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + + background do + login_as(user1) + group.add_owner(user1) + end + + scenario 'show members from project and group' do + project.add_developer(user2) + + visit_members_page + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + scenario 'show user once if member of both group and project' do + project.add_developer(user1) + + visit_members_page + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + scenario 'update user acess level', :js do + project.add_developer(user2) + + visit_members_page + + page.within(second_row) do + click_button('Developer') + click_link('Reporter') + + expect(page).to have_button('Reporter') + end + end + + scenario 'add user to project', :js do + visit_members_page + + add_user(user2.id, 'Reporter') + + page.within(second_row) do + expect(page).to have_content(user2.name) + expect(page).to have_button('Reporter') + end + end + + scenario 'invite user to project', :js do + visit_members_page + + add_user('test@example.com', 'Reporter') + + page.within(second_row) do + expect(page).to have_content('test@example.com') + expect(page).to have_content('Invited') + expect(page).to have_button('Reporter') + end + end + + def first_row + page.all('ul.content-list > li')[0] + end + + def second_row + page.all('ul.content-list > li')[1] + end + + def add_user(id, role) + page.within ".users-project-form" do + select2(id, from: "#user_ids", multiple: true) + select(role, from: "access_level") + end + + click_button "Add to project" + end + + def visit_members_page + visit namespace_project_settings_members_path(project.namespace, project) + end +end diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index ce5c5f21167..34c6a10950f 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -25,7 +25,7 @@ describe 'View on environment', js: true do project, user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Add .gitlab/route-map.yml", file_path: '.gitlab/route-map.yml', file_content: route_map @@ -36,7 +36,7 @@ describe 'View on environment', js: true do project, user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Update feature", file_path: file_path, file_content: "# Noop" diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb new file mode 100644 index 00000000000..c1f6b0cce3b --- /dev/null +++ b/spec/features/projects/wiki/shortcuts_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +feature 'Wiki shortcuts', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project, namespace: user.namespace) } + let(:wiki_page) do + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + end + + before do + login_as(user) + visit namespace_project_wiki_path(project.namespace, project, wiki_page) + end + + scenario 'Visit edit wiki page using "e" keyboard shortcut' do + find('body').native.send_key('e') + + expect(find('.wiki-page-title')).to have_content('Edit Page') + end +end diff --git a/spec/fixtures/trace/ansi-sequence-and-unicode b/spec/fixtures/trace/ansi-sequence-and-unicode new file mode 100644 index 00000000000..5d2466f0d0f --- /dev/null +++ b/spec/fixtures/trace/ansi-sequence-and-unicode @@ -0,0 +1,5 @@ +[0m[01;34m.[0m +[30;42m..[0m +😺 +ヾ(´༎ຶД༎ຶ`)ノ +[01;32m許功蓋[0m diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 40efab6e4f7..a7fc5d14859 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -265,4 +265,27 @@ describe ProjectsHelper do end end end + + describe "#visibility_select_options" do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it "does not include the Public restricted level" do + expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).not_to include('Public') + end + + it "includes the Internal level" do + expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Internal') + end + + it "includes the Private level" do + expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private') + end + end end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 28b8def331d..345bc33a67b 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -70,10 +70,12 @@ describe SubmoduleHelper do expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash']) end - it 'returns original with non-standard url' do + it 'handles urls with no .git on the end' do stub_url('http://github.com/gitlab-org/gitlab-ce') - expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) + expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash']) + end + it 'returns original with non-standard url' do stub_url('http://github.com/another/gitlab-org/gitlab-ce.git') expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) end @@ -95,10 +97,12 @@ describe SubmoduleHelper do expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) end - it 'returns original with non-standard url' do + it 'handles urls with no .git on the end' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce') - expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) + expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) + end + it 'returns original with non-standard url' do stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git') expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index ea7753c7a1d..68ad5f66676 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -3,6 +3,8 @@ import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; +require('~/lib/utils/common_utils'); + (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; @@ -28,7 +30,7 @@ import AwardsHandler from '~/awards_handler'; loadFixtures('issues/issue_with_comment.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { - return function(url, emoji, cb) { + return function(button, url, emoji, cb) { return cb(); }; })(this)); @@ -63,7 +65,7 @@ import AwardsHandler from '~/awards_handler'; $emojiMenu = $('.emoji-menu'); expect($emojiMenu.length).toBe(1); expect($emojiMenu.hasClass('is-visible')).toBe(true); - expect($emojiMenu.find('#emoji_search').length).toBe(1); + expect($emojiMenu.find('.js-emoji-menu-search').length).toBe(1); return expect($('.js-awards-block.current').length).toBe(1); }); }); @@ -115,6 +117,27 @@ import AwardsHandler from '~/awards_handler'; return expect($emojiButton.next('.js-counter').text()).toBe('4'); }); }); + describe('::userAuthored', function() { + it('should update tooltip to user authored title', function() { + var $thumbsUpEmoji, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); + $thumbsUpEmoji.attr('data-title', 'sam'); + awardsHandler.userAuthored($thumbsUpEmoji); + return expect($thumbsUpEmoji.data("original-title")).toBe("You cannot vote on your own issue, MR and note"); + }); + it('should restore tooltip back to initial vote list', function() { + var $thumbsUpEmoji, $votesBlock; + jasmine.clock().install(); + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); + $thumbsUpEmoji.attr('data-title', 'sam'); + awardsHandler.userAuthored($thumbsUpEmoji); + jasmine.clock().tick(2801); + jasmine.clock().uninstall(); + return expect($thumbsUpEmoji.data("original-title")).toBe("sam"); + }); + }); describe('::getAwardUrl', function() { return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); @@ -194,16 +217,35 @@ import AwardsHandler from '~/awards_handler'; return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); }); }); - describe('search', function() { - return it('should filter the emoji', function(done) { + describe('::searchEmojis', () => { + it('should filter the emoji', function(done) { return openAndWaitForEmojiMenu() .then(() => { expect($('[data-name=angel]').is(':visible')).toBe(true); expect($('[data-name=anger]').is(':visible')).toBe(true); - $('#emoji_search').val('ali').trigger('input'); + awardsHandler.searchEmojis('ali'); expect($('[data-name=angel]').is(':visible')).toBe(false); expect($('[data-name=anger]').is(':visible')).toBe(false); expect($('[data-name=alien]').is(':visible')).toBe(true); + expect($('.js-emoji-menu-search').val()).toBe('ali'); + }) + .then(done) + .catch((err) => { + done.fail(`Failed to open and build emoji menu: ${err.message}`); + }); + }); + it('should clear the search when searching for nothing', function(done) { + return openAndWaitForEmojiMenu() + .then(() => { + awardsHandler.searchEmojis('ali'); + expect($('[data-name=angel]').is(':visible')).toBe(false); + expect($('[data-name=anger]').is(':visible')).toBe(false); + expect($('[data-name=alien]').is(':visible')).toBe(true); + awardsHandler.searchEmojis(''); + expect($('[data-name=angel]').is(':visible')).toBe(true); + expect($('[data-name=anger]').is(':visible')).toBe(true); + expect($('[data-name=alien]').is(':visible')).toBe(true); + expect($('.js-emoji-menu-search').val()).toBe(''); }) .then(done) .catch((err) => { @@ -211,6 +253,7 @@ import AwardsHandler from '~/awards_handler'; }); }); }); + describe('emoji menu', function() { const emojiSelector = '[data-name="sunglasses"]'; const openEmojiMenuAndAddEmoji = function() { diff --git a/spec/javascripts/blob/blob_fork_suggestion_spec.js b/spec/javascripts/blob/blob_fork_suggestion_spec.js new file mode 100644 index 00000000000..d0d64d75957 --- /dev/null +++ b/spec/javascripts/blob/blob_fork_suggestion_spec.js @@ -0,0 +1,37 @@ +import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; + +describe('BlobForkSuggestion', () => { + let blobForkSuggestion; + + const openButtons = [document.createElement('div')]; + const forkButtons = [document.createElement('a')]; + const cancelButtons = [document.createElement('div')]; + const suggestionSections = [document.createElement('div')]; + const actionTextPieces = [document.createElement('div')]; + + beforeEach(() => { + blobForkSuggestion = new BlobForkSuggestion({ + openButtons, + forkButtons, + cancelButtons, + suggestionSections, + actionTextPieces, + }); + }); + + afterEach(() => { + blobForkSuggestion.destroy(); + }); + + it('showSuggestionSection', () => { + blobForkSuggestion.showSuggestionSection('/foo', 'foo'); + expect(suggestionSections[0].classList.contains('hidden')).toEqual(false); + expect(forkButtons[0].getAttribute('href')).toEqual('/foo'); + expect(actionTextPieces[0].textContent).toEqual('foo'); + }); + + it('hideSuggestionSection', () => { + blobForkSuggestion.hideSuggestionSection(); + expect(suggestionSections[0].classList.contains('hidden')).toEqual(true); + }); +}); diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js index 0e4431548c4..79f40559817 100644 --- a/spec/javascripts/blob/sketch/index_spec.js +++ b/spec/javascripts/blob/sketch/index_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-new */ +/* eslint-disable no-new, promise/catch-or-return */ import JSZip from 'jszip'; import SketchLoader from '~/blob/sketch'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index a9d4c6ef76f..24a2da9f6b6 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -107,4 +107,44 @@ describe('List model', () => { expect(gl.boardService.moveIssue) .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined); }); + + describe('page number', () => { + beforeEach(() => { + spyOn(list, 'getIssues'); + }); + + it('increase page number if current issue count is more than the page size', () => { + for (let i = 0; i < 30; i += 1) { + list.issues.push(new ListIssue({ + title: 'Testing', + iid: _.random(10000) + i, + confidential: false, + labels: [list.label] + })); + } + list.issuesSize = 50; + + expect(list.issues.length).toBe(30); + + list.nextPage(); + + expect(list.page).toBe(2); + expect(list.getIssues).toHaveBeenCalled(); + }); + + it('does not increase page number if issue count is less than the page size', () => { + list.issues.push(new ListIssue({ + title: 'Testing', + iid: _.random(10000), + confidential: false, + labels: [list.label] + })); + list.issuesSize = 2; + + list.nextPage(); + + expect(list.page).toBe(1); + expect(list.getIssues).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 7174bf1e041..8ec96bdb583 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -1,11 +1,11 @@ /* eslint-disable no-new */ /* global Build */ - -require('~/lib/utils/datetime_utility'); -require('~/lib/utils/url_utility'); -require('~/build'); -require('~/breakpoints'); -require('vendor/jquery.nicescroll'); +import { bytesToKiB } from '~/lib/utils/number_utils'; +import '~/lib/utils/datetime_utility'; +import '~/lib/utils/url_utility'; +import '~/build'; +import '~/breakpoints'; +import 'vendor/jquery.nicescroll'; describe('Build', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; @@ -144,24 +144,6 @@ describe('Build', () => { expect($('#build-trace .js-build-output').text()).toMatch(/Different/); }); - it('shows information about truncated log', () => { - jasmine.clock().tick(4001); - const [{ success }] = $.ajax.calls.argsFor(0); - - success.call($, { - html: '<span>Update</span>', - status: 'success', - append: false, - truncated: true, - size: '50', - }); - - expect( - $('#build-trace .js-truncated-info').text().trim(), - ).toContain('Showing last 50 KiB of log'); - expect($('#build-trace .js-truncated-info-size').text()).toMatch('50'); - }); - it('reloads the page when the build is done', () => { spyOn(gl.utils, 'visitUrl'); @@ -176,6 +158,107 @@ describe('Build', () => { expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); }); + + describe('truncated information', () => { + describe('when size is less than total', () => { + it('shows information about truncated log', () => { + jasmine.clock().tick(4001); + const [{ success }] = $.ajax.calls.argsFor(0); + + success.call($, { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, + }); + + expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + }); + + it('shows the size in KiB', () => { + jasmine.clock().tick(4001); + const [{ success }] = $.ajax.calls.argsFor(0); + const size = 50; + + success.call($, { + html: '<span>Update</span>', + status: 'success', + append: false, + size, + total: 100, + }); + + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${bytesToKiB(size)}`); + }); + + it('shows incremented size', () => { + jasmine.clock().tick(4001); + let args = $.ajax.calls.argsFor(0)[0]; + args.success.call($, { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, + }); + + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${bytesToKiB(50)}`); + + jasmine.clock().tick(4001); + args = $.ajax.calls.argsFor(2)[0]; + args.success.call($, { + html: '<span>Update</span>', + status: 'success', + append: true, + size: 10, + total: 100, + }); + + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${bytesToKiB(60)}`); + }); + + it('renders the raw link', () => { + jasmine.clock().tick(4001); + const [{ success }] = $.ajax.calls.argsFor(0); + + success.call($, { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, + }); + + expect( + document.querySelector('.js-raw-link').textContent.trim(), + ).toContain('Complete Raw'); + }); + }); + + describe('when size is equal than total', () => { + it('does not show the trunctated information', () => { + jasmine.clock().tick(4001); + const [{ success }] = $.ajax.calls.argsFor(0); + + success.call($, { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 100, + total: 100, + }); + + expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + }); + }); + }); }); }); }); diff --git a/spec/javascripts/ci_status_icon_spec.js b/spec/javascripts/ci_status_icon_spec.js new file mode 100644 index 00000000000..c83416c15ef --- /dev/null +++ b/spec/javascripts/ci_status_icon_spec.js @@ -0,0 +1,44 @@ +import * as icons from '~/ci_status_icons'; + +describe('CI status icons', () => { + const statuses = [ + 'canceled', + 'created', + 'failed', + 'manual', + 'pending', + 'running', + 'skipped', + 'success', + 'warning', + ]; + + statuses.forEach((status) => { + it(`should export a ${status} svg`, () => { + const key = `${status.toUpperCase()}_SVG`; + + expect(Object.hasOwnProperty.call(icons, key)).toBe(true); + expect(icons[key]).toMatch(/^<svg/); + }); + }); + + describe('default export map', () => { + const entityIconNames = [ + 'icon_status_canceled', + 'icon_status_created', + 'icon_status_failed', + 'icon_status_manual', + 'icon_status_pending', + 'icon_status_running', + 'icon_status_skipped', + 'icon_status_success', + 'icon_status_warning', + ]; + + entityIconNames.forEach((iconName) => { + it(`should have a '${iconName}' key`, () => { + expect(Object.hasOwnProperty.call(icons.default, iconName)).toBe(true); + }); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 8cac3cad232..ad31448f81c 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -36,6 +36,7 @@ describe('Pipelines table in Commits and Merge requests', () => { setTimeout(() => { expect(this.component.$el.querySelector('.empty-state')).toBeDefined(); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); + expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null); done(); }, 1); }); @@ -67,6 +68,8 @@ describe('Pipelines table in Commits and Merge requests', () => { setTimeout(() => { expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); + expect(this.component.$el.querySelector('.empty-state')).toBe(null); + expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null); done(); }, 0); }); @@ -95,10 +98,12 @@ describe('Pipelines table in Commits and Merge requests', () => { this.component.$destroy(); }); - it('should render empty state', function (done) { + it('should render error state', function (done) { setTimeout(() => { expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); + expect(this.component.$el.querySelector('.js-empty-state')).toBe(null); + expect(this.component.$el.querySelector('table')).toBe(null); done(); }, 0); }); diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js index 84cf98c930a..66ece7e4f41 100644 --- a/spec/javascripts/diff_comments_store_spec.js +++ b/spec/javascripts/diff_comments_store_spec.js @@ -5,129 +5,127 @@ require('~/diff_notes/models/discussion'); require('~/diff_notes/models/note'); require('~/diff_notes/stores/comments'); -(() => { - function createDiscussion(noteId = 1, resolved = true) { - CommentsStore.create({ - discussionId: 'a', - noteId, - canResolve: true, - resolved, - resolvedBy: 'test', - authorName: 'test', - authorAvatar: 'test', - noteTruncated: 'test...', - }); - } - - beforeEach(() => { - CommentsStore.state = {}; +function createDiscussion(noteId = 1, resolved = true) { + CommentsStore.create({ + discussionId: 'a', + noteId, + canResolve: true, + resolved, + resolvedBy: 'test', + authorName: 'test', + authorAvatar: 'test', + noteTruncated: 'test...', }); +} - describe('New discussion', () => { - it('creates new discussion', () => { - expect(Object.keys(CommentsStore.state).length).toBe(0); - createDiscussion(); - expect(Object.keys(CommentsStore.state).length).toBe(1); - }); +beforeEach(() => { + CommentsStore.state = {}; +}); - it('creates new note in discussion', () => { - createDiscussion(); - createDiscussion(2); +describe('New discussion', () => { + it('creates new discussion', () => { + expect(Object.keys(CommentsStore.state).length).toBe(0); + createDiscussion(); + expect(Object.keys(CommentsStore.state).length).toBe(1); + }); - const discussion = CommentsStore.state['a']; - expect(Object.keys(discussion.notes).length).toBe(2); - }); + it('creates new note in discussion', () => { + createDiscussion(); + createDiscussion(2); + + const discussion = CommentsStore.state['a']; + expect(Object.keys(discussion.notes).length).toBe(2); }); +}); - describe('Get note', () => { - beforeEach(() => { - expect(Object.keys(CommentsStore.state).length).toBe(0); - createDiscussion(); - }); +describe('Get note', () => { + beforeEach(() => { + expect(Object.keys(CommentsStore.state).length).toBe(0); + createDiscussion(); + }); - it('gets note by ID', () => { - const note = CommentsStore.get('a', 1); - expect(note).toBeDefined(); - expect(note.id).toBe(1); - }); + it('gets note by ID', () => { + const note = CommentsStore.get('a', 1); + expect(note).toBeDefined(); + expect(note.id).toBe(1); }); +}); - describe('Delete discussion', () => { - beforeEach(() => { - expect(Object.keys(CommentsStore.state).length).toBe(0); - createDiscussion(); - }); +describe('Delete discussion', () => { + beforeEach(() => { + expect(Object.keys(CommentsStore.state).length).toBe(0); + createDiscussion(); + }); - it('deletes discussion by ID', () => { - CommentsStore.delete('a', 1); - expect(Object.keys(CommentsStore.state).length).toBe(0); - }); + it('deletes discussion by ID', () => { + CommentsStore.delete('a', 1); + expect(Object.keys(CommentsStore.state).length).toBe(0); + }); - it('deletes discussion when no more notes', () => { - createDiscussion(); - createDiscussion(2); - expect(Object.keys(CommentsStore.state).length).toBe(1); - expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2); + it('deletes discussion when no more notes', () => { + createDiscussion(); + createDiscussion(2); + expect(Object.keys(CommentsStore.state).length).toBe(1); + expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2); - CommentsStore.delete('a', 1); - CommentsStore.delete('a', 2); - expect(Object.keys(CommentsStore.state).length).toBe(0); - }); + CommentsStore.delete('a', 1); + CommentsStore.delete('a', 2); + expect(Object.keys(CommentsStore.state).length).toBe(0); }); +}); - describe('Update note', () => { - beforeEach(() => { - expect(Object.keys(CommentsStore.state).length).toBe(0); - createDiscussion(); - }); +describe('Update note', () => { + beforeEach(() => { + expect(Object.keys(CommentsStore.state).length).toBe(0); + createDiscussion(); + }); - it('updates note to be unresolved', () => { - CommentsStore.update('a', 1, false, 'test'); + it('updates note to be unresolved', () => { + CommentsStore.update('a', 1, false, 'test'); - const note = CommentsStore.get('a', 1); - expect(note.resolved).toBe(false); - }); + const note = CommentsStore.get('a', 1); + expect(note.resolved).toBe(false); }); +}); - describe('Discussion resolved', () => { - beforeEach(() => { - expect(Object.keys(CommentsStore.state).length).toBe(0); - createDiscussion(); - }); +describe('Discussion resolved', () => { + beforeEach(() => { + expect(Object.keys(CommentsStore.state).length).toBe(0); + createDiscussion(); + }); - it('is resolved with single note', () => { - const discussion = CommentsStore.state['a']; - expect(discussion.isResolved()).toBe(true); - }); + it('is resolved with single note', () => { + const discussion = CommentsStore.state['a']; + expect(discussion.isResolved()).toBe(true); + }); - it('is unresolved with 2 notes', () => { - const discussion = CommentsStore.state['a']; - createDiscussion(2, false); + it('is unresolved with 2 notes', () => { + const discussion = CommentsStore.state['a']; + createDiscussion(2, false); - expect(discussion.isResolved()).toBe(false); - }); + expect(discussion.isResolved()).toBe(false); + }); - it('is resolved with 2 notes', () => { - const discussion = CommentsStore.state['a']; - createDiscussion(2); + it('is resolved with 2 notes', () => { + const discussion = CommentsStore.state['a']; + createDiscussion(2); - expect(discussion.isResolved()).toBe(true); - }); + expect(discussion.isResolved()).toBe(true); + }); - it('resolve all notes', () => { - const discussion = CommentsStore.state['a']; - createDiscussion(2, false); + it('resolve all notes', () => { + const discussion = CommentsStore.state['a']; + createDiscussion(2, false); - discussion.resolveAllNotes(); - expect(discussion.isResolved()).toBe(true); - }); + discussion.resolveAllNotes(); + expect(discussion.isResolved()).toBe(true); + }); - it('unresolve all notes', () => { - const discussion = CommentsStore.state['a']; - createDiscussion(2); + it('unresolve all notes', () => { + const discussion = CommentsStore.state['a']; + createDiscussion(2); - discussion.unResolveAllNotes(); - expect(discussion.isResolved()).toBe(false); - }); + discussion.unResolveAllNotes(); + expect(discussion.isResolved()).toBe(false); }); -})(); +}); diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js index 35239e4fb8e..fd153a49fcd 100644 --- a/spec/javascripts/droplab/constants_spec.js +++ b/spec/javascripts/droplab/constants_spec.js @@ -26,4 +26,10 @@ describe('constants', function () { expect(constants.ACTIVE_CLASS).toBe('droplab-item-active'); }); }); + + describe('IGNORE_CLASS', function () { + it('should be `droplab-item-ignore`', function() { + expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore'); + }); + }); }); diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js index 802e2435672..7516b301917 100644 --- a/spec/javascripts/droplab/drop_down_spec.js +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -2,7 +2,7 @@ import DropDown from '~/droplab/drop_down'; import utils from '~/droplab/utils'; -import { SELECTED_CLASS } from '~/droplab/constants'; +import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants'; describe('DropDown', function () { describe('class constructor', function () { @@ -128,9 +128,10 @@ describe('DropDown', function () { describe('clickEvent', function () { beforeEach(function () { + this.classList = jasmine.createSpyObj('classList', ['contains']); this.list = { dispatchEvent: () => {} }; this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} }; - this.event = { preventDefault: () => {}, target: {} }; + this.event = { preventDefault: () => {}, target: { classList: this.classList } }; this.customEvent = {}; this.closestElement = {}; @@ -140,6 +141,7 @@ describe('DropDown', function () { spyOn(this.event, 'preventDefault'); spyOn(window, 'CustomEvent').and.returnValue(this.customEvent); spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined); + this.classList.contains.and.returnValue(false); DropDown.prototype.clickEvent.call(this.dropdown, this.event); }); @@ -164,15 +166,35 @@ describe('DropDown', function () { expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); }); + it('should call .classList.contains checking for IGNORE_CLASS', function () { + expect(this.classList.contains).toHaveBeenCalledWith(IGNORE_CLASS); + }); + it('should call .dispatchEvent with the customEvent', function () { expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent); }); describe('if the target is a UL element', function () { beforeEach(function () { - this.event = { preventDefault: () => {}, target: { tagName: 'UL' } }; + this.event = { preventDefault: () => {}, target: { tagName: 'UL', classList: this.classList } }; + + spyOn(this.event, 'preventDefault'); + utils.closest.calls.reset(); + + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + }); + + it('should return immediately', function () { + expect(utils.closest).not.toHaveBeenCalled(); + }); + }); + + describe('if the target has the IGNORE_CLASS class', function () { + beforeEach(function () { + this.event = { preventDefault: () => {}, target: { tagName: 'LI', classList: this.classList } }; spyOn(this.event, 'preventDefault'); + this.classList.contains.and.returnValue(true); utils.closest.calls.reset(); DropDown.prototype.clickEvent.call(this.dropdown, this.event); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 6348d97b0a5..676bf61cfd9 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import actionsComp from '~/environments/components/environment_actions'; +import actionsComp from '~/environments/components/environment_actions.vue'; describe('Actions Component', () => { let ActionsComponent; diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js index 9af218a27ff..056d68a26e9 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js +++ b/spec/javascripts/environments/environment_external_url_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import externalUrlComp from '~/environments/components/environment_external_url'; +import externalUrlComp from '~/environments/components/environment_external_url.vue'; describe('External URL Component', () => { let ExternalUrlComponent; diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 4d42de4d549..0e141adb628 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -1,6 +1,6 @@ import 'timeago.js'; import Vue from 'vue'; -import environmentItemComp from '~/environments/components/environment_item'; +import environmentItemComp from '~/environments/components/environment_item.vue'; describe('Environment item', () => { let EnvironmentItem; diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js index fc451cce641..0f3dba66230 100644 --- a/spec/javascripts/environments/environment_monitoring_spec.js +++ b/spec/javascripts/environments/environment_monitoring_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import monitoringComp from '~/environments/components/environment_monitoring'; +import monitoringComp from '~/environments/components/environment_monitoring.vue'; describe('Monitoring Component', () => { let MonitoringComponent; diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js index 7cb39d9df03..25397714a76 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/javascripts/environments/environment_rollback_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import rollbackComp from '~/environments/components/environment_rollback'; +import rollbackComp from '~/environments/components/environment_rollback.vue'; describe('Rollback Component', () => { const retryURL = 'https://gitlab.com/retry'; diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 01055e3f255..942e4aaabd4 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import stopComp from '~/environments/components/environment_stop'; +import stopComp from '~/environments/components/environment_stop.vue'; describe('Stop Component', () => { let StopComponent; diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index 3df967848a7..effbc6c3ee1 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import environmentTableComp from '~/environments/components/environments_table'; +import environmentTableComp from '~/environments/components/environments_table.vue'; describe('Environment item', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js index be2289edc2b..858472af4b6 100644 --- a/spec/javascripts/environments/environment_terminal_button_spec.js +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import terminalComp from '~/environments/components/environment_terminal_button'; +import terminalComp from '~/environments/components/environment_terminal_button.vue'; describe('Stop Component', () => { let TerminalComponent; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index 2b1fe5e3eef..3f92fe4701e 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -3,69 +3,67 @@ require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown'); require('~/filtered_search/dropdown_user'); -(() => { - describe('Dropdown User', () => { - describe('getSearchInput', () => { - let dropdownUser; +describe('Dropdown User', () => { + describe('getSearchInput', () => { + let dropdownUser; - beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + beforeEach(() => { + spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); - dropdownUser = new gl.DropdownUser(); - }); - - it('should not return the double quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: '"johnny appleseed', - }); + dropdownUser = new gl.DropdownUser(); + }); - expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + it('should not return the double quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: '"johnny appleseed', }); - it('should not return the single quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: '\'larry boy', - }); + expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + }); - expect(dropdownUser.getSearchInput()).toBe('larry boy'); + it('should not return the single quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: '\'larry boy', }); + + expect(dropdownUser.getSearchInput()).toBe('larry boy'); }); + }); - describe('config AjaxFilter\'s endpoint', () => { - beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - }); + describe('config AjaxFilter\'s endpoint', () => { + beforeEach(() => { + spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + }); - it('should return endpoint', () => { - window.gon = { - relative_url_root: '', - }; - const dropdown = new gl.DropdownUser(); + it('should return endpoint', () => { + window.gon = { + relative_url_root: '', + }; + const dropdown = new gl.DropdownUser(); - expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); - }); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); - it('should return endpoint when relative_url_root is undefined', () => { - const dropdown = new gl.DropdownUser(); + it('should return endpoint when relative_url_root is undefined', () => { + const dropdown = new gl.DropdownUser(); - expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); - }); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); - it('should return endpoint with relative url when available', () => { - window.gon = { - relative_url_root: '/gitlab_directory', - }; - const dropdown = new gl.DropdownUser(); + it('should return endpoint with relative url when available', () => { + window.gon = { + relative_url_root: '/gitlab_directory', + }; + const dropdown = new gl.DropdownUser(); - expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); - }); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); + }); - afterEach(() => { - window.gon = {}; - }); + afterEach(() => { + window.gon = {}; }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index e6538020896..c820c955172 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -3,308 +3,306 @@ require('~/filtered_search/dropdown_utils'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); -(() => { - describe('Dropdown Utils', () => { - describe('getEscapedText', () => { - it('should return same word when it has no space', () => { - const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); - expect(escaped).toBe('textWithoutSpace'); - }); +describe('Dropdown Utils', () => { + describe('getEscapedText', () => { + it('should return same word when it has no space', () => { + const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + expect(escaped).toBe('textWithoutSpace'); + }); - it('should escape with double quotes', () => { - let escaped = gl.DropdownUtils.getEscapedText('text with space'); - expect(escaped).toBe('"text with space"'); + it('should escape with double quotes', () => { + let escaped = gl.DropdownUtils.getEscapedText('text with space'); + expect(escaped).toBe('"text with space"'); - escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); - expect(escaped).toBe('"won\'t fix"'); - }); + escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + expect(escaped).toBe('"won\'t fix"'); + }); - it('should escape with single quotes', () => { - const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); - expect(escaped).toBe('\'won"t fix\''); - }); + it('should escape with single quotes', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + expect(escaped).toBe('\'won"t fix\''); + }); - it('should escape with single quotes by default', () => { - const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); - expect(escaped).toBe('\'won"t\' fix\''); - }); + it('should escape with single quotes by default', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + expect(escaped).toBe('\'won"t\' fix\''); }); + }); - describe('filterWithSymbol', () => { - let input; - const item = { - title: '@root', - }; + describe('filterWithSymbol', () => { + let input; + const item = { + title: '@root', + }; - beforeEach(() => { - setFixtures(` - <input type="text" id="test" /> - `); + beforeEach(() => { + setFixtures(` + <input type="text" id="test" /> + `); - input = document.getElementById('test'); - }); + input = document.getElementById('test'); + }); - it('should filter without symbol', () => { - input.value = 'roo'; + it('should filter without symbol', () => { + input.value = 'roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with symbol', () => { - input.value = '@roo'; + it('should filter with symbol', () => { + input.value = '@roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + expect(updatedItem.droplab_hidden).toBe(false); + }); - describe('filters multiple word title', () => { - const multipleWordItem = { - title: 'Community Contributions', - }; + describe('filters multiple word title', () => { + const multipleWordItem = { + title: 'Community Contributions', + }; - it('should filter with double quote', () => { - input.value = '"'; + it('should filter with double quote', () => { + input.value = '"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with double quote and symbol', () => { - input.value = '~"'; + it('should filter with double quote and symbol', () => { + input.value = '~"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with double quote and multiple words', () => { - input.value = '"community con'; + it('should filter with double quote and multiple words', () => { + input.value = '"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with double quote, symbol and multiple words', () => { - input.value = '~"community con'; + it('should filter with double quote, symbol and multiple words', () => { + input.value = '~"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote', () => { - input.value = '\''; + it('should filter with single quote', () => { + input.value = '\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote and symbol', () => { - input.value = '~\''; + it('should filter with single quote and symbol', () => { + input.value = '~\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote and multiple words', () => { - input.value = '\'community con'; + it('should filter with single quote and multiple words', () => { + input.value = '\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote, symbol and multiple words', () => { - input.value = '~\'community con'; + it('should filter with single quote, symbol and multiple words', () => { + input.value = '~\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); }); }); + }); - describe('filterHint', () => { - let input; - - beforeEach(() => { - setFixtures(` - <ul class="tokens-container"> - <li class="input-token"> - <input class="filtered-search" type="text" id="test" /> - </li> - </ul> - `); - - input = document.getElementById('test'); - }); + describe('filterHint', () => { + let input; - it('should filter', () => { - input.value = 'l'; - let updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'label', - }); - expect(updatedItem.droplab_hidden).toBe(false); + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search" type="text" id="test" /> + </li> + </ul> + `); - input.value = 'o'; - updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'label', - }); - expect(updatedItem.droplab_hidden).toBe(true); - }); + input = document.getElementById('test'); + }); - it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); - expect(updatedItem.droplab_hidden).toBe(false); + it('should filter', () => { + input.value = 'l'; + let updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', }); + expect(updatedItem.droplab_hidden).toBe(false); - it('should allow multiple if item.type is array', () => { - input.value = 'label:~first la'; - const updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'label', - type: 'array', - }); - expect(updatedItem.droplab_hidden).toBe(false); + input.value = 'o'; + updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', }); + expect(updatedItem.droplab_hidden).toBe(true); + }); - it('should prevent multiple if item.type is not array', () => { - input.value = 'milestone:~first mile'; - let updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'milestone', - }); - expect(updatedItem.droplab_hidden).toBe(true); + it('should return droplab_hidden false when item has no hint', () => { + const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); + expect(updatedItem.droplab_hidden).toBe(false); + }); - updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'milestone', - type: 'string', - }); - expect(updatedItem.droplab_hidden).toBe(true); + it('should allow multiple if item.type is array', () => { + input.value = 'label:~first la'; + const updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', + type: 'array', }); + expect(updatedItem.droplab_hidden).toBe(false); }); - describe('setDataValueIfSelected', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') - .and.callFake(() => {}); + it('should prevent multiple if item.type is not array', () => { + input.value = 'milestone:~first mile'; + let updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'milestone', }); + expect(updatedItem.droplab_hidden).toBe(true); - it('calls addWordToInput when dataValue exists', () => { - const selected = { - getAttribute: () => 'value', - }; - - gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'milestone', + type: 'string', }); + expect(updatedItem.droplab_hidden).toBe(true); + }); + }); - it('returns true when dataValue exists', () => { - const selected = { - getAttribute: () => 'value', - }; + describe('setDataValueIfSelected', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + .and.callFake(() => {}); + }); - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(result).toBe(true); - }); + it('calls addWordToInput when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; - it('returns false when dataValue does not exist', () => { - const selected = { - getAttribute: () => null, - }; + gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + }); - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(result).toBe(false); - }); + it('returns true when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(true); }); - describe('getInputSelectionPosition', () => { - describe('word with trailing spaces', () => { - const value = 'label:none '; + it('returns false when dataValue does not exist', () => { + const selected = { + getAttribute: () => null, + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(false); + }); + }); - it('should return selectionStart when cursor is at the trailing space', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 11, - value, - }); + describe('getInputSelectionPosition', () => { + describe('word with trailing spaces', () => { + const value = 'label:none '; - expect(left).toBe(11); - expect(right).toBe(11); + it('should return selectionStart when cursor is at the trailing space', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 11, + value, }); - it('should return input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 0, - value, - }); + expect(left).toBe(11); + expect(right).toBe(11); + }); - expect(left).toBe(0); - expect(right).toBe(10); + it('should return input when cursor is at the start of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 0, + value, }); - it('should return input when cursor is at the middle of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 7, - value, - }); + expect(left).toBe(0); + expect(right).toBe(10); + }); - expect(left).toBe(0); - expect(right).toBe(10); + it('should return input when cursor is at the middle of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 7, + value, }); - it('should return input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 10, - value, - }); + expect(left).toBe(0); + expect(right).toBe(10); + }); - expect(left).toBe(0); - expect(right).toBe(10); + it('should return input when cursor is at the end of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 10, + value, }); - }); - describe('multiple words', () => { - const value = 'label:~"Community Contribution"'; + expect(left).toBe(0); + expect(right).toBe(10); + }); + }); - it('should return input when cursor is after the first word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 17, - value, - }); + describe('multiple words', () => { + const value = 'label:~"Community Contribution"'; - expect(left).toBe(0); - expect(right).toBe(31); + it('should return input when cursor is after the first word', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 17, + value, }); - it('should return input when cursor is before the second word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 18, - value, - }); + expect(left).toBe(0); + expect(right).toBe(31); + }); - expect(left).toBe(0); - expect(right).toBe(31); + it('should return input when cursor is before the second word', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 18, + value, }); - }); - describe('incomplete multiple words', () => { - const value = 'label:~"Community Contribution'; + expect(left).toBe(0); + expect(right).toBe(31); + }); + }); - it('should return entire input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 0, - value, - }); + describe('incomplete multiple words', () => { + const value = 'label:~"Community Contribution'; - expect(left).toBe(0); - expect(right).toBe(30); + it('should return entire input when cursor is at the start of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 0, + value, }); - it('should return entire input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 30, - value, - }); + expect(left).toBe(0); + expect(right).toBe(30); + }); - expect(left).toBe(0); - expect(right).toBe(30); + it('should return entire input when cursor is at the end of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 30, + value, }); + + expect(left).toBe(0); + expect(right).toBe(30); }); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index a1da3396d7b..17bf8932489 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -3,99 +3,97 @@ require('~/filtered_search/filtered_search_visual_tokens'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); -(() => { - describe('Filtered Search Dropdown Manager', () => { - describe('addWordToInput', () => { - function getInputValue() { - return document.querySelector('.filtered-search').value; - } - - function setInputValue(value) { - document.querySelector('.filtered-search').value = value; - } - - beforeEach(() => { - setFixtures(` - <ul class="tokens-container"> - <li class="input-token"> - <input class="filtered-search"> - </li> - </ul> - `); - }); +describe('Filtered Search Dropdown Manager', () => { + describe('addWordToInput', () => { + function getInputValue() { + return document.querySelector('.filtered-search').value; + } + + function setInputValue(value) { + document.querySelector('.filtered-search').value = value; + } + + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search"> + </li> + </ul> + `); + }); - describe('input has no existing value', () => { - it('should add just tokenName', () => { - gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + describe('input has no existing value', () => { + it('should add just tokenName', () => { + gl.FilteredSearchDropdownManager.addWordToInput('milestone'); - const token = document.querySelector('.tokens-container .js-visual-token'); + const token = document.querySelector('.tokens-container .js-visual-token'); - expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('milestone'); - expect(getInputValue()).toBe(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('milestone'); + expect(getInputValue()).toBe(''); + }); - it('should add tokenName and tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label'); - let token = document.querySelector('.tokens-container .js-visual-token'); + let token = document.querySelector('.tokens-container .js-visual-token'); - expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); - expect(getInputValue()).toBe(''); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(getInputValue()).toBe(''); - gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); - // We have to get that reference again - // Because gl.FilteredSearchDropdownManager deletes the previous token - token = document.querySelector('.tokens-container .js-visual-token'); + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + // We have to get that reference again + // Because gl.FilteredSearchDropdownManager deletes the previous token + token = document.querySelector('.tokens-container .js-visual-token'); - expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); - expect(token.querySelector('.value').innerText).toBe('none'); - expect(getInputValue()).toBe(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.value').innerText).toBe('none'); + expect(getInputValue()).toBe(''); }); + }); - describe('input has existing value', () => { - it('should be able to just add tokenName', () => { - setInputValue('a'); - gl.FilteredSearchDropdownManager.addWordToInput('author'); + describe('input has existing value', () => { + it('should be able to just add tokenName', () => { + setInputValue('a'); + gl.FilteredSearchDropdownManager.addWordToInput('author'); - const token = document.querySelector('.tokens-container .js-visual-token'); + const token = document.querySelector('.tokens-container .js-visual-token'); - expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('author'); - expect(getInputValue()).toBe(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(getInputValue()).toBe(''); + }); - it('should replace tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('author'); + it('should replace tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('author'); - setInputValue('roo'); - gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); + setInputValue('roo'); + gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); - const token = document.querySelector('.tokens-container .js-visual-token'); + const token = document.querySelector('.tokens-container .js-visual-token'); - expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('author'); - expect(token.querySelector('.value').innerText).toBe('@root'); - expect(getInputValue()).toBe(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(token.querySelector('.value').innerText).toBe('@root'); + expect(getInputValue()).toBe(''); + }); - it('should add tokenValues containing spaces', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + it('should add tokenValues containing spaces', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label'); - setInputValue('"test '); - gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); + setInputValue('"test '); + gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); - const token = document.querySelector('.tokens-container .js-visual-token'); + const token = document.querySelector('.tokens-container .js-visual-token'); - expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); - expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); - expect(getInputValue()).toBe(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); + expect(getInputValue()).toBe(''); }); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 97af681429b..6683489f63c 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -6,271 +6,269 @@ require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_manager'); const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); -(() => { - describe('Filtered Search Manager', () => { - let input; - let manager; - let tokensContainer; - const placeholder = 'Search or filter results...'; - - function dispatchBackspaceEvent(element, eventType) { - const backspaceKey = 8; - const event = new Event(eventType); - event.keyCode = backspaceKey; - element.dispatchEvent(event); - } +describe('Filtered Search Manager', () => { + let input; + let manager; + let tokensContainer; + const placeholder = 'Search or filter results...'; + + function dispatchBackspaceEvent(element, eventType) { + const backspaceKey = 8; + const event = new Event(eventType); + event.keyCode = backspaceKey; + element.dispatchEvent(event); + } + + function dispatchDeleteEvent(element, eventType) { + const deleteKey = 46; + const event = new Event(eventType); + event.keyCode = deleteKey; + element.dispatchEvent(event); + } + + beforeEach(() => { + setFixtures(` + <div class="filtered-search-box"> + <form> + <ul class="tokens-container list-unstyled"> + ${FilteredSearchSpecHelper.createInputHTML(placeholder)} + </ul> + <button class="clear-search" type="button"> + <i class="fa fa-times"></i> + </button> + </form> + </div> + `); + + spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + + input = document.querySelector('.filtered-search'); + tokensContainer = document.querySelector('.tokens-container'); + manager = new gl.FilteredSearchManager(); + }); - function dispatchDeleteEvent(element, eventType) { - const deleteKey = 46; - const event = new Event(eventType); - event.keyCode = deleteKey; - element.dispatchEvent(event); - } + afterEach(() => { + manager.cleanup(); + }); - beforeEach(() => { - setFixtures(` - <div class="filtered-search-box"> - <form> - <ul class="tokens-container list-unstyled"> - ${FilteredSearchSpecHelper.createInputHTML(placeholder)} - </ul> - <button class="clear-search" type="button"> - <i class="fa fa-times"></i> - </button> - </form> - </div> - `); + describe('search', () => { + const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; - spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); - spyOn(gl.utils, 'getParameterByName').and.returnValue(null); - spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + it('should search with a single word', (done) => { + input.value = 'searchTerm'; - input = document.querySelector('.filtered-search'); - tokensContainer = document.querySelector('.tokens-container'); - manager = new gl.FilteredSearchManager(); - }); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&search=searchTerm`); + done(); + }); - afterEach(() => { - manager.cleanup(); + manager.search(); }); - describe('search', () => { - const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; - - it('should search with a single word', (done) => { - input.value = 'searchTerm'; + it('should search with multiple words', (done) => { + input.value = 'awesome search terms'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&search=searchTerm`); - done(); - }); - - manager.search(); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); + done(); }); - it('should search with multiple words', (done) => { - input.value = 'awesome search terms'; + manager.search(); + }); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); - done(); - }); + it('should search with special characters', (done) => { + input.value = '~!@#$%^&*()_+{}:<>,.?/'; - manager.search(); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); + done(); }); - it('should search with special characters', (done) => { - input.value = '~!@#$%^&*()_+{}:<>,.?/'; + manager.search(); + }); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); - done(); - }); + it('removes duplicated tokens', (done) => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `); - manager.search(); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&label_name[]=bug`); + done(); }); - it('removes duplicated tokens', (done) => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} - `); - - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&label_name[]=bug`); - done(); - }); + manager.search(); + }); + }); - manager.search(); - }); + describe('handleInputPlaceholder', () => { + it('should render placeholder when there is no input', () => { + expect(input.placeholder).toEqual(placeholder); }); - describe('handleInputPlaceholder', () => { - it('should render placeholder when there is no input', () => { - expect(input.placeholder).toEqual(placeholder); - }); + it('should not render placeholder when there is input', () => { + input.value = 'test words'; + + const event = new Event('input'); + input.dispatchEvent(event); - it('should not render placeholder when there is input', () => { - input.value = 'test words'; + expect(input.placeholder).toEqual(''); + }); - const event = new Event('input'); - input.dispatchEvent(event); + it('should not render placeholder when there are tokens and no input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); - expect(input.placeholder).toEqual(''); - }); + const event = new Event('input'); + input.dispatchEvent(event); - it('should not render placeholder when there are tokens and no input', () => { + expect(input.placeholder).toEqual(''); + }); + }); + + describe('checkForBackspace', () => { + describe('tokens and no input', () => { + beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), ); - - const event = new Event('input'); - input.dispatchEvent(event); - - expect(input.placeholder).toEqual(''); }); - }); - - describe('checkForBackspace', () => { - describe('tokens and no input', () => { - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), - ); - }); - it('removes last token', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - dispatchBackspaceEvent(input, 'keyup'); - - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); - }); - - it('sets the input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); - dispatchDeleteEvent(input, 'keyup'); + it('removes last token', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + dispatchBackspaceEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); - expect(input.value).toEqual('~bug'); - }); + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); }); - it('does not remove token or change input when there is existing input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + it('sets the input', () => { spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); - - input.value = 'text'; dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); - expect(input.value).toEqual('text'); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(input.value).toEqual('~bug'); }); }); - describe('removeSelectedToken', () => { - function getVisualTokens() { - return tokensContainer.querySelectorAll('.js-visual-token'); - } + it('does not remove token or change input when there is existing input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), - ); - }); + input.value = 'text'; + dispatchDeleteEvent(input, 'keyup'); - it('removes selected token when the backspace key is pressed', () => { - expect(getVisualTokens().length).toEqual(1); + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(input.value).toEqual('text'); + }); + }); - dispatchBackspaceEvent(document, 'keydown'); + describe('removeSelectedToken', () => { + function getVisualTokens() { + return tokensContainer.querySelectorAll('.js-visual-token'); + } - expect(getVisualTokens().length).toEqual(0); - }); + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + }); - it('removes selected token when the delete key is pressed', () => { - expect(getVisualTokens().length).toEqual(1); + it('removes selected token when the backspace key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); - dispatchDeleteEvent(document, 'keydown'); + dispatchBackspaceEvent(document, 'keydown'); - expect(getVisualTokens().length).toEqual(0); - }); + expect(getVisualTokens().length).toEqual(0); + }); - it('updates the input placeholder after removal', () => { - manager.handleInputPlaceholder(); + it('removes selected token when the delete key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); - expect(input.placeholder).toEqual(''); - expect(getVisualTokens().length).toEqual(1); + dispatchDeleteEvent(document, 'keydown'); - dispatchBackspaceEvent(document, 'keydown'); + expect(getVisualTokens().length).toEqual(0); + }); - expect(input.placeholder).not.toEqual(''); - expect(getVisualTokens().length).toEqual(0); - }); + it('updates the input placeholder after removal', () => { + manager.handleInputPlaceholder(); - it('updates the clear button after removal', () => { - manager.toggleClearSearchButton(); + expect(input.placeholder).toEqual(''); + expect(getVisualTokens().length).toEqual(1); - const clearButton = document.querySelector('.clear-search'); + dispatchBackspaceEvent(document, 'keydown'); - expect(clearButton.classList.contains('hidden')).toEqual(false); - expect(getVisualTokens().length).toEqual(1); + expect(input.placeholder).not.toEqual(''); + expect(getVisualTokens().length).toEqual(0); + }); - dispatchBackspaceEvent(document, 'keydown'); + it('updates the clear button after removal', () => { + manager.toggleClearSearchButton(); - expect(clearButton.classList.contains('hidden')).toEqual(true); - expect(getVisualTokens().length).toEqual(0); - }); + const clearButton = document.querySelector('.clear-search'); + + expect(clearButton.classList.contains('hidden')).toEqual(false); + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(clearButton.classList.contains('hidden')).toEqual(true); + expect(getVisualTokens().length).toEqual(0); }); + }); - describe('unselects token', () => { - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} - ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} - `); - }); + describe('unselects token', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} + `); + }); - it('unselects token when input is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + it('unselects token when input is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - // Click directly on input attached to document - // so that the click event will propagate properly - document.querySelector('.filtered-search').click(); + // Click directly on input attached to document + // so that the click event will propagate properly + document.querySelector('.filtered-search').click(); - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - expect(selectedToken.classList.contains('selected')).toEqual(false); - }); + expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); + expect(selectedToken.classList.contains('selected')).toEqual(false); + }); - it('unselects token when document.body is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + it('unselects token when document.body is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - document.body.click(); + document.body.click(); - expect(selectedToken.classList.contains('selected')).toEqual(false); - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - }); + expect(selectedToken.classList.contains('selected')).toEqual(false); + expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); }); + }); - describe('toggleInputContainerFocus', () => { - it('toggles on focus', () => { - input.focus(); - expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); - }); + describe('toggleInputContainerFocus', () => { + it('toggles on focus', () => { + input.focus(); + expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); + }); - it('toggles on blur', () => { - input.blur(); - expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); - }); + it('toggles on blur', () => { + input.blur(); + expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index cf409a7e509..6f9fa434c35 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,110 +1,108 @@ require('~/extensions/array'); require('~/filtered_search/filtered_search_token_keys'); -(() => { - describe('Filtered Search Token Keys', () => { - describe('get', () => { - let tokenKeys; - - beforeEach(() => { - tokenKeys = gl.FilteredSearchTokenKeys.get(); - }); - - it('should return tokenKeys', () => { - expect(tokenKeys !== null).toBe(true); - }); - - it('should return tokenKeys as an array', () => { - expect(tokenKeys instanceof Array).toBe(true); - }); - }); - - describe('getConditions', () => { - let conditions; - - beforeEach(() => { - conditions = gl.FilteredSearchTokenKeys.getConditions(); - }); - - it('should return conditions', () => { - expect(conditions !== null).toBe(true); - }); - - it('should return conditions as an array', () => { - expect(conditions instanceof Array).toBe(true); - }); - }); - - describe('searchByKey', () => { - it('should return null when key not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); - expect(tokenKey === null).toBe(true); - }); - - it('should return tokenKey when found by key', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); - expect(result).toEqual(tokenKeys[0]); - }); - }); - - describe('searchBySymbol', () => { - it('should return null when symbol not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); - expect(tokenKey === null).toBe(true); - }); - - it('should return tokenKey when found by symbol', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); - expect(result).toEqual(tokenKeys[0]); - }); - }); - - describe('searchByKeyParam', () => { - it('should return null when key param not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); - expect(tokenKey === null).toBe(true); - }); - - it('should return tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); - expect(result).toEqual(tokenKeys[0]); - }); - - it('should return alternative tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); - expect(result).toEqual(tokenKeys[0]); - }); - }); - - describe('searchByConditionUrl', () => { - it('should return null when condition url not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); - expect(condition === null).toBe(true); - }); - - it('should return condition when found by url', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); - expect(result).toBe(conditions[0]); - }); - }); - - describe('searchByConditionKeyValue', () => { - it('should return null when condition tokenKey and value not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); - expect(condition === null).toBe(true); - }); - - it('should return condition when found by tokenKey and value', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys - .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); - expect(result).toEqual(conditions[0]); - }); +describe('Filtered Search Token Keys', () => { + describe('get', () => { + let tokenKeys; + + beforeEach(() => { + tokenKeys = gl.FilteredSearchTokenKeys.get(); + }); + + it('should return tokenKeys', () => { + expect(tokenKeys !== null).toBe(true); + }); + + it('should return tokenKeys as an array', () => { + expect(tokenKeys instanceof Array).toBe(true); + }); + }); + + describe('getConditions', () => { + let conditions; + + beforeEach(() => { + conditions = gl.FilteredSearchTokenKeys.getConditions(); + }); + + it('should return conditions', () => { + expect(conditions !== null).toBe(true); + }); + + it('should return conditions as an array', () => { + expect(conditions instanceof Array).toBe(true); + }); + }); + + describe('searchByKey', () => { + it('should return null when key not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchBySymbol', () => { + it('should return null when symbol not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by symbol', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByKeyParam', () => { + it('should return null when key param not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + + it('should return alternative tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByConditionUrl', () => { + it('should return null when condition url not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by url', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + expect(result).toBe(conditions[0]); + }); + }); + + describe('searchByConditionKeyValue', () => { + it('should return null when condition tokenKey and value not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by tokenKey and value', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); + expect(result).toEqual(conditions[0]); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index cabbc694ec4..3e2e577f115 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -2,134 +2,132 @@ require('~/extensions/array'); require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_tokenizer'); -(() => { - describe('Filtered Search Tokenizer', () => { - describe('processTokens', () => { - it('returns for input containing only search value', () => { - const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); - expect(results.searchToken).toBe('searchTerm'); - expect(results.tokens.length).toBe(0); - expect(results.lastToken).toBe(results.searchToken); - }); - - it('returns for input containing only tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); - expect(results.searchToken).toBe(''); - expect(results.tokens.length).toBe(4); - expect(results.tokens[3]).toBe(results.lastToken); - - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('root'); - expect(results.tokens[0].symbol).toBe('@'); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('"Very Important"'); - expect(results.tokens[1].symbol).toBe('~'); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('v1.0'); - expect(results.tokens[2].symbol).toBe('%'); - - expect(results.tokens[3].key).toBe('assignee'); - expect(results.tokens[3].value).toBe('none'); - expect(results.tokens[3].symbol).toBe(''); - }); - - it('returns for input starting with search value and ending with tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('searchTerm anotherSearchTerm milestone:none'); - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - expect(results.tokens.length).toBe(1); - expect(results.tokens[0]).toBe(results.lastToken); - expect(results.tokens[0].key).toBe('milestone'); - expect(results.tokens[0].value).toBe('none'); - expect(results.tokens[0].symbol).toBe(''); - }); - - it('returns for input starting with tokens and ending with search value', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('assignee:@user searchTerm'); - - expect(results.searchToken).toBe('searchTerm'); - expect(results.tokens.length).toBe(1); - expect(results.tokens[0].key).toBe('assignee'); - expect(results.tokens[0].value).toBe('user'); - expect(results.tokens[0].symbol).toBe('@'); - expect(results.lastToken).toBe(results.searchToken); - }); - - it('returns for input containing search value wrapped between tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); - - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - expect(results.tokens.length).toBe(3); - expect(results.tokens[2]).toBe(results.lastToken); - - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('root'); - expect(results.tokens[0].symbol).toBe('@'); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('"Won\'t fix"'); - expect(results.tokens[1].symbol).toBe('~'); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('none'); - expect(results.tokens[2].symbol).toBe(''); - }); - - it('returns for input containing search value in between tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - expect(results.tokens.length).toBe(3); - expect(results.tokens[2]).toBe(results.lastToken); - - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('root'); - expect(results.tokens[0].symbol).toBe('@'); - - expect(results.tokens[1].key).toBe('assignee'); - expect(results.tokens[1].value).toBe('none'); - expect(results.tokens[1].symbol).toBe(''); - - expect(results.tokens[2].key).toBe('label'); - expect(results.tokens[2].value).toBe('Doing'); - expect(results.tokens[2].symbol).toBe('~'); - }); - - it('returns search value for invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); - expect(results.lastToken).toBe('fake:token'); - expect(results.searchToken).toBe('fake:token'); - expect(results.tokens.length).toEqual(0); - }); - - it('returns search value and token for mix of valid and invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); - expect(results.tokens.length).toEqual(1); - expect(results.tokens[0].key).toBe('label'); - expect(results.tokens[0].value).toBe('real'); - expect(results.tokens[0].symbol).toBe(''); - expect(results.lastToken).toBe('fake:token'); - expect(results.searchToken).toBe('fake:token'); - }); - - it('returns search value for invalid symbols', () => { - const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); - expect(results.lastToken).toBe('std::includes'); - expect(results.searchToken).toBe('std::includes'); - }); - - it('removes duplicated values', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); - expect(results.tokens.length).toBe(1); - expect(results.tokens[0].key).toBe('label'); - expect(results.tokens[0].value).toBe('foo'); - expect(results.tokens[0].symbol).toBe('~'); - }); +describe('Filtered Search Tokenizer', () => { + describe('processTokens', () => { + it('returns for input containing only search value', () => { + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(0); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing only tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + expect(results.searchToken).toBe(''); + expect(results.tokens.length).toBe(4); + expect(results.tokens[3]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Very Important"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('v1.0'); + expect(results.tokens[2].symbol).toBe('%'); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].symbol).toBe(''); + }); + + it('returns for input starting with search value and ending with tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0]).toBe(results.lastToken); + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].symbol).toBe(''); + }); + + it('returns for input starting with tokens and ending with search value', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('user'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing search value wrapped between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Won\'t fix"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].symbol).toBe(''); + }); + + it('returns for input containing search value in between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].symbol).toBe(''); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('Doing'); + expect(results.tokens[2].symbol).toBe('~'); + }); + + it('returns search value for invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + expect(results.tokens.length).toEqual(0); + }); + + it('returns search value and token for mix of valid and invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); + expect(results.tokens.length).toEqual(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('real'); + expect(results.tokens[0].symbol).toBe(''); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + }); + + it('returns search value for invalid symbols', () => { + const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); + expect(results.lastToken).toBe('std::includes'); + expect(results.searchToken).toBe('std::includes'); + }); + + it('removes duplicated values', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('foo'); + expect(results.tokens[0].symbol).toBe('~'); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js index 2a58fb3a7df..c255bf7c939 100644 --- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js +++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js @@ -1,3 +1,5 @@ +/* eslint-disable promise/catch-or-return */ + import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; describe('RecentSearchesService', () => { diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index fddeaaf504d..47d904b865b 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -7,6 +7,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) } let(:pipeline) do create( :ci_pipeline, @@ -32,6 +33,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end + it 'merge_requests/merged_merge_request.html.raw' do |example| + allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true) + render_merge_request(example.description, merged_merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/issue_show/issue_title_spec.js b/spec/javascripts/issue_show/issue_title_spec.js index 806d728a874..03edbf9f947 100644 --- a/spec/javascripts/issue_show/issue_title_spec.js +++ b/spec/javascripts/issue_show/issue_title_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import issueTitle from '~/issue_show/issue_title'; +import issueTitle from '~/issue_show/issue_title.vue'; describe('Issue Title', () => { let IssueTitleComponent; diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 03f3c206f44..a00efa10119 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,3 +1,5 @@ +/* eslint-disable promise/catch-or-return */ + require('~/lib/utils/common_utils'); (() => { @@ -313,7 +315,7 @@ require('~/lib/utils/common_utils'); describe('gl.utils.setFavicon', () => { it('should set page favicon to provided favicon', () => { - const faviconName = 'custom_favicon'; + const faviconPath = '//custom_favicon'; const fakeLink = { setAttribute() {}, }; @@ -321,9 +323,9 @@ require('~/lib/utils/common_utils'); spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { expect(attr).toEqual('href'); - expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true); + expect(val.indexOf(faviconPath) > -1).toBe(true); }); - gl.utils.setFavicon(faviconName); + gl.utils.setFavicon(faviconPath); }); }); @@ -345,13 +347,12 @@ require('~/lib/utils/common_utils'); describe('gl.utils.setCiStatusFavicon', () => { it('should set page favicon to CI status favicon based on provided status', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`; - const FAVICON_PATH = 'ci_favicons/'; - const FAVICON = 'icon_status_success'; + const FAVICON_PATH = '//icon_status_success'; const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); spyOn($, 'ajax').and.callFake(function (options) { - options.success({ icon: FAVICON }); - expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON); + options.success({ favicon: FAVICON_PATH }); + expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH); options.success(); expect(spyResetFavicon).toHaveBeenCalled(); options.error(); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index 5fde8be9123..90b12c9f115 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,4 @@ -import { formatRelevantDigits } from '~/lib/utils/number_utils'; +import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { @@ -38,4 +38,11 @@ describe('Number Utils', () => { expect(leftFromDecimal.length).toBe(3); }); }); + + describe('bytesToKiB', () => { + it('calculates KiB for the given bytes', () => { + expect(bytesToKiB(1024)).toEqual(1); + expect(bytesToKiB(1000)).toEqual(0.9765625); + }); + }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 4200e943121..daef9b93fa5 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,110 +1,108 @@ require('~/lib/utils/text_utility'); -(() => { - describe('text_utility', () => { - describe('gl.text.getTextWidth', () => { - it('returns zero width when no text is passed', () => { - expect(gl.text.getTextWidth('')).toBe(0); - }); +describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); - it('returns zero width when no text is passed and font is passed', () => { - expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); - }); + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); - it('returns width when text is passed', () => { - expect(gl.text.getTextWidth('foo') > 0).toBe(true); - }); + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); - it('returns bigger width when font is larger', () => { - const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); - const regular = gl.text.getTextWidth('foo', '10px sans-serif'); - expect(largeFont > regular).toBe(true); - }); + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); }); + }); - describe('gl.text.pluralize', () => { - it('returns pluralized', () => { - expect(gl.text.pluralize('test', 2)).toBe('tests'); - }); + describe('gl.text.pluralize', () => { + it('returns pluralized', () => { + expect(gl.text.pluralize('test', 2)).toBe('tests'); + }); - it('returns pluralized when count is 0', () => { - expect(gl.text.pluralize('test', 0)).toBe('tests'); - }); + it('returns pluralized when count is 0', () => { + expect(gl.text.pluralize('test', 0)).toBe('tests'); + }); - it('does not return pluralized', () => { - expect(gl.text.pluralize('test', 1)).toBe('test'); - }); + it('does not return pluralized', () => { + expect(gl.text.pluralize('test', 1)).toBe('test'); }); + }); - describe('gl.text.highCountTrim', () => { - it('returns 99+ for count >= 100', () => { - expect(gl.text.highCountTrim(105)).toBe('99+'); - expect(gl.text.highCountTrim(100)).toBe('99+'); - }); + describe('gl.text.highCountTrim', () => { + it('returns 99+ for count >= 100', () => { + expect(gl.text.highCountTrim(105)).toBe('99+'); + expect(gl.text.highCountTrim(100)).toBe('99+'); + }); - it('returns exact number for count < 100', () => { - expect(gl.text.highCountTrim(45)).toBe(45); - }); + it('returns exact number for count < 100', () => { + expect(gl.text.highCountTrim(45)).toBe(45); }); + }); - describe('gl.text.insertText', () => { - let textArea; + describe('gl.text.insertText', () => { + let textArea; - beforeAll(() => { - textArea = document.createElement('textarea'); - document.querySelector('body').appendChild(textArea); - }); + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + }); - afterAll(() => { - textArea.parentNode.removeChild(textArea); - }); + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); - describe('without selection', () => { - it('inserts the tag on an empty line', () => { - const initialValue = ''; + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); + }); - it('inserts the tag on a new line if the current one is not empty', () => { - const initialValue = 'some text'; + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}\n* `); - }); + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); - it('inserts the tag on the same line if the current line only contains spaces', () => { - const initialValue = ' '; + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); + }); - it('inserts the tag on the same line if the current line only contains tabs', () => { - const initialValue = '\t\t\t'; + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); }); }); }); -})(); +}); diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js new file mode 100644 index 00000000000..b5c5e60dd97 --- /dev/null +++ b/spec/javascripts/merged_buttons_spec.js @@ -0,0 +1,44 @@ +/* global MergedButtons */ + +import '~/merged_buttons'; + +describe('MergedButtons', () => { + const fixturesPath = 'merge_requests/merged_merge_request.html.raw'; + preloadFixtures(fixturesPath); + + beforeEach(() => { + loadFixtures(fixturesPath); + this.mergedButtons = new MergedButtons(); + this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)'); + this.$removeBranchProgress = $('.remove_source_branch_in_progress'); + this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); + this.$removeBranchButton = $('.remove_source_branch'); + }); + + describe('removeSourceBranch', () => { + it('shows loader', () => { + $('.remove_source_branch').trigger('click'); + expect(this.$removeBranchProgress).toBeVisible(); + expect(this.$removeBranchWidget).not.toBeVisible(); + }); + }); + + describe('removeBranchSuccess', () => { + it('refreshes page when branch removed', () => { + spyOn(gl.utils, 'refreshCurrentPage').and.stub(); + const response = { status: 200 }; + this.$removeBranchButton.trigger('ajax:success', response, 'xhr'); + expect(gl.utils.refreshCurrentPage).toHaveBeenCalled(); + }); + }); + + describe('removeBranchError', () => { + it('shows error message', () => { + const response = { status: 500 }; + this.$removeBranchButton.trigger('ajax:error', response, 'xhr'); + expect(this.$removeBranchFailed).toBeVisible(); + expect(this.$removeBranchProgress).not.toBeVisible(); + expect(this.$removeBranchWidget).not.toBeVisible(); + }); + }); +}); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index d81a5bbb6a5..ca8ee04d955 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -72,5 +72,157 @@ require('~/lib/utils/text_utility'); expect(this.autoSizeSpy).toHaveBeenTriggered(); }); }); + + describe('renderNote', () => { + let notes; + let note; + let $notesList; + + beforeEach(() => { + note = { + discussion_html: null, + valid: true, + html: '<div></div>', + }; + $notesList = jasmine.createSpyObj('$notesList', ['find']); + + notes = jasmine.createSpyObj('notes', [ + 'refresh', + 'isNewNote', + 'collapseLongCommitList', + 'updateNotesCount', + ]); + notes.taskList = jasmine.createSpyObj('tasklist', ['init']); + notes.note_ids = []; + + spyOn(window, '$').and.returnValue($notesList); + spyOn(gl.utils, 'localTimeAgo'); + spyOn(Notes, 'animateAppendNote'); + notes.isNewNote.and.returnValue(true); + + Notes.prototype.renderNote.call(notes, note); + }); + + it('should query for the notes list', () => { + expect(window.$).toHaveBeenCalledWith('ul.main-notes-list'); + }); + + it('should call .animateAppendNote', () => { + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList); + }); + }); + + describe('renderDiscussionNote', () => { + let discussionContainer; + let note; + let notes; + let $form; + let row; + + beforeEach(() => { + note = { + html: '<li></li>', + discussion_html: '<div></div>', + discussion_id: 1, + discussion_resolvable: false, + diff_discussion_html: false, + }; + $form = jasmine.createSpyObj('$form', ['closest', 'find']); + row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']); + + notes = jasmine.createSpyObj('notes', [ + 'isNewNote', + 'isParallelView', + 'updateNotesCount', + ]); + notes.note_ids = []; + + spyOn(gl.utils, 'localTimeAgo'); + spyOn(Notes, 'animateAppendNote'); + notes.isNewNote.and.returnValue(true); + notes.isParallelView.and.returnValue(false); + row.prevAll.and.returnValue(row); + row.first.and.returnValue(row); + row.find.and.returnValue(row); + }); + + describe('Discussion root note', () => { + let $notesList; + let body; + + beforeEach(() => { + body = jasmine.createSpyObj('body', ['attr']); + discussionContainer = { length: 0 }; + + spyOn(window, '$').and.returnValues(discussionContainer, body, $notesList); + $form.closest.and.returnValues(row, $form); + $form.find.and.returnValues(discussionContainer); + body.attr.and.returnValue(''); + + Notes.prototype.renderDiscussionNote.call(notes, note, $form); + }); + + it('should query for the notes list', () => { + expect(window.$.calls.argsFor(2)).toEqual(['ul.main-notes-list']); + }); + + it('should call Notes.animateAppendNote', () => { + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $notesList); + }); + }); + + describe('Discussion sub note', () => { + beforeEach(() => { + discussionContainer = { length: 1 }; + + spyOn(window, '$').and.returnValues(discussionContainer); + $form.closest.and.returnValues(row); + + Notes.prototype.renderDiscussionNote.call(notes, note, $form); + }); + + it('should query foor the discussion container', () => { + expect(window.$).toHaveBeenCalledWith(`.notes[data-discussion-id="${note.discussion_id}"]`); + }); + + it('should call Notes.animateAppendNote', () => { + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer); + }); + }); + }); + + describe('animateAppendNote', () => { + let noteHTML; + let $note; + let $notesList; + + beforeEach(() => { + noteHTML = '<div></div>'; + $note = jasmine.createSpyObj('$note', ['addClass', 'renderGFM', 'removeClass']); + $notesList = jasmine.createSpyObj('$notesList', ['append']); + + spyOn(window, '$').and.returnValue($note); + spyOn(window, 'setTimeout').and.callThrough(); + $note.addClass.and.returnValue($note); + $note.renderGFM.and.returnValue($note); + + Notes.animateAppendNote(noteHTML, $notesList); + }); + + it('should init the note jquery object', () => { + expect(window.$).toHaveBeenCalledWith(noteHTML); + }); + + it('should call addClass', () => { + expect($note.addClass).toHaveBeenCalledWith('fade-in'); + }); + it('should call renderGFM', () => { + expect($note.renderGFM).toHaveBeenCalledWith(); + }); + + it('should append note to the notes list', () => { + expect($notesList.append).toHaveBeenCalledWith($note); + }); + }); }); }).call(window); diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 6e910d2dc71..28c9c7ab282 100644 --- a/spec/javascripts/vue_pipelines_index/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import asyncButtonComp from '~/vue_pipelines_index/components/async_button.vue'; +import asyncButtonComp from '~/pipelines/components/async_button.vue'; describe('Pipelines Async Button', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 2b10d54babe..bb47a28d9fe 100644 --- a/spec/javascripts/vue_pipelines_index/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import emptyStateComp from '~/vue_pipelines_index/components/empty_state.vue'; +import emptyStateComp from '~/pipelines/components/empty_state.vue'; describe('Pipelines Empty State', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js index 7999c15c18d..f667d351f72 100644 --- a/spec/javascripts/vue_pipelines_index/error_state_spec.js +++ b/spec/javascripts/pipelines/error_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import errorStateComp from '~/vue_pipelines_index/components/error_state.vue'; +import errorStateComp from '~/pipelines/components/error_state.vue'; describe('Pipelines Error State', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/pipelines/mock_data.js index 2365a662b9f..2365a662b9f 100644 --- a/spec/javascripts/vue_pipelines_index/mock_data.js +++ b/spec/javascripts/pipelines/mock_data.js diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index 659c4854a56..601eebce38a 100644 --- a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import navControlsComp from '~/vue_pipelines_index/components/nav_controls'; +import navControlsComp from '~/pipelines/components/nav_controls'; describe('Pipelines Nav Controls', () => { let NavControlsComponent; diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 96a2a37b5f7..53931d67ad7 100644 --- a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url'; +import pipelineUrlComp from '~/pipelines/components/pipeline_url'; describe('Pipeline Url Component', () => { let PipelineUrlComponent; diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index 0910df61915..c89dacbcd93 100644 --- a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions'; +import pipelinesActionsComp from '~/pipelines/components/pipelines_actions'; describe('Pipelines Actions dropdown', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js index f7f49649c1c..9724b63d957 100644 --- a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js +++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts'; +import artifactsComp from '~/pipelines/components/pipelines_artifacts'; describe('Pipelines Artifacts dropdown', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 725f6cb2d7a..e9c05f74ce6 100644 --- a/spec/javascripts/vue_pipelines_index/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import pipelinesComp from '~/vue_pipelines_index/pipelines'; -import Store from '~/vue_pipelines_index/stores/pipelines_store'; +import pipelinesComp from '~/pipelines/pipelines'; +import Store from '~/pipelines/stores/pipelines_store'; import pipelinesData from './mock_data'; describe('Pipelines', () => { diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/pipelines/pipelines_store_spec.js index 5c0934404bb..10ff0c6bb84 100644 --- a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js +++ b/spec/javascripts/pipelines/pipelines_store_spec.js @@ -1,4 +1,4 @@ -import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store'; +import PipelineStore from '~/pipelines/stores/pipelines_store'; describe('Pipelines Store', () => { let store; diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js new file mode 100644 index 00000000000..66b57a82363 --- /dev/null +++ b/spec/javascripts/pipelines/stage_spec.js @@ -0,0 +1,66 @@ +import Vue from 'vue'; +import { SUCCESS_SVG } from '~/ci_status_icons'; +import Stage from '~/pipelines/components/stage'; + +function minify(string) { + return string.replace(/\s/g, ''); +} + +describe('Pipelines Stage', () => { + describe('data', () => { + let stageReturnValue; + + beforeEach(() => { + stageReturnValue = Stage.data(); + }); + + it('should return object with .builds and .spinner', () => { + expect(stageReturnValue).toEqual({ + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }); + }); + }); + + describe('computed', () => { + describe('svgHTML', function () { + let stage; + let svgHTML; + + beforeEach(() => { + stage = { stage: { status: { icon: 'icon_status_success' } } }; + + svgHTML = Stage.computed.svgHTML.call(stage); + }); + + it("should return the correct icon for the stage's status", () => { + expect(svgHTML).toBe(SUCCESS_SVG); + }); + }); + }); + + describe('when mounted', () => { + let StageComponent; + let renderedComponent; + let stage; + + beforeEach(() => { + stage = { status: { icon: 'icon_status_success' } }; + + StageComponent = Vue.extend(Stage); + + renderedComponent = new StageComponent({ + propsData: { + stage, + }, + }).$mount(); + }); + + it('should render the correct status svg', () => { + const minifiedComponent = minify(renderedComponent.$el.outerHTML); + const expectedSVG = minify(SUCCESS_SVG); + + expect(minifiedComponent).toContain(expectedSVG); + }); + }); +}); diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js new file mode 100644 index 00000000000..9b8373df29e --- /dev/null +++ b/spec/javascripts/shortcuts_spec.js @@ -0,0 +1,45 @@ +/* global Shortcuts */ +describe('Shortcuts', () => { + const fixtureName = 'issues/issue_with_comment.html.raw'; + const createEvent = (type, target) => $.Event(type, { + target, + }); + + preloadFixtures(fixtureName); + + describe('toggleMarkdownPreview', () => { + let sc; + + beforeEach(() => { + loadFixtures(fixtureName); + + spyOnEvent('.js-new-note-form .js-md-preview-button', 'focus'); + spyOnEvent('.edit-note .js-md-preview-button', 'focus'); + + sc = new Shortcuts(); + }); + + it('focuses preview button in form', () => { + sc.toggleMarkdownPreview( + createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'), + )); + + expect('focus').toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button'); + }); + + it('focues preview button inside edit comment form', (done) => { + document.querySelector('.js-note-edit').click(); + + setTimeout(() => { + sc.toggleMarkdownPreview( + createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'), + )); + + expect('focus').not.toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button'); + expect('focus').toHaveBeenTriggeredOn('.edit-note .js-md-preview-button'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js index c0375ebc61c..28d0c7dcd99 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js @@ -14,7 +14,6 @@ describe('UserCallout', function () { this.userCallout = new UserCallout(); this.closeButton = $('.js-close-callout.close'); this.userCalloutBtn = $('.js-close-callout:not(.close)'); - this.userCalloutContainer = $('.user-callout'); }); it('hides when user clicks on the dismiss-icon', (done) => { diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb index 603b79a323c..600f3c123ed 100644 --- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -5,9 +5,10 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do include FilterSpecHelper let(:user) { create(:user) } + let(:context) { { current_user: user, issuable_state_filter_enabled: true } } - def create_link(data) - link_to('text', '', class: 'gfm has-tooltip', data: data) + def create_link(text, data) + link_to(text, '', class: 'gfm has-tooltip', data: data) end it 'ignores non-GFM links' do @@ -19,8 +20,62 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do it 'ignores non-issuable links' do project = create(:empty_project, :public) - link = create_link(project: project, reference_type: 'issue') - doc = filter(link, current_user: user) + link = create_link('text', project: project, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('text') + end + + it 'ignores issuable links with empty content' do + issue = create(:issue, :closed) + link = create_link('', issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('') + end + + it 'ignores issuable links with custom anchor' do + issue = create(:issue, :closed) + link = create_link('something', issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq('something') + end + + it 'ignores issuable links to specific comments' do + issue = create(:issue, :closed) + link = create_link("#{issue.to_reference} (comment 1)", issue: issue.id, reference_type: 'issue') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{issue.to_reference} (comment 1)") + end + + it 'ignores merge request links to diffs tab' do + merge_request = create(:merge_request, :closed) + link = create_link( + "#{merge_request.to_reference} (diffs)", + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (diffs)") + end + + it 'handles cross project references' do + issue = create(:issue, :closed) + project = create(:empty_project) + link = create_link(issue.to_reference(project), issue: issue.id, reference_type: 'issue') + doc = filter(link, context.merge(project: project)) + + expect(doc.css('a').last.text).to eq("#{issue.to_reference(project)} (closed)") + end + + it 'does not append state when filter is not enabled' do + issue = create(:issue, :closed) + link = create_link('text', issue: issue.id, reference_type: 'issue') + context = { current_user: user } + doc = filter(link, context) expect(doc.css('a').last.text).to eq('text') end @@ -28,68 +83,88 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do context 'for issue references' do it 'ignores open issue references' do issue = create(:issue) - link = create_link(issue: issue.id, reference_type: 'issue') - doc = filter(link, current_user: user) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(issue.to_reference) end it 'ignores reopened issue references' do - reopened_issue = create(:issue, :reopened) - link = create_link(issue: reopened_issue.id, reference_type: 'issue') - doc = filter(link, current_user: user) + issue = create(:issue, :reopened) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text') + expect(doc.css('a').last.text).to eq(issue.to_reference) end - it 'appends [closed] to closed issue references' do - closed_issue = create(:issue, :closed) - link = create_link(issue: closed_issue.id, reference_type: 'issue') - doc = filter(link, current_user: user) + it 'appends state to closed issue references' do + issue = create(:issue, :closed) + link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text [closed]') + expect(doc.css('a').last.text).to eq("#{issue.to_reference} (closed)") end end context 'for merge request references' do it 'ignores open merge request references' do - mr = create(:merge_request) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') - doc = filter(link, current_user: user) - - expect(doc.css('a').last.text).to eq('text') + merge_request = create(:merge_request) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(merge_request.to_reference) end it 'ignores reopened merge request references' do - mr = create(:merge_request, :reopened) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') - doc = filter(link, current_user: user) - - expect(doc.css('a').last.text).to eq('text') + merge_request = create(:merge_request, :reopened) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(merge_request.to_reference) end it 'ignores locked merge request references' do - mr = create(:merge_request, :locked) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') - doc = filter(link, current_user: user) - - expect(doc.css('a').last.text).to eq('text') + merge_request = create(:merge_request, :locked) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq(merge_request.to_reference) end - it 'appends [closed] to closed merge request references' do - mr = create(:merge_request, :closed) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') - doc = filter(link, current_user: user) + it 'appends state to closed merge request references' do + merge_request = create(:merge_request, :closed) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text [closed]') + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (closed)") end - it 'appends [merged] to merged merge request references' do - mr = create(:merge_request, :merged) - link = create_link(merge_request: mr.id, reference_type: 'merge_request') - doc = filter(link, current_user: user) + it 'appends state to merged merge request references' do + merge_request = create(:merge_request, :merged) + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request' + ) + doc = filter(link, context) - expect(doc.css('a').last.text).to eq('text [merged]') + expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)") end end end diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index f85a5dcbd8b..9b8ecb201f3 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -5,7 +5,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should replace plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' doc = filter(input) @@ -14,8 +14,8 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' - output = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre></pre></pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' + output = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' doc = filter(input) expect(doc.to_s).to eq output @@ -23,7 +23,7 @@ describe Banzai::Filter::PlantumlFilter, lib: true do it 'should not replace plantuml pre tag with img tag if url is invalid' do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") - input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' doc = filter(input) diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 4817fcd031a..dd2674f9f20 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -4,13 +4,13 @@ describe Banzai::ObjectRenderer do let(:project) { create(:empty_project) } let(:user) { project.owner } let(:renderer) { described_class.new(project, user, custom_value: 'value') } - let(:object) { Note.new(note: 'hello', note_html: '<p>hello</p>') } + let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } describe '#render' do it 'renders and redacts an Array of objects' do renderer.render([object], :note) - expect(object.redacted_note_html).to eq '<p>hello</p>' + expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>' expect(object.user_visible_reference_count).to eq 0 end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 6d2c141e18b..e6f2963193c 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -42,6 +42,31 @@ describe Banzai::Redactor do end end + context 'when project is in pending delete' do + let!(:issue) { create(:issue, project: project) } + let(:redactor) { described_class.new(project, user) } + + before do + project.update(pending_delete: true) + end + + it 'redacts an issue attached' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>") + + redactor.redact([doc]) + + expect(doc.to_html).to eq('foo') + end + + it 'redacts an external issue' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>") + + redactor.redact([doc]) + + expect(doc.to_html).to eq('foo') + end + end + context 'when reference visible to user' do it 'does not redact an array of documents' do doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index a3141894c74..d5746107ee1 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -114,8 +114,27 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do expect(hash).to eq({ link => user }) end - it 'returns an empty Hash when the list of nodes is empty' do - expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({}) + it 'returns an empty Hash when entry does not exist in the database' do + link = double(:link) + + expect(link).to receive(:has_attribute?). + with('data-user'). + and_return(true) + + expect(link).to receive(:attr). + with('data-user'). + and_return('1') + + nodes = [link] + bad_id = user.id + 100 + + expect(subject).to receive(:unique_attribute_values). + with(nodes, 'data-user'). + and_return([bad_id.to_s]) + + hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') + + expect(hash).to eq({}) end end diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index aaa6b12e67e..e6f8d2a1fed 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -1,73 +1,36 @@ require 'spec_helper' describe Banzai::Renderer do - def expect_render(project = :project) - expected_context = { project: project } - expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context) - end - - def expect_cache_update - expect(object).to receive(:update_column).with("field_html", :html) - end - - def fake_object(*features) - markdown = :markdown if features.include?(:markdown) - html = :html if features.include?(:html) - - object = double( - "object", - banzai_render_context: { project: :project }, - field: markdown, - field_html: html - ) + def fake_object(fresh:) + object = double('object') - allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html") - allow(object).to receive(:new_record?).and_return(features.include?(:new)) - allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed)) + allow(object).to receive(:cached_html_up_to_date?).with(:field).and_return(fresh) + allow(object).to receive(:cached_html_for).with(:field).and_return('field_html') object end - describe "#render_field" do + describe '#render_field' do let(:renderer) { Banzai::Renderer } - let(:subject) { renderer.render_field(object, :field) } + subject { renderer.render_field(object, :field) } - context "with an empty cache" do - let(:object) { fake_object(:markdown) } - it "caches and returns the result" do - expect_render - expect_cache_update - expect(subject).to eq(:html) - end - end + context 'with a stale cache' do + let(:object) { fake_object(fresh: false) } - context "with a filled cache" do - let(:object) { fake_object(:markdown, :html) } + it 'caches and returns the result' do + expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) - it "uses the cache" do - expect_render.never - expect_cache_update.never - should eq(:html) + is_expected.to eq('field_html') end end - context "new object" do - let(:object) { fake_object(:new, :markdown) } - - it "doesn't cache the result" do - expect_render - expect_cache_update.never - expect(subject).to eq(:html) - end - end + context 'with an up-to-date cache' do + let(:object) { fake_object(fresh: true) } - context "destroyed object" do - let(:object) { fake_object(:destroyed, :markdown) } + it 'uses the cache' do + expect(object).to receive(:refresh_markdown_cache!).never - it "doesn't cache the result" do - expect_render - expect_cache_update.never - expect(subject).to eq(:html) + is_expected.to eq('field_html') end end end diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb index b9c4572c269..c2bcb54210b 100644 --- a/spec/lib/container_registry/path_spec.rb +++ b/spec/lib/container_registry/path_spec.rb @@ -33,10 +33,20 @@ describe ContainerRegistry::Path do end describe '#to_s' do - let(:path) { 'some/image' } + context 'when path does not have uppercase characters' do + let(:path) { 'some/image' } - it 'return a string with a repository path' do - expect(subject.to_s).to eq path + it 'return a string with a repository path' do + expect(subject.to_s).to eq 'some/image' + end + end + + context 'when path has uppercase characters' do + let(:path) { 'SoMe/ImAgE' } + + it 'return a string with a repository path' do + expect(subject.to_s).to eq 'some/image' + end end end @@ -70,6 +80,12 @@ describe ContainerRegistry::Path do it { is_expected.to be_valid } end + + context 'when path contains uppercase letters' do + let(:path) { 'Some/Registry' } + + it { is_expected.to be_valid } + end end describe '#has_repository?' do @@ -173,15 +189,10 @@ describe ContainerRegistry::Path do end context 'when project exists' do - let(:group) { create(:group, path: 'some_group') } - - let(:project) do - create(:empty_project, group: group, name: 'some_project') - end + let(:group) { create(:group, path: 'Some_Group') } before do - allow(path).to receive(:repository_project) - .and_return(project) + create(:empty_project, group: group, name: 'some_project') end context 'when project path equal repository path' do @@ -209,4 +220,27 @@ describe ContainerRegistry::Path do end end end + + describe '#project_path' do + context 'when project does not exist' do + let(:path) { 'some/name' } + + it 'returns nil' do + expect(subject.project_path).to be_nil + end + end + + context 'when project with uppercase characters in path exists' do + let(:path) { 'somegroup/myproject/my/image' } + let(:group) { create(:group, path: 'SomeGroup') } + + before do + create(:empty_project, group: group, name: 'MyProject') + end + + it 'returns downcased project path' do + expect(subject.project_path).to eq 'somegroup/myproject' + end + end + end end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 2e57ccef182..40ac5a3ed37 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -17,12 +17,12 @@ describe Gitlab::Ci::Trace::Stream do describe '#limit' do let(:stream) do described_class.new do - StringIO.new("12345678") + StringIO.new((1..8).to_a.join("\n")) end end - it 'if size is larger we start from beggining' do - stream.limit(10) + it 'if size is larger we start from beginning' do + stream.limit(20) expect(stream.tell).to eq(0) end @@ -30,17 +30,61 @@ describe Gitlab::Ci::Trace::Stream do it 'if size is smaller we start from the end' do stream.limit(2) - expect(stream.tell).to eq(6) + expect(stream.raw).to eq("8") + end + + context 'when the trace contains ANSI sequence and Unicode' do + let(:stream) do + described_class.new do + File.open(expand_fixture_path('trace/ansi-sequence-and-unicode')) + end + end + + it 'forwards to the next linefeed, case 1' do + stream.limit(7) + + result = stream.raw + + expect(result).to eq('') + expect(result.encoding).to eq(Encoding.default_external) + end + + it 'forwards to the next linefeed, case 2' do + stream.limit(29) + + result = stream.raw + + expect(result).to eq("\e[01;32m許功蓋\e[0m\n") + expect(result.encoding).to eq(Encoding.default_external) + end + + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796 + it 'reads in binary, output as Encoding.default_external' do + stream.limit(52) + + result = stream.html + + expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>") + expect(result.encoding).to eq(Encoding.default_external) + end end end describe '#append' do + let(:tempfile) { Tempfile.new } + let(:stream) do described_class.new do - StringIO.new("12345678") + tempfile.write("12345678") + tempfile.rewind + tempfile end end + after do + tempfile.unlink + end + it "truncates and append content" do stream.append("89", 4) stream.seek(0) @@ -48,6 +92,17 @@ describe Gitlab::Ci::Trace::Stream do expect(stream.size).to eq(6) expect(stream.raw).to eq("123489") end + + it 'appends in binary mode' do + '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset| + stream.append(byte, offset) + end + + stream.seek(0) + + expect(stream.size).to eq(4) + expect(stream.raw).to eq('😺') + end end describe '#set' do diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 994995b57b8..c166f83664a 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -100,7 +100,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Create file", file_path: file_name, file_content: content @@ -113,7 +113,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Update file", file_path: file_name, file_content: content @@ -122,11 +122,11 @@ describe Gitlab::Diff::PositionTracer, lib: true do end def delete_file(branch_name, file_name) - Files::DestroyService.new( + Files::DeleteService.new( project, current_user, start_branch: branch_name, - target_branch: branch_name, + branch_name: branch_name, commit_message: "Delete file", file_path: file_name ).execute diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 3f494257545..e6a07a58d73 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -234,7 +234,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it { expect(blob.lfs_pointer?).to eq(true) } it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") } - it { expect(blob.lfs_size).to eq("19548") } + it { expect(blob.lfs_size).to eq(19548) } it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") } it { expect(blob.name).to eq("image.jpg") } it { expect(blob.path).to eq("files/lfs/image.jpg") } @@ -273,7 +273,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it { expect(blob.lfs_pointer?).to eq(false) } it { expect(blob.lfs_oid).to eq(nil) } - it { expect(blob.lfs_size).to eq("1575078") } + it { expect(blob.lfs_size).to eq(1575078) } it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") } it { expect(blob.name).to eq("picture-invalid.png") } it { expect(blob.path).to eq("files/lfs/picture-invalid.png") } diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb index 27bcc241b82..f6ac7b23d1d 100644 --- a/spec/lib/gitlab/git/encoding_helper_spec.rb +++ b/spec/lib/gitlab/git/encoding_helper_spec.rb @@ -56,6 +56,10 @@ describe Gitlab::Git::EncodingHelper do expect(r.encoding.name).to eq('UTF-8') end end + + it 'returns empty string on conversion errors' do + expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError) + end end describe '#clean' do diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb index 07d71f6777d..21b71654251 100644 --- a/spec/lib/gitlab/git/index_spec.rb +++ b/spec/lib/gitlab/git/index_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create(options) }.to raise_error('Filename already exists') + expect { index.create(options) }.to raise_error('A file with this name already exists') end end @@ -89,7 +89,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') + expect { index.create_dir(options) }.to raise_error('A file with this name already exists') end end @@ -99,7 +99,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('Directory already exists') + expect { index.create_dir(options) }.to raise_error('A directory with this name already exists') end end end @@ -118,7 +118,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.update(options) }.to raise_error("File doesn't exist") + expect { index.update(options) }.to raise_error("A file with this name doesn't exist") end end @@ -156,7 +156,15 @@ describe Gitlab::Git::Index, seed_helper: true do it 'raises an error' do options[:previous_path] = 'documents/story.txt' - expect { index.move(options) }.to raise_error("File doesn't exist") + expect { index.move(options) }.to raise_error("A file with this name doesn't exist") + end + end + + context 'when a file at the new path already exists' do + it 'raises an error' do + options[:file_path] = 'CHANGELOG' + + expect { index.move(options) }.to raise_error("A file with this name already exists") end end @@ -203,7 +211,7 @@ describe Gitlab::Git::Index, seed_helper: true do end it 'raises an error' do - expect { index.delete(options) }.to raise_error("File doesn't exist") + expect { index.delete(options) }.to raise_error("A file with this name doesn't exist") end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 703b41f95ac..d8b72615fab 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -211,7 +211,7 @@ describe Gitlab::GitAccess, lib: true do target_branch = project.repository.lookup('feature') source_branch = project.repository.create_file( user, - 'John Doe', + 'filename', 'This is the file content', message: 'This is a good commit message', branch_name: unprotected_branch) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index ba45e2d758c..127cd8c78d8 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -32,12 +32,6 @@ describe Gitlab::Regex, lib: true do it { is_expected.to match('foo@bar') } end - describe '.file_path_regex' do - subject { described_class.file_path_regex } - - it { is_expected.to match('foo@/bar') } - end - describe '.environment_slug_regex' do subject { described_class.environment_slug_regex } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb new file mode 100644 index 00000000000..7f21288cf88 --- /dev/null +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe Gitlab::UsageData do + let!(:project) { create(:empty_project) } + let!(:project2) { create(:empty_project) } + let!(:board) { create(:board, project: project) } + + describe '#data' do + subject { Gitlab::UsageData.data } + + it "gathers usage data" do + expect(subject.keys).to match_array(%i( + active_user_count + counts + recorded_at + mattermost_enabled + edition + version + uuid + )) + end + + it "gathers usage counts" do + count_data = subject[:counts] + + expect(count_data[:boards]).to eq(1) + expect(count_data[:projects]).to eq(2) + + expect(count_data.keys).to match_array(%i( + boards + ci_builds + ci_pipelines + ci_runners + ci_triggers + deploy_keys + deployments + environments + groups + issues + keys + labels + lfs_objects + merge_requests + milestones + notes + projects + projects_prometheus_active + pages_domains + protected_branches + releases + services + snippets + todos + uploads + web_hooks + )) + end + end + + describe '#license_usage_data' do + subject { Gitlab::UsageData.license_usage_data } + + it "gathers license data" do + expect(subject[:uuid]).to eq(current_application_settings.uuid) + expect(subject[:version]).to eq(Gitlab::VERSION) + expect(subject[:active_user_count]).to eq(User.active.count) + expect(subject[:recorded_at]).to be_a(Time) + end + end +end diff --git a/spec/lib/gitlab/user_activities_spec.rb b/spec/lib/gitlab/user_activities_spec.rb new file mode 100644 index 00000000000..187d88c8c58 --- /dev/null +++ b/spec/lib/gitlab/user_activities_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Gitlab::UserActivities, :redis, lib: true do + let(:now) { Time.now } + + describe '.record' do + context 'with no time given' do + it 'uses Time.now and records an activity in Redis' do + Timecop.freeze do + now # eager-load now + described_class.record(42) + end + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + end + end + + context 'with a time given' do + it 'uses the given time and records an activity in Redis' do + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + end + end + end + + describe '.delete' do + context 'with a single key' do + context 'and key exists' do + it 'removes the pair from Redis' do + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + + subject.delete(42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + + context 'and key does not exist' do + it 'removes the pair from Redis' do + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + + subject.delete(42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + end + + context 'with multiple keys' do + context 'and all keys exist' do + it 'removes the pair from Redis' do + described_class.record(41, now) + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['41', now.to_i.to_s], ['42', now.to_i.to_s]]]) + end + + subject.delete(41, 42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + + context 'and some keys does not exist' do + it 'removes the existing pair from Redis' do + described_class.record(42, now) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', [['42', now.to_i.to_s]]]) + end + + subject.delete(41, 42) + + Gitlab::Redis.with do |redis| + expect(redis.hscan(described_class::KEY, 0)).to eq(['0', []]) + end + end + end + end + end + + describe 'Enumerable' do + before do + described_class.record(40, now) + described_class.record(41, now) + described_class.record(42, now) + end + + it 'allows to read the activities sequentially' do + expected = { '40' => now.to_i.to_s, '41' => now.to_i.to_s, '42' => now.to_i.to_s } + + actual = described_class.new.each_with_object({}) do |(key, time), actual| + actual[key] = time + end + + expect(actual).to eq(expected) + end + + context 'with many records' do + before do + 1_000.times { |i| described_class.record(i, now) } + end + + it 'is possible to loop through all the records' do + expect(described_class.new.count).to eq(1_000) + end + end + end +end diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb new file mode 100644 index 00000000000..1db9bc002ae --- /dev/null +++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb @@ -0,0 +1,49 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb') + +describe MigrateUserActivitiesToUsersLastActivityOn, :redis do + let(:migration) { described_class.new } + let!(:user_active_1) { create(:user) } + let!(:user_active_2) { create(:user) } + + def record_activity(user, time) + Gitlab::Redis.with do |redis| + redis.zadd(described_class::USER_ACTIVITY_SET_KEY, time.to_i, user.username) + end + end + + around do |example| + Timecop.freeze { example.run } + end + + before do + record_activity(user_active_1, described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 2.months) + record_activity(user_active_2, described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 3.months) + mute_stdout { migration.up } + end + + describe '#up' do + it 'fills last_activity_on from the legacy Redis Sorted Set' do + expect(user_active_1.reload.last_activity_on).to eq((described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 2.months).to_date) + expect(user_active_2.reload.last_activity_on).to eq((described_class::TIME_WHEN_ACTIVITY_SET_WAS_INTRODUCED + 3.months).to_date) + end + end + + describe '#down' do + it 'sets last_activity_on to NULL for all users' do + mute_stdout { migration.down } + + expect(user_active_1.reload.last_activity_on).to be_nil + expect(user_active_2.reload.last_activity_on).to be_nil + end + end + + def mute_stdout + orig_stdout = $stdout + $stdout = StringIO.new + yield + $stdout = orig_stdout + end +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 4e71597521d..ced93c8f762 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -29,7 +29,8 @@ RSpec.describe AbuseReport, type: :model do it 'lets a worker delete the user' do expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, - delete_solo_owned_groups: true) + delete_solo_owned_groups: true, + hard_delete: true) subject.remove_user(deleted_by: user) end diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 6151d53cd91..de0069bdcac 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -1,9 +1,6 @@ require 'spec_helper' describe CacheMarkdownField do - caching_classes = CacheMarkdownField::CACHING_CLASSES - CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze - # The minimum necessary ActiveModel to test this concern class ThingWithMarkdownFields include ActiveModel::Model @@ -27,18 +24,19 @@ describe CacheMarkdownField do cache_markdown_field :foo cache_markdown_field :baz, pipeline: :single_line - def self.add_attr(attr_name) - self.attribute_names += [attr_name] - define_attribute_methods(attr_name) - attr_reader(attr_name) - define_method("#{attr_name}=") do |val| - send("#{attr_name}_will_change!") unless val == send(attr_name) - instance_variable_set("@#{attr_name}", val) + def self.add_attr(name) + self.attribute_names += [name] + define_attribute_methods(name) + attr_reader(name) + define_method("#{name}=") do |value| + write_attribute(name, value) end end - [:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name| - add_attr(attr_name) + add_attr :cached_markdown_version + + [:foo, :foo_html, :bar, :baz, :baz_html].each do |name| + add_attr(name) end def initialize(*) @@ -48,6 +46,15 @@ describe CacheMarkdownField do clear_changes_information end + def read_attribute(name) + instance_variable_get("@#{name}") + end + + def write_attribute(name, value) + send("#{name}_will_change!") unless value == read_attribute(name) + instance_variable_set("@#{name}", value) + end + def save run_callbacks :save do changes_applied @@ -55,127 +62,236 @@ describe CacheMarkdownField do end end - CacheMarkdownField::CACHING_CLASSES = caching_classes - def thing_subclass(new_attr) Class.new(ThingWithMarkdownFields) { add_attr(new_attr) } end - let(:markdown) { "`Foo`" } - let(:html) { "<p><code>Foo</code></p>" } + let(:markdown) { '`Foo`' } + let(:html) { '<p dir="auto"><code>Foo</code></p>' } - let(:updated_markdown) { "`Bar`" } - let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" } + let(:updated_markdown) { '`Bar`' } + let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' } - subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) } + let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } - describe ".attributes" do - it "excludes cache attributes" do - expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux]) + describe '.attributes' do + it 'excludes cache attributes' do + expect(thing.attributes.keys.sort).to eq(%w[bar baz foo]) end end - describe ".cache_markdown_field" do - it "refuses to allow untracked classes" do - expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError) + context 'an unchanged markdown field' do + before do + thing.foo = thing.foo + thing.save end + + it { expect(thing.foo).to eq(markdown) } + it { expect(thing.foo_html).to eq(html) } + it { expect(thing.foo_html_changed?).not_to be_truthy } + it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } end - context "an unchanged markdown field" do + context 'a changed markdown field' do before do - subject.foo = subject.foo - subject.save + thing.foo = updated_markdown + thing.save end - it { expect(subject.foo).to eq(markdown) } - it { expect(subject.foo_html).to eq(html) } - it { expect(subject.foo_html_changed?).not_to be_truthy } + it { expect(thing.foo_html).to eq(updated_html) } + it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } end - context "a changed markdown field" do + context 'a non-markdown field changed' do + before do + thing.bar = 'OK' + thing.save + end + + it { expect(thing.bar).to eq('OK') } + it { expect(thing.foo).to eq(markdown) } + it { expect(thing.foo_html).to eq(html) } + it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } + end + + context 'version is out of date' do + let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) } + before do - subject.foo = updated_markdown - subject.save + thing.save + end + + it { expect(thing.foo_html).to eq(updated_html) } + it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } + end + + describe '#cached_html_up_to_date?' do + subject { thing.cached_html_up_to_date?(:foo) } + + it 'returns false when the version is absent' do + thing.cached_markdown_version = nil + + is_expected.to be_falsy + end + + it 'returns false when the version is too early' do + thing.cached_markdown_version -= 1 + + is_expected.to be_falsy + end + + it 'returns false when the version is too late' do + thing.cached_markdown_version += 1 + + is_expected.to be_falsy + end + + it 'returns true when the version is just right' do + thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION + + is_expected.to be_truthy end - it { expect(subject.foo_html).to eq(updated_html) } + it 'returns false if markdown has been changed but html has not' do + thing.foo = updated_html + + is_expected.to be_falsy + end + + it 'returns true if markdown has not been changed but html has' do + thing.foo_html = updated_html + + is_expected.to be_truthy + end + + it 'returns true if markdown and html have both been changed' do + thing.foo = updated_markdown + thing.foo_html = updated_html + + is_expected.to be_truthy + end end - context "a non-markdown field changed" do + describe '#refresh_markdown_cache!' do before do - subject.bar = "OK" - subject.save + thing.foo = updated_markdown + end + + context 'do_update: false' do + it 'fills all html fields' do + thing.refresh_markdown_cache! + + expect(thing.foo_html).to eq(updated_html) + expect(thing.foo_html_changed?).to be_truthy + expect(thing.baz_html_changed?).to be_truthy + end + + it 'does not save the result' do + expect(thing).not_to receive(:update_columns) + + thing.refresh_markdown_cache! + end + + it 'updates the markdown cache version' do + thing.cached_markdown_version = nil + thing.refresh_markdown_cache! + + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) + end end - it { expect(subject.bar).to eq("OK") } - it { expect(subject.foo).to eq(markdown) } - it { expect(subject.foo_html).to eq(html) } + context 'do_update: true' do + it 'fills all html fields' do + thing.refresh_markdown_cache!(do_update: true) + + expect(thing.foo_html).to eq(updated_html) + expect(thing.foo_html_changed?).to be_truthy + expect(thing.baz_html_changed?).to be_truthy + end + + it 'skips saving if not persisted' do + expect(thing).to receive(:persisted?).and_return(false) + expect(thing).not_to receive(:update_columns) + + thing.refresh_markdown_cache!(do_update: true) + end + + it 'saves the changes using #update_columns' do + expect(thing).to receive(:persisted?).and_return(true) + expect(thing).to receive(:update_columns) + .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION) + + thing.refresh_markdown_cache!(do_update: true) + end + end end describe '#banzai_render_context' do - it "sets project to nil if the object lacks a project" do - context = subject.banzai_render_context(:foo) - expect(context).to have_key(:project) + subject(:context) { thing.banzai_render_context(:foo) } + + it 'sets project to nil if the object lacks a project' do + is_expected.to have_key(:project) expect(context[:project]).to be_nil end - it "excludes author if the object lacks an author" do - context = subject.banzai_render_context(:foo) - expect(context).not_to have_key(:author) + it 'excludes author if the object lacks an author' do + is_expected.not_to have_key(:author) end - it "raises if the context for an unrecognised field is requested" do - expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError) + it 'raises if the context for an unrecognised field is requested' do + expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError) end - it "includes the pipeline" do - context = subject.banzai_render_context(:baz) - expect(context[:pipeline]).to eq(:single_line) + it 'includes the pipeline' do + baz = thing.banzai_render_context(:baz) + + expect(baz[:pipeline]).to eq(:single_line) end - it "returns copies of the context template" do - template = subject.cached_markdown_fields[:baz] - copy = subject.banzai_render_context(:baz) + it 'returns copies of the context template' do + template = thing.cached_markdown_fields[:baz] + copy = thing.banzai_render_context(:baz) + expect(copy).not_to be(template) end - context "with a project" do - subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) } + context 'with a project' do + let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project_value) } - it "sets the project in the context" do - context = subject.banzai_render_context(:foo) - expect(context).to have_key(:project) - expect(context[:project]).to eq(:project) + it 'sets the project in the context' do + is_expected.to have_key(:project) + expect(context[:project]).to eq(:project_value) end - it "invalidates the cache when project changes" do - subject.project = :new_project + it 'invalidates the cache when project changes' do + thing.project = :new_project allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html) - subject.save + thing.save - expect(subject.foo_html).to eq(updated_html) - expect(subject.baz_html).to eq(updated_html) + expect(thing.foo_html).to eq(updated_html) + expect(thing.baz_html).to eq(updated_html) + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) end end - context "with an author" do - subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) } + context 'with an author' do + let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) } - it "sets the author in the context" do - context = subject.banzai_render_context(:foo) - expect(context).to have_key(:author) - expect(context[:author]).to eq(:author) + it 'sets the author in the context' do + is_expected.to have_key(:author) + expect(context[:author]).to eq(:author_value) end - it "invalidates the cache when author changes" do - subject.author = :new_author + it 'invalidates the cache when author changes' do + thing.author = :new_author allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html) - subject.save + thing.save - expect(subject.foo_html).to eq(updated_html) - expect(subject.baz_html).to eq(updated_html) + expect(thing.foo_html).to eq(updated_html) + expect(thing.baz_html).to eq(updated_html) + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) end end end diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 6d6c9f2adfc..eff41d85972 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -34,8 +34,18 @@ describe ContainerRepository do end describe '#path' do - it 'returns a full path to the repository' do - expect(repository.path).to eq('group/test/my_image') + context 'when project path does not contain uppercase letters' do + it 'returns a full path to the repository' do + expect(repository.path).to eq('group/test/my_image') + end + end + + context 'when path contains uppercase letters' do + let(:project) { create(:project, path: 'MY_PROJECT', group: group) } + + it 'returns a full path without capital letters' do + expect(repository.path).to eq('group/my_project/my_image') + end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index d057c9cf6e9..11befd4edfe 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -361,7 +361,10 @@ describe Issue, models: true do it 'updates when assignees change' do user1 = create(:user) user2 = create(:user) - issue = create(:issue, assignee: user1) + project = create(:empty_project) + issue = create(:issue, assignee: user1, project: project) + project.add_developer(user1) + project.add_developer(user2) expect(user1.assigned_open_issues_count).to eq(1) expect(user2.assigned_open_issues_count).to eq(0) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index c720cc9f2c2..b0f3657d3b5 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -386,6 +386,31 @@ describe Member, models: true do end end + describe '.add_users' do + %w[project group].each do |source_type| + context "when source is a #{source_type}" do + let!(:source) { create(source_type, :public, :access_requestable) } + let!(:user) { create(:user) } + let!(:admin) { create(:admin) } + + it 'returns a <Source>Member objects' do + members = described_class.add_users(source, [user], :master) + + expect(members).to be_a Array + expect(members.first).to be_a "#{source_type.classify}Member".constantize + expect(members.first).to be_persisted + end + + it 'returns an empty array' do + members = described_class.add_users(source, [], :master) + + expect(members).to be_a Array + expect(members).to be_empty + end + end + end + end + describe '#accept_request' do let(:member) { create(:project_member, requested_at: Time.now.utc) } diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 024380b7ebb..17765b25856 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -13,12 +13,12 @@ describe GroupMember, models: true do end end - describe '.add_users_to_group' do + describe '.add_users' do it 'adds the given users to the given group' do group = create(:group) users = create_list(:user, 2) - described_class.add_users_to_group( + described_class.add_users( group, [users.first.id, users.second], described_class::MASTER diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 90b3a2ba42d..415d3e7b200 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -820,15 +820,17 @@ describe MergeRequest, models: true do user1 = create(:user) user2 = create(:user) mr = create(:merge_request, assignee: user1) + mr.project.add_developer(user1) + mr.project.add_developer(user2) - expect(user1.assigned_open_merge_request_count).to eq(1) - expect(user2.assigned_open_merge_request_count).to eq(0) + expect(user1.assigned_open_merge_requests_count).to eq(1) + expect(user2.assigned_open_merge_requests_count).to eq(0) mr.assignee = user2 mr.save - expect(user1.assigned_open_merge_request_count).to eq(0) - expect(user2.assigned_open_merge_request_count).to eq(1) + expect(user1.assigned_open_merge_requests_count).to eq(0) + expect(user2.assigned_open_merge_requests_count).to eq(1) end end diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb index c98e7ee14fd..592c90cda36 100644 --- a/spec/models/project_services/chat_notification_service_spec.rb +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -1,11 +1,29 @@ require 'spec_helper' describe ChatNotificationService, models: true do - describe "Associations" do + describe 'Associations' do before do allow(subject).to receive(:activated?).and_return(true) end it { is_expected.to validate_presence_of :webhook } end + + describe '#can_test?' do + context 'with empty repository' do + it 'returns false' do + subject.project = create(:empty_project, :empty_repo) + + expect(subject.can_test?).to be false + end + end + + context 'with repository' do + it 'returns true' do + subject.project = create(:project) + + expect(subject.can_test?).to be true + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 5e5c2b016b6..74d5ebc6db0 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -171,6 +171,27 @@ describe Repository, models: true do end end + describe '#commits' do + it 'sets follow when path is a single path' do + expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice + + repository.commits('master', path: 'README.md') + repository.commits('master', path: ['README.md']) + end + + it 'does not set follow when path is multiple paths' do + expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original + + repository.commits('master', path: ['README.md', 'CHANGELOG']) + end + + it 'does not set follow when there are no paths' do + expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original + + repository.commits('master') + end + end + describe '#find_commits_by_message' do it 'returns commits with messages containing a given string' do commit_ids = repository.find_commits_by_message('submodule').map(&:id) @@ -1259,7 +1280,6 @@ describe Repository, models: true do :changelog, :license, :contributing, - :version, :gitignore, :koding, :gitlab_ci, diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb index c4ec7625cb0..838fba6c92d 100644 --- a/spec/models/spam_log_spec.rb +++ b/spec/models/spam_log_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe SpamLog, models: true do + let(:admin) { create(:admin) } + describe 'associations' do it { is_expected.to belong_to(:user) } end @@ -13,13 +15,18 @@ describe SpamLog, models: true do it 'blocks the user' do spam_log = build(:spam_log) - expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true) + expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true) end it 'removes the user' do spam_log = build(:spam_log) + user = spam_log.user + + Sidekiq::Testing.inline! do + spam_log.remove_user(deleted_by: admin) + end - expect { spam_log.remove_user }.to change { User.count }.by(-1) + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9de16c41e94..0a2860f2505 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -24,9 +24,7 @@ describe User, models: true do it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) } it { is_expected.to have_many(:notes).dependent(:destroy) } - it { is_expected.to have_many(:assigned_issues).dependent(:nullify) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } - it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } @@ -1631,4 +1629,16 @@ describe User, models: true do end end end + + context '.active' do + before do + User.ghost + create(:user, name: 'user', state: 'active') + create(:user, name: 'user', state: 'blocked') + end + + it 'only counts active and non internal users' do + expect(User.active.count).to eq(1) + end + end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a10d876ffad..42dbab586cd 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -599,8 +599,7 @@ describe API::Commits, api: true do post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. - A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') + expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.') end it 'returns 400 if you are not allowed to push to the target branch' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 8012530f139..6db2faed76b 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -205,7 +205,7 @@ describe API::Files, api: true do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file). - and_return(false) + and_raise(Repository::CommitError, 'Cannot create file') post api(route("any%2Etxt"), user), valid_params @@ -299,8 +299,8 @@ describe API::Files, api: true do expect(response).to have_http_status(400) end - it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) + it "returns a 400 if fails to delete file" do + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') delete api(route(file_path), user), valid_params diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 4be67df5a00..3d6010ede73 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -147,10 +147,15 @@ describe API::Internal, api: true do end end - describe "POST /internal/allowed" do + describe "POST /internal/allowed", :redis do context "access granted" do before do project.team << [user, :developer] + Timecop.freeze + end + + after do + Timecop.return end context 'with env passed as a JSON' do @@ -176,6 +181,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) + expect(user).not_to have_an_activity_record end end @@ -186,6 +192,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) + expect(user).to have_an_activity_record end end @@ -196,6 +203,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(user).to have_an_activity_record end end @@ -206,6 +214,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) + expect(user).not_to have_an_activity_record end context 'project as /namespace/project' do @@ -241,6 +250,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_falsey + expect(user).not_to have_an_activity_record end end @@ -250,6 +260,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_falsey + expect(user).not_to have_an_activity_record end end end @@ -267,6 +278,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_falsey + expect(user).not_to have_an_activity_record end end @@ -276,6 +288,7 @@ describe API::Internal, api: true do expect(response).to have_http_status(200) expect(json_response["status"]).to be_falsey + expect(user).not_to have_an_activity_record end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 74bc4847247..40365585a56 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -24,6 +24,7 @@ describe API::Projects, :api do namespace: user.namespace, merge_requests_enabled: false, issues_enabled: false, wiki_enabled: false, + builds_enabled: false, snippets_enabled: false) end let(:project_member3) do @@ -342,6 +343,7 @@ describe API::Projects, :api do project = attributes_for(:project, { path: 'camelCasePath', issues_enabled: false, + jobs_enabled: false, merge_requests_enabled: false, wiki_enabled: false, only_allow_merge_if_pipeline_succeeds: false, @@ -351,6 +353,8 @@ describe API::Projects, :api do post api('/projects', user), project + expect(response).to have_http_status(201) + project.each_pair do |k, v| next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) expect(json_response[k.to_s]).to eq(v) @@ -1078,7 +1082,9 @@ describe API::Projects, :api do it 'returns 400 when nothing sent' do project_param = {} + put api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(400) expect(json_response['error']).to match('at least one parameter must be provided') end @@ -1086,7 +1092,9 @@ describe API::Projects, :api do context 'when unauthenticated' do it 'returns authentication error' do project_param = { name: 'bar' } + put api("/projects/#{project.id}"), project_param + expect(response).to have_http_status(401) end end @@ -1094,8 +1102,11 @@ describe API::Projects, :api do context 'when authenticated as project owner' do it 'updates name' do project_param = { name: 'bar' } + put api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| expect(json_response[k.to_s]).to eq(v) end @@ -1103,8 +1114,11 @@ describe API::Projects, :api do it 'updates visibility_level' do project_param = { visibility: 'public' } + put api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| expect(json_response[k.to_s]).to eq(v) end @@ -1113,17 +1127,23 @@ describe API::Projects, :api do it 'updates visibility_level from public to private' do project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) project_param = { visibility: 'private' } + put api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| expect(json_response[k.to_s]).to eq(v) end + expect(json_response['visibility']).to eq('private') end it 'does not update name to existing name' do project_param = { name: project3.name } + put api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(400) expect(json_response['message']['name']).to eq(['has already been taken']) end @@ -1139,8 +1159,23 @@ describe API::Projects, :api do it 'updates path & name to existing path & name in different namespace' do project_param = { path: project4.path, name: project4.name } + put api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates jobs_enabled' do + project_param = { jobs_enabled: true } + + put api("/projects/#{project3.id}", user), project_param + + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| expect(json_response[k.to_s]).to eq(v) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f793c0db2f3..165ab389917 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -describe API::Users, api: true do +describe API::Users, api: true do include ApiHelpers - let(:user) { create(:user) } + let(:user) { create(:user) } let(:admin) { create(:admin) } - let(:key) { create(:key, user: user) } - let(:email) { create(:email, user: user) } + let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } let(:omniauth_user) { create(:omniauth_user) } let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } @@ -72,6 +72,12 @@ describe API::Users, api: true do expect(json_response).to be_an Array expect(json_response.first['username']).to eq(omniauth_user.username) end + + it "returns a 403 when non-admin user searches by external UID" do + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", user) + + expect(response).to have_http_status(403) + end end context "when admin" do @@ -100,6 +106,27 @@ describe API::Users, api: true do expect(json_response).to be_an Array expect(json_response).to all(include('external' => true)) end + + it "returns one user by external UID" do + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['username']).to eq(omniauth_user.username) + end + + it "returns 400 error if provider with no extern_uid" do + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin) + + expect(response).to have_http_status(400) + end + + it "returns 400 error if provider with no extern_uid" do + get api("/users?provider=#{omniauth_user.identities.first.provider}", admin) + + expect(response).to have_http_status(400) + end end end @@ -129,7 +156,7 @@ describe API::Users, api: true do end describe "POST /users" do - before{ admin } + before { admin } it "creates user" do expect do @@ -214,9 +241,9 @@ describe API::Users, api: true do it "does not create user with invalid email" do post api('/users', admin), - email: 'invalid email', - password: 'password', - name: 'test' + email: 'invalid email', + password: 'password', + name: 'test' expect(response).to have_http_status(400) end @@ -242,12 +269,12 @@ describe API::Users, api: true do it 'returns 400 error if user does not validate' do post api('/users', admin), - password: 'pass', - email: 'test@example.com', - username: 'test!', - name: 'test', - bio: 'g' * 256, - projects_limit: -1 + password: 'pass', + email: 'test@example.com', + username: 'test!', + name: 'test', + bio: 'g' * 256, + projects_limit: -1 expect(response).to have_http_status(400) expect(json_response['message']['password']). to eq(['is too short (minimum is 8 characters)']) @@ -267,19 +294,19 @@ describe API::Users, api: true do context 'with existing user' do before do post api('/users', admin), - email: 'test@example.com', - password: 'password', - username: 'test', - name: 'foo' + email: 'test@example.com', + password: 'password', + username: 'test', + name: 'foo' end it 'returns 409 conflict error if user with same email exists' do expect do post api('/users', admin), - name: 'foo', - email: 'test@example.com', - password: 'password', - username: 'foo' + name: 'foo', + email: 'test@example.com', + password: 'password', + username: 'foo' end.to change { User.count }.by(0) expect(response).to have_http_status(409) expect(json_response['message']).to eq('Email has already been taken') @@ -288,10 +315,10 @@ describe API::Users, api: true do it 'returns 409 conflict error if same username exists' do expect do post api('/users', admin), - name: 'foo', - email: 'foo@example.com', - password: 'password', - username: 'test' + name: 'foo', + email: 'foo@example.com', + password: 'password', + username: 'test' end.to change { User.count }.by(0) expect(response).to have_http_status(409) expect(json_response['message']).to eq('Username has already been taken') @@ -416,12 +443,12 @@ describe API::Users, api: true do it 'returns 400 error if user does not validate' do put api("/users/#{user.id}", admin), - password: 'pass', - email: 'test@example.com', - username: 'test!', - name: 'test', - bio: 'g' * 256, - projects_limit: -1 + password: 'pass', + email: 'test@example.com', + username: 'test!', + name: 'test', + bio: 'g' * 256, + projects_limit: -1 expect(response).to have_http_status(400) expect(json_response['message']['password']). to eq(['is too short (minimum is 8 characters)']) @@ -488,7 +515,7 @@ describe API::Users, api: true do key_attrs = attributes_for :key expect do post api("/users/#{user.id}/keys", admin), key_attrs - end.to change{ user.keys.count }.by(1) + end.to change { user.keys.count }.by(1) end it "returns 400 for invalid ID" do @@ -580,7 +607,7 @@ describe API::Users, api: true do email_attrs = attributes_for :email expect do post api("/users/#{user.id}/emails", admin), email_attrs - end.to change{ user.emails.count }.by(1) + end.to change { user.emails.count }.by(1) end it "returns a 400 for invalid ID" do @@ -842,7 +869,7 @@ describe API::Users, api: true do key_attrs = attributes_for :key expect do post api("/user/keys", user), key_attrs - end.to change{ user.keys.count }.by(1) + end.to change { user.keys.count }.by(1) expect(response).to have_http_status(201) end @@ -880,7 +907,7 @@ describe API::Users, api: true do delete api("/user/keys/#{key.id}", user) expect(response).to have_http_status(204) - end.to change{user.keys.count}.by(-1) + end.to change { user.keys.count}.by(-1) end it "returns 404 if key ID not found" do @@ -963,7 +990,7 @@ describe API::Users, api: true do email_attrs = attributes_for :email expect do post api("/user/emails", user), email_attrs - end.to change{ user.emails.count }.by(1) + end.to change { user.emails.count }.by(1) expect(response).to have_http_status(201) end @@ -989,7 +1016,7 @@ describe API::Users, api: true do delete api("/user/emails/#{email.id}", user) expect(response).to have_http_status(204) - end.to change{user.emails.count}.by(-1) + end.to change { user.emails.count}.by(-1) end it "returns 404 if email ID not found" do @@ -1158,6 +1185,49 @@ describe API::Users, api: true do end end + context "user activities", :redis do + let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) } + let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) } + + context 'last activity as normal user' do + it 'has no permission' do + get api("/user/activities", user) + + expect(response).to have_http_status(403) + end + end + + context 'as admin' do + it 'returns the activities from the last 6 months' do + get api("/user/activities", admin) + + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + + activity = json_response.last + + expect(activity['username']).to eq(newly_active_user.username) + expect(activity['last_activity_on']).to eq(2.days.ago.to_date.to_s) + expect(activity['last_activity_at']).to eq(2.days.ago.to_date.to_s) + end + + context 'passing a :from parameter' do + it 'returns the activities from the given date' do + get api("/user/activities?from=2000-1-1", admin) + + expect(response).to include_pagination_headers + expect(json_response.size).to eq(2) + + activity = json_response.first + + expect(activity['username']).to eq(old_active_user.username) + expect(activity['last_activity_on']).to eq(Time.utc(2000, 1, 1).to_date.to_s) + expect(activity['last_activity_at']).to eq(Time.utc(2000, 1, 1).to_date.to_s) + end + end + end + end + describe 'GET /users/:user_id/impersonation_tokens' do let!(:active_personal_access_token) { create(:personal_access_token, user: user) } let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index adba3a787aa..0a28cb9bddb 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -485,8 +485,7 @@ describe API::V3::Commits, api: true do post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. - A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') + expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.') end it 'returns 400 if you are not allowed to push to the target branch' do diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 349fd6b3415..c45e2028e1d 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -129,7 +129,7 @@ describe API::V3::Files, api: true do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file). - and_return(false) + and_raise(Repository::CommitError, 'Cannot create file') post v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -229,8 +229,8 @@ describe API::V3::Files, api: true do expect(response).to have_http_status(400) end - it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) + it "returns a 400 if fails to delete file" do + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') delete v3_api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 006d6a6af1c..316742ff076 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe 'Git HTTP requests', lib: true do include GitHttpHelpers include WorkhorseHelpers + include UserActivitiesHelpers it "gives WWW-Authenticate hints" do clone_get('doesnt/exist.git') @@ -255,6 +256,14 @@ describe 'Git HTTP requests', lib: true do expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end + + it 'updates the user last activity', :redis do + expect(user_activity(user)).to be_nil + + download(path, env) do |response| + expect(user_activity(user)).to be_present + end + end end context "when an oauth token is provided" do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 4baccacd448..a3de022d242 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -484,7 +484,7 @@ describe 'project routing' do end it 'to #list' do - expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.json') end end diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb index 3cc791bca50..7f1abecfafe 100644 --- a/spec/serializers/build_serializer_spec.rb +++ b/spec/serializers/build_serializer_spec.rb @@ -38,7 +38,7 @@ describe BuildSerializer do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq(status.label) expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to eq(status.favicon) + expect(subject[:favicon]).to eq("/assets/ci_favicons/#{status.favicon}.ico") end end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index f6249ab4664..ecde45a6d44 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -144,7 +144,7 @@ describe PipelineSerializer do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq(status.label) expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to eq(status.favicon) + expect(subject[:favicon]).to eq("/assets/ci_favicons/#{status.favicon}.ico") end end end diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb new file mode 100644 index 00000000000..1e99442fdcb --- /dev/null +++ b/spec/services/cohorts_service_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe CohortsService do + describe '#execute' do + def month_start(months_ago) + months_ago.months.ago.beginning_of_month.to_date + end + + # In the interests of speed and clarity, this example has minimal data. + it 'returns a list of user cohorts' do + 6.times do |months_ago| + months_ago_time = (months_ago * 2).months.ago + + create(:user, created_at: months_ago_time, last_activity_on: Time.now) + create(:user, created_at: months_ago_time, last_activity_on: months_ago_time) + end + + create(:user) # this user is inactive and belongs to the current month + + expected_cohorts = [ + { + registration_month: month_start(11), + activity_months: Array.new(12) { { total: 0, percentage: 0 } }, + total: 0, + inactive: 0 + }, + { + registration_month: month_start(10), + activity_months: [{ total: 2, percentage: 100 }] + Array.new(10) { { total: 1, percentage: 50 } }, + total: 2, + inactive: 0 + }, + { + registration_month: month_start(9), + activity_months: Array.new(10) { { total: 0, percentage: 0 } }, + total: 0, + inactive: 0 + }, + { + registration_month: month_start(8), + activity_months: [{ total: 2, percentage: 100 }] + Array.new(8) { { total: 1, percentage: 50 } }, + total: 2, + inactive: 0 + }, + { + registration_month: month_start(7), + activity_months: Array.new(8) { { total: 0, percentage: 0 } }, + total: 0, + inactive: 0 + }, + { + registration_month: month_start(6), + activity_months: [{ total: 2, percentage: 100 }] + Array.new(6) { { total: 1, percentage: 50 } }, + total: 2, + inactive: 0 + }, + { + registration_month: month_start(5), + activity_months: Array.new(6) { { total: 0, percentage: 0 } }, + total: 0, + inactive: 0 + }, + { + registration_month: month_start(4), + activity_months: [{ total: 2, percentage: 100 }] + Array.new(4) { { total: 1, percentage: 50 } }, + total: 2, + inactive: 0 + }, + { + registration_month: month_start(3), + activity_months: Array.new(4) { { total: 0, percentage: 0 } }, + total: 0, + inactive: 0 + }, + { + registration_month: month_start(2), + activity_months: [{ total: 2, percentage: 100 }] + Array.new(2) { { total: 1, percentage: 50 } }, + total: 2, + inactive: 0 + }, + { + registration_month: month_start(1), + activity_months: Array.new(2) { { total: 0, percentage: 0 } }, + total: 0, + inactive: 0 + }, + { + registration_month: month_start(0), + activity_months: [{ total: 2, percentage: 100 }], + total: 2, + inactive: 1 + }, + ] + + expect(described_class.new.execute).to eq(months_included: 12, + cohorts: expected_cohorts) + end + end +end diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index a41a421fa6e..7b921f606f8 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -42,6 +42,19 @@ describe DeleteMergedBranchesService, services: true do expect { described_class.new(project, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError) end end + + context 'open merge requests' do + it 'does not delete branches from open merge requests' do + fork_link = create(:forked_project_link, forked_from_project: project) + create(:merge_request, :reopened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') + create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') + + service.execute + + expect(project.repository.branch_names).to include('branch-merged') + expect(project.repository.branch_names).to include('improve/awesome') + end + end end context '#async_execute' do diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index f2c2009bcbf..b06cefe071d 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe EventCreateService, services: true do + include UserActivitiesHelpers + let(:service) { EventCreateService.new } describe 'Issues' do @@ -111,6 +113,19 @@ describe EventCreateService, services: true do end end + describe '#push', :redis do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + it 'creates a new event' do + expect { service.push(project, user, {}) }.to change { Event.count } + end + + it 'updates user last activity' do + expect { service.push(project, user, {}) }.to change { user_activity(user) } + end + end + describe 'Project' do let(:user) { create :user } let(:project) { create(:empty_project) } diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 26aa5b432d4..16bca66766a 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -7,7 +7,7 @@ describe Files::UpdateService do let(:user) { create(:user) } let(:file_path) { 'files/ruby/popen.rb' } let(:new_contents) { 'New Content' } - let(:target_branch) { project.default_branch } + let(:branch_name) { project.default_branch } let(:last_commit_sha) { nil } let(:commit_params) do @@ -19,7 +19,7 @@ describe Files::UpdateService do last_commit_sha: last_commit_sha, start_project: project, start_branch: project.default_branch, - target_branch: target_branch + branch_name: branch_name } end @@ -73,7 +73,7 @@ describe Files::UpdateService do end context 'when target branch is different than source branch' do - let(:target_branch) { "#{project.default_branch}-new" } + let(:branch_name) { "#{project.default_branch}-new" } it 'fires hooks only once' do expect(GitHooksService).to receive(:new).once.and_call_original diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 2ee11fc8b4c..a37257d1bf4 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -7,6 +7,7 @@ describe Groups::DestroyService, services: true do let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } let!(:project) { create(:empty_project, namespace: group) } + let!(:notification_setting) { create(:notification_setting, source: group)} let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } @@ -23,6 +24,7 @@ describe Groups::DestroyService, services: true do it { expect(Group.unscoped.all).not_to include(group) } it { expect(Group.unscoped.all).not_to include(nested_group) } it { expect(Project.unscoped.all).not_to include(project) } + it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) } end context 'file system' do diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb new file mode 100644 index 00000000000..3b35a3b8e3a --- /dev/null +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Members::AuthorizedDestroyService, services: true do + let(:member_user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let(:group) { create(:group, :public) } + let(:group_project) { create(:empty_project, :public, group: group) } + + def number_of_assigned_issuables(user) + Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count + end + + context 'Group member' do + it "unassigns issues and merge requests" do + group.add_developer(member_user) + + issue = create :issue, project: group_project, assignee: member_user + create :issue, assignee: member_user + merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user + create :merge_request, target_project: project, source_project: project, assignee: member_user + + member = group.members.find_by(user_id: member_user.id) + + expect { described_class.new(member, member_user).execute } + .to change { number_of_assigned_issuables(member_user) }.from(4).to(2) + + expect(issue.reload.assignee_id).to be_nil + expect(merge_request.reload.assignee_id).to be_nil + end + end + + context 'Project member' do + it "unassigns issues and merge requests" do + project.team << [member_user, :developer] + + create :issue, project: project, assignee: member_user + create :merge_request, target_project: project, source_project: project, assignee: member_user + + member = project.members.find_by(user_id: member_user.id) + + expect { described_class.new(member, member_user).execute } + .to change { number_of_assigned_issuables(member_user) }.from(2).to(0) + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 62f21049b0b..7a07ea618c0 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -144,6 +144,20 @@ describe Projects::CreateService, '#execute', services: true do end end + context 'when a bad service template is created' do + before do + create(:service, type: 'DroneCiService', project: nil, template: true, active: true) + end + + it 'reports an error in the imported project' do + opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce' + project = create_project(user, opts) + + expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/ + expect(project.services.count).to eq 0 + end + end + def create_project(user, opts) Projects::CreateService.new(user, opts).execute end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 09cfa36b3b9..852a4ac852f 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -54,6 +54,15 @@ describe Projects::ImportService, services: true do expect(result[:status]).to eq :error expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" end + + it 'does not remove the GitHub remote' do + expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) + + subject.execute + + expect(project.repository.raw_repository.remote_names).to include('github') + end end context 'with a non Github repository' do diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb index 2531607acad..cbf4f56213d 100644 --- a/spec/services/search/global_service_spec.rb +++ b/spec/services/search/global_service_spec.rb @@ -40,27 +40,6 @@ describe Search::GlobalService, services: true do expect(results.objects('projects')).to match_array [found_project] end - - context 'nested group' do - let!(:nested_group) { create(:group, :nested) } - let!(:project) { create(:empty_project, namespace: nested_group) } - - before do - project.add_master(user) - end - - it 'returns result from nested group' do - results = Search::GlobalService.new(user, search: project.path).execute - - expect(results.objects('projects')).to match_array [project] - end - - it 'returns result from descendants when search inside group' do - results = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent).execute - - expect(results.objects('projects')).to match_array [project] - end - end end end end diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb new file mode 100644 index 00000000000..38f264f6e7b --- /dev/null +++ b/spec/services/search/group_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Search::GroupService, services: true do + shared_examples_for 'group search' do + context 'finding projects by name' do + let(:user) { create(:user) } + let(:term) { "Project Name" } + let(:nested_group) { create(:group, :nested) } + + # These projects shouldn't be found + let!(:outside_project) { create(:empty_project, :public, name: "Outside #{term}") } + let!(:private_project) { create(:empty_project, :private, namespace: nested_group, name: "Private #{term}" )} + let!(:other_project) { create(:empty_project, :public, namespace: nested_group, name: term.reverse) } + + # These projects should be found + let!(:project1) { create(:empty_project, :internal, namespace: nested_group, name: "Inner #{term} 1") } + let!(:project2) { create(:empty_project, :internal, namespace: nested_group, name: "Inner #{term} 2") } + let!(:project3) { create(:empty_project, :internal, namespace: nested_group.parent, name: "Outer #{term}") } + + let(:results) { Search::GroupService.new(user, search_group, search: term).execute } + subject { results.objects('projects') } + + context 'in parent group' do + let(:search_group) { nested_group.parent } + + it { is_expected.to match_array([project1, project2, project3]) } + end + + context 'in subgroup' do + let(:search_group) { nested_group } + + it { is_expected.to match_array([project1, project2]) } + end + end + end + + describe 'basic search' do + include_examples 'group search' + end +end diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb new file mode 100644 index 00000000000..8d67ebe3231 --- /dev/null +++ b/spec/services/users/activity_service_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Users::ActivityService, services: true do + include UserActivitiesHelpers + + let(:user) { create(:user) } + + subject(:service) { described_class.new(user, 'type') } + + describe '#execute', :redis do + context 'when last activity is nil' do + before do + service.execute + end + + it 'sets the last activity timestamp for the user' do + expect(last_hour_user_ids).to eq([user.id]) + end + + it 'updates the same user' do + service.execute + + expect(last_hour_user_ids).to eq([user.id]) + end + + it 'updates the timestamp of an existing user' do + Timecop.freeze(Date.tomorrow) do + expect { service.execute }.to change { user_activity(user) }.to(Time.now.to_i.to_s) + end + end + + describe 'other user' do + it 'updates other user' do + other_user = create(:user) + described_class.new(other_user, 'type').execute + + expect(last_hour_user_ids).to match_array([user.id, other_user.id]) + end + end + end + end + + def last_hour_user_ids + Gitlab::UserActivities.new. + select { |k, v| v >= 1.hour.ago.to_i.to_s }. + map { |k, _| k.to_i } + end +end diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb new file mode 100644 index 00000000000..2a6bfc1b3a0 --- /dev/null +++ b/spec/services/users/build_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Users::BuildService, services: true do + describe '#execute' do + let(:params) do + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } + end + + context 'with an admin user' do + let(:admin_user) { create(:admin) } + let(:service) { described_class.new(admin_user, params) } + + it 'returns a valid user' do + expect(service.execute).to be_valid + end + end + + context 'with non admin user' do + let(:user) { create(:user) } + let(:service) { described_class.new(user, params) } + + it 'raises AccessDeniedError exception' do + expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'with nil user' do + let(:service) { described_class.new(nil, params) } + + it 'returns a valid user' do + expect(service.execute).to be_valid + end + + context 'when "send_user_confirmation_email" application setting is true' do + before do + stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true) + end + + it 'does not confirm the user' do + expect(service.execute).not_to be_confirmed + end + end + + context 'when "send_user_confirmation_email" application setting is false' do + before do + stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true) + end + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + end + end +end diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb index a111aec2f89..75746278573 100644 --- a/spec/services/users/create_service_spec.rb +++ b/spec/services/users/create_service_spec.rb @@ -1,38 +1,6 @@ require 'spec_helper' describe Users::CreateService, services: true do - describe '#build' do - let(:params) do - { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } - end - - context 'with an admin user' do - let(:admin_user) { create(:admin) } - let(:service) { described_class.new(admin_user, params) } - - it 'returns a valid user' do - expect(service.build).to be_valid - end - end - - context 'with non admin user' do - let(:user) { create(:user) } - let(:service) { described_class.new(user, params) } - - it 'raises AccessDeniedError exception' do - expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError - end - end - - context 'with nil user' do - let(:service) { described_class.new(nil, params) } - - it 'returns a valid user' do - expect(service.build).to be_valid - end - end - end - describe '#execute' do let(:admin_user) { create(:admin) } @@ -185,40 +153,18 @@ describe Users::CreateService, services: true do end let(:service) { described_class.new(nil, params) } - context 'when "send_user_confirmation_email" application setting is true' do - before do - current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true) - allow(service).to receive(:current_application_settings).and_return(current_application_settings) - end - - it 'does not confirm the user' do - expect(service.execute).not_to be_confirmed - end - end - - context 'when "send_user_confirmation_email" application setting is false' do - before do - current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true) - allow(service).to receive(:current_application_settings).and_return(current_application_settings) - end - - it 'confirms the user' do - expect(service.execute).to be_confirmed - end - - it 'persists the given attributes' do - user = service.execute - user.reload - - expect(user).to have_attributes( - name: params[:name], - username: params[:username], - email: params[:email], - password: params[:password], - created_by_id: nil, - admin: false - ) - end + it 'persists the given attributes' do + user = service.execute + user.reload + + expect(user).to have_attributes( + name: params[:name], + username: params[:username], + email: params[:email], + password: params[:password], + created_by_id: nil, + admin: false + ) end end end diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 43c18992d1a..4bc30018ebd 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -152,6 +152,12 @@ describe Users::DestroyService, services: true do service.execute(user) end + + it 'does not run `MigrateToGhostUser` if hard_delete option is given' do + expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute) + + service.execute(user, hard_delete: true) + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a3665795452..e67ad8f3455 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,8 +9,14 @@ require 'rspec/rails' require 'shoulda/matchers' require 'rspec/retry' -if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) && - (!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master') +rspec_profiling_is_configured = + ENV['RSPEC_PROFILING_POSTGRES_URL'] || + ENV['RSPEC_PROFILING'] +branch_can_be_profiled = + ENV['CI_COMMIT_REF_NAME'] == 'master' || + ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/ + +if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled) require 'rspec_profiling/rspec' end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 1a061ef069e..bb4542b1683 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -73,9 +73,15 @@ shared_examples 'discussion comments' do |resource_name| expect(page).not_to have_selector menu_selector end - it 'clicking the ul padding should not change the text' do + it 'clicking the ul padding or divider should not change the text' do find(menu_selector).trigger 'click' + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' + + find("#{menu_selector} .divider").trigger 'click' + + expect(page).to have_selector menu_selector expect(find(dropdown_selector)).to have_content 'Comment' end diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb index a05c9d18002..5515c355cea 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/fixture_helpers.rb @@ -1,8 +1,11 @@ module FixtureHelpers def fixture_file(filename) return '' if filename.blank? - file_path = File.expand_path(Rails.root.join('spec/fixtures/', filename)) - File.read(file_path) + File.read(expand_fixture_path(filename)) + end + + def expand_fixture_path(filename) + File.expand_path(Rails.root.join('spec/fixtures/', filename)) end end diff --git a/spec/support/matchers/user_activity_matchers.rb b/spec/support/matchers/user_activity_matchers.rb new file mode 100644 index 00000000000..ce3b683b6d2 --- /dev/null +++ b/spec/support/matchers/user_activity_matchers.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :have_an_activity_record do |expected| + match do |user| + expect(Gitlab::UserActivities.new.find { |k, _| k == user.id.to_s }).to be_present + end +end diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb index 20d5849bcab..431f20a2a5c 100644 --- a/spec/support/mobile_helpers.rb +++ b/spec/support/mobile_helpers.rb @@ -1,4 +1,8 @@ module MobileHelpers + def resize_screen_xs + resize_window(767, 768) + end + def resize_screen_sm resize_window(900, 768) end diff --git a/spec/support/user_activities_helpers.rb b/spec/support/user_activities_helpers.rb new file mode 100644 index 00000000000..f7ca9a31edd --- /dev/null +++ b/spec/support/user_activities_helpers.rb @@ -0,0 +1,7 @@ +module UserActivitiesHelpers + def user_activity(user) + Gitlab::UserActivities.new. + find { |k, _| k == user.id.to_s }&. + second + end +end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index b369dcbb305..aaf998a546f 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -8,7 +8,7 @@ describe 'gitlab:gitaly namespace rake task' do describe 'install' do let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s } - let(:tag) { "v#{File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp}" } + let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp } context 'no dir given' do it 'aborts and display a help message' do @@ -21,7 +21,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).and_raise 'Git error' + to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' end @@ -32,9 +32,9 @@ describe 'gitlab:gitaly namespace rake task' do expect(Dir).to receive(:chdir).with(clone_path) end - it 'calls checkout_or_clone_tag with the right arguments' do + it 'calls checkout_or_clone_version with the right arguments' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path) + to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -48,7 +48,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) end @@ -62,7 +62,7 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is not available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) end diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index 86e42d845ce..3d9ba7cdc6f 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -10,19 +10,38 @@ describe Gitlab::TaskHelpers do let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' } let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s } + let(:version) { '1.1.0' } let(:tag) { 'v1.1.0' } - describe '#checkout_or_clone_tag' do + describe '#checkout_or_clone_version' do before do allow(subject).to receive(:run_command!) - expect(subject).to receive(:reset_to_tag).with(tag, clone_path) end - context 'target_dir does not exist' do - it 'clones the repo, retrieve the tag from origin, and checkout the tag' do + it 'checkout the version and reset to it' do + expect(subject).to receive(:checkout_version).with(tag, clone_path) + expect(subject).to receive(:reset_to_version).with(tag, clone_path) + + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) + end + + context 'with a branch version' do + let(:version) { '=branch_name' } + let(:branch) { 'branch_name' } + + it 'checkout the version and reset to it with a branch name' do + expect(subject).to receive(:checkout_version).with(branch, clone_path) + expect(subject).to receive(:reset_to_version).with(branch, clone_path) + + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) + end + end + + context "target_dir doesn't exist" do + it 'clones the repo' do expect(subject).to receive(:clone_repo).with(repo, clone_path) - subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) end end @@ -31,10 +50,10 @@ describe Gitlab::TaskHelpers do expect(Dir).to receive(:exist?).and_return(true) end - it 'fetch and checkout the tag' do - expect(subject).to receive(:checkout_tag).with(tag, clone_path) + it "doesn't clone the repository" do + expect(subject).not_to receive(:clone_repo) - subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path) + subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path) end end end @@ -48,49 +67,23 @@ describe Gitlab::TaskHelpers do end end - describe '#checkout_tag' do + describe '#checkout_version' do it 'clones the repo in the target dir' do expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet]) + to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet]) expect(subject). to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}]) - subject.checkout_tag(tag, clone_path) + subject.checkout_version(tag, clone_path) end end - describe '#reset_to_tag' do - let(:tag) { 'v1.1.0' } - before do + describe '#reset_to_version' do + it 'resets --hard to the given version' do expect(subject). to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}]) - end - context 'when the tag is not checked out locally' do - before do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError) - end - - it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin]) - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag) - - subject.reset_to_tag(tag, clone_path) - end - end - - context 'when the tag is checked out locally' do - before do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag) - end - - it 'resets --hard to the given tag' do - subject.reset_to_tag(tag, clone_path) - end + subject.reset_to_version(tag, clone_path) end end end diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 8a66a4aa047..63d1cf2bbe5 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -8,7 +8,7 @@ describe 'gitlab:workhorse namespace rake task' do describe 'install' do let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s } - let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" } + let(:version) { File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp } context 'no dir given' do it 'aborts and display a help message' do @@ -21,7 +21,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).and_raise 'Git error' + to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error' end @@ -32,9 +32,9 @@ describe 'gitlab:workhorse namespace rake task' do expect(Dir).to receive(:chdir).with(clone_path) end - it 'calls checkout_or_clone_tag with the right arguments' do + it 'calls checkout_or_clone_version with the right arguments' do expect_any_instance_of(Object). - to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path) + to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:workhorse:install', clone_path) end @@ -48,7 +48,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) end @@ -62,7 +62,7 @@ describe 'gitlab:workhorse namespace rake task' do context 'gmake is not available' do before do - expect_any_instance_of(Object).to receive(:checkout_or_clone_tag) + expect_any_instance_of(Object).to receive(:checkout_or_clone_version) allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) end diff --git a/spec/views/layouts/nav/_project.html.haml_spec.rb b/spec/views/layouts/nav/_project.html.haml_spec.rb new file mode 100644 index 00000000000..fd1637ca91b --- /dev/null +++ b/spec/views/layouts/nav/_project.html.haml_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'layouts/nav/_project' do + describe 'container registry tab' do + before do + stub_container_registry_config(enabled: true) + + assign(:project, create(:project)) + allow(view).to receive(:current_ref).and_return('master') + + allow(view).to receive(:can?).and_return(true) + allow(controller).to receive(:controller_name) + .and_return('repositories') + allow(controller).to receive(:controller_path) + .and_return('projects/registry/repositories') + end + + it 'has both Registry and Repository tabs' do + render + + expect(rendered).to have_text 'Repository' + expect(rendered).to have_text 'Registry' + end + + it 'highlights only one tab' do + render + + expect(rendered).to have_css('.active', count: 1) + end + + it 'highlights container registry tab only' do + render + + expect(rendered).to have_css('.active', text: 'Registry') + end + end +end diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb new file mode 100644 index 00000000000..b6c080f36f4 --- /dev/null +++ b/spec/workers/gitlab_usage_ping_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe GitlabUsagePingWorker do + subject { GitlabUsagePingWorker.new } + + it "sends POST request" do + stub_application_setting(usage_ping_enabled: true) + + stub_request(:post, "https://version.gitlab.com/usage_data"). + to_return(status: 200, body: '', headers: {}) + expect(Gitlab::UsageData).to receive(:to_json).with({ force_refresh: true }).and_call_original + expect(subject).to receive(:try_obtain_lease).and_return(true) + + expect(subject.perform.response.code.to_i).to eq(200) + end + + it "does not run if usage ping is disabled" do + stub_application_setting(usage_ping_enabled: false) + + expect(subject).not_to receive(:try_obtain_lease) + expect(subject).not_to receive(:perform) + end +end diff --git a/spec/workers/schedule_update_user_activity_worker_spec.rb b/spec/workers/schedule_update_user_activity_worker_spec.rb new file mode 100644 index 00000000000..e583c3203aa --- /dev/null +++ b/spec/workers/schedule_update_user_activity_worker_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ScheduleUpdateUserActivityWorker, :redis do + let(:now) { Time.now } + + before do + Gitlab::UserActivities.record('1', now) + Gitlab::UserActivities.record('2', now) + end + + it 'schedules UpdateUserActivityWorker once' do + expect(UpdateUserActivityWorker).to receive(:perform_async).with({ '1' => now.to_i.to_s, '2' => now.to_i.to_s }) + + subject.perform + end + + context 'when specifying a batch size' do + it 'schedules UpdateUserActivityWorker twice' do + expect(UpdateUserActivityWorker).to receive(:perform_async).with({ '1' => now.to_i.to_s }) + expect(UpdateUserActivityWorker).to receive(:perform_async).with({ '2' => now.to_i.to_s }) + + subject.perform(1) + end + end +end diff --git a/spec/workers/update_user_activity_worker_spec.rb b/spec/workers/update_user_activity_worker_spec.rb new file mode 100644 index 00000000000..43e9511f116 --- /dev/null +++ b/spec/workers/update_user_activity_worker_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe UpdateUserActivityWorker, :redis do + let(:user_active_2_days_ago) { create(:user, current_sign_in_at: 10.months.ago) } + let(:user_active_yesterday_1) { create(:user) } + let(:user_active_yesterday_2) { create(:user) } + let(:user_active_today) { create(:user) } + let(:data) do + { + user_active_2_days_ago.id.to_s => 2.days.ago.at_midday.to_i.to_s, + user_active_yesterday_1.id.to_s => 1.day.ago.at_midday.to_i.to_s, + user_active_yesterday_2.id.to_s => 1.day.ago.at_midday.to_i.to_s, + user_active_today.id.to_s => Time.now.to_i.to_s + } + end + + it 'updates users.last_activity_on' do + subject.perform(data) + + aggregate_failures do + expect(user_active_2_days_ago.reload.last_activity_on).to eq(2.days.ago.to_date) + expect(user_active_yesterday_1.reload.last_activity_on).to eq(1.day.ago.to_date) + expect(user_active_yesterday_2.reload.last_activity_on).to eq(1.day.ago.to_date) + expect(user_active_today.reload.reload.last_activity_on).to eq(Date.today) + end + end + + it 'deletes the pairs from Redis' do + data.each { |id, time| Gitlab::UserActivities.record(id, time) } + + subject.perform(data) + + expect(Gitlab::UserActivities.new.to_a).to be_empty + end +end |