summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb37
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb24
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb4
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb2
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb4
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb10
-rw-r--r--spec/controllers/sessions_controller_spec.rb10
-rw-r--r--spec/factories/merge_requests.rb4
-rw-r--r--spec/factories/notes.rb3
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb48
-rw-r--r--spec/features/discussion_comments/commit_spec.rb18
-rw-r--r--spec/features/discussion_comments/issue_spec.rb16
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb16
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb16
-rw-r--r--spec/features/groups/members/list_spec.rb54
-rw-r--r--spec/features/groups/milestone_spec.rb36
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb14
-rw-r--r--spec/features/issues/note_polling_spec.rb14
-rw-r--r--spec/features/issues_spec.rb7
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb8
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_spec.rb238
-rw-r--r--spec/features/merge_requests/discussion_spec.rb51
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb13
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb294
-rw-r--r--spec/features/merge_requests/user_posts_notes.rb145
-rw-r--r--spec/features/merge_requests/user_sees_system_notes_spec.rb31
-rw-r--r--spec/features/merge_requests/versions_spec.rb (renamed from spec/features/merge_requests/merge_request_versions_spec.rb)28
-rw-r--r--spec/features/merge_requests/widget_spec.rb21
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb285
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb4
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb6
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb10
-rw-r--r--spec/features/projects/members/list_spec.rb90
-rw-r--r--spec/features/projects/view_on_env_spec.rb4
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb20
-rw-r--r--spec/fixtures/trace/ansi-sequence-and-unicode5
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb23
-rw-r--r--spec/helpers/submodule_helper_spec.rb12
-rw-r--r--spec/javascripts/awards_handler_spec.js53
-rw-r--r--spec/javascripts/blob/blob_fork_suggestion_spec.js37
-rw-r--r--spec/javascripts/blob/sketch/index_spec.js2
-rw-r--r--spec/javascripts/boards/list_spec.js40
-rw-r--r--spec/javascripts/build_spec.js137
-rw-r--r--spec/javascripts/ci_status_icon_spec.js44
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js7
-rw-r--r--spec/javascripts/diff_comments_store_spec.js194
-rw-r--r--spec/javascripts/droplab/constants_spec.js6
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js28
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js2
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js2
-rw-r--r--spec/javascripts/environments/environment_item_spec.js2
-rw-r--r--spec/javascripts/environments/environment_monitoring_spec.js2
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js2
-rw-r--r--spec/javascripts/environments/environment_spec.js5
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js2
-rw-r--r--spec/javascripts/environments/environment_table_spec.js2
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js2
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js5
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js94
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js446
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js144
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js394
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js208
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js256
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_spec.js2
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb7
-rw-r--r--spec/javascripts/issue_show/issue_title_spec.js2
-rw-r--r--spec/javascripts/issue_spec.js194
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js15
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js9
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js93
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js148
-rw-r--r--spec/javascripts/merged_buttons_spec.js44
-rw-r--r--spec/javascripts/notes_spec.js152
-rw-r--r--spec/javascripts/pipelines/async_button_spec.js (renamed from spec/javascripts/vue_pipelines_index/async_button_spec.js)2
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js (renamed from spec/javascripts/vue_pipelines_index/empty_state_spec.js)2
-rw-r--r--spec/javascripts/pipelines/error_state_spec.js (renamed from spec/javascripts/vue_pipelines_index/error_state_spec.js)2
-rw-r--r--spec/javascripts/pipelines/mock_data.js (renamed from spec/javascripts/vue_pipelines_index/mock_data.js)0
-rw-r--r--spec/javascripts/pipelines/nav_controls_spec.js (renamed from spec/javascripts/vue_pipelines_index/nav_controls_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipeline_url_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipelines_actions_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipelines_artifacts_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js)2
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_spec.js)4
-rw-r--r--spec/javascripts/pipelines/pipelines_store_spec.js (renamed from spec/javascripts/vue_pipelines_index/pipelines_store_spec.js)2
-rw-r--r--spec/javascripts/pipelines/stage_spec.js66
-rw-r--r--spec/javascripts/shortcuts_spec.js45
-rw-r--r--spec/javascripts/user_callout_spec.js1
-rw-r--r--spec/lib/banzai/filter/issuable_state_filter_spec.rb157
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb8
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb4
-rw-r--r--spec/lib/banzai/redactor_spec.rb25
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb23
-rw-r--r--spec/lib/banzai/renderer_spec.rb69
-rw-r--r--spec/lib/container_registry/path_spec.rb54
-rw-r--r--spec/lib/container_registry/tag_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb71
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb20
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb432
-rw-r--r--spec/lib/gitlab/database/multi_threaded_migration_spec.rb41
-rw-r--r--spec/lib/gitlab/database_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb8
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb83
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb4
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/git/env_spec.rb102
-rw-r--r--spec/lib/gitlab/git/index_spec.rb20
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb107
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb92
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/regex_spec.rb6
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb70
-rw-r--r--spec/lib/gitlab/user_activities_spec.rb127
-rw-r--r--spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb49
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb17
-rw-r--r--spec/migrations/schema_spec.rb23
-rw-r--r--spec/models/abuse_report_spec.rb3
-rw-r--r--spec/models/blob_spec.rb10
-rw-r--r--spec/models/commit_spec.rb55
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb270
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb38
-rw-r--r--spec/models/container_repository_spec.rb67
-rw-r--r--spec/models/diff_note_spec.rb17
-rw-r--r--spec/models/environment_spec.rb23
-rw-r--r--spec/models/issue_spec.rb14
-rw-r--r--spec/models/label_spec.rb16
-rw-r--r--spec/models/member_spec.rb25
-rw-r--r--spec/models/members/group_member_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb10
-rw-r--r--spec/models/note_spec.rb12
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb20
-rw-r--r--spec/models/repository_spec.rb49
-rw-r--r--spec/models/spam_log_spec.rb11
-rw-r--r--spec/models/user_spec.rb14
-rw-r--r--spec/policies/group_policy_spec.rb3
-rw-r--r--spec/requests/api/commits_spec.rb3
-rw-r--r--spec/requests/api/files_spec.rb6
-rw-r--r--spec/requests/api/internal_spec.rb36
-rw-r--r--spec/requests/api/project_hooks_spec.rb13
-rw-r--r--spec/requests/api/projects_spec.rb42
-rw-r--r--spec/requests/api/users_spec.rb146
-rw-r--r--spec/requests/api/v3/commits_spec.rb3
-rw-r--r--spec/requests/api/v3/files_spec.rb6
-rw-r--r--spec/requests/git_http_spec.rb9
-rw-r--r--spec/routing/project_routing_spec.rb2
-rw-r--r--spec/serializers/build_serializer_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/services/cohorts_service_spec.rb99
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb13
-rw-r--r--spec/services/event_create_service_spec.rb15
-rw-r--r--spec/services/files/update_service_spec.rb6
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb45
-rw-r--r--spec/services/notification_service_spec.rb16
-rw-r--r--spec/services/projects/create_service_spec.rb14
-rw-r--r--spec/services/projects/import_service_spec.rb9
-rw-r--r--spec/services/search/global_service_spec.rb21
-rw-r--r--spec/services/search/group_service_spec.rb40
-rw-r--r--spec/services/users/activity_service_spec.rb48
-rw-r--r--spec/services/users/build_service_spec.rb55
-rw-r--r--spec/services/users/create_service_spec.rb78
-rw-r--r--spec/services/users/destroy_service_spec.rb6
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb18
-rw-r--r--spec/spec_helper.rb14
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb (renamed from spec/features/discussion_comments_spec.rb)114
-rw-r--r--spec/support/fixture_helpers.rb7
-rw-r--r--spec/support/gitaly.rb7
-rw-r--r--spec/support/matchers/user_activity_matchers.rb5
-rw-r--r--spec/support/mobile_helpers.rb4
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb52
-rw-r--r--spec/support/test_env.rb30
-rw-r--r--spec/support/user_activities_helpers.rb7
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb12
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb73
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb12
-rw-r--r--spec/views/layouts/nav/_project.html.haml_spec.rb37
-rw-r--r--spec/views/projects/registry/repositories/index.html.haml_spec.rb36
-rw-r--r--spec/workers/gitlab_usage_ping_worker_spec.rb23
-rw-r--r--spec/workers/schedule_update_user_activity_worker_spec.rb25
-rw-r--r--spec/workers/update_user_activity_worker_spec.rb35
186 files changed, 5651 insertions, 2446 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/factories/notes.rb b/spec/factories/notes.rb
index 90c35e2c7f8..93f4903119c 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -40,6 +40,7 @@ FactoryGirl.define do
transient do
line_number 14
+ diff_refs { noteable.try(:diff_refs) }
end
position do
@@ -48,7 +49,7 @@ FactoryGirl.define do
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: line_number,
- diff_refs: noteable.try(:diff_refs)
+ diff_refs: diff_refs
)
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/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index fa5524e18d8..4c9adcabe34 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -1,27 +1,49 @@
require 'spec_helper'
feature 'Dashboard shortcuts', feature: true, js: true do
- before do
- login_as :user
- visit root_dashboard_path
- end
+ context 'logged in' do
+ before do
+ login_as :user
+ visit root_dashboard_path
+ end
+
+ scenario 'Navigate to tabs' do
+ find('body').native.send_keys([:shift, 'P'])
+
+ check_page_title('Projects')
+
+ find('body').native.send_key([:shift, 'I'])
+
+ check_page_title('Issues')
- scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
+ find('body').native.send_key([:shift, 'M'])
+
+ check_page_title('Merge Requests')
+
+ find('body').native.send_keys([:shift, 'T'])
+
+ check_page_title('Todos')
+ end
+ end
- check_page_title('Projects')
+ context 'logged out' do
+ before do
+ visit explore_root_path
+ end
- find('body').native.send_key([:shift, 'I'])
+ scenario 'Navigate to tabs' do
+ find('body').native.send_keys([:shift, 'P'])
- check_page_title('Issues')
+ expect(page).to have_content('No projects found')
- find('body').native.send_key([:shift, 'M'])
+ find('body').native.send_keys([:shift, 'G'])
- check_page_title('Merge Requests')
+ expect(page).to have_content('No public groups')
- find('body').native.send_keys([:shift, 'T'])
+ find('body').native.send_keys([:shift, 'S'])
- check_page_title('Todos')
+ expect(page).to have_selector('.snippets-list-holder')
+ end
end
def check_page_title(title)
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
new file mode 100644
index 00000000000..96e0b78f6b9
--- /dev/null
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Merge Request', :feature, :js do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
+
+ it_behaves_like 'discussion comments', 'commit'
+end
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
new file mode 100644
index 00000000000..ccc9efccd18
--- /dev/null
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Issue', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'discussion comments', 'issue'
+end
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
new file mode 100644
index 00000000000..f99ebeb9cd9
--- /dev/null
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Merge Request', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'discussion comments', 'merge request'
+end
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
new file mode 100644
index 00000000000..19a306511b2
--- /dev/null
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Issue', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ before do
+ project.add_master(user)
+ login_as(user)
+
+ visit namespace_project_snippet_path(project.namespace, project, snippet)
+ end
+
+ it_behaves_like 'discussion comments', 'snippet'
+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/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb
new file mode 100644
index 00000000000..f59d0faa274
--- /dev/null
+++ b/spec/features/merge_requests/discussion_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Merge Request Discussions', feature: true do
+ before do
+ login_as :admin
+ end
+
+ context "Diff discussions" do
+ let(:merge_request) { create(:merge_request, importing: true) }
+ let(:project) { merge_request.source_project }
+ let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) }
+ let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create }
+
+ let!(:outdated_discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position).to_discussion }
+ let!(:active_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: outdated_diff_refs
+ )
+ end
+
+ let(:outdated_diff_refs) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs }
+
+ before(:each) do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'active discussions' do
+ it 'shows a link to the diff' do
+ within(".discussion[data-discussion-id='#{active_discussion.id}']") do
+ path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: active_discussion.line_code)
+ expect(page).to have_link('the diff', href: path)
+ end
+ end
+ end
+
+ context 'outdated discussions' do
+ it 'shows a link to the outdated diff' do
+ within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do
+ path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: old_merge_request_diff.id, anchor: outdated_discussion.line_code)
+ expect(page).to have_link('an outdated diff', href: path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index 646e7bab265..cd540ca113a 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -89,6 +89,19 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
it_behaves_like 'Merge when pipeline succeeds activator'
end
end
+
+ describe 'enabling Merge when pipeline succeeds via dropdown' do
+ it 'activates the Merge when pipeline succeeds feature' do
+ click_button 'Select merge moment'
+ within('.js-merge-dropdown') do
+ click_link 'Merge when pipeline succeeds'
+ end
+
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
+ expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_link "Cancel automatic merge"
+ end
+ end
end
context 'when merge when pipeline succeeds is enabled' do
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/merge_request_versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 04e85ed3f73..7a2da623c58 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -36,8 +36,23 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '5 changed files'
end
- it 'show the message about disabled comments' do
- expect(page).to have_content 'Comments are disabled'
+ it 'show the message about disabled comment creation' do
+ expect(page).to have_content 'comment creation is disabled'
+ end
+
+ it 'shows comments that were last relevant at that version' do
+ position = Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ outdated_diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ outdated_diff_note.position = outdated_diff_note.original_position
+ outdated_diff_note.save!
+
+ expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
end
@@ -92,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
@@ -114,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 @@
+.
+..
+😺
+ヾ(´༎ຶД༎ຶ`)ノ
+許功蓋
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index f3e79cc7290..2c0e9975f73 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -86,10 +86,10 @@ describe PreferencesHelper do
context 'when repository is not empty' do
let(:project) { create(:project, :public, :repository) }
- it 'returns readme if user has repository access' do
+ it 'returns files and readme if user has repository access' do
allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
- expect(helper.default_project_view).to eq('readme')
+ expect(helper.default_project_view).to eq('files')
end
it 'returns activity if user does not have repository access' do
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 edd4b3c1440..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`;
@@ -98,9 +98,9 @@ describe('Build', () => {
jasmine.clock().tick(4001);
- expect($.ajax.calls.count()).toBe(2);
+ expect($.ajax.calls.count()).toBe(3);
- args = $.ajax.calls.argsFor(1)[0];
+ args = $.ajax.calls.argsFor(2)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.data.state).toBe('newstate');
@@ -133,7 +133,7 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001);
- args = $.ajax.calls.argsFor(1)[0];
+ args = $.ajax.calls.argsFor(2)[0];
args.success.call($, {
html: '<span>Different</span>',
status: 'running',
@@ -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_spec.js b/spec/javascripts/environments/environment_spec.js
index 4431baa4b96..9762688af1a 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -83,9 +83,10 @@ describe('Environment', () => {
it('should render a table with environments', (done) => {
setTimeout(() => {
+ expect(component.$el.querySelectorAll('table')).toBeDefined();
expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(1);
+ component.$el.querySelector('.environment-name').textContent.trim(),
+ ).toEqual(environment.name);
done();
}, 0);
});
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/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 43a217a67f5..72f3db29a66 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -47,9 +47,10 @@ describe('Environments Folder View', () => {
it('should render a table with environments', (done) => {
setTimeout(() => {
+ expect(component.$el.querySelectorAll('table')).toBeDefined();
expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(2);
+ component.$el.querySelector('.environment-name').textContent.trim(),
+ ).toEqual(environmentsList[0].name);
done();
}, 0);
});
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/issue_spec.js b/spec/javascripts/issue_spec.js
index aabc8bea12f..9a2570ef7e9 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,18 +1,17 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
+/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue';
require('~/lib/utils/text_utility');
describe('Issue', function() {
- var INVALID_URL = 'http://goesnowhere.nothing/whereami';
- var $boxClosed, $boxOpen, $btnClose, $btnReopen;
+ let $boxClosed, $boxOpen, $btnClose, $btnReopen;
preloadFixtures('issues/closed-issue.html.raw');
preloadFixtures('issues/issue-with-task-list.html.raw');
preloadFixtures('issues/open-issue.html.raw');
function expectErrorMessage() {
- var $flashMessage = $('div.flash-alert');
+ const $flashMessage = $('div.flash-alert');
expect($flashMessage).toExist();
expect($flashMessage).toBeVisible();
expect($flashMessage).toHaveText('Unable to update this issue at this time.');
@@ -26,10 +25,28 @@ describe('Issue', function() {
expectVisibility($btnReopen, !isIssueOpen);
}
- function expectPendingRequest(req, $triggeredButton) {
- expect(req.type).toBe('PUT');
- expect(req.url).toBe($triggeredButton.attr('href'));
- expect($triggeredButton).toHaveProp('disabled', true);
+ function expectNewBranchButtonState(isPending, canCreate) {
+ if (Issue.$btnNewBranch.length === 0) {
+ return;
+ }
+
+ const $available = Issue.$btnNewBranch.find('.available');
+ expect($available).toHaveText('New branch');
+
+ if (!isPending && canCreate) {
+ expect($available).toBeVisible();
+ } else {
+ expect($available).toBeHidden();
+ }
+
+ const $unavailable = Issue.$btnNewBranch.find('.unavailable');
+ expect($unavailable).toHaveText('New branch unavailable');
+
+ if (!isPending && !canCreate) {
+ expect($unavailable).toBeVisible();
+ } else {
+ expect($unavailable).toBeHidden();
+ }
}
function expectVisibility($element, shouldBeVisible) {
@@ -81,100 +98,107 @@ describe('Issue', function() {
});
});
- describe('close issue', function() {
- beforeEach(function() {
- loadFixtures('issues/open-issue.html.raw');
- findElements();
- this.issue = new Issue();
-
- expectIssueState(true);
- });
+ [true, false].forEach((isIssueInitiallyOpen) => {
+ describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
+ const action = isIssueInitiallyOpen ? 'close' : 'reopen';
+
+ function ajaxSpy(req) {
+ if (req.url === this.$triggeredButton.attr('href')) {
+ expect(req.type).toBe('PUT');
+ expect(this.$triggeredButton).toHaveProp('disabled', true);
+ expectNewBranchButtonState(true, false);
+ return this.issueStateDeferred;
+ } else if (req.url === Issue.$btnNewBranch.data('path')) {
+ expect(req.type).toBe('get');
+ expectNewBranchButtonState(true, false);
+ return this.canCreateBranchDeferred;
+ }
+
+ expect(req.url).toBe('unexpected');
+ return null;
+ }
+
+ beforeEach(function() {
+ if (isIssueInitiallyOpen) {
+ loadFixtures('issues/open-issue.html.raw');
+ } else {
+ loadFixtures('issues/closed-issue.html.raw');
+ }
+
+ findElements();
+ this.issue = new Issue();
+ expectIssueState(isIssueInitiallyOpen);
+ this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen;
+
+ this.$projectIssuesCounter = $('.issue_counter');
+ this.$projectIssuesCounter.text('1,001');
+
+ this.issueStateDeferred = new jQuery.Deferred();
+ this.canCreateBranchDeferred = new jQuery.Deferred();
+
+ spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
+ });
- it('closes an issue', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.success({
+ it(`${action}s the issue`, function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.resolve({
id: 34
});
- });
-
- $btnClose.trigger('click');
+ this.canCreateBranchDeferred.resolve({
+ can_create_branch: !isIssueInitiallyOpen
+ });
- expectIssueState(false);
- expect($btnClose).toHaveProp('disabled', false);
- expect($('.issue_counter')).toHaveText(0);
- });
+ expectIssueState(!isIssueInitiallyOpen);
+ expect(this.$triggeredButton).toHaveProp('disabled', false);
+ expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
+ expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+ });
- it('fails to close an issue with success:false', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.success({
+ it(`fails to ${action} the issue if saved:false`, function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.resolve({
saved: false
});
- });
-
- $btnClose.attr('href', INVALID_URL);
- $btnClose.trigger('click');
-
- expectIssueState(true);
- expect($btnClose).toHaveProp('disabled', false);
- expectErrorMessage();
- expect($('.issue_counter')).toHaveText(1);
- });
+ this.canCreateBranchDeferred.resolve({
+ can_create_branch: isIssueInitiallyOpen
+ });
- it('fails to closes an issue with HTTP error', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.error();
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton).toHaveProp('disabled', false);
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
});
- $btnClose.attr('href', INVALID_URL);
- $btnClose.trigger('click');
-
- expectIssueState(true);
- expect($btnClose).toHaveProp('disabled', true);
- expectErrorMessage();
- expect($('.issue_counter')).toHaveText(1);
- });
-
- it('updates counter', () => {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnClose);
- req.success({
- id: 34
+ it(`fails to ${action} the issue if HTTP error occurs`, function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.reject();
+ this.canCreateBranchDeferred.resolve({
+ can_create_branch: isIssueInitiallyOpen
});
- });
- expect($('.issue_counter')).toHaveText(1);
- $('.issue_counter').text('1,001');
- expect($('.issue_counter').text()).toEqual('1,001');
- $btnClose.trigger('click');
- expect($('.issue_counter').text()).toEqual('1,000');
- });
- });
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton).toHaveProp('disabled', true);
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
+ });
- describe('reopen issue', function() {
- beforeEach(function() {
- loadFixtures('issues/closed-issue.html.raw');
- findElements();
- this.issue = new Issue();
+ it('disables the new branch button if Ajax call fails', function() {
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.reject();
+ this.canCreateBranchDeferred.reject();
- expectIssueState(false);
- });
-
- it('reopens an issue', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expectPendingRequest(req, $btnReopen);
- req.success({
- id: 34
- });
+ expectNewBranchButtonState(false, false);
});
- $btnReopen.trigger('click');
+ it('does not trigger Ajax call if new branch button is missing', function() {
+ Issue.$btnNewBranch = $();
+ this.canCreateBranchDeferred = null;
- expectIssueState(true);
- expect($btnReopen).toHaveProp('disabled', false);
- expect($('.issue_counter')).toHaveText(1);
+ this.$triggeredButton.trigger('click');
+ this.issueStateDeferred.reject();
+ });
});
});
});
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/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index e3429c2a1cb..918b6d32c43 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -4,6 +4,20 @@ import Poll from '~/lib/utils/poll';
Vue.use(VueResource);
+const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
+ const timer = () => {
+ setTimeout(() => {
+ if (service.fetch.calls.count() === waitForCount) {
+ successCallback();
+ } else {
+ timer();
+ }
+ }, 5);
+ };
+
+ timer();
+};
+
class ServiceMock {
constructor(endpoint) {
this.service = Vue.resource(endpoint);
@@ -16,6 +30,7 @@ class ServiceMock {
describe('Poll', () => {
let callbacks;
+ let service;
beforeEach(() => {
callbacks = {
@@ -23,8 +38,11 @@ describe('Poll', () => {
error: () => {},
};
+ service = new ServiceMock('endpoint');
+
spyOn(callbacks, 'success');
spyOn(callbacks, 'error');
+ spyOn(service, 'fetch').and.callThrough();
});
it('calls the success callback when no header for interval is provided', (done) => {
@@ -35,19 +53,20 @@ describe('Poll', () => {
Vue.http.interceptors.push(successInterceptor);
new Poll({
- resource: new ServiceMock('endpoint'),
+ resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 1, () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
+
done();
}, 0);
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
});
it('calls the error callback whe the http request returns an error', (done) => {
@@ -58,19 +77,19 @@ describe('Poll', () => {
Vue.http.interceptors.push(errorInterceptor);
new Poll({
- resource: new ServiceMock('endpoint'),
+ resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 1, () => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
- done();
- }, 0);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
- Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
+ done();
+ });
});
it('should call the success callback when the interval header is -1', (done) => {
@@ -81,7 +100,7 @@ describe('Poll', () => {
Vue.http.interceptors.push(intervalInterceptor);
new Poll({
- resource: new ServiceMock('endpoint'),
+ resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
@@ -90,10 +109,11 @@ describe('Poll', () => {
setTimeout(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
+
done();
}, 0);
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
});
it('starts polling when http status is 200 and interval header is provided', (done) => {
@@ -103,26 +123,28 @@ describe('Poll', () => {
Vue.http.interceptors.push(pollInterceptor);
- const service = new ServiceMock('endpoint');
- spyOn(service, 'fetch').and.callThrough();
-
- new Poll({
+ const Polling = new Poll({
resource: service,
method: 'fetch',
data: { page: 1 },
successCallback: callbacks.success,
errorCallback: callbacks.error,
- }).makeRequest();
+ });
+
+ Polling.makeRequest();
+
+ waitForAllCallsToFinish(service, 2, () => {
+ Polling.stop();
- setTimeout(() => {
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- done();
- }, 5);
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+ done();
+ });
});
describe('stop', () => {
@@ -133,9 +155,6 @@ describe('Poll', () => {
Vue.http.interceptors.push(pollInterceptor);
- const service = new ServiceMock('endpoint');
- spyOn(service, 'fetch').and.callThrough();
-
const Polling = new Poll({
resource: service,
method: 'fetch',
@@ -150,14 +169,15 @@ describe('Poll', () => {
Polling.makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 1, () => {
expect(service.fetch.calls.count()).toEqual(1);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
- done();
- }, 100);
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+ done();
+ });
});
});
@@ -169,10 +189,6 @@ describe('Poll', () => {
Vue.http.interceptors.push(pollInterceptor);
- const service = new ServiceMock('endpoint');
-
- spyOn(service, 'fetch').and.callThrough();
-
const Polling = new Poll({
resource: service,
method: 'fetch',
@@ -187,17 +203,22 @@ describe('Poll', () => {
});
spyOn(Polling, 'stop').and.callThrough();
+ spyOn(Polling, 'restart').and.callThrough();
Polling.makeRequest();
- setTimeout(() => {
+ waitForAllCallsToFinish(service, 2, () => {
+ Polling.stop();
+
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
- done();
- }, 10);
+ expect(Polling.restart).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+ done();
+ });
});
});
});
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 bc8e504c413..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';
+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 733337168dc..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';
+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 524e018b1fa..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';
+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 -&gt; Sara : Hello</code><pre></pre></pre>'
+ input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
+ output = '<pre><code lang="plantuml">Bob -&gt; 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/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index bc1912d8e6c..f8fffbdca41 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -50,6 +50,13 @@ describe ContainerRegistry::Tag do
end
end
+ describe '#location' do
+ it 'returns a full location of the tag' do
+ expect(tag.location)
+ .to eq 'registry.gitlab/group/test:tag'
+ end
+ end
+
context 'manifest processing' do
context 'schema v1' do
before do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index f1a1a71c528..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
@@ -167,7 +222,7 @@ describe Gitlab::Ci::Trace::Stream do
let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
let(:regex) { '\(\d+.\d+\%\) covered' }
- it { is_expected.to eq(98.29) }
+ it { is_expected.to eq("98.29") }
end
context 'valid content & bad regex' do
@@ -188,14 +243,14 @@ describe Gitlab::Ci::Trace::Stream do
let(:data) { ' (98.39%) covered. (98.29%) covered' }
let(:regex) { '\(\d+.\d+\%\) covered' }
- it { is_expected.to eq(98.29) }
+ it { is_expected.to eq("98.29") }
end
context 'using a regex capture' do
let(:data) { 'TOTAL 9926 3489 65%' }
let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
- it { is_expected.to eq(65) }
+ it { is_expected.to eq("65") }
end
end
end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 69e8dc9220d..9cb0b62590a 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -40,12 +40,24 @@ describe Gitlab::Ci::Trace do
describe '#extract_coverage' do
let(:regex) { '\(\d+.\d+\%\) covered' }
- before do
- trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ context 'matching coverage' do
+ before do
+ trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ end
+
+ it "returns valid coverage" do
+ expect(trace.extract_coverage(regex)).to eq("98.29")
+ end
end
- it "returns valid coverage" do
- expect(trace.extract_coverage(regex)).to eq(98.29)
+ context 'no coverage' do
+ before do
+ trace.set('No coverage')
+ end
+
+ it 'returs nil' do
+ expect(trace.extract_coverage(regex)).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4ac79454647..a044b871730 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -175,6 +175,50 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#true_value' do
+ context 'using PostgreSQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.true_value).to eq("'t'")
+ end
+ end
+
+ context 'using MySQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.true_value).to eq(1)
+ end
+ end
+ end
+
+ describe '#false_value' do
+ context 'using PostgreSQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.false_value).to eq("'f'")
+ end
+ end
+
+ context 'using MySQL' do
+ before do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns the appropriate value' do
+ expect(model.false_value).to eq(0)
+ end
+ end
+ end
+
describe '#update_column_in_batches' do
before do
create_list(:empty_project, 5)
@@ -294,4 +338,392 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
end
+
+ describe '#rename_column_concurrently' do
+ context 'in a transaction' do
+ it 'raises RuntimeError' do
+ allow(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.rename_column_concurrently(:users, :old, :new) }.
+ to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ let(:old_column) do
+ double(:column,
+ type: :integer,
+ limit: 8,
+ default: 0,
+ null: false,
+ precision: 5,
+ scale: 1)
+ end
+
+ let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) }
+
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:column_for).and_return(old_column)
+
+ # Since MySQL and PostgreSQL use different quoting styles we'll just
+ # stub the methods used for this to make testing easier.
+ allow(model).to receive(:quote_column_name) { |name| name.to_s }
+ allow(model).to receive(:quote_table_name) { |name| name.to_s }
+ end
+
+ context 'using MySQL' do
+ it 'renames a column concurrently' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:install_rename_triggers_for_mysql).
+ with(trigger_name, 'users', 'old', 'new')
+
+ expect(model).to receive(:add_column).
+ with(:users, :new, :integer,
+ limit: old_column.limit,
+ default: old_column.default,
+ null: old_column.null,
+ precision: old_column.precision,
+ scale: old_column.scale)
+
+ expect(model).to receive(:update_column_in_batches)
+
+ expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+ expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+ model.rename_column_concurrently(:users, :old, :new)
+ end
+ end
+
+ context 'using PostgreSQL' do
+ it 'renames a column concurrently' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:install_rename_triggers_for_postgresql).
+ with(trigger_name, 'users', 'old', 'new')
+
+ expect(model).to receive(:add_column).
+ with(:users, :new, :integer,
+ limit: old_column.limit,
+ default: old_column.default,
+ null: old_column.null,
+ precision: old_column.precision,
+ scale: old_column.scale)
+
+ expect(model).to receive(:update_column_in_batches)
+
+ expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+ expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+ model.rename_column_concurrently(:users, :old, :new)
+ end
+ end
+ end
+ end
+
+ describe '#cleanup_concurrent_column_rename' do
+ it 'cleans up the renaming procedure for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:remove_rename_triggers_for_postgresql).
+ with(:users, /trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, :old)
+
+ model.cleanup_concurrent_column_rename(:users, :old, :new)
+ end
+
+ it 'cleans up the renaming procedure for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:remove_rename_triggers_for_mysql).
+ with(/trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, :old)
+
+ model.cleanup_concurrent_column_rename(:users, :old, :new)
+ end
+ end
+
+ describe '#change_column_type_concurrently' do
+ it 'changes the column type' do
+ expect(model).to receive(:rename_column_concurrently).
+ with('users', 'username', 'username_for_type_change', type: :text)
+
+ model.change_column_type_concurrently('users', 'username', :text)
+ end
+ end
+
+ describe '#cleanup_concurrent_column_type_change' do
+ it 'cleans up the type changing procedure' do
+ expect(model).to receive(:cleanup_concurrent_column_rename).
+ with('users', 'username', 'username_for_type_change')
+
+ expect(model).to receive(:rename_column).
+ with('users', 'username_for_type_change', 'username')
+
+ model.cleanup_concurrent_column_type_change('users', 'username')
+ end
+ end
+
+ describe '#install_rename_triggers_for_postgresql' do
+ it 'installs the triggers for PostgreSQL' do
+ expect(model).to receive(:execute).
+ with(/CREATE OR REPLACE FUNCTION foo()/m)
+
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo/m)
+
+ model.install_rename_triggers_for_postgresql('foo', :users, :old, :new)
+ end
+ end
+
+ describe '#install_rename_triggers_for_mysql' do
+ it 'installs the triggers for MySQL' do
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo_insert.+ON users/m)
+
+ expect(model).to receive(:execute).
+ with(/CREATE TRIGGER foo_update.+ON users/m)
+
+ model.install_rename_triggers_for_mysql('foo', :users, :old, :new)
+ end
+ end
+
+ describe '#remove_rename_triggers_for_postgresql' do
+ it 'removes the function and trigger' do
+ expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar')
+ expect(model).to receive(:execute).with('DROP FUNCTION foo()')
+
+ model.remove_rename_triggers_for_postgresql('bar', 'foo')
+ end
+ end
+
+ describe '#remove_rename_triggers_for_mysql' do
+ it 'removes the triggers' do
+ expect(model).to receive(:execute).with('DROP TRIGGER foo_insert')
+ expect(model).to receive(:execute).with('DROP TRIGGER foo_update')
+
+ model.remove_rename_triggers_for_mysql('foo')
+ end
+ end
+
+ describe '#rename_trigger_name' do
+ it 'returns a String' do
+ expect(model.rename_trigger_name(:users, :foo, :bar)).
+ to match(/trigger_.{12}/)
+ end
+ end
+
+ describe '#indexes_for' do
+ it 'returns the indexes for a column' do
+ idx1 = double(:idx, columns: %w(project_id))
+ idx2 = double(:idx, columns: %w(user_id))
+
+ allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])
+
+ expect(model.indexes_for('table', :user_id)).to eq([idx2])
+ end
+ end
+
+ describe '#foreign_keys_for' do
+ it 'returns the foreign keys for a column' do
+ fk1 = double(:fk, column: 'project_id')
+ fk2 = double(:fk, column: 'user_id')
+
+ allow(model).to receive(:foreign_keys).with('table').and_return([fk1, fk2])
+
+ expect(model.foreign_keys_for('table', :user_id)).to eq([fk2])
+ end
+ end
+
+ describe '#copy_indexes' do
+ context 'using a regular index using a single column' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [])
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using a regular index with multiple columns' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id foobar),
+ name: 'index_on_issues_project_id_foobar',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id foobar),
+ unique: false,
+ name: 'index_on_issues_gl_project_id_foobar',
+ length: [],
+ order: [])
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with a WHERE clause' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: 'foo',
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ where: 'foo')
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with a USING clause' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ where: nil,
+ using: 'foo',
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ using: 'foo')
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with custom operator classes' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_on_issues_project_id',
+ using: nil,
+ where: nil,
+ opclasses: { 'project_id' => 'bar' },
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect(model).to receive(:add_concurrent_index).
+ with(:issues,
+ %w(gl_project_id),
+ unique: false,
+ name: 'index_on_issues_gl_project_id',
+ length: [],
+ order: [],
+ opclasses: { 'gl_project_id' => 'bar' })
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ describe 'using an index of which the name does not contain the source column' do
+ it 'raises RuntimeError' do
+ index = double(:index,
+ columns: %w(project_id),
+ name: 'index_foobar_index',
+ using: nil,
+ where: nil,
+ opclasses: {},
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+ and_return([index])
+
+ expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '#copy_foreign_keys' do
+ it 'copies foreign keys from one column to another' do
+ fk = double(:fk,
+ from_table: 'issues',
+ to_table: 'projects',
+ on_delete: :cascade)
+
+ allow(model).to receive(:foreign_keys_for).with(:issues, :project_id).
+ and_return([fk])
+
+ expect(model).to receive(:add_concurrent_foreign_key).
+ with('issues', 'projects', column: :gl_project_id, on_delete: :cascade)
+
+ model.copy_foreign_keys(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ describe '#column_for' do
+ it 'returns a column object for an existing column' do
+ column = model.column_for(:users, :id)
+
+ expect(column.name).to eq('id')
+ end
+
+ it 'returns nil when a column does not exist' do
+ expect(model.column_for(:users, :kittens)).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/multi_threaded_migration_spec.rb b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
new file mode 100644
index 00000000000..6c45f13bb5a
--- /dev/null
+++ b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Database::MultiThreadedMigration do
+ let(:migration) do
+ Class.new { include Gitlab::Database::MultiThreadedMigration }.new
+ end
+
+ describe '#connection' do
+ after do
+ Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = nil
+ end
+
+ it 'returns the thread-local connection if present' do
+ Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = 10
+
+ expect(migration.connection).to eq(10)
+ end
+
+ it 'returns the global connection if no thread-local connection was set' do
+ expect(migration.connection).to eq(ActiveRecord::Base.connection)
+ end
+ end
+
+ describe '#with_multiple_threads' do
+ it 'starts multiple threads and yields the supplied block in every thread' do
+ output = Queue.new
+
+ migration.with_multiple_threads(2) do
+ output << migration.connection.execute('SELECT 1')
+ end
+
+ expect(output.size).to eq(2)
+ end
+
+ it 'joins the threads when the join parameter is set' do
+ expect_any_instance_of(Thread).to receive(:join).and_call_original
+
+ migration.with_multiple_threads(1) { }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 4ce4e6e1034..9b1d66a1b1c 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -150,13 +150,13 @@ describe Gitlab::Database, lib: true do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
- expect(MigrationTest.new.true_value).to eq "'t'"
+ expect(described_class.true_value).to eq "'t'"
end
it 'returns correct value for MySQL' do
expect(described_class).to receive(:postgresql?).and_return(false)
- expect(MigrationTest.new.true_value).to eq 1
+ expect(described_class.true_value).to eq 1
end
end
@@ -164,13 +164,13 @@ describe Gitlab::Database, lib: true do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
- expect(MigrationTest.new.false_value).to eq "'f'"
+ expect(described_class.false_value).to eq "'f'"
end
it 'returns correct value for MySQL' do
expect(described_class).to receive(:postgresql?).and_return(false)
- expect(MigrationTest.new.false_value).to eq 0
+ expect(described_class.false_value).to eq 0
end
end
end
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/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
new file mode 100644
index 00000000000..f3dacb4ef04
--- /dev/null
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe Gitlab::EtagCaching::Router do
+ it 'matches issue notes endpoint' do
+ env = build_env(
+ '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_notes'
+ end
+
+ it 'matches issue title endpoint' do
+ env = build_env(
+ '/my-group/my-project/issues/123/rendered_title'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_title'
+ end
+
+ it 'matches project pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'project_pipelines'
+ end
+
+ it 'matches commit pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'commit_pipelines'
+ end
+
+ it 'matches new merge request pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/merge_requests/new.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'new_merge_request_pipelines'
+ end
+
+ it 'matches merge request pipelines endpoint' do
+ env = build_env(
+ '/my-group/my-project/merge_requests/234/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_present
+ expect(result.name).to eq 'merge_request_pipelines'
+ end
+
+ it 'does not match blob with confusing name' do
+ env = build_env(
+ '/my-group/my-project/blob/master/pipelines.json'
+ )
+
+ result = described_class.match(env)
+
+ expect(result).to be_blank
+ end
+
+ def build_env(path)
+ { 'PATH_INFO' => path }
+ end
+end
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/env_spec.rb b/spec/lib/gitlab/git/env_spec.rb
new file mode 100644
index 00000000000..d9df99bfe05
--- /dev/null
+++ b/spec/lib/gitlab/git/env_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Env do
+ describe "#set" do
+ context 'with RequestStore.store disabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(false)
+ end
+
+ it 'does not store anything' do
+ described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+
+ expect(described_class.all).to be_empty
+ end
+ end
+
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ end
+
+ it 'whitelist some `GIT_*` variables and stores them using RequestStore' do
+ described_class.set(
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
+ GIT_EXEC_PATH: 'baz',
+ PATH: '~/.bin:/bin')
+
+ expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+ expect(described_class[:GIT_ALTERNATE_OBJECT_DIRECTORIES]).to eq('bar')
+ expect(described_class[:GIT_EXEC_PATH]).to be_nil
+ expect(described_class[:bar]).to be_nil
+ end
+ end
+ end
+
+ describe "#all" do
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ described_class.set(
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
+ end
+
+ it 'returns an env hash' do
+ expect(described_class.all).to eq({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ })
+ end
+ end
+ end
+
+ describe "#[]" do
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ end
+
+ before do
+ described_class.set(
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
+ end
+
+ it 'returns a stored value for an existing key' do
+ expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+ end
+
+ it 'returns nil for an non-existing key' do
+ expect(described_class[:foo]).to be_nil
+ end
+ end
+ end
+
+ describe 'thread-safety' do
+ context 'with RequestStore.store enabled' do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+ end
+
+ it 'is thread-safe' do
+ another_thread = Thread.new do
+ described_class.set(GIT_OBJECT_DIRECTORY: 'bar')
+
+ Thread.stop
+ described_class[:GIT_OBJECT_DIRECTORY]
+ end
+
+ # Ensure another_thread runs first
+ sleep 0.1 until another_thread.stop?
+
+ expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+
+ another_thread.run
+ expect(another_thread.value).to eq('bar')
+ end
+ end
+ end
+end
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/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 7e8bb796e03..3d6d7292b42 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -24,18 +24,49 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context 'with gitaly enabled' do
- before { stub_gitaly }
+ # TODO: Uncomment when feature is reenabled
+ # context 'with gitaly enabled' do
+ # before { stub_gitaly }
+ #
+ # it 'gets the branch name from GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+ # repository.root_ref
+ # end
+ #
+ # it 'wraps GRPC exceptions' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
+ # and_raise(GRPC::Unknown)
+ # expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
+ # end
+ # end
+ end
- it 'gets the branch name from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
- repository.root_ref
+ describe "#rugged" do
+ context 'with no Git env stored' do
+ before do
+ expect(Gitlab::Git::Env).to receive(:all).and_return({})
+ end
+
+ it "whitelist some variables and pass them via the alternates keyword argument" do
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: [])
+
+ repository.rugged
end
+ end
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
- and_raise(GRPC::Unknown)
- expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
+ context 'with some Git env stored' do
+ before do
+ expect(Gitlab::Git::Env).to receive(:all).and_return({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar',
+ 'GIT_OTHER' => 'another_env'
+ })
+ end
+
+ it "whitelist some variables and pass them via the alternates keyword argument" do
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar])
+
+ repository.rugged
end
end
end
@@ -82,20 +113,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
- context 'with gitaly enabled' do
- before { stub_gitaly }
-
- it 'gets the branch names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
- subject
- end
-
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
- and_raise(GRPC::Unknown)
- expect { subject }.to raise_error(Gitlab::Git::CommandError)
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # context 'with gitaly enabled' do
+ # before { stub_gitaly }
+ #
+ # it 'gets the branch names from GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+ # subject
+ # end
+ #
+ # it 'wraps GRPC exceptions' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
+ # and_raise(GRPC::Unknown)
+ # expect { subject }.to raise_error(Gitlab::Git::CommandError)
+ # end
+ # end
end
describe '#tag_names' do
@@ -113,20 +145,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
- context 'with gitaly enabled' do
- before { stub_gitaly }
-
- it 'gets the tag names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
- subject
- end
-
- it 'wraps GRPC exceptions' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
- and_raise(GRPC::Unknown)
- expect { subject }.to raise_error(Gitlab::Git::CommandError)
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # context 'with gitaly enabled' do
+ # before { stub_gitaly }
+ #
+ # it 'gets the tag names from GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+ # subject
+ # end
+ #
+ # it 'wraps GRPC exceptions' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
+ # and_raise(GRPC::Unknown)
+ # expect { subject }.to raise_error(Gitlab::Git::CommandError)
+ # end
+ # end
end
shared_examples 'archive check' do |extenstion|
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index d48629a296d..78894ba9409 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -3,58 +3,54 @@ require 'spec_helper'
describe Gitlab::Git::RevList, lib: true do
let(:project) { create(:project, :repository) }
- context "validations" do
- described_class::ALLOWED_VARIABLES.each do |var|
- context var do
- it "accepts values starting with the project repo path" do
- env = { var => "#{project.repository.path_to_repo}/objects" }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).to be_valid
- end
-
- it "rejects values starting not with the project repo path" do
- env = { var => "/some/other/path" }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).not_to be_valid
- end
-
- it "rejects values containing the project repo path but not starting with it" do
- env = { var => "/some/other/path/#{project.repository.path_to_repo}" }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).not_to be_valid
- end
-
- it "ignores nil values" do
- env = { var => nil }
- rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
-
- expect(rev_list).to be_valid
- end
- end
- end
+ before do
+ expect(Gitlab::Git::Env).to receive(:all).and_return({
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
+ })
end
- context "#execute" do
- let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } }
- let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) }
-
- it "calls out to `popen` without environment variables if the record is invalid" do
- allow(rev_list).to receive(:valid?).and_return(false)
-
- expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args)
-
- rev_list.execute
+ context "#new_refs" do
+ let(:rev_list) { Gitlab::Git::RevList.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+
+ it 'calls out to `popen`' do
+ expect(Gitlab::Popen).to receive(:popen).with([
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ 'newrev',
+ '--not',
+ '--all'
+ ],
+ nil,
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }).and_return(["sha1\nsha2", 0])
+
+ expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
+ end
- it "calls out to `popen` with environment variables if the record is valid" do
- allow(rev_list).to receive(:valid?).and_return(true)
-
- expect(Open3).to receive(:popen3).with(hash_including(env), any_args)
-
- rev_list.execute
+ context "#missed_ref" do
+ let(:rev_list) { Gitlab::Git::RevList.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+
+ it 'calls out to `popen`' do
+ expect(Gitlab::Popen).to receive(:popen).with([
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ '--max-count=1',
+ 'oldrev',
+ '^newrev'
+ ],
+ nil,
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }).and_return(["sha1\nsha2", 0])
+
+ expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
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/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
new file mode 100644
index 00000000000..dacaa834aa9
--- /dev/null
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -0,0 +1,17 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb')
+
+describe MigrateUserProjectView do
+ let(:migration) { described_class.new }
+ let!(:user) { create(:user, project_view: 'readme') }
+
+ describe '#up' do
+ it 'updates project view setting with new value' do
+ migration.up
+
+ expect(user.reload.project_view).to eq('files')
+ end
+ end
+end
diff --git a/spec/migrations/schema_spec.rb b/spec/migrations/schema_spec.rb
new file mode 100644
index 00000000000..e132529d8d8
--- /dev/null
+++ b/spec/migrations/schema_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+# Check consistency of db/schema.rb version, migrations' timestamps, and the latest migration timestamp
+# stored in the database's schema_migrations table.
+
+describe ActiveRecord::Schema do
+ let(:latest_migration_timestamp) do
+ migrations = Dir[Rails.root.join('db', 'migrate', '*'), Rails.root.join('db', 'post_migrate', '*')]
+ migrations.map { |migration| File.basename(migration).split('_').first.to_i }.max
+ end
+
+ it '> schema version equals last migration timestamp' do
+ defined_schema_version = File.open(Rails.root.join('db', 'schema.rb')) do |file|
+ file.find { |line| line =~ /ActiveRecord::Schema.define/ }
+ end.match(/(\d+)/)[0].to_i
+
+ expect(defined_schema_version).to eq(latest_migration_timestamp)
+ end
+
+ it '> schema version should equal the latest migration timestamp stored in schema_migrations table' do
+ expect(latest_migration_timestamp).to eq(ActiveRecord::Migrator.current_version.to_i)
+ 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/blob_spec.rb b/spec/models/blob_spec.rb
index 0f29766db41..e5dd57fc4bb 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -55,13 +55,13 @@ describe Blob do
describe '#pdf?' do
it 'is falsey when file extension is not .pdf' do
- git_blob = double(name: 'git_blob.txt')
+ git_blob = Gitlab::Git::Blob.new(name: 'git_blob.txt')
expect(described_class.decorate(git_blob)).not_to be_pdf
end
it 'is truthy when file extension is .pdf' do
- git_blob = double(name: 'git_blob.pdf')
+ git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf')
expect(described_class.decorate(git_blob)).to be_pdf
end
@@ -140,7 +140,7 @@ describe Blob do
stl?: false
)
- described_class.decorate(double).tap do |blob|
+ described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob|
allow(blob).to receive_messages(overrides)
end
end
@@ -158,7 +158,7 @@ describe Blob do
it 'handles SVGs' do
blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
+ expect(blob.to_partial_path(project)).to eq 'svg'
end
it 'handles images' do
@@ -167,7 +167,7 @@ describe Blob do
end
it 'handles text' do
- blob = stubbed_blob(text?: true)
+ blob = stubbed_blob(text?: true, name: 'test.txt')
expect(blob.to_partial_path(project)).to eq 'text'
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 980a1b70ef5..ce31c8ed94c 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -389,31 +389,32 @@ eos
end
end
- describe '#raw_diffs' do
- context 'Gitaly commit_raw_diffs feature enabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
- end
-
- context 'when a truthy deltas_only is not passed to args' do
- it 'fetches diffs from Gitaly server' do
- expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
- with(commit)
-
- commit.raw_diffs
- end
- end
-
- context 'when a truthy deltas_only is passed to args' do
- it 'fetches diffs using Rugged' do
- opts = { deltas_only: true }
-
- expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
- expect(commit.raw).to receive(:diffs).with(opts)
-
- commit.raw_diffs(opts)
- end
- end
- end
- end
+ # describe '#raw_diffs' do
+ # TODO: Uncomment when feature is reenabled
+ # context 'Gitaly commit_raw_diffs feature enabled' do
+ # before do
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+ # end
+ #
+ # context 'when a truthy deltas_only is not passed to args' do
+ # it 'fetches diffs from Gitaly server' do
+ # expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+ # with(commit)
+ #
+ # commit.raw_diffs
+ # end
+ # end
+ #
+ # context 'when a truthy deltas_only is passed to args' do
+ # it 'fetches diffs using Rugged' do
+ # opts = { deltas_only: true }
+ #
+ # expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+ # expect(commit.raw).to receive(:diffs).with(opts)
+ #
+ # commit.raw_diffs(opts)
+ # end
+ # end
+ # end
+ # end
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/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
new file mode 100644
index 00000000000..dba9fe43327
--- /dev/null
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe IgnorableColumn do
+ let :base_class do
+ Class.new do
+ def self.columns
+ # This method does not have access to "double"
+ [Struct.new(:name).new('id'), Struct.new(:name).new('title')]
+ end
+ end
+ end
+
+ let :model do
+ Class.new(base_class) do
+ include IgnorableColumn
+ end
+ end
+
+ describe '.columns' do
+ it 'returns the columns, excluding the ignored ones' do
+ model.ignore_column(:title)
+
+ expect(model.columns.map(&:name)).to eq(%w(id))
+ end
+ end
+
+ describe '.ignored_columns' do
+ it 'returns a Set' do
+ expect(model.ignored_columns).to be_an_instance_of(Set)
+ end
+
+ it 'returns the names of the ignored columns' do
+ model.ignore_column(:title)
+
+ expect(model.ignored_columns).to eq(Set.new(%w(title)))
+ end
+ end
+end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index f7ee0b57072..eff41d85972 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -4,7 +4,7 @@ describe ContainerRepository do
let(:group) { create(:group, name: 'group') }
let(:project) { create(:project, path: 'test', group: group) }
- let(:container_repository) do
+ let(:repository) do
create(:container_repository, name: 'my_image', project: project)
end
@@ -23,48 +23,58 @@ describe ContainerRepository do
describe 'associations' do
it 'belongs to the project' do
- expect(container_repository).to belong_to(:project)
+ expect(repository).to belong_to(:project)
end
end
describe '#tag' do
it 'has a test tag' do
- expect(container_repository.tag('test')).not_to be_nil
+ expect(repository.tag('test')).not_to be_nil
end
end
describe '#path' do
- it 'returns a full path to the repository' do
- expect(container_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
describe '#manifest' do
- subject { container_repository.manifest }
-
- it { is_expected.not_to be_nil }
+ it 'returns non-empty manifest' do
+ expect(repository.manifest).not_to be_nil
+ end
end
describe '#valid?' do
- subject { container_repository.valid? }
-
- it { is_expected.to be_truthy }
+ it 'is a valid repository' do
+ expect(repository).to be_valid
+ end
end
describe '#tags' do
- subject { container_repository.tags }
-
- it { is_expected.not_to be_empty }
+ it 'returns non-empty tags list' do
+ expect(repository.tags).not_to be_empty
+ end
end
describe '#has_tags?' do
it 'has tags' do
- expect(container_repository).to have_tags
+ expect(repository).to have_tags
end
end
describe '#delete_tags!' do
- let(:container_repository) do
+ let(:repository) do
create(:container_repository, name: 'my_image',
tags: %w[latest rc1],
project: project)
@@ -72,21 +82,36 @@ describe ContainerRepository do
context 'when action succeeds' do
it 'returns status that indicates success' do
- expect(container_repository.client)
+ expect(repository.client)
.to receive(:delete_repository_tag)
.and_return(true)
- expect(container_repository.delete_tags!).to be_truthy
+ expect(repository.delete_tags!).to be_truthy
end
end
context 'when action fails' do
it 'returns status that indicates failure' do
- expect(container_repository.client)
+ expect(repository.client)
.to receive(:delete_repository_tag)
.and_return(false)
- expect(container_repository.delete_tags!).to be_falsey
+ expect(repository.delete_tags!).to be_falsey
+ end
+ end
+ end
+
+ describe '#location' do
+ context 'when registry is running on a custom port' do
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab:5000',
+ host_port: 'registry.gitlab:5000')
+ end
+
+ it 'returns a full location of the repository' do
+ expect(repository.location)
+ .to eq 'registry.gitlab:5000/group/test/my_image'
end
end
end
@@ -102,7 +127,7 @@ describe ContainerRepository do
context 'when repository is not a root repository' do
it 'returns false' do
- expect(container_repository).not_to be_root_repository
+ expect(repository).not_to be_root_repository
end
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index fb80b74b226..f32b6b99b3d 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -155,6 +155,23 @@ describe DiffNote, models: true do
end
end
+ describe '#latest_merge_request_diff' do
+ context 'when active' do
+ it 'returns the current merge request diff' do
+ expect(subject.latest_merge_request_diff).to eq(merge_request.merge_request_diff)
+ end
+ end
+
+ context 'when outdated' do
+ let!(:old_merge_request_diff) { merge_request.merge_request_diff }
+ let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: commit.diff_refs) }
+
+ it 'returns the latest merge request diff that this diff note applied to' do
+ expect(subject.latest_merge_request_diff).to eq(old_merge_request_diff)
+ end
+ end
+ end
+
describe "creation" do
describe "updating of position" do
context "when noteable is a commit" do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index af7753caba6..070716e859a 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -110,17 +110,18 @@ describe Environment, models: true do
end
end
- context 'Gitaly find_ref_name feature enabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
- end
-
- it 'calls GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
-
- environment.first_deployment_for(commit)
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # context 'Gitaly find_ref_name feature enabled' do
+ # before do
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
+ # end
+ #
+ # it 'calls GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
+ #
+ # environment.first_deployment_for(commit)
+ # end
+ # end
end
describe '#environment_type' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 4bdd46a581d..11befd4edfe 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -134,15 +134,6 @@ describe Issue, models: true do
end
end
- describe '#is_being_reassigned?' do
- it 'returns issues assigned to user' do
- user = create(:user)
- create_list(:issue, 2, assignee: user)
-
- expect(Issue.open_for(user).count).to eq 2
- end
- end
-
describe '#closed_by_merge_requests' do
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project)}
@@ -370,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/label_spec.rb b/spec/models/label_spec.rb
index a9139f7d4ab..80ca19acdda 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -42,11 +42,27 @@ describe Label, models: true do
end
end
+ describe '#color' do
+ it 'strips color' do
+ label = described_class.new(color: ' #abcdef ')
+ label.valid?
+
+ expect(label.color).to eq('#abcdef')
+ end
+ end
+
describe '#title' do
it 'sanitizes title' do
label = described_class.new(title: '<b>foo & bar?</b>')
expect(label.title).to eq('foo & bar?')
end
+
+ it 'strips title' do
+ label = described_class.new(title: ' label ')
+ label.valid?
+
+ expect(label.title).to eq('label')
+ end
end
describe 'priorization' do
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/note_spec.rb b/spec/models/note_spec.rb
index 3c4bf3f4ddb..557ea97b008 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -622,12 +622,22 @@ describe Note, models: true do
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
- it "expires cache for note's issue when note is saved" do
+ def expect_expiration(note)
expect_any_instance_of(Gitlab::EtagCaching::Store)
.to receive(:touch)
.with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+ end
+
+ it "expires cache for note's issue when note is saved" do
+ expect_expiration(note)
note.save!
end
+
+ it "expires cache for note's issue when note is destroyed" do
+ expect_expiration(note)
+
+ note.destroy!
+ end
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 d805e65b3c6..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,
@@ -1283,8 +1303,6 @@ describe Repository, models: true do
describe '#after_import' do
it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache)
- expect(repository).to receive(:expire_tags_cache)
- expect(repository).to receive(:expire_branches_cache)
repository.after_import
end
@@ -1831,16 +1849,17 @@ describe Repository, models: true do
end
end
- describe '#is_ancestor?' do
- context 'Gitaly is_ancestor feature enabled' do
- it 'asks Gitaly server if it\'s an ancestor' do
- commit = repository.commit
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
- expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
- with(repository.raw_repository, commit.id, commit.id).and_return(true)
-
- expect(repository.is_ancestor?(commit.id, commit.id)).to be true
- end
- end
- end
+ # TODO: Uncomment when feature is reenabled
+ # describe '#is_ancestor?' do
+ # context 'Gitaly is_ancestor feature enabled' do
+ # it 'asks Gitaly server if it\'s an ancestor' do
+ # commit = repository.commit
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+ # expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
+ # with(repository.raw_repository, commit.id, commit.id).and_return(true)
+ #
+ # expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+ # end
+ # end
+ # end
end
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/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 5c34ff04152..2077c14ff7a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -22,7 +22,8 @@ describe GroupPolicy, models: true do
:admin_group,
:admin_namespace,
:admin_group_member,
- :change_visibility_level
+ :change_visibility_level,
+ :create_subgroup
]
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 eed45d37444..3d6010ede73 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -147,10 +147,31 @@ 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
+ it 'sets env in RequestStore' do
+ expect(Gitlab::Git::Env).to receive(:set).with({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ })
+
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
+ }.to_json)
+
+ expect(response).to have_http_status(200)
+ end
end
context "git push with project.wiki" do
@@ -160,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
@@ -170,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
@@ -180,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
@@ -190,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
@@ -225,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
@@ -234,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
@@ -251,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
@@ -260,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
@@ -463,7 +492,7 @@ describe API::Internal, api: true do
)
end
- def push(key, project, protocol = 'ssh')
+ def push(key, project, protocol = 'ssh', env: nil)
post(
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
@@ -471,7 +500,8 @@ describe API::Internal, api: true do
project: project.repository.path_to_repo,
action: 'git-receive-pack',
secret_token: secret_token,
- protocol: protocol
+ protocol: protocol,
+ env: env
)
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index b1f8c249092..b1603233f9e 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -22,8 +22,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
context "authorized user" do
it "returns project hooks" do
get api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response).to include_pagination_headers
expect(json_response.count).to eq(1)
@@ -43,6 +43,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
context "unauthorized user" do
it "does not access project hooks" do
get api("/projects/#{project.id}/hooks", user3)
+
expect(response).to have_http_status(403)
end
end
@@ -52,6 +53,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
context "authorized user" do
it "returns a project hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
@@ -67,6 +69,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
+
expect(response).to have_http_status(404)
end
end
@@ -88,7 +91,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "adds hook to project" do
expect do
post api("/projects/#{project.id}/hooks", user),
- url: "http://example.com", issues_events: true, wiki_page_events: true
+ url: "http://example.com", issues_events: true, wiki_page_events: true,
+ job_events: true
end.to change {project.hooks.count}.by(1)
expect(response).to have_http_status(201)
@@ -98,7 +102,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
- expect(json_response['job_events']).to eq(false)
+ expect(json_response['job_events']).to eq(true)
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(true)
@@ -136,7 +140,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
describe "PUT /projects/:id/hooks/:hook_id" do
it "updates an existing project hook" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user),
- url: 'http://example.org', push_events: false
+ url: 'http://example.org', push_events: false, job_events: true
+
expect(response).to have_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2e291eb3cea..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)
@@ -1076,10 +1080,21 @@ describe API::Projects, :api do
before { project_member3 }
before { project_member2 }
+ 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
+
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
@@ -1087,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
@@ -1096,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
@@ -1106,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
@@ -1132,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/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 617c8eaf3d5..989fd90cda9 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1181,6 +1181,22 @@ describe NotificationService, services: true do
should_not_email(@u_disabled)
end
end
+
+ describe '#project_exported' do
+ it do
+ notification.project_exported(project, @u_disabled)
+
+ should_only_email(@u_disabled)
+ end
+ end
+
+ describe '#project_not_exported' do
+ it do
+ notification.project_not_exported(project, @u_disabled, ['error'])
+
+ should_only_email(@u_disabled)
+ end
+ end
end
describe 'GroupMember' do
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/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index 8c5b7e41c15..9e1edf1ac30 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -60,5 +60,23 @@ describe Users::MigrateToGhostUserService, services: true do
end
end
end
+
+ context "when record migration fails with a rollback exception" do
+ before do
+ expect_any_instance_of(MergeRequest::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::Rollback)
+ end
+
+ context "for records that were already migrated" do
+ let!(:issue) { create(:issue, project: project, author: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, target_branch: "first") }
+
+ it "reverses the migration" do
+ service.execute
+
+ expect(issue.reload.author).to eq(user)
+ end
+ end
+ end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4eb5b150af5..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
@@ -59,6 +65,10 @@ RSpec.configure do |config|
TestEnv.init
end
+ config.after(:suite) do
+ TestEnv.cleanup
+ end
+
if ENV['CI']
# Retry only on feature specs that use JS
config.around :each, :js do |ex|
diff --git a/spec/features/discussion_comments_spec.rb b/spec/support/features/discussion_comments_shared_example.rb
index ae778118c5c..bb4542b1683 100644
--- a/spec/features/discussion_comments_spec.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
shared_examples 'discussion comments' do |resource_name|
let(:form_selector) { '.js-main-target-form' }
let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
@@ -9,11 +7,9 @@ shared_examples 'discussion comments' do |resource_name|
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
let(:comments_selector) { '.timeline > .note.timeline-entry' }
- it 'should show a comment type toggle' do
+ it 'clicking "Comment" will post a comment' do
expect(page).to have_selector toggle_selector
- end
- it 'clicking "Comment" will post a comment' do
find("#{form_selector} .note-textarea").send_keys('a')
find(submit_selector).click
@@ -49,52 +45,43 @@ shared_examples 'discussion comments' do |resource_name|
find(toggle_selector).click
end
- it 'opens a comment type dropdown with "Comment" and "Start discussion"' do
+ it 'has a "Comment" item (selected by default) and "Start discussion" item' do
expect(page).to have_selector menu_selector
- end
-
- it 'has a "Comment" item' do
- menu = find(menu_selector)
-
- expect(menu).to have_content 'Comment'
- expect(menu).to have_content "Add a general comment to this #{resource_name}."
- end
- it 'has a "Start discussion" item' do
- menu = find(menu_selector)
-
- expect(menu).to have_content 'Start discussion'
- expect(menu).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
- end
-
- it 'has the "Comment" item selected by default' do
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
expect(items.first).to have_content 'Comment'
+ expect(items.first).to have_content "Add a general comment to this #{resource_name}."
expect(items.first).to have_selector '.fa-check'
expect(items.first['class']).to match 'droplab-item-selected'
expect(items.last).to have_content 'Start discussion'
+ expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
expect(items.last).not_to have_selector '.fa-check'
expect(items.last['class']).not_to match 'droplab-item-selected'
end
- it 'closes the menu when clicking the toggle' do
+ it 'closes the menu when clicking the toggle or body' do
find(toggle_selector).click
expect(page).not_to have_selector menu_selector
- end
- it 'closes the menu when clicking the body' do
+ find(toggle_selector).click
find('body').click
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
@@ -104,12 +91,10 @@ shared_examples 'discussion comments' do |resource_name|
all("#{menu_selector} li").last.click
end
- it 'updates the note_type input to "DiscussionNote"' do
- expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
- end
-
- it 'updates the submit button text' do
+ it 'updates the submit button text, note_type input and closes the dropdown' do
expect(find(dropdown_selector)).to have_content 'Start discussion'
+ expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
+ expect(page).not_to have_selector menu_selector
end
if resource_name =~ /(issue|merge request)/
@@ -124,10 +109,6 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'closes the dropdown' do
- expect(page).not_to have_selector menu_selector
- end
-
it 'clicking "Start discussion" will post a discussion' do
find(submit_selector).click
@@ -176,12 +157,10 @@ shared_examples 'discussion comments' do |resource_name|
find("#{menu_selector} li", match: :first).click
end
- it 'clears the note_type input"' do
- expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
- end
-
- it 'updates the submit button text' do
+ it 'updates the submit button text, clears the note_type input and closes the dropdown' do
expect(find(dropdown_selector)).to have_content 'Comment'
+ expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
+ expect(page).not_to have_selector menu_selector
end
if resource_name =~ /(issue|merge request)/
@@ -196,10 +175,6 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'closes the dropdown' do
- expect(page).not_to have_selector menu_selector
- end
-
it 'should have "Comment" selected when opening the menu' do
find(toggle_selector).click
@@ -242,54 +217,3 @@ shared_examples 'discussion comments' do |resource_name|
end
end
end
-
-describe 'Discussion Comments', :feature, :js do
- include RepoHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- before do
- project.team << [user, :developer]
-
- login_as(user)
- end
-
- describe 'on a merge request' do
- let(:merge_request) { create(:merge_request, source_project: project) }
-
- before do
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
- end
-
- it_behaves_like 'discussion comments', 'merge request'
- end
-
- describe 'on an issue' do
- let(:issue) { create(:issue, project: project) }
-
- before do
- visit namespace_project_issue_path(project.namespace, project, issue)
- end
-
- it_behaves_like 'discussion comments', 'issue'
- end
-
- describe 'on an snippet' do
- let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
-
- before do
- visit namespace_project_snippet_path(project.namespace, project, snippet)
- end
-
- it_behaves_like 'discussion comments', 'snippet'
- end
-
- describe 'on a commit' do
- before do
- visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
- end
-
- it_behaves_like 'discussion comments', 'commit'
- end
-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/gitaly.rb b/spec/support/gitaly.rb
new file mode 100644
index 00000000000..7aca902fc61
--- /dev/null
+++ b/spec/support/gitaly.rb
@@ -0,0 +1,7 @@
+if Gitlab::GitalyClient.enabled?
+ RSpec.configure do |config|
+ config.before(:each) do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ end
+ 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/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 0eac587e973..dcc562c684b 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -35,5 +35,57 @@ shared_examples "migrating a deleted user's associated records to the ghost user
expect(user).to be_blocked
end
+
+ context "race conditions" do
+ context "when #{record_class_name} migration fails and is rolled back" do
+ before do
+ expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ActiveRecord::Rollback)
+ end
+
+ it 'rolls back the user block' do
+ service.execute
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock an previously-blocked user" do
+ user.block
+
+ service.execute
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ context "when #{record_class_name} migration fails with a non-rollback exception" do
+ before do
+ expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ .to receive(:update_all).and_raise(ArgumentError)
+ end
+
+ it 'rolls back the user block' do
+ service.execute rescue nil
+
+ expect(user.reload).not_to be_blocked
+ end
+
+ it "doesn't unblock an previously-blocked user" do
+ user.block
+
+ service.execute rescue nil
+
+ expect(user.reload).to be_blocked
+ end
+ end
+
+ it "blocks the user before #{record_class_name} migration begins" do
+ expect(service).to receive("migrate_#{record_class_name.parameterize('_')}s".to_sym) do
+ expect(user.reload).to be_blocked
+ end
+
+ service.execute
+ end
+ end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 1b5cb71a6b0..60c2096a126 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -64,6 +64,8 @@ module TestEnv
# Setup GitLab shell for test instance
setup_gitlab_shell
+ setup_gitaly if Gitlab::GitalyClient.enabled?
+
# Create repository for FactoryGirl.create(:project)
setup_factory_repo
@@ -71,6 +73,10 @@ module TestEnv
setup_forked_repo
end
+ def cleanup
+ stop_gitaly
+ end
+
def disable_mailer
allow_any_instance_of(NotificationService).to receive(:mailer).
and_return(double.as_null_object)
@@ -92,7 +98,7 @@ module TestEnv
tmp_test_path = Rails.root.join('tmp', 'tests', '**')
Dir[tmp_test_path].each do |entry|
- unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork|test-fork_bare)\z/
+ unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
FileUtils.rm_rf(entry)
end
end
@@ -110,6 +116,28 @@ module TestEnv
end
end
+ def setup_gitaly
+ socket_path = Gitlab::GitalyClient.get_address('default').sub(/\Aunix:/, '')
+ gitaly_dir = File.dirname(socket_path)
+
+ unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
+ raise "Can't clone gitaly"
+ end
+
+ start_gitaly(gitaly_dir, socket_path)
+ end
+
+ def start_gitaly(gitaly_dir, socket_path)
+ gitaly_exec = File.join(gitaly_dir, 'gitaly')
+ @gitaly_pid = spawn({ "GITALY_SOCKET_PATH" => socket_path }, gitaly_exec, [:out, :err] => '/dev/null')
+ end
+
+ def stop_gitaly
+ return unless @gitaly_pid
+
+ Process.kill('KILL', @gitaly_pid)
+ end
+
def setup_factory_repo
setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
BRANCH_SHA)
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/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb
new file mode 100644
index 00000000000..ceeace3dc8d
--- /dev/null
+++ b/spec/views/projects/registry/repositories/index.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/registry/repositories/index', :view do
+ let(:group) { create(:group, path: 'group') }
+ let(:project) { create(:empty_project, group: group, path: 'test') }
+
+ let(:repository) do
+ create(:container_repository, project: project, name: 'image')
+ end
+
+ before do
+ stub_container_registry_config(enabled: true,
+ host_port: 'registry.gitlab',
+ api_url: 'http://registry.gitlab')
+
+ stub_container_registry_tags(repository: :any, tags: [:latest])
+
+ assign(:project, project)
+ assign(:images, [repository])
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'contains container repository path' do
+ render
+
+ expect(rendered).to have_content 'group/test/image'
+ end
+
+ it 'contains attribute for copying tag location into clipboard' do
+ render
+
+ expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \
+ 'registry.gitlab/group/test/image:latest"]'
+ 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