diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-06-25 16:21:13 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-06-25 16:21:13 +0900 |
commit | 6c3eea0db04be446a1daebf629b6ddf9e0f18d18 (patch) | |
tree | 7de3db2d1addf9ea8e018817c1b8de378d33fb9d /spec | |
parent | 24ba0989878a363c37d86758844a17a99fc7ae0c (diff) | |
parent | 2bac2918b2d6f12d94f739f4b6865b9e9221c642 (diff) | |
download | gitlab-ce-6c3eea0db04be446a1daebf629b6ddf9e0f18d18.tar.gz |
Merge branch 'master' into build-chunks-on-object-storage
Diffstat (limited to 'spec')
206 files changed, 5352 insertions, 1735 deletions
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 9e696e9cb29..4dcb7dc6c87 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -122,10 +122,64 @@ describe Projects::BlobController do end context 'when essential params are present' do - it 'renders the diff content' do - do_get(since: 1, to: 5, offset: 10) + context 'when rendering for commit' do + it 'renders the diff content' do + do_get(since: 1, to: 5, offset: 10) - expect(response.body).to be_present + expect(response.body).to be_present + end + end + + context 'when rendering for merge request' do + it 'renders diff context lines Gitlab::Diff::Line array' do + do_get(since: 1, to: 5, offset: 10, from_merge_request: true) + + lines = JSON.parse(response.body) + + expect(lines.first).to have_key('type') + expect(lines.first).to have_key('rich_text') + expect(lines.first).to have_key('rich_text') + end + + context 'when rendering match lines' do + it 'adds top match line when "since" is less than 1' do + do_get(since: 5, to: 10, offset: 10, from_merge_request: true) + + match_line = JSON.parse(response.body).first + + expect(match_line['type']).to eq('match') + expect(match_line['meta_data']).to have_key('old_pos') + expect(match_line['meta_data']).to have_key('new_pos') + end + + it 'does not add top match line when when "since" is equal 1' do + do_get(since: 1, to: 10, offset: 10, from_merge_request: true) + + match_line = JSON.parse(response.body).first + + expect(match_line['type']).to eq('context') + end + + it 'adds bottom match line when "t"o is less than blob size' do + do_get(since: 1, to: 5, offset: 10, from_merge_request: true, bottom: true) + + match_line = JSON.parse(response.body).last + + expect(match_line['type']).to eq('match') + expect(match_line['meta_data']).to have_key('old_pos') + expect(match_line['meta_data']).to have_key('new_pos') + end + + it 'does not add bottom match line when "to" is less than blob size' do + commit_id = project.repository.commit('master').id + blob = project.repository.blob_at(commit_id, 'CHANGELOG') + do_get(since: 1, to: blob.lines.count, offset: 10, from_merge_request: true, bottom: true) + + match_line = JSON.parse(response.body).last + + expect(match_line['type']).to eq('context') + end + end end end end diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index 53647749a60..4aa33dbbb01 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -110,7 +110,7 @@ describe Projects::DiscussionsController do it "returns the name of the resolving user" do post :resolve, request_params - expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name) + expect(JSON.parse(response.body)['resolved_by']['name']).to eq(user.name) end it "returns status 200" do @@ -119,16 +119,21 @@ describe Projects::DiscussionsController do expect(response).to have_gitlab_http_status(200) end - context "when vue_mr_discussions cookie is present" do - before do - allow(controller).to receive(:cookies).and_return(vue_mr_discussions: 'true') - end + it "renders discussion with serializer" do + expect_any_instance_of(DiscussionSerializer).to receive(:represent) + .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true }) - it "renders discussion with serializer" do - expect_any_instance_of(DiscussionSerializer).to receive(:represent) - .with(instance_of(Discussion), { context: instance_of(described_class) }) + post :resolve, request_params + end + context 'diff discussion' do + let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + let(:discussion) { note.discussion } + + it "returns truncated diff lines" do post :resolve, request_params + + expect(JSON.parse(response.body)['truncated_diff_lines']).to be_present end end end @@ -187,7 +192,7 @@ describe Projects::DiscussionsController do it "renders discussion with serializer" do expect_any_instance_of(DiscussionSerializer).to receive(:represent) - .with(instance_of(Discussion), { context: instance_of(described_class) }) + .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true }) delete :unresolve, request_params end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 011843baffc..812833cc86b 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -29,7 +29,7 @@ describe Projects::ImportsController do context 'when import is in progress' do before do - project.update_attribute(:import_status, :started) + project.update_attributes(import_status: :started) end it 'renders template' do @@ -47,7 +47,7 @@ describe Projects::ImportsController do context 'when import failed' do before do - project.update_attribute(:import_status, :failed) + project.update_attributes(import_status: :failed) end it 'redirects to new_namespace_project_import_path' do @@ -59,7 +59,7 @@ describe Projects::ImportsController do context 'when import finished' do before do - project.update_attribute(:import_status, :finished) + project.update_attributes(import_status: :finished) end context 'when project is a fork' do @@ -108,7 +108,7 @@ describe Projects::ImportsController do context 'when import never happened' do before do - project.update_attribute(:import_status, :none) + project.update_attributes(import_status: :none) end it 'redirects to namespace_project_path' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 106611b37c9..3a41f0fc07a 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -990,7 +990,7 @@ describe Projects::IssuesController do it 'returns discussion json' do get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid - expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolved]) + expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion discussion_path individual_note resolvable resolved resolved_at resolved_by resolved_by_push commit_id for_commit project_id]) end context 'with cross-reference system note', :request_store do diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index 5d297c654bf..ec82b35f227 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -26,12 +26,13 @@ describe Projects::MergeRequests::DiffsController do context 'with default params' do context 'for the same project' do before do - go + allow(controller).to receive(:rendered_for_merge_request?).and_return(true) end - it 'renders the diffs template to a string' do - expect(response).to render_template('projects/merge_requests/diffs/_diffs') - expect(json_response).to have_key('html') + it 'serializes merge request diff collection' do + expect_any_instance_of(DiffsSerializer).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash)) + + go end end @@ -56,17 +57,6 @@ describe Projects::MergeRequests::DiffsController do end end - context 'with ignore_whitespace_change' do - before do - go(w: 1) - end - - it 'renders the diffs template to a string' do - expect(response).to render_template('projects/merge_requests/diffs/_diffs') - expect(json_response).to have_key('html') - end - end - context 'with view' do before do go(view: 'parallel') @@ -105,12 +95,11 @@ describe Projects::MergeRequests::DiffsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - diff_for_path(old_path: existing_path, new_path: existing_path) + + paths = JSON.parse(response.body)["diff_files"].map { |file| file['new_path'] } + + expect(paths).to include(existing_path) end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 22858de0475..7f5f0b76c51 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -234,7 +234,7 @@ describe Projects::MergeRequestsController do body = JSON.parse(response.body) expect(body['assignee'].keys) - .to match_array(%w(name username avatar_url)) + .to match_array(%w(name username avatar_url id state web_url)) end end @@ -337,7 +337,12 @@ describe Projects::MergeRequestsController do context 'when the sha parameter matches the source SHA' do def merge_with_sha(params = {}) - post :merge, base_params.merge(sha: merge_request.diff_head_sha).merge(params) + post_params = base_params.merge(sha: merge_request.diff_head_sha).merge(params) + if Gitlab.rails5? + post :merge, params: post_params, as: :json + else + post :merge, post_params + end end it 'returns :success' do diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index de132dfaa21..1458113b90c 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -51,7 +51,7 @@ describe Projects::NotesController do let(:project) { create(:project, :repository) } let!(:note) { create(:discussion_note_on_merge_request, project: project) } - let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } + let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) } it 'responds with the expected attributes' do get :index, params @@ -67,7 +67,7 @@ describe Projects::NotesController do let(:project) { create(:project, :repository) } let!(:note) { create(:diff_note_on_merge_request, project: project) } - let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } + let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) } it 'responds with the expected attributes' do get :index, params @@ -86,7 +86,7 @@ describe Projects::NotesController do context 'when displayed on a merge request' do let(:merge_request) { create(:merge_request, source_project: project) } - let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id) } + let(:params) { request_params.merge(target_type: 'merge_request', target_id: merge_request.id, html: true) } it 'responds with the expected attributes' do get :index, params @@ -99,7 +99,7 @@ describe Projects::NotesController do end context 'when displayed on the commit' do - let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id) } + let(:params) { request_params.merge(target_type: 'commit', target_id: note.commit_id, html: true) } it 'responds with the expected attributes' do get :index, params @@ -128,7 +128,7 @@ describe Projects::NotesController do context 'for a regular note' do let!(:note) { create(:note_on_merge_request, project: project) } - let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } + let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) } it 'responds with the expected attributes' do get :index, params @@ -293,7 +293,7 @@ describe Projects::NotesController do context 'when a noteable is not found' do it 'returns 404 status' do - request_params[:note][:noteable_id] = 9999 + request_params[:target_id] = 9999 post :create, request_params.merge(format: :json) expect(response).to have_gitlab_http_status(404) @@ -475,7 +475,7 @@ describe Projects::NotesController do end it "returns the name of the resolving user" do - post :resolve, request_params + post :resolve, request_params.merge(html: true) expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name) end diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb index 11f54eef531..8d2fa6a1740 100644 --- a/spec/controllers/projects/pages_controller_spec.rb +++ b/spec/controllers/projects/pages_controller_spec.rb @@ -71,7 +71,7 @@ describe Projects::PagesController do { namespace_id: project.namespace, project_id: project, - project: { pages_https_only: false } + project: { pages_https_only: 'false' } } end @@ -96,7 +96,7 @@ describe Projects::PagesController do it 'calls the update service' do expect(Projects::UpdateService) .to receive(:new) - .with(project, user, request_params[:project]) + .with(project, user, ActionController::Parameters.new(request_params[:project]).permit!) .and_return(update_service) patch :update, request_params diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index 3506305f755..4cdaa54e0bc 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -310,9 +310,19 @@ describe Projects::PipelineSchedulesController do end def go - put :update, namespace_id: project.namespace.to_param, - project_id: project, id: pipeline_schedule, - schedule: schedule + if Gitlab.rails5? + put :update, params: { namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule, + schedule: schedule }, + as: :html + + else + put :update, namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule, + schedule: schedule + end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 705b30f0130..90e698925b6 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -597,6 +597,22 @@ describe ProjectsController do expect(parsed_body["Tags"]).to include("v1.0.0") expect(parsed_body["Commits"]).to include("123456") end + + context "when preferred language is Japanese" do + before do + user.update!(preferred_language: 'ja') + sign_in(user) + end + + it "gets a list of branches, tags and commits" do + get :refs, namespace_id: public_project.namespace, id: public_project, ref: "123456" + + parsed_body = JSON.parse(response.body) + expect(parsed_body["Branches"]).to include("master") + expect(parsed_body["Tags"]).to include("v1.0.0") + expect(parsed_body["Commits"]).to include("123456") + end + end end describe 'POST #preview_markdown' do diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 555b186fe31..cdec26bd421 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -53,21 +53,22 @@ describe SessionsController do include UserActivitiesHelpers let(:user) { create(:user) } + let(:user_params) { { login: user.username, password: user.password } } it 'authenticates user correctly' do - post(:create, user: { login: user.username, password: user.password }) + post(:create, user: user_params) expect(subject.current_user). to eq user end it 'creates an audit log record' do - expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1) + expect { post(:create, user: user_params) }.to change { SecurityEvent.count }.by(1) expect(SecurityEvent.last.details[:with]).to eq('standard') end include_examples 'user login request with unique ip limit', 302 do def request - post(:create, user: { login: user.username, password: user.password }) + post(:create, user: user_params) expect(subject.current_user).to eq user subject.sign_out user end @@ -75,10 +76,40 @@ describe SessionsController do it 'updates the user activity' do expect do - post(:create, user: { login: user.username, password: user.password }) + post(:create, user: user_params) end.to change { user_activity(user) } end end + + context 'when reCAPTCHA is enabled' do + let(:user) { create(:user) } + let(:user_params) { { login: user.username, password: user.password } } + + before do + stub_application_setting(recaptcha_enabled: true) + request.headers[described_class::CAPTCHA_HEADER] = 1 + end + + it 'displays an error when the reCAPTCHA is not solved' do + # Without this, `verify_recaptcha` arbitraily returns true in test env + Recaptcha.configuration.skip_verify_env.delete('test') + + post(:create, user: user_params) + + expect(response).to render_template(:new) + expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + expect(subject.current_user).to be_nil + end + + it 'successfully logs in a user when reCAPTCHA is solved' do + # Avoid test ordering issue and ensure `verify_recaptcha` returns true + Recaptcha.configuration.skip_verify_env << 'test' + + post(:create, user: user_params) + + expect(subject.current_user).to eq user + end + end end context 'when using two-factor authentication via OTP' do @@ -257,15 +288,15 @@ describe SessionsController do end end - describe '#new' do + describe "#new" do before do set_devise_mapping(context: @request) end - it 'redirects correctly for referer on same host with params' do - search_path = '/search?search=seed_project' - allow(controller.request).to receive(:referer) - .and_return('http://%{host}%{path}' % { host: 'test.host', path: search_path }) + it "redirects correctly for referer on same host with params" do + host = "test.host" + search_path = "/search?search=seed_project" + request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}" get(:new, redirect_to_referer: :yes) diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index d5e603baeae..a4226d7a682 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -31,6 +31,7 @@ feature 'Admin Groups' do path_component = 'gitlab' group_name = 'GitLab group name' group_description = 'Description of group for GitLab' + fill_in 'group_path', with: path_component fill_in 'group_name', with: group_name fill_in 'group_description', with: group_description diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index ed47f7ed390..29280bd6e06 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -65,7 +65,11 @@ feature 'Dashboard Groups page', :js do fill_in 'filter', with: group.name wait_for_requests + expect(page).to have_content(group.name) + expect(page).not_to have_content(nested_group.parent.name) + fill_in 'filter', with: '' + page.find('[name="filter"]').send_keys(:enter) wait_for_requests expect(page).to have_content(group.name) diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 801a33979ff..ad02b454aee 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -35,7 +35,11 @@ describe 'Explore Groups page', :js do fill_in 'filter', with: group.name wait_for_requests + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(public_group.full_name) + fill_in 'filter', with: "" + page.find('[name="filter"]').send_keys(:enter) wait_for_requests expect(page).to have_content(group.full_name) diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb index 5d6cd44ad1c..90d02f7e40f 100644 --- a/spec/features/ics/dashboard_issues_spec.rb +++ b/spec/features/ics/dashboard_issues_spec.rb @@ -11,13 +11,25 @@ describe 'Dashboard Issues Calendar Feed' do end context 'when authenticated' do - it 'renders calendar feed' do - sign_in user - visit issues_dashboard_path(:ics) + context 'with no referer' do + it 'renders calendar feed' do + sign_in user + visit issues_dashboard_path(:ics) - expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') - expect(body).to have_text('BEGIN:VCALENDAR') + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'with GitLab as the referer' do + it 'renders calendar feed as text/plain' do + sign_in user + page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url)) + visit issues_dashboard_path(:ics) + + expect(response_headers['Content-Type']).to have_content('text/plain') + expect(body).to have_text('BEGIN:VCALENDAR') + end end end @@ -28,7 +40,6 @@ describe 'Dashboard Issues Calendar Feed' do visit issues_dashboard_path(:ics, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end @@ -38,7 +49,6 @@ describe 'Dashboard Issues Calendar Feed' do visit issues_dashboard_path(:ics, feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb index 0a049be2ffe..24de5b4b7c6 100644 --- a/spec/features/ics/group_issues_spec.rb +++ b/spec/features/ics/group_issues_spec.rb @@ -13,13 +13,25 @@ describe 'Group Issues Calendar Feed' do end context 'when authenticated' do - it 'renders calendar feed' do - sign_in user - visit issues_group_path(group, :ics) + context 'with no referer' do + it 'renders calendar feed' do + sign_in user + visit issues_group_path(group, :ics) - expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') - expect(body).to have_text('BEGIN:VCALENDAR') + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'with GitLab as the referer' do + it 'renders calendar feed as text/plain' do + sign_in user + page.driver.header('Referer', issues_group_url(group, host: Settings.gitlab.base_url)) + visit issues_group_path(group, :ics) + + expect(response_headers['Content-Type']).to have_content('text/plain') + expect(body).to have_text('BEGIN:VCALENDAR') + end end end @@ -30,7 +42,6 @@ describe 'Group Issues Calendar Feed' do visit issues_group_path(group, :ics, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end @@ -40,7 +51,6 @@ describe 'Group Issues Calendar Feed' do visit issues_group_path(group, :ics, feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb index b99e9607f1d..2ca3d52a5be 100644 --- a/spec/features/ics/project_issues_spec.rb +++ b/spec/features/ics/project_issues_spec.rb @@ -12,13 +12,25 @@ describe 'Project Issues Calendar Feed' do end context 'when authenticated' do - it 'renders calendar feed' do - sign_in user - visit project_issues_path(project, :ics) + context 'with no referer' do + it 'renders calendar feed' do + sign_in user + visit project_issues_path(project, :ics) - expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') - expect(body).to have_text('BEGIN:VCALENDAR') + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'with GitLab as the referer' do + it 'renders calendar feed as text/plain' do + sign_in user + page.driver.header('Referer', project_issues_url(project, host: Settings.gitlab.base_url)) + visit project_issues_path(project, :ics) + + expect(response_headers['Content-Type']).to have_content('text/plain') + expect(body).to have_text('BEGIN:VCALENDAR') + end end end @@ -29,7 +41,6 @@ describe 'Project Issues Calendar Feed' do visit project_issues_path(project, :ics, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end @@ -39,7 +50,6 @@ describe 'Project Issues Calendar Feed' do visit project_issues_path(project, :ics, feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb index fa0ab88624e..8eaccfc0949 100644 --- a/spec/features/issuables/markdown_references/jira_spec.rb +++ b/spec/features/issuables/markdown_references/jira_spec.rb @@ -163,7 +163,7 @@ describe "Jira", :js do HEREDOC page.within("#diff-notes-app") do - fill_in("note_note", with: markdown) + fill_in("note-body", with: markdown) end end diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb index e25fd1a6249..0a19086ffbd 100644 --- a/spec/features/issuables/shortcuts_issuable_spec.rb +++ b/spec/features/issuables/shortcuts_issuable_spec.rb @@ -12,6 +12,15 @@ feature 'Blob shortcuts', :js do sign_in(user) end + shared_examples "quotes the selected text" do + it "quotes the selected text" do + select_element('.note-text') + find('body').native.send_key('r') + + expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text) + end + end + describe 'pressing "r"' do describe 'On an Issue' do before do @@ -20,12 +29,7 @@ feature 'Blob shortcuts', :js do wait_for_requests end - it 'quotes the selected text' do - select_element('.note-text') - find('body').native.send_key('r') - - expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text) - end + include_examples 'quotes the selected text' end describe 'On a Merge Request' do @@ -35,12 +39,7 @@ feature 'Blob shortcuts', :js do wait_for_requests end - it 'quotes the selected text' do - select_element('.note-text') - find('body').native.send_key('r') - - expect(find('.js-main-target-form #note_note').value).to include(note_text) - end + include_examples 'quotes the selected text' end end end diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index e0466aaf422..52962002c33 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -6,6 +6,12 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d let(:merge_request) { create(:merge_request, source_project: project) } let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion } + def resolve_all_discussions_link_selector + text = "Resolve all discussions in new issue" + url = new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) + %Q{a[data-original-title="#{text}"][href="#{url}"]} + end + describe 'as a user with access to the project' do before do project.add_master(user) @@ -14,8 +20,8 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d end it 'shows a button to resolve all discussions by creating a new issue' do - within('#resolve-count-app') do - expect(page).to have_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) + within('.line-resolve-all-container') do + expect(page).to have_selector resolve_all_discussions_link_selector end end @@ -25,13 +31,13 @@ feature 'Resolving all open discussions in a merge request from an issue', :js d end it 'hides the link for creating a new issue' do - expect(page).not_to have_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) + expect(page).not_to have_selector resolve_all_discussions_link_selector end end context 'creating an issue for discussions' do before do - click_link "Resolve all discussions in new issue", href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) + find(resolve_all_discussions_link_selector).click end it_behaves_like 'creating an issue for a discussion' diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb index 34beb282bad..9170f9295f0 100644 --- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb @@ -1,11 +1,17 @@ require 'rails_helper' -feature 'Resolve an open discussion in a merge request by creating an issue' do +feature 'Resolve an open discussion in a merge request by creating an issue', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) } let(:merge_request) { create(:merge_request, source_project: project) } let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion } + def resolve_discussion_selector + title = 'Resolve this discussion in a new issue' + url = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid) + "a[data-original-title=\"#{title}\"][href=\"#{url}\"]" + end + describe 'As a user with access to the project' do before do project.add_master(user) @@ -20,17 +26,17 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do end it 'does not show a link to create a new issue' do - expect(page).not_to have_link 'Resolve this discussion in a new issue' + expect(page).not_to have_css resolve_discussion_selector end end - context 'resolving the discussion', :js do + context 'resolving the discussion' do before do click_button 'Resolve discussion' end it 'hides the link for creating a new issue' do - expect(page).not_to have_link 'Resolve this discussion in a new issue' + expect(page).not_to have_css resolve_discussion_selector end it 'shows the link for creating a new issue when unresolving a discussion' do @@ -38,19 +44,17 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do click_button 'Unresolve discussion' end - expect(page).to have_link 'Resolve this discussion in a new issue' + expect(page).to have_css resolve_discussion_selector end end it 'has a link to create a new issue for a discussion' do - new_issue_link = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid) - - expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link + expect(page).to have_css resolve_discussion_selector end context 'creating the issue' do before do - click_link 'Resolve this discussion in a new issue', href: new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid) + find(resolve_discussion_selector).click end it 'has a hidden field for the discussion' do diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index bc42618306f..8dca81a8627 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -10,6 +10,7 @@ describe 'Filter issues', :js do # When the name is longer, the filtered search input can end up scrolling # horizontally, and PhantomJS can't handle it. let(:user) { create(:user, name: 'Ann') } + let(:user2) { create(:user, name: 'jane') } let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') } @@ -25,8 +26,6 @@ describe 'Filter issues', :js do before do project.add_master(user) - user2 = create(:user) - create(:issue, project: project, author: user2, title: "Bug report 1") create(:issue, project: project, author: user2, title: "Bug report 2") @@ -113,6 +112,24 @@ describe 'Filter issues', :js do expect_issues_list_count(3) expect_filtered_search_input_empty end + + it 'filters issues by invalid assignee' do + skip('to be tested, issue #26546') + end + + it 'filters issues by multiple assignees' do + create(:issue, project: project, author: user, assignees: [user2, user]) + + input_filtered_search("assignee:@#{user.username} assignee:@#{user2.username}") + + expect_tokens([ + assignee_token(user.name), + assignee_token(user2.name) + ]) + + expect_issues_list_count(1) + expect_filtered_search_input_empty + end end end @@ -491,6 +508,21 @@ describe 'Filter issues', :js do it_behaves_like 'updates atom feed link', :group do let(:path) { issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) } end + + it 'updates atom feed link for group issues' do + visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a[title="Subscribe to RSS feed"]', visible: false) + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + + expect(params).to include('feed_token' => [user.feed_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('feed_token' => [user.feed_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end end context 'URL has a trailing slash' do diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index 25c408516d1..728e89db400 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -114,7 +114,8 @@ feature 'Merge request > User creates image diff notes', :js do create_image_diff_note end - it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do indicator = find('.js-image-badge', match: :first) badge = find('.image-diff-avatar-link .badge', match: :first) @@ -156,7 +157,8 @@ feature 'Merge request > User creates image diff notes', :js do visit project_merge_request_path(project, merge_request) end - it 'render diff indicators within the image frame' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'render diff indicators within the image frame' do diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) wait_for_requests diff --git a/spec/features/merge_request/user_locks_discussion_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb index a68df872334..76c759ab8d3 100644 --- a/spec/features/merge_request/user_locks_discussion_spec.rb +++ b/spec/features/merge_request/user_locks_discussion_spec.rb @@ -38,9 +38,9 @@ describe 'Merge request > User locks discussion', :js do end it 'the user can not create a comment' do - page.within('.issuable-discussion #notes') do + page.within('.js-vue-notes-event') do expect(page).not_to have_selector('js-main-target-form') - expect(page.find('.disabled-comment')) + expect(page.find('.issuable-note-warning')) .to have_content('This merge request is locked. Only project members can comment.') end end diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index 2b4623d6dc9..13cc5f256eb 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -65,11 +65,13 @@ describe 'Merge request > User posts diff notes', :js do 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') + line_holder = find('.match', match: :first).find(:xpath, '..') + should_not_allow_commenting(line_holder, 'left') end it 'does not allow commenting on the right side' do - should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right') + line_holder = find('.match', match: :first).find(:xpath, '..') + should_not_allow_commenting(line_holder, 'right') end end @@ -81,7 +83,7 @@ describe 'Merge request > User posts diff notes', :js do # The first `.js-unfold` unfolds upwards, therefore the first # `.line_holder` will be an unfolded line. - let(:line_holder) { first('.line_holder[id="1"]') } + let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') } it 'does not allow commenting on the left side' do should_not_allow_commenting(line_holder, 'left') @@ -143,7 +145,7 @@ describe 'Merge request > User posts diff notes', :js do # The first `.js-unfold` unfolds upwards, therefore the first # `.line_holder` will be an unfolded line. - let(:line_holder) { first('.line_holder[id="1"]') } + let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') } it 'does not allow commenting' do should_not_allow_commenting line_holder @@ -183,7 +185,7 @@ describe 'Merge request > User posts diff notes', :js do end describe 'posting a note' do - it 'adds as discussion' do + xit '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) @@ -201,20 +203,23 @@ describe 'Merge request > User posts diff notes', :js do end context 'with a new line' do - it 'allows commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]').find(:xpath, '..')) end end context 'with an old line' do - it 'allows commenting' do - should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'allows commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]').find(:xpath, '..')) end end context 'with an unchanged line' do - it 'allows commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]').find(:xpath, '..')) end end diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index 3bd9f5e2298..fa819cbc385 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -24,10 +24,9 @@ describe 'Merge request > User posts notes', :js do 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') + expect(find('.js-main-target-form')).to have_selector('button', text: 'Comment') page.within('.js-main-target-form') do - expect(page).not_to have_link('Cancel') + expect(page).not_to have_button('Cancel') end end @@ -60,8 +59,9 @@ describe 'Merge request > User posts notes', :js 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) + expect(page).to have_css('.js-vue-md-preview', visible: :hidden) end + wait_for_requests page.within('.js-main-target-form') do is_expected.to have_css('.js-note-text', visible: true) end @@ -76,6 +76,7 @@ describe 'Merge request > User posts notes', :js do end it 'hides the toolbar buttons when previewing a note' do + wait_for_requests find('.js-md-preview-button').click page.within('.js-main-target-form') do expect(page).not_to have_css('.md-header-toolbar.active') @@ -84,11 +85,6 @@ describe 'Merge request > User posts notes', :js do 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 @@ -108,8 +104,8 @@ describe 'Merge request > User posts notes', :js 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 + expect(find('.js-note-text').text).to eq '' end it 'allows using markdown buttons after saving a note and then trying to edit it again' do @@ -118,8 +114,8 @@ describe 'Merge request > User posts notes', :js do find('.btn-save').click end - wait_for_requests find('.note').hover + wait_for_requests find('.js-note-edit').click @@ -151,13 +147,15 @@ describe 'Merge request > User posts notes', :js do find('.js-note-edit').click end - it 'shows the delete link' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit '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 + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'removes the attachment div and resets the edit form' do accept_confirm { find('.js-note-attachment-delete').click } is_expected.not_to have_css('.note-attachment') is_expected.not_to have_css('.current-note-edit-form') diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb index 59aa90fc86f..629052442b4 100644 --- a/spec/features/merge_request/user_resolves_conflicts_spec.rb +++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb @@ -44,7 +44,9 @@ describe 'Merge request > User resolves conflicts', :js do within find('.diff-file', text: 'files/ruby/regex.rb') do expect(page).to have_selector('.line_content.new', text: "def username_regexp") + expect(page).not_to have_selector('.line_content.new', text: "def username_regex") expect(page).to have_selector('.line_content.new', text: "def project_name_regexp") + expect(page).not_to have_selector('.line_content.new', text: "def project_name_regex") expect(page).to have_selector('.line_content.new', text: "def path_regexp") expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp") expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp") @@ -108,8 +110,12 @@ describe 'Merge request > User resolves conflicts', :js do click_link('conflicts', href: %r{/conflicts\Z}) end - include_examples "conflicts are resolved in Interactive mode" - include_examples "conflicts are resolved in Edit inline mode" + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + # include_examples "conflicts are resolved in Interactive mode" + # include_examples "conflicts are resolved in Edit inline mode" + + it 'prevents RSpec/EmptyExampleGroup' do + end end context 'in Parallel view mode' do @@ -118,8 +124,12 @@ describe 'Merge request > User resolves conflicts', :js do click_button 'Side-by-side' end - include_examples "conflicts are resolved in Interactive mode" - include_examples "conflicts are resolved in Edit inline mode" + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + # include_examples "conflicts are resolved in Interactive mode" + # include_examples "conflicts are resolved in Edit inline mode" + + it 'prevents RSpec/EmptyExampleGroup' do + end end end @@ -138,7 +148,8 @@ describe 'Merge request > User resolves conflicts', :js do end end - it 'conflicts are resolved in Edit inline mode' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'conflicts are resolved in Edit inline mode' do within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do wait_for_requests find('.files-wrapper .diff-file pre') diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 0fd2840c426..a0b9d6cb059 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -102,7 +102,8 @@ describe 'Merge request > User resolves diff notes and discussions', :js do describe 'timeline view' do it 'hides when resolve discussion is clicked' do - expect(page).to have_selector('.discussion-body', visible: false) + expect(page).to have_selector('.discussion-header') + expect(page).not_to have_selector('.discussion-body') end it 'shows resolved discussion when toggled' do @@ -129,7 +130,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end it 'hides when resolve discussion is clicked' do - expect(page).to have_selector('.diffs .diff-file .notes_holder', visible: false) + expect(page).not_to have_selector('.diffs .diff-file .notes_holder') end it 'shows resolved discussion when toggled' do @@ -218,10 +219,13 @@ describe 'Merge request > User resolves diff notes and discussions', :js do it 'updates updated text after resolving note' do page.within '.diff-content .note' do - find('.line-resolve-btn').click - end + resolve_button = find('.line-resolve-btn') + + resolve_button.click + wait_for_requests - expect(page).to have_content("Resolved by #{user.name}") + expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}") + end end it 'hides jump to next discussion button' do @@ -254,11 +258,16 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end it 'resolves discussion' do - page.all('.note .line-resolve-btn').each do |button| + resolve_buttons = page.all('.note .line-resolve-btn', count: 2) + resolve_buttons.each do |button| button.click end - expect(page).to have_content('Resolved by') + wait_for_requests + + resolve_buttons.each do |button| + expect(button['data-original-title']).to eq("Resolved by #{user.name}") + end page.within '.line-resolve-all-container' do expect(page).to have_content('1/1 discussion resolved') @@ -287,7 +296,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end it 'allows user to mark all notes as resolved' do - page.all('.line-resolve-btn').each do |btn| + page.all('.note .line-resolve-btn', count: 2).each do |btn| btn.click end @@ -298,7 +307,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end it 'allows user user to mark all discussions as resolved' do - page.all('.discussion-reply-holder').each do |reply_holder| + page.all('.discussion-reply-holder', count: 2).each do |reply_holder| page.within reply_holder do click_button 'Resolve discussion' end @@ -311,7 +320,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end it 'allows user to quickly scroll to next unresolved discussion' do - page.within first('.discussion-reply-holder') do + page.within('.discussion-reply-holder', match: :first) do click_button 'Resolve discussion' end @@ -323,19 +332,22 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end it 'updates updated text after resolving note' do - page.within first('.diff-content .note') do - find('.line-resolve-btn').click - end + page.within('.diff-content .note', match: :first) do + resolve_button = find('.line-resolve-btn') - expect(page).to have_content("Resolved by #{user.name}") + resolve_button.click + wait_for_requests + + expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}") + end end it 'shows jump to next discussion button' do - expect(page.all('.discussion-reply-holder')).to all(have_selector('.discussion-next-btn')) + expect(page.all('.discussion-reply-holder', count: 2)).to all(have_selector('.discussion-next-btn')) end it 'displays next discussion even if hidden' do - page.all('.note-discussion').each do |discussion| + page.all('.note-discussion', count: 2).each do |discussion| page.within discussion do click_button 'Toggle discussion' end diff --git a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb index 9ba9e8b9585..fdf9a84e997 100644 --- a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb +++ b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb @@ -63,7 +63,7 @@ feature 'Merge request > User resolves outdated diff discussions', :js do it 'shows that as automatically resolved' do within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do - expect(page).to have_css('.discussion-body', visible: false) + expect(page).not_to have_css('.discussion-body') expect(page).to have_content('Automatically resolved') end end diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb index 3b6fffb7abd..8c2599615cb 100644 --- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb +++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb @@ -17,11 +17,12 @@ describe 'Merge request > User scrolls to note on load', :js do it 'scrolls note into view' do visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}" + wait_for_requests + page_height = page.current_window.size[1] page_scroll_y = page.evaluate_script("window.scrollY") fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)") - expect(find('.js-toggle-content').visible?).to eq true expect(find(fragment_id).visible?).to eq true expect(fragment_position_top).to be >= page_scroll_y expect(fragment_position_top).to be < (page_scroll_y + page_height) @@ -35,7 +36,7 @@ describe 'Merge request > User scrolls to note on load', :js do page.execute_script "window.scrollTo(0,0)" note_element = find(fragment_id) - note_container = note_element.ancestor('.js-toggle-container') + note_container = note_element.ancestor('.js-discussion-container') expect(note_element.visible?).to eq true @@ -44,10 +45,11 @@ describe 'Merge request > User scrolls to note on load', :js do end end - it 'expands collapsed notes' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'expands collapsed notes' do visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}" note_element = find(collapsed_fragment_id) - note_container = note_element.ancestor('.js-toggle-container') + note_container = note_element.ancestor('.timeline-content') expect(note_element.visible?).to eq true expect(note_container.find('.line_content.noteable_line.old', match: :first).visible?).to eq true diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index 9c0a04405a6..0a8296bd722 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -35,7 +35,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do expect(page).not_to have_selector('.diff-comment-avatar-holders') end - it 'does not render avatars after commening on discussion tab' do + it 'does not render avatars after commenting on discussion tab' do click_button 'Reply...' page.within('.js-discussion-note-form') do @@ -75,7 +75,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do end end - %w(inline parallel).each do |view| + %w(parallel).each do |view| context "#{view} view" do before do visit diffs_project_merge_request_path(project, merge_request, view: view) @@ -104,7 +104,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do find('.diff-notes-collapse').send_keys(:return) end - expect(page).to have_selector('.notes_holder', visible: false) + expect(page).not_to have_selector('.notes_holder') page.within find_line(position.line_code(project.repository)) do first('img.js-diff-comment-avatar').click diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb index a9063f2bcb3..d6e7ff33d5d 100644 --- a/spec/features/merge_request/user_sees_diff_spec.rb +++ b/spec/features/merge_request/user_sees_diff_spec.rb @@ -6,20 +6,6 @@ describe 'Merge request > User sees diff', :js do let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } - context 'when visit with */* as accept header' do - it 'renders the notes' do - create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master' - - inspect_requests(inject_headers: { 'Accept' => '*/*' }) do - visit diffs_project_merge_request_path(project, merge_request) - end - - # Load notes and diff through AJAX - expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master') - expect(page).to have_css('.diffs.tab-pane.active') - end - end - context 'when linking to note' do describe 'with unresolved note' do let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request } @@ -51,6 +37,7 @@ describe 'Merge request > User sees diff', :js do context 'when merge request has overflow' do it 'displays warning' do allow(Commit).to receive(:max_diff_options).and_return(max_files: 3) + allow_any_instance_of(DiffHelper).to receive(:render_overflow_warning?).and_return(true) visit diffs_project_merge_request_path(project, merge_request) diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb index d6e8c8e86ba..10390bd5864 100644 --- a/spec/features/merge_request/user_sees_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Merge request > User sees discussions' do +describe 'Merge request > User sees discussions', :js do let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } @@ -53,11 +53,13 @@ describe 'Merge request > User sees discussions' do shared_examples 'a functional discussion' do let(:discussion_id) { note.discussion_id(merge_request) } - it 'is displayed' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'is displayed' do expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']") end - it 'can be replied to' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'can be replied to' do within(".discussion[data-discussion-id='#{discussion_id}']") do click_button 'Reply...' fill_in 'note[note]', with: 'Test!' diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index d3104b448e0..0272d300e06 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -31,7 +31,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do create(:ci_build, :manual, pipeline: pipeline, when: 'manual') end - it 'avoids repeated database queries' do + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + xit 'avoids repeated database queries' do before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2) diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb index b4cda269852..d4ad0b0a377 100644 --- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb +++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb @@ -25,7 +25,7 @@ describe 'Merge request > User sees notes from forked project', :js do page.within('.discussion-notes') do find('.btn-text-field').click find('#note_note').send_keys('A reply comment') - find('.comment-btn').click + find('.js-comment-button').click end wait_for_requests diff --git a/spec/features/merge_request/user_sees_system_notes_spec.rb b/spec/features/merge_request/user_sees_system_notes_spec.rb index a00a682757d..c6811d4161a 100644 --- a/spec/features/merge_request/user_sees_system_notes_spec.rb +++ b/spec/features/merge_request/user_sees_system_notes_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Merge request > User sees system notes' do +describe 'Merge request > User sees system notes', :js do let(:public_project) { create(:project, :public, :repository) } let(:private_project) { create(:project, :private, :repository) } let(:user) { private_project.creator } diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index 3a15d70979a..11e0806ba62 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -143,9 +143,9 @@ describe 'Merge request > User sees versions', :js do end it_behaves_like 'allows commenting', - file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44', - line_code: '4_4', - comment: 'Typo, please fix.' + file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44', + line_code: '4_4', + comment: 'Typo, please fix.' end describe 'compare with same version' do diff --git a/spec/features/merge_request/user_uses_slash_commands_spec.rb b/spec/features/merge_request/user_uses_slash_commands_spec.rb index 7f261b580f7..83ad4b45b5a 100644 --- a/spec/features/merge_request/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_request/user_uses_slash_commands_spec.rb @@ -22,16 +22,24 @@ describe 'Merge request > User uses quick actions', :js do before do project.add_master(user) - sign_in(user) - visit project_merge_request_path(project, merge_request) end describe 'time tracking' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + it_behaves_like 'issuable time tracker' end describe 'toggling the WIP prefix in the title from note' do context 'when the current user can toggle the WIP prefix' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + it 'adds the WIP: prefix to the title' do add_note("/wip") @@ -56,7 +64,6 @@ describe 'Merge request > User uses quick actions', :js do context 'when the current user cannot toggle the WIP prefix' do before do project.add_guest(guest) - sign_out(:user) sign_in(guest) visit project_merge_request_path(project, merge_request) end @@ -74,6 +81,11 @@ describe 'Merge request > User uses quick actions', :js do describe 'merging the MR from the note' do context 'when the current user can merge the MR' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + it 'merges the MR' do add_note("/merge") @@ -87,6 +99,8 @@ describe 'Merge request > User uses quick actions', :js do before do merge_request.source_branch = 'another_branch' merge_request.save + sign_in(user) + visit project_merge_request_path(project, merge_request) end it 'does not merge the MR' do @@ -101,7 +115,6 @@ describe 'Merge request > User uses quick actions', :js do context 'when the current user cannot merge the MR' do before do project.add_guest(guest) - sign_out(:user) sign_in(guest) visit project_merge_request_path(project, merge_request) end @@ -117,6 +130,11 @@ describe 'Merge request > User uses quick actions', :js do end describe 'adding a due date from note' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + it 'does not recognize the command nor create a note' do add_note('/due 2016-08-28') @@ -129,7 +147,6 @@ describe 'Merge request > User uses quick actions', :js do let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } before do - sign_out(:user) another_project.add_master(user) sign_in(user) end @@ -161,6 +178,11 @@ describe 'Merge request > User uses quick actions', :js do describe '/target_branch command from note' do context 'when the current user can change target branch' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + it 'changes target branch from a note' do add_note("message start \n/target_branch merge-test\n message end.") @@ -184,7 +206,6 @@ describe 'Merge request > User uses quick actions', :js do context 'when current user can not change target branch' do before do project.add_guest(guest) - sign_out(:user) sign_in(guest) visit project_merge_request_path(project, merge_request) end diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index 96f6df587e1..b3bb8c48b4a 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -14,10 +14,10 @@ feature 'Member autocomplete', :js do shared_examples "open suggestions when typing @" do |resource_name| before do page.within('.new-note') do - if resource_name == 'issue' - find('#note-body').send_keys('@') - else + if resource_name == 'commit' find('#note_note').send_keys('@') + else + find('#note-body').send_keys('@') end end end diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index 215b658eb7b..95947d2f111 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -67,4 +67,6 @@ def update_username(new_username) page.within('.modal') do find('.js-modal-primary-action').click end + + wait_for_requests end diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb new file mode 100644 index 00000000000..6397a8ad845 --- /dev/null +++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb @@ -0,0 +1,109 @@ +require "spec_helper" + +describe "User comments on commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:comment_text) { "XML attached" } + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + end + + context "when adding new comment" do + it "adds comment" do + emoji_code = ":+1:" + + page.within(".js-main-target-form") do + expect(page).not_to have_link("Cancel") + + fill_in("note[note]", with: "#{comment_text} #{emoji_code}") + + # Check on `Preview` tab + click_link("Preview") + + expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji") + expect(page).not_to have_css(".js-note-text") + + # Check on `Write` tab + click_link("Write") + + expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji_code}") + + # Submit comment from the `Preview` tab to get rid of a separate `it` block + # which would specially tests if everything gets cleared from the note form. + click_link("Preview") + click_button("Comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(comment_text).and have_css("gl-emoji") + end + + page.within(".js-main-target-form") do + expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") + end + end + end + + context "when editing comment" do + before do + add_note(comment_text) + end + + it "edits comment" do + new_comment_text = "+1 Awesome!" + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + note.find(".js-note-edit").click + end + + page.find(".current-note-edit-form textarea") + + page.within(".current-note-edit-form") do + fill_in("note[note]", with: new_comment_text) + click_button("Save comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(new_comment_text) + end + end + end + + context "when deleting comment" do + before do + add_note(comment_text) + end + + it "deletes comment" do + page.within(".note") do + expect(page).to have_content(comment_text) + end + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + find(".more-actions").click + find(".more-actions .dropdown-menu li", match: :first) + + accept_confirm { find(".js-note-delete").click } + end + + expect(page).not_to have_css(".note") + end + end +end diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb index e3f90a78cb5..1828b60fec7 100644 --- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb +++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb @@ -91,7 +91,7 @@ describe 'User comments on a diff', :js do # Check the same comments in the side-by-side view. execute_script("window.scrollTo(0,0);") - click_link('Side-by-side') + click_button 'Side-by-side' wait_for_requests @@ -120,7 +120,7 @@ describe 'User comments on a diff', :js do click_button('Comment') end - page.within('.diff-file:nth-of-type(5) .note') do + page.within('.diff-file:nth-of-type(5) .discussion .note') do find('.js-note-edit').click page.within('.current-note-edit-form') do @@ -131,7 +131,7 @@ describe 'User comments on a diff', :js do expect(page).not_to have_button('Save comment', disabled: true) end - page.within('.diff-file:nth-of-type(5) .note') do + page.within('.diff-file:nth-of-type(5) .discussion .note') do expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong') end end @@ -150,7 +150,7 @@ describe 'User comments on a diff', :js do expect(page).to have_content('1') end - page.within('.diff-file:nth-of-type(5) .note') do + page.within('.diff-file:nth-of-type(5) .discussion .note') do find('.more-actions').click find('.more-actions .dropdown-menu li', match: :first) diff --git a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb index 2eb652147ce..f90aaba3caf 100644 --- a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb @@ -16,7 +16,7 @@ describe 'User comments on a merge request', :js do it 'adds a comment' do page.within('.js-main-target-form') do - fill_in(:note_note, with: '# Comment with a header') + fill_in('note[note]', with: '# Comment with a header') click_button('Comment') end @@ -32,7 +32,6 @@ describe 'User comments on a merge request', :js do # Add new comment in background in order to check # if it's going to be loaded automatically for current user. create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: user, note: 'Line is wrong') - # Trigger a refresh of notes. execute_script("$(document).trigger('visibilitychange');") wait_for_requests diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb index f3e97bc9eb2..67b6aefb2d8 100644 --- a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb @@ -13,6 +13,8 @@ describe 'User reverts a merge request', :js do click_button('Merge') + wait_for_requests + visit(merge_request_path(merge_request)) end diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/projects/merge_requests/user_views_diffs_spec.rb index d36aafdbc54..b1bfe9e5de3 100644 --- a/spec/features/projects/merge_requests/user_views_diffs_spec.rb +++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb @@ -16,7 +16,7 @@ describe 'User views diffs', :js do it 'unfolds diffs' do first('.js-unfold').click - expect(first('.text-file')).to have_content('.bundle') + expect(find('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"] .text-file')).to have_content('.bundle') end end @@ -36,7 +36,7 @@ describe 'User views diffs', :js do context 'when in the side-by-side view' do before do - click_link('Side-by-side') + click_button 'Side-by-side' wait_for_requests end @@ -45,6 +45,14 @@ describe 'User views diffs', :js do expect(page).to have_css('.parallel') end + it 'toggles container class' do + expect(page).not_to have_css('.content-wrapper > .container-fluid.container-limited') + + click_link 'Commits' + + expect(page).to have_css('.content-wrapper > .container-fluid.container-limited') + end + include_examples 'unfold diffs' end end diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index 7f547a4ca1f..84ec32b3fac 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -59,7 +59,9 @@ describe 'View on environment', :js do it 'has a "View on env" button' do within '.diffs' do - expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + text = 'View on feature.review.example.com' + url = 'http://feature.review.example.com/ruby/feature' + expect(page).to have_selector("a[data-original-title='#{text}'][href='#{url}']") end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index f1ae2c7ab65..232f35c86f9 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -133,7 +133,7 @@ describe NotesFinder do it 'raises an exception for an invalid target_type' do params[:target_type] = 'invalid' - expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') + expect { described_class.new(project, user, params).execute }.to raise_error("invalid target_type '#{params[:target_type]}'") end it 'filters out old notes' do diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index f7bc137c90c..cf257ac00de 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -14,7 +14,21 @@ "subscribed": { "type": ["boolean", "null"] }, "participants": { "type": "array" }, "allow_collaboration": { "type": "boolean"}, - "allow_maintainer_to_push": { "type": "boolean"} + "allow_maintainer_to_push": { "type": "boolean"}, + "assignee": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user.json" } + ] + }, + "milestone": { + "type": [ "object", "null" ] + }, + "labels": { + "type": [ "array", "null" ] + }, + "task_status": { "type": "string" }, + "task_status_short": { "type": "string" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index ee5588fa6c6..38ce92a5dc7 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -109,6 +109,7 @@ "ff_only_enabled": { "type": ["boolean", false] }, "should_be_rebased": { "type": "boolean" }, "create_note_path": { "type": ["string", "null"] }, + "preview_note_path": { "type": ["string", "null"] }, "rebase_commit_sha": { "type": ["string", "null"] }, "rebase_in_progress": { "type": "boolean" }, "can_push_to_source_branch": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/branch.json b/spec/fixtures/api/schemas/public_api/v4/branch.json index a3581178974..a8891680d06 100644 --- a/spec/fixtures/api/schemas/public_api/v4/branch.json +++ b/spec/fixtures/api/schemas/public_api/v4/branch.json @@ -14,7 +14,8 @@ "merged": { "type": "boolean" }, "protected": { "type": "boolean" }, "developers_can_push": { "type": "boolean" }, - "developers_can_merge": { "type": "boolean" } + "developers_can_merge": { "type": "boolean" }, + "can_push": { "type": "boolean" } }, "additionalProperties": false } diff --git a/spec/javascripts/.eslintrc.yml b/spec/javascripts/.eslintrc.yml index 8bceb2c50fc..78e2f3b521f 100644 --- a/spec/javascripts/.eslintrc.yml +++ b/spec/javascripts/.eslintrc.yml @@ -32,3 +32,7 @@ rules: - branch no-console: off prefer-arrow-callback: off + import/no-unresolved: + - error + - ignore: + - 'fixtures/blob' diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index 5dbdcd24296..068b8eb65bc 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */ +/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */ import $ from 'jquery'; import 'vendor/jquery.endless-scroll'; diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index e81055bc08f..ada26b37f4a 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ +/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, no-unused-vars, prefer-template, max-len */ import $ from 'jquery'; import Cookies from 'js-cookie'; @@ -21,20 +21,21 @@ import '~/lib/utils/common_utils'; return setTimeout(function() { assertFn(); return done(); - // Maybe jasmine.clock here? + // Maybe jasmine.clock here? }, 333); }; describe('AwardsHandler', function() { - preloadFixtures('merge_requests/diff_comment.html.raw'); + preloadFixtures('snippets/show.html.raw'); beforeEach(function(done) { - loadFixtures('merge_requests/diff_comment.html.raw'); - $('body').attr('data-page', 'projects:merge_requests:show'); - loadAwardsHandler(true).then((obj) => { - awardsHandler = obj; - spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); - done(); - }).catch(fail); + loadFixtures('snippets/show.html.raw'); + loadAwardsHandler(true) + .then(obj => { + awardsHandler = obj; + spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); + done(); + }) + .catch(fail); let isEmojiMenuBuilt = false; openAndWaitForEmojiMenu = function() { @@ -42,7 +43,9 @@ import '~/lib/utils/common_utils'; if (isEmojiMenuBuilt) { resolve(); } else { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); const $menu = $('.emoji-menu'); $menu.one('build-emoji-menu-finish', () => { isEmojiMenuBuilt = true; @@ -63,7 +66,9 @@ import '~/lib/utils/common_utils'; }); describe('::showEmojiMenu', function() { it('should show emoji menu when Add emoji button clicked', function(done) { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); return lazyAssert(done, function() { var $emojiMenu; $emojiMenu = $('.emoji-menu'); @@ -81,7 +86,9 @@ import '~/lib/utils/common_utils'; }); }); it('should remove emoji menu when body is clicked', function(done) { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); return lazyAssert(done, function() { var $emojiMenu; $emojiMenu = $('.emoji-menu'); @@ -92,7 +99,9 @@ import '~/lib/utils/common_utils'; }); }); it('should not remove emoji menu when search is clicked', function(done) { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); return lazyAssert(done, function() { var $emojiMenu; $emojiMenu = $('.emoji-menu'); @@ -103,6 +112,7 @@ import '~/lib/utils/common_utils'; }); }); }); + describe('::addAwardToEmojiBar', function() { it('should add emoji to votes block', function() { var $emojiButton, $votesBlock; @@ -139,7 +149,9 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.userAuthored($thumbsUpEmoji); - return expect($thumbsUpEmoji.data("originalTitle")).toBe("You cannot vote on your own issue, MR and note"); + return expect($thumbsUpEmoji.data('originalTitle')).toBe( + 'You cannot vote on your own issue, MR and note', + ); }); it('should restore tooltip back to initial vote list', function() { var $thumbsUpEmoji, $votesBlock; @@ -150,12 +162,14 @@ import '~/lib/utils/common_utils'; awardsHandler.userAuthored($thumbsUpEmoji); jasmine.clock().tick(2801); jasmine.clock().uninstall(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe("sam"); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); describe('::getAwardUrl', function() { return it('returns the url for request', function() { - return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji'); + return expect(awardsHandler.getAwardUrl()).toBe( + 'http://test.host/snippets/1/toggle_award_emoji', + ); }); }); describe('::addAward and ::checkMutuality', function() { @@ -195,7 +209,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('You, sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -205,7 +219,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('You and sam'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('You and sam'); }); }); describe('::removeYouToUserList', function() { @@ -218,7 +232,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -229,7 +243,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); describe('::searchEmojis', () => { @@ -245,7 +259,7 @@ import '~/lib/utils/common_utils'; expect($('.js-emoji-menu-search').val()).toBe('ali'); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -263,7 +277,7 @@ import '~/lib/utils/common_utils'; expect($('.js-emoji-menu-search').val()).toBe(''); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -272,37 +286,40 @@ import '~/lib/utils/common_utils'; describe('emoji menu', function() { const emojiSelector = '[data-name="sunglasses"]'; const openEmojiMenuAndAddEmoji = function() { - return openAndWaitForEmojiMenu() - .then(() => { - const $menu = $('.emoji-menu'); - const $block = $('.js-awards-block'); - const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector); + return openAndWaitForEmojiMenu().then(() => { + const $menu = $('.emoji-menu'); + const $block = $('.js-awards-block'); + const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector); - expect($emoji.length).toBe(1); - expect($block.find(emojiSelector).length).toBe(0); - $emoji.click(); - expect($menu.hasClass('.is-visible')).toBe(false); - expect($block.find(emojiSelector).length).toBe(1); - }); + expect($emoji.length).toBe(1); + expect($block.find(emojiSelector).length).toBe(0); + $emoji.click(); + expect($menu.hasClass('.is-visible')).toBe(false); + expect($block.find(emojiSelector).length).toBe(1); + }); }; it('should add selected emoji to awards block', function(done) { return openEmojiMenuAndAddEmoji() .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); it('should remove already selected emoji', function(done) { return openEmojiMenuAndAddEmoji() .then(() => { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); const $block = $('.js-awards-block'); - const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`); + const $emoji = $('.emoji-menu').find( + `.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`, + ); $emoji.click(); expect($block.find(emojiSelector).length).toBe(0); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -318,12 +335,12 @@ import '~/lib/utils/common_utils'; return openAndWaitForEmojiMenu() .then(() => { const emojiMenu = document.querySelector('.emoji-menu'); - Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => { + Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => { expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used'); }); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -334,14 +351,15 @@ import '~/lib/utils/common_utils'; return openAndWaitForEmojiMenu() .then(() => { const emojiMenu = document.querySelector('.emoji-menu'); - const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => - title.textContent.trim().toLowerCase() === 'frequently used' + const hasFrequentlyUsedHeading = Array.prototype.some.call( + emojiMenu.querySelectorAll('.emoji-menu-title'), + title => title.textContent.trim().toLowerCase() === 'frequently used', ); expect(hasFrequentlyUsedHeading).toBe(true); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -361,4 +379,4 @@ import '~/lib/utils/common_utils'; }); }); }); -}).call(window); +}.call(window)); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index d03836d10f9..d8aa5c636da 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -4,12 +4,11 @@ import '~/behaviors/quick_submit'; describe('Quick Submit behavior', function () { const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options); - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + preloadFixtures('snippets/show.html.raw'); beforeEach(() => { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - $('body').attr('data-page', 'projects:merge_requests:show'); - $('form').submit((e) => { + loadFixtures('snippets/show.html.raw'); + $('form').submit(e => { // Prevent a form submit from moving us off the testing page e.preventDefault(); }); @@ -26,24 +25,30 @@ describe('Quick Submit behavior', function () { }); it('does not respond to other keyCodes', () => { - this.textarea.trigger(keydownEvent({ - keyCode: 32, - })); + this.textarea.trigger( + keydownEvent({ + keyCode: 32, + }), + ); expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to Enter alone', () => { - this.textarea.trigger(keydownEvent({ - ctrlKey: false, - metaKey: false, - })); + this.textarea.trigger( + keydownEvent({ + ctrlKey: false, + metaKey: false, + }), + ); expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to repeated events', () => { - this.textarea.trigger(keydownEvent({ - repeat: true, - })); + this.textarea.trigger( + keydownEvent({ + repeat: true, + }), + ); expect(this.spies.submit).not.toHaveBeenTriggered(); }); @@ -83,15 +88,21 @@ describe('Quick Submit behavior', function () { }); it('excludes other modifier keys', () => { - this.textarea.trigger(keydownEvent({ - altKey: true, - })); - this.textarea.trigger(keydownEvent({ - ctrlKey: true, - })); - this.textarea.trigger(keydownEvent({ - shiftKey: true, - })); + this.textarea.trigger( + keydownEvent({ + altKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + ctrlKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + shiftKey: true, + }), + ); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); }); @@ -102,15 +113,21 @@ describe('Quick Submit behavior', function () { }); it('excludes other modifier keys', () => { - this.textarea.trigger(keydownEvent({ - altKey: true, - })); - this.textarea.trigger(keydownEvent({ - metaKey: true, - })); - this.textarea.trigger(keydownEvent({ - shiftKey: true, - })); + this.textarea.trigger( + keydownEvent({ + altKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + metaKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + shiftKey: true, + }), + ); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); } diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js index acd0aaf2a86..c726fa8e428 100644 --- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js +++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable import/no-unresolved */ - import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer'; import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr'; diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js index 51bf3086627..bbe2500f8e3 100644 --- a/spec/javascripts/blob/pdf/index_spec.js +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable import/no-unresolved */ - import renderPDF from '~/blob/pdf'; import testPDF from '../../fixtures/blob/pdf/test.pdf'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 3f5ed4f3d07..f7af099b3bf 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, one-var, no-unused-vars */ +/* eslint-disable comma-dangle, no-unused-vars */ /* global ListIssue */ import Vue from 'vue'; diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js index 0fd6f9dc810..052465d8d88 100644 --- a/spec/javascripts/bootstrap_jquery_spec.js +++ b/spec/javascripts/bootstrap_jquery_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var */ +/* eslint-disable no-var */ import $ from 'jquery'; import '~/commons/bootstrap'; diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/app_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/changed_files_dropdown_spec.js b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/changed_files_spec.js b/spec/javascripts/diffs/components/changed_files_spec.js new file mode 100644 index 00000000000..2d57af6137c --- /dev/null +++ b/spec/javascripts/diffs/components/changed_files_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import $ from 'jquery'; +import { mountComponentWithStore } from 'spec/helpers'; +import store from '~/diffs/store'; +import ChangedFiles from '~/diffs/components/changed_files.vue'; + +describe('ChangedFiles', () => { + const Component = Vue.extend(ChangedFiles); + const createComponent = props => mountComponentWithStore(Component, { props, store }); + let vm; + + beforeEach(() => { + setFixtures(` + <div id="dummy-element"></div> + <div class="js-tabs-affix"></div> + `); + const props = { + diffFiles: [ + { + addedLines: 10, + removedLines: 20, + blob: { + path: 'some/code.txt', + }, + filePath: 'some/code.txt', + }, + ], + }; + vm = createComponent(props); + }); + + describe('with single file added', () => { + it('shows files changes', () => { + expect(vm.$el).toContainText('1 changed file'); + }); + + it('shows file additions and deletions', () => { + expect(vm.$el).toContainText('10 additions'); + expect(vm.$el).toContainText('20 deletions'); + }); + }); + + describe('template', () => { + describe('diff view mode buttons', () => { + let inlineButton; + let parallelButton; + + beforeEach(() => { + inlineButton = vm.$el.querySelector('.js-inline-diff-button'); + parallelButton = vm.$el.querySelector('.js-parallel-diff-button'); + }); + + it('should have Inline and Side-by-side buttons', () => { + expect(inlineButton).toBeDefined(); + expect(parallelButton).toBeDefined(); + }); + + it('should add active class to Inline button', done => { + vm.$store.state.diffs.diffViewType = 'inline'; + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(true); + expect(parallelButton.classList.contains('active')).toEqual(false); + + done(); + }); + }); + + it('should toggle active state of buttons when diff view type changed', done => { + vm.$store.state.diffs.diffViewType = 'parallel'; + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(false); + expect(parallelButton.classList.contains('active')).toEqual(true); + + done(); + }); + }); + + describe('clicking them', () => { + it('should toggle the diff view type', done => { + $(parallelButton).click(); + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(false); + expect(parallelButton.classList.contains('active')).toEqual(true); + + $(inlineButton).click(); + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(true); + expect(parallelButton.classList.contains('active')).toEqual(false); + done(); + }); + }); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js new file mode 100644 index 00000000000..270f363825f --- /dev/null +++ b/spec/javascripts/diffs/components/diff_discussions_spec.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('DiffDiscussions', () => { + let component; + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + component = createComponentWithStore(Vue.extend(DiffDiscussions), store, { + discussions: getDiscussionsMockData(), + }).$mount(); + }); + + describe('template', () => { + it('should have notes list', () => { + const { $el } = component; + + expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js new file mode 100644 index 00000000000..d0f1700bee6 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -0,0 +1,433 @@ +import Vue from 'vue'; +import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +const discussionFixture = 'merge_requests/diff_discussion.json'; + +describe('diff_file_header', () => { + let vm; + let props; + const Component = Vue.extend(DiffFileHeader); + + beforeEach(() => { + const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; + const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file, { deep: true }); + props = { + diffFile, + currentUser: {}, + }; + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('icon', () => { + beforeEach(() => { + props.diffFile.blob.icon = 'dummy icon'; + }); + + it('returns the blob icon for files', () => { + props.diffFile.submodule = false; + + vm = mountComponent(Component, props); + + expect(vm.icon).toBe(props.diffFile.blob.icon); + }); + + it('returns the archive icon for submodules', () => { + props.diffFile.submodule = true; + + vm = mountComponent(Component, props); + + expect(vm.icon).toBe('archive'); + }); + }); + + describe('titleLink', () => { + beforeEach(() => { + Object.assign(props.diffFile, { + fileHash: 'badc0ffee', + submoduleLink: 'link://to/submodule', + submoduleTreeUrl: 'some://tree/url', + }); + }); + + it('returns the fileHash for files', () => { + props.diffFile.submodule = false; + + vm = mountComponent(Component, props); + + expect(vm.titleLink).toBe(`#${props.diffFile.fileHash}`); + }); + + it('returns the submoduleTreeUrl for submodules', () => { + props.diffFile.submodule = true; + + vm = mountComponent(Component, props); + + expect(vm.titleLink).toBe(props.diffFile.submoduleTreeUrl); + }); + + it('returns the submoduleLink for submodules without submoduleTreeUrl', () => { + Object.assign(props.diffFile, { + submodule: true, + submoduleTreeUrl: null, + }); + + vm = mountComponent(Component, props); + + expect(vm.titleLink).toBe(props.diffFile.submoduleLink); + }); + }); + + describe('filePath', () => { + beforeEach(() => { + Object.assign(props.diffFile, { + blob: { id: 'b10b1db10b1d' }, + filePath: 'path/to/file', + }); + }); + + it('returns the filePath for files', () => { + props.diffFile.submodule = false; + + vm = mountComponent(Component, props); + + expect(vm.filePath).toBe(props.diffFile.filePath); + }); + + it('appends the truncated blob id for submodules', () => { + props.diffFile.submodule = true; + + vm = mountComponent(Component, props); + + expect(vm.filePath).toBe( + `${props.diffFile.filePath} @ ${props.diffFile.blob.id.substr(0, 8)}`, + ); + }); + }); + + describe('titleTag', () => { + it('returns a link tag if fileHash is set', () => { + props.diffFile.fileHash = 'some hash'; + + vm = mountComponent(Component, props); + + expect(vm.titleTag).toBe('a'); + }); + + it('returns a span tag if fileHash is not set', () => { + props.diffFile.fileHash = null; + + vm = mountComponent(Component, props); + + expect(vm.titleTag).toBe('span'); + }); + }); + + describe('isUsingLfs', () => { + beforeEach(() => { + Object.assign(props.diffFile, { + storedExternally: true, + externalStorage: 'lfs', + }); + }); + + it('returns true if file is stored in LFS', () => { + vm = mountComponent(Component, props); + + expect(vm.isUsingLfs).toBe(true); + }); + + it('returns false if file is not stored externally', () => { + props.diffFile.storedExternally = false; + + vm = mountComponent(Component, props); + + expect(vm.isUsingLfs).toBe(false); + }); + + it('returns false if file is not stored in LFS', () => { + props.diffFile.externalStorage = 'not lfs'; + + vm = mountComponent(Component, props); + + expect(vm.isUsingLfs).toBe(false); + }); + }); + + describe('collapseIcon', () => { + it('returns chevron-down if the diff is expanded', () => { + props.expanded = true; + + vm = mountComponent(Component, props); + + expect(vm.collapseIcon).toBe('chevron-down'); + }); + + it('returns chevron-right if the diff is collapsed', () => { + props.expanded = false; + + vm = mountComponent(Component, props); + + expect(vm.collapseIcon).toBe('chevron-right'); + }); + }); + + describe('isDiscussionsExpanded', () => { + beforeEach(() => { + Object.assign(props, { + discussionsExpanded: true, + expanded: true, + }); + }); + + it('returns true if diff and discussion are expanded', () => { + vm = mountComponent(Component, props); + + expect(vm.isDiscussionsExpanded).toBe(true); + }); + + it('returns false if discussion is collapsed', () => { + props.discussionsExpanded = false; + + vm = mountComponent(Component, props); + + expect(vm.isDiscussionsExpanded).toBe(false); + }); + + it('returns false if diff is collapsed', () => { + props.expanded = false; + + vm = mountComponent(Component, props); + + expect(vm.isDiscussionsExpanded).toBe(false); + }); + }); + + describe('viewFileButtonText', () => { + it('contains the truncated content SHA', () => { + const dummySha = 'deebd00f is no SHA'; + props.diffFile.contentSha = dummySha; + + vm = mountComponent(Component, props); + + expect(vm.viewFileButtonText).not.toContain(dummySha); + expect(vm.viewFileButtonText).toContain(dummySha.substr(0, 8)); + }); + }); + + describe('viewReplacedFileButtonText', () => { + it('contains the truncated base SHA', () => { + const dummySha = 'deadabba sings no more'; + props.diffFile.diffRefs.baseSha = dummySha; + + vm = mountComponent(Component, props); + + expect(vm.viewReplacedFileButtonText).not.toContain(dummySha); + expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8)); + }); + }); + }); + + describe('methods', () => { + describe('handleToggle', () => { + beforeEach(() => { + spyOn(vm, '$emit').and.stub(); + }); + + it('emits toggleFile if checkTarget is false', () => { + vm.handleToggle(null, false); + + expect(vm.$emit).toHaveBeenCalledWith('toggleFile'); + }); + + it('emits toggleFile if checkTarget is true and event target is header', () => { + vm.handleToggle({ target: vm.$refs.header }, true); + + expect(vm.$emit).toHaveBeenCalledWith('toggleFile'); + }); + + it('does not emit toggleFile if checkTarget is true and event target is not header', () => { + vm.handleToggle({ target: 'not header' }, true); + + expect(vm.$emit).not.toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + describe('collapse toggle', () => { + const collapseToggle = () => vm.$el.querySelector('.diff-toggle-caret'); + + it('is visible if collapsible is true', () => { + props.collapsible = true; + + vm = mountComponent(Component, props); + + expect(collapseToggle()).not.toBe(null); + }); + + it('is hidden if collapsible is false', () => { + props.collapsible = false; + + vm = mountComponent(Component, props); + + expect(collapseToggle()).toBe(null); + }); + }); + + it('displays an icon in the title', () => { + vm = mountComponent(Component, props); + + const icon = vm.$el.querySelector(`i[class="fa fa-fw fa-${vm.icon}"]`); + expect(icon).not.toBe(null); + }); + + describe('file paths', () => { + const filePaths = () => vm.$el.querySelectorAll('.file-title-name'); + + it('displays the path of a added file', () => { + props.diffFile.renamedFile = false; + + vm = mountComponent(Component, props); + + expect(filePaths()).toHaveLength(1); + expect(filePaths()[0]).toHaveText(props.diffFile.filePath); + }); + + it('displays path for deleted file', () => { + props.diffFile.renamedFile = false; + props.diffFile.deletedFile = true; + + vm = mountComponent(Component, props); + + expect(filePaths()).toHaveLength(1); + expect(filePaths()[0]).toHaveText(`${props.diffFile.filePath} deleted`); + }); + + it('displays old and new path if the file was renamed', () => { + props.diffFile.renamedFile = true; + + vm = mountComponent(Component, props); + + expect(filePaths()).toHaveLength(2); + expect(filePaths()[0]).toHaveText(props.diffFile.oldPath); + expect(filePaths()[1]).toHaveText(props.diffFile.newPath); + }); + }); + + it('displays a copy to clipboard button', () => { + vm = mountComponent(Component, props); + + const button = vm.$el.querySelector('.btn-clipboard'); + expect(button).not.toBe(null); + expect(button.dataset.clipboardText).toBe(props.diffFile.filePath); + }); + + describe('file mode', () => { + it('it displays old and new file mode if it changed', () => { + props.diffFile.modeChanged = true; + + vm = mountComponent(Component, props); + + const { fileMode } = vm.$refs; + expect(fileMode).not.toBe(undefined); + expect(fileMode).toContainText(props.diffFile.aMode); + expect(fileMode).toContainText(props.diffFile.bMode); + }); + + it('does not display the file mode if it has not changed', () => { + props.diffFile.modeChanged = false; + + vm = mountComponent(Component, props); + + const { fileMode } = vm.$refs; + expect(fileMode).toBe(undefined); + }); + }); + + describe('LFS label', () => { + const lfsLabel = () => vm.$el.querySelector('.label-lfs'); + + it('displays the LFS label for files stored in LFS', () => { + Object.assign(props.diffFile, { + storedExternally: true, + externalStorage: 'lfs', + }); + + vm = mountComponent(Component, props); + + expect(lfsLabel()).not.toBe(null); + expect(lfsLabel()).toHaveText('LFS'); + }); + + it('does not display the LFS label for files stored in repository', () => { + props.diffFile.storedExternally = false; + + vm = mountComponent(Component, props); + + expect(lfsLabel()).toBe(null); + }); + }); + + describe('edit button', () => { + it('should not render edit button if addMergeRequestButtons is not true', () => { + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); + }); + + it('should show edit button when file is editable', () => { + props.addMergeRequestButtons = true; + props.diffFile.editPath = '/'; + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit'); + }); + + it('should not show edit button when file is deleted', () => { + props.addMergeRequestButtons = true; + props.diffFile.deletedFile = true; + props.diffFile.editPath = '/'; + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); + }); + }); + + describe('addMergeRequestButtons', () => { + beforeEach(() => { + props.addMergeRequestButtons = true; + props.diffFile.editPath = ''; + }); + + describe('view on environment button', () => { + const url = 'some.external.url/'; + const title = 'url.title'; + + it('displays link to external url', () => { + props.diffFile.externalUrl = url; + props.diffFile.formattedExternalUrl = title; + + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector(`a[href="${url}"]`)).not.toBe(null); + expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).not.toBe(null); + }); + + it('hides link if no external url', () => { + props.diffFile.externalUrl = ''; + props.diffFile.formattedExternalUrl = title; + + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js new file mode 100644 index 00000000000..1c1edfac68c --- /dev/null +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffFile', () => { + let vm; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, { + file: getDiffFileMock(), + currentUser: {}, + }).$mount(); + }); + + describe('template', () => { + it('should render component with file header, file content components', () => { + const el = vm.$el; + const { fileHash, filePath } = diffFileMockData; + + expect(el.id).toEqual(fileHash); + expect(el.classList.contains('diff-file')).toEqual(true); + expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); + expect(el.querySelector('.js-file-title')).toBeDefined(); + expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true); + expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); + expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + }); + + describe('collapsed', () => { + it('should not have file content', done => { + expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(0); + expect(vm.file.collapsed).toEqual(false); + vm.file.collapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(1); + + done(); + }); + }); + + it('should have collapsed text and link', done => { + vm.file.collapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.innerText).toContain('This diff is collapsed'); + expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + + done(); + }); + }); + + it('should have loading icon while loading a collapsed diffs', done => { + vm.file.collapsed = true; + vm.isLoadingCollapsedDiff = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1); + + done(); + }); + }); + }); + }); + + describe('too large diff', () => { + it('should have too large warning and blob link', done => { + const BLOB_LINK = '/file/view/path'; + vm.file.tooLarge = true; + vm.file.viewPath = BLOB_LINK; + + vm.$nextTick(() => { + expect(vm.$el.innerText).toContain( + 'This source diff could not be displayed because it is too large', + ); + expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined(); + expect(vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK) > -1).toEqual( + true, + ); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js new file mode 100644 index 00000000000..0085a16815a --- /dev/null +++ b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue'; +import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('DiffGutterAvatars', () => { + let component; + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, { + discussions: getDiscussionsMockData(), + }).$mount(); + }); + + describe('computed', () => { + describe('discussionsExpanded', () => { + it('should return true when all discussions are expanded', () => { + expect(component.discussionsExpanded).toEqual(true); + }); + + it('should return false when all discussions are not expanded', () => { + component.discussions[0].expanded = false; + expect(component.discussionsExpanded).toEqual(false); + }); + }); + + describe('allDiscussions', () => { + it('should return an array of notes', () => { + expect(component.allDiscussions).toEqual([...component.discussions[0].notes]); + }); + }); + + describe('notesInGutter', () => { + it('should return a subset of discussions to show in gutter', () => { + expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER); + expect(component.notesInGutter[0]).toEqual({ + note: component.discussions[0].notes[0].note, + author: component.discussions[0].notes[0].author, + }); + }); + }); + + describe('moreCount', () => { + it('should return count of remaining discussions from gutter', () => { + expect(component.moreCount).toEqual(2); + }); + }); + + describe('moreText', () => { + it('should return proper text if moreCount > 0', () => { + expect(component.moreText).toEqual('2 more comments'); + }); + + it('should return empty string if there is no discussion', () => { + component.discussions = []; + expect(component.moreText).toEqual(''); + }); + }); + }); + + describe('methods', () => { + describe('getTooltipText', () => { + it('should return original comment if it is shorter than max length', () => { + const note = component.discussions[0].notes[0]; + + expect(component.getTooltipText(note)).toEqual('Administrator: comment 1'); + }); + + it('should return truncated version of comment', () => { + const note = component.discussions[0].notes[1]; + + expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...'); + }); + }); + + describe('toggleDiscussions', () => { + it('should toggle all discussions', () => { + expect(component.discussions[0].expanded).toEqual(true); + + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + component.discussions = component.$store.state.notes.discussions; + component.toggleDiscussions(); + + expect(component.discussions[0].expanded).toEqual(false); + component.$store.dispatch('setInitialNotes', []); + }); + }); + }); + + describe('template', () => { + const buttonSelector = '.js-diff-comment-button'; + const svgSelector = `${buttonSelector} svg`; + const avatarSelector = '.js-diff-comment-avatar'; + const plusCountSelector = '.js-diff-comment-plus'; + + it('should have button to collapse discussions when the discussions expanded', () => { + expect(component.$el.querySelector(buttonSelector)).toBeDefined(); + expect(component.$el.querySelector(svgSelector)).toBeDefined(); + }); + + it('should have user avatars when discussions collapsed', () => { + component.discussions[0].expanded = false; + + Vue.nextTick(() => { + expect(component.$el.querySelector(buttonSelector)).toBeNull(); + expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4); + expect(component.$el.querySelector(plusCountSelector)).toBeDefined(); + expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2'); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js new file mode 100644 index 00000000000..312a684f4d2 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js @@ -0,0 +1,153 @@ +import Vue from 'vue'; +import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { + MATCH_LINE_TYPE, + CONTEXT_LINE_TYPE, + OLD_NO_NEW_LINE_TYPE, + NEW_NO_NEW_LINE_TYPE, +} from '~/diffs/constants'; +import discussionsMockData from '../mock_data/diff_discussions'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffLineGutterContent', () => { + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const createComponent = (options = {}) => { + const cmp = Vue.extend(DiffLineGutterContent); + const props = Object.assign({}, options); + props.fileHash = getDiffFileMock().fileHash; + props.contextLinesPath = '/context/lines/path'; + + return createComponentWithStore(cmp, store, props).$mount(); + }; + const setDiscussions = component => { + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + }; + + const resetDiscussions = component => { + component.$store.dispatch('setInitialNotes', []); + }; + + describe('computed', () => { + describe('isMatchLine', () => { + it('should return true for match line type', () => { + const component = createComponent({ lineType: MATCH_LINE_TYPE }); + expect(component.isMatchLine).toEqual(true); + }); + + it('should return false for non-match line type', () => { + const component = createComponent({ lineType: CONTEXT_LINE_TYPE }); + expect(component.isMatchLine).toEqual(false); + }); + }); + + describe('isContextLine', () => { + it('should return true for context line type', () => { + const component = createComponent({ lineType: CONTEXT_LINE_TYPE }); + expect(component.isContextLine).toEqual(true); + }); + + it('should return false for non-context line type', () => { + const component = createComponent({ lineType: MATCH_LINE_TYPE }); + expect(component.isContextLine).toEqual(false); + }); + }); + + describe('isMetaLine', () => { + it('should return true for meta line type', () => { + const component = createComponent({ lineType: NEW_NO_NEW_LINE_TYPE }); + expect(component.isMetaLine).toEqual(true); + + const component2 = createComponent({ lineType: OLD_NO_NEW_LINE_TYPE }); + expect(component2.isMetaLine).toEqual(true); + }); + + it('should return false for non-meta line type', () => { + const component = createComponent({ lineType: MATCH_LINE_TYPE }); + expect(component.isMetaLine).toEqual(false); + }); + }); + + describe('lineHref', () => { + it('should prepend # to lineCode', () => { + const lineCode = 'LC_42'; + const component = createComponent({ lineCode }); + expect(component.lineHref).toEqual(`#${lineCode}`); + }); + + it('should return # if there is no lineCode', () => { + const component = createComponent({ lineCode: null }); + expect(component.lineHref).toEqual('#'); + }); + }); + + describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => { + it('should return empty array when there is no discussion', () => { + const component = createComponent({ lineCode: 'LC_42' }); + expect(component.discussions).toEqual([]); + expect(component.hasDiscussions).toEqual(false); + expect(component.shouldShowAvatarsOnGutter).toEqual(false); + }); + + it('should return discussions for the given lineCode', () => { + const lineCode = getDiffFileMock().highlightedDiffLines[1].lineCode; + const component = createComponent({ lineCode, showCommentButton: true }); + + setDiscussions(component); + + expect(component.discussions).toEqual(getDiscussionsMockData()); + expect(component.hasDiscussions).toEqual(true); + expect(component.shouldShowAvatarsOnGutter).toEqual(true); + + resetDiscussions(component); + }); + }); + }); + + describe('template', () => { + it('should render three dots for context lines', () => { + const component = createComponent({ + lineType: MATCH_LINE_TYPE, + }); + + expect(component.$el.querySelector('span').classList.contains('context-cell')).toEqual(true); + expect(component.$el.innerText).toEqual('...'); + }); + + it('should render comment button', () => { + const component = createComponent({ + showCommentButton: true, + }); + Object.defineProperty(component, 'isLoggedIn', { + get() { + return true; + }, + }); + + expect(component.$el.querySelector('.js-add-diff-note-button')).toBeDefined(); + }); + + it('should render line link', () => { + const lineNumber = 42; + const lineCode = `LC_${lineNumber}`; + const component = createComponent({ lineNumber, lineCode }); + const link = component.$el.querySelector('a'); + + expect(link.href.indexOf(`#${lineCode}`) > -1).toEqual(true); + expect(link.dataset.linenumber).toEqual(lineNumber.toString()); + }); + + it('should render user avatars', () => { + const component = createComponent({ + showCommentButton: true, + lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode, + }); + + setDiscussions(component); + expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined(); + resetDiscussions(component); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js new file mode 100644 index 00000000000..724d1948214 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffLineNoteForm', () => { + let component; + let diffFile; + let diffLines; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + + beforeEach(() => { + diffFile = getDiffFileMock(); + diffLines = diffFile.highlightedDiffLines; + + component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, { + diffFile, + diffLines, + line: diffLines[0], + noteTargetLine: diffLines[0], + }).$mount(); + }); + + describe('methods', () => { + describe('handleCancelCommentForm', () => { + it('should call cancelCommentForm with lineCode', () => { + spyOn(component, 'cancelCommentForm'); + component.handleCancelCommentForm(); + + expect(component.cancelCommentForm).toHaveBeenCalledWith({ + lineCode: diffLines[0].lineCode, + }); + }); + }); + + describe('saveNoteForm', () => { + it('should call saveNote action with proper params', done => { + let isPromiseCalled = false; + const formDataSpy = spyOnDependency(DiffLineNoteForm, 'getNoteFormData').and.returnValue({ + postData: 1, + }); + const saveNoteSpy = spyOn(component, 'saveNote').and.returnValue( + new Promise(() => { + isPromiseCalled = true; + done(); + }), + ); + + component.handleSaveNote('note body'); + + expect(formDataSpy).toHaveBeenCalled(); + expect(saveNoteSpy).toHaveBeenCalled(); + expect(isPromiseCalled).toEqual(true); + }); + }); + }); + + describe('template', () => { + it('should have note form', () => { + const { $el } = component; + + expect($el.querySelector('.js-vue-textarea')).toBeDefined(); + expect($el.querySelector('.js-vue-issue-save')).toBeDefined(); + expect($el.querySelector('.js-vue-markdown-field')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/edit_button_spec.js b/spec/javascripts/diffs/components/edit_button_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/edit_button_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/hidden_files_warning_spec.js b/spec/javascripts/diffs/components/hidden_files_warning_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/hidden_files_warning_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js new file mode 100644 index 00000000000..0d5a3576204 --- /dev/null +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -0,0 +1,111 @@ +import Vue from 'vue'; +import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; +import store from '~/mr_notes/stores'; +import * as constants from '~/diffs/constants'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('InlineDiffView', () => { + let component; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + const diffFile = getDiffFileMock(); + + component = createComponentWithStore(Vue.extend(InlineDiffView), store, { + diffFile, + diffLines: diffFile.highlightedDiffLines, + }).$mount(); + }); + + describe('methods', () => { + describe('handleMouse', () => { + it('should set hoveredLineCode', () => { + expect(component.hoveredLineCode).toEqual(null); + + component.handleMouse('lineCode1', true); + expect(component.hoveredLineCode).toEqual('lineCode1'); + + component.handleMouse('lineCode1', false); + expect(component.hoveredLineCode).toEqual(null); + }); + }); + + describe('getLineClass', () => { + it('should return line class object', () => { + const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants; + const { MATCH_LINE_TYPE, NEW_LINE_TYPE } = constants; + + expect(component.getLineClass(component.diffLines[0])).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: false, + }); + + component.handleMouse(component.diffLines[0].lineCode, true); + Object.defineProperty(component, 'isLoggedIn', { + get() { + return true; + }, + }); + + expect(component.getLineClass(component.diffLines[0])).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: true, + }); + + expect(component.getLineClass(component.diffLines[5])).toEqual({ + [MATCH_LINE_TYPE]: MATCH_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: true, + [LINE_HOVER_CLASS_NAME]: false, + }); + }); + }); + }); + + describe('template', () => { + it('should have rendered diff lines', () => { + const el = component.$el; + + expect(el.querySelectorAll('tr.line_holder').length).toEqual(6); + expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2); + expect(el.querySelectorAll('tr.line_holder.match').length).toEqual(1); + expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true); + }); + + it('should render discussions', done => { + const el = component.$el; + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.notes_holder').length).toEqual(1); + expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); + expect(el.innerText.indexOf('comment 5') > -1).toEqual(true); + component.$store.dispatch('setInitialNotes', []); + + done(); + }); + }); + + it('should render new discussion forms', done => { + const el = component.$el; + const lines = getDiffFileMock().highlightedDiffLines; + + component.handleShowCommentForm({ lineCode: lines[0].lineCode }); + component.handleShowCommentForm({ lineCode: lines[1].lineCode }); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2); + expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true); + expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true); + + store.state.diffs.diffLineCommentForms = {}; + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/javascripts/diffs/components/no_changes_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/no_changes_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js new file mode 100644 index 00000000000..cab533217c0 --- /dev/null +++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js @@ -0,0 +1,224 @@ +import Vue from 'vue'; +import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; +import store from '~/mr_notes/stores'; +import * as constants from '~/diffs/constants'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('ParallelDiffView', () => { + let component; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + const diffFile = getDiffFileMock(); + + component = createComponentWithStore(Vue.extend(ParallelDiffView), store, { + diffFile, + diffLines: diffFile.parallelDiffLines, + }).$mount(); + }); + + describe('computed', () => { + describe('parallelDiffLines', () => { + it('should normalize lines for empty cells', () => { + expect(component.parallelDiffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE); + expect(component.parallelDiffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE); + }); + }); + }); + + describe('methods', () => { + describe('hasDiscussion', () => { + it('it should return true if there is a discussion either for left or right section', () => { + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { line_42: true }; + }, + }); + + expect(component.hasDiscussion({ left: {}, right: {} })).toEqual(undefined); + expect(component.hasDiscussion({ left: { lineCode: 'line_42' }, right: {} })).toEqual(true); + expect(component.hasDiscussion({ left: {}, right: { lineCode: 'line_42' } })).toEqual(true); + }); + }); + + describe('getClassName', () => { + it('should return line class object', () => { + const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants; + const { MATCH_LINE_TYPE, NEW_LINE_TYPE, LINE_POSITION_RIGHT } = constants; + + expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: false, + }); + + const eventMock = { + target: component.$refs.rightLines[1], + }; + + component.handleMouse(eventMock, component.diffLines[1], true); + Object.defineProperty(component, 'isLoggedIn', { + get() { + return true; + }, + }); + + expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: true, + }); + + expect(component.getClassName(component.diffLines[5], LINE_POSITION_RIGHT)).toEqual({ + [MATCH_LINE_TYPE]: MATCH_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: true, + [LINE_HOVER_CLASS_NAME]: false, + }); + }); + }); + + describe('handleMouse', () => { + it('should set hovered line code and line section to null when isHover is false', () => { + const rightLineEventMock = { target: component.$refs.rightLines[1] }; + expect(component.hoveredLineCode).toEqual(null); + expect(component.hoveredSection).toEqual(null); + + component.handleMouse(rightLineEventMock, null, false); + expect(component.hoveredLineCode).toEqual(null); + expect(component.hoveredSection).toEqual(null); + }); + + it('should set hovered line code and line section for right section', () => { + const rightLineEventMock = { target: component.$refs.rightLines[1] }; + component.handleMouse(rightLineEventMock, component.diffLines[1], true); + expect(component.hoveredLineCode).toEqual(component.diffLines[1].right.lineCode); + expect(component.hoveredSection).toEqual(constants.LINE_POSITION_RIGHT); + }); + + it('should set hovered line code and line section for left section', () => { + const leftLineEventMock = { target: component.$refs.leftLines[2] }; + component.handleMouse(leftLineEventMock, component.diffLines[2], true); + expect(component.hoveredLineCode).toEqual(component.diffLines[2].left.lineCode); + expect(component.hoveredSection).toEqual(constants.LINE_POSITION_LEFT); + }); + }); + + describe('shouldRenderDiscussions', () => { + it('should return true if there is a discussion on left side and it is expanded', () => { + const line = { left: { lineCode: 'lineCode1' } }; + spyOn(component, 'isDiscussionExpanded').and.returnValue(true); + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { + [line.left.lineCode]: true, + }; + }, + }); + + expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual(true); + expect(component.isDiscussionExpanded).toHaveBeenCalledWith(line.left.lineCode); + }); + + it('should return false if there is a discussion on left side but it is collapsed', () => { + const line = { left: { lineCode: 'lineCode1' } }; + spyOn(component, 'isDiscussionExpanded').and.returnValue(false); + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { + [line.left.lineCode]: true, + }; + }, + }); + + expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual( + false, + ); + }); + + it('should return false for discussions on the right side if there is no line type', () => { + const CUSTOM_RIGHT_LINE_TYPE = 'CUSTOM_RIGHT_LINE_TYPE'; + const line = { right: { lineCode: 'lineCode1', type: CUSTOM_RIGHT_LINE_TYPE } }; + spyOn(component, 'isDiscussionExpanded').and.returnValue(true); + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { + [line.right.lineCode]: true, + }; + }, + }); + + expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_RIGHT)).toEqual( + CUSTOM_RIGHT_LINE_TYPE, + ); + }); + }); + + describe('hasAnyExpandedDiscussion', () => { + const LINE_CODE_LEFT = 'LINE_CODE_LEFT'; + const LINE_CODE_RIGHT = 'LINE_CODE_RIGHT'; + + it('should return true if there is a discussion either on the left or the right side', () => { + const mockLineOne = { + right: { lineCode: LINE_CODE_RIGHT }, + left: {}, + }; + const mockLineTwo = { + left: { lineCode: LINE_CODE_LEFT }, + right: {}, + }; + + spyOn(component, 'isDiscussionExpanded').and.callFake(lc => lc === LINE_CODE_RIGHT); + expect(component.hasAnyExpandedDiscussion(mockLineOne)).toEqual(true); + expect(component.hasAnyExpandedDiscussion(mockLineTwo)).toEqual(false); + }); + }); + }); + + describe('template', () => { + it('should have rendered diff lines', () => { + const el = component.$el; + + expect(el.querySelectorAll('tr.line_holder.parallel').length).toEqual(6); + expect(el.querySelectorAll('td.empty-cell').length).toEqual(4); + expect(el.querySelectorAll('td.line_content.parallel.right-side').length).toEqual(6); + expect(el.querySelectorAll('td.line_content.parallel.left-side').length).toEqual(6); + expect(el.querySelectorAll('td.match').length).toEqual(4); + expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true); + }); + + it('should render discussions', done => { + const el = component.$el; + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.notes_holder').length).toEqual(1); + expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); + expect(el.innerText.indexOf('comment 5') > -1).toEqual(true); + component.$store.dispatch('setInitialNotes', []); + + done(); + }); + }); + + it('should render new discussion forms', done => { + const el = component.$el; + const lines = getDiffFileMock().parallelDiffLines; + + component.handleShowCommentForm({ lineCode: lines[0].lineCode }); + component.handleShowCommentForm({ lineCode: lines[1].lineCode }); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2); + expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true); + expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true); + + store.state.diffs.diffLineCommentForms = {}; + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js new file mode 100644 index 00000000000..41d0dfd8939 --- /dev/null +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -0,0 +1,496 @@ +export default { + id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + position: { + formatter: { + old_line: null, + new_line: 2, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + }, + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + expanded: true, + notes: [ + { + id: 1749, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-03T21:06:21.521Z', + updated_at: '2018-04-08T08:50:41.762Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 1', + note_html: '<p dir="auto">comment 1</p>', + last_edited_at: '2018-04-08T08:50:41.762Z', + last_edited_by: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1749', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1753, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Fatih Acet', + username: 'fatihacet', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/fatihacevt', + }, + created_at: '2018-04-08T08:49:35.804Z', + updated_at: '2018-04-08T08:50:45.915Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 2 is really long one', + note_html: '<p dir="auto">comment 2 is really long one</p>', + last_edited_at: '2018-04-08T08:50:45.915Z', + last_edited_by: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1753', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1754, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:48.294Z', + updated_at: '2018-04-08T08:50:48.294Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 3', + note_html: '<p dir="auto">comment 3</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1754', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1755, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:50.911Z', + updated_at: '2018-04-08T08:50:50.911Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 4', + note_html: '<p dir="auto">comment 4</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1755', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1756, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:53.895Z', + updated_at: '2018-04-08T08:50:53.895Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 5', + note_html: '<p dir="auto">comment 5</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1756', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + ], + individual_note: false, + resolvable: true, + resolved: false, + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + diff_file: { + submodule: false, + submodule_link: null, + blob: { + id: '9e10516ca50788acf18c518a231914a21e5f16f7', + path: 'CHANGELOG', + name: 'CHANGELOG', + mode: '100644', + readable_text: true, + icon: 'file-text-o', + }, + blob_path: 'CHANGELOG', + blob_name: 'CHANGELOG', + blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', + file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', + file_path: 'CHANGELOG', + new_file: false, + deleted_file: false, + renamed_file: false, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + mode_changed: false, + a_mode: '100644', + b_mode: '100644', + text: true, + added_lines: 2, + removed_lines: 0, + diff_refs: { + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + stored_externally: null, + external_storage: null, + old_path_html: ['CHANGELOG', 'CHANGELOG'], + new_path_html: 'CHANGELOG', + context_lines_path: + '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', + highlighted_diff_lines: [ + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + ], + parallel_diff_lines: [ + { + left: null, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }, + }, + { + left: null, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + right: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + }, + ], + }, + diff_discussion: true, + truncated_diff_lines: + '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', + image_diff_html: + '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n', +}; diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js new file mode 100644 index 00000000000..d3bf9525924 --- /dev/null +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -0,0 +1,220 @@ +export default { + submodule: false, + submoduleLink: null, + blob: { + id: '9e10516ca50788acf18c518a231914a21e5f16f7', + path: 'CHANGELOG', + name: 'CHANGELOG', + mode: '100644', + readableText: true, + icon: 'file-text-o', + }, + blobPath: 'CHANGELOG', + blobName: 'CHANGELOG', + blobIcon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', + fileHash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', + filePath: 'CHANGELOG', + newFile: false, + deletedFile: false, + renamedFile: false, + oldPath: 'CHANGELOG', + newPath: 'CHANGELOG', + modeChanged: false, + aMode: '100644', + bMode: '100644', + text: true, + addedLines: 2, + removedLines: 0, + diffRefs: { + baseSha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + startSha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + headSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + contentSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + storedExternally: null, + externalStorage: null, + oldPathHtml: ['CHANGELOG', 'CHANGELOG'], + newPathHtml: 'CHANGELOG', + editPath: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG', + viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG', + replacedViewPath: null, + collapsed: false, + tooLarge: false, + contextLinesPath: + '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', + highlightedDiffLines: [ + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + oldLine: null, + newLine: 1, + text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + oldLine: null, + newLine: 2, + text: '+<span id="LC2" class="line" lang="plaintext"></span>\n', + richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + oldLine: 1, + newLine: 3, + text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + oldLine: 2, + newLine: 4, + text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + oldLine: 3, + newLine: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + metaData: null, + }, + { + lineCode: null, + type: 'match', + oldLine: null, + newLine: null, + text: '', + richText: '', + metaData: { + oldPos: 3, + newPos: 5, + }, + }, + ], + parallelDiffLines: [ + { + left: { + type: 'empty-cell', + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + oldLine: null, + newLine: 1, + text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + metaData: null, + }, + }, + { + left: { + type: 'empty-cell', + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + oldLine: null, + newLine: 2, + text: '+<span id="LC2" class="line" lang="plaintext"></span>\n', + richText: '<span id="LC2" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + oldLine: 1, + newLine: 3, + text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + metaData: null, + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + oldLine: 1, + newLine: 3, + text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + oldLine: 2, + newLine: 4, + text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + richText: '<span id="LC4" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + oldLine: 2, + newLine: 4, + text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + richText: '<span id="LC4" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + oldLine: 3, + newLine: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + metaData: null, + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + oldLine: 3, + newLine: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: null, + type: 'match', + oldLine: null, + newLine: null, + text: '', + richText: '', + metaData: { + oldPos: 3, + newPos: 5, + }, + }, + right: { + lineCode: null, + type: 'match', + oldLine: null, + newLine: null, + text: '', + richText: '', + metaData: { + oldPos: 3, + newPos: 5, + }, + }, + }, + ], +}; diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js new file mode 100644 index 00000000000..e61780c9928 --- /dev/null +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -0,0 +1,210 @@ +import MockAdapter from 'axios-mock-adapter'; +import Cookies from 'js-cookie'; +import { + DIFF_VIEW_COOKIE_NAME, + INLINE_DIFF_VIEW_TYPE, + PARALLEL_DIFF_VIEW_TYPE, +} from '~/diffs/constants'; +import store from '~/diffs/store'; +import * as actions from '~/diffs/store/actions'; +import * as types from '~/diffs/store/mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import testAction from '../../helpers/vuex_action_helper'; + +describe('DiffsStoreActions', () => { + describe('setEndpoint', () => { + it('should set given endpoint', done => { + const endpoint = '/diffs/set/endpoint'; + + testAction( + actions.setEndpoint, + endpoint, + { endpoint: '' }, + [{ type: types.SET_ENDPOINT, payload: endpoint }], + [], + done, + ); + }); + }); + + describe('setLoadingState', () => { + it('should set loading state', done => { + expect(store.state.diffs.isLoading).toEqual(true); + const loadingState = false; + + testAction( + actions.setLoadingState, + loadingState, + {}, + [{ type: types.SET_LOADING, payload: loadingState }], + [], + done, + ); + }); + }); + + describe('fetchDiffFiles', () => { + it('should fetch diff files', done => { + const endpoint = '/fetch/diff/files'; + const mock = new MockAdapter(axios); + const res = { diff_files: 1, merge_request_diffs: [] }; + mock.onGet(endpoint).reply(200, res); + + testAction( + actions.fetchDiffFiles, + {}, + { endpoint }, + [ + { type: types.SET_LOADING, payload: true }, + { type: types.SET_LOADING, payload: false }, + { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs }, + { type: types.SET_DIFF_DATA, payload: res }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('setInlineDiffViewType', () => { + it('should set diff view type to inline and also set the cookie properly', done => { + testAction( + actions.setInlineDiffViewType, + null, + {}, + [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }], + [], + () => { + setTimeout(() => { + expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE); + done(); + }, 0); + }, + ); + }); + }); + + describe('setParallelDiffViewType', () => { + it('should set diff view type to parallel and also set the cookie properly', done => { + testAction( + actions.setParallelDiffViewType, + null, + {}, + [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], + [], + () => { + setTimeout(() => { + expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE); + done(); + }, 0); + }, + ); + }); + }); + + describe('showCommentForm', () => { + it('should call mutation to show comment form', done => { + const payload = { lineCode: 'lineCode' }; + + testAction( + actions.showCommentForm, + payload, + {}, + [{ type: types.ADD_COMMENT_FORM_LINE, payload }], + [], + done, + ); + }); + }); + + describe('cancelCommentForm', () => { + it('should call mutation to cancel comment form', done => { + const payload = { lineCode: 'lineCode' }; + + testAction( + actions.cancelCommentForm, + payload, + {}, + [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }], + [], + done, + ); + }); + }); + + describe('loadMoreLines', () => { + it('should call mutation to show comment form', done => { + const endpoint = '/diffs/load/more/lines'; + const params = { since: 6, to: 26 }; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const fileHash = 'ff9200'; + const options = { endpoint, params, lineNumbers, fileHash }; + const mock = new MockAdapter(axios); + const contextLines = { contextLines: [{ lineCode: 6 }] }; + mock.onGet(endpoint).reply(200, contextLines); + + testAction( + actions.loadMoreLines, + options, + {}, + [ + { + type: types.ADD_CONTEXT_LINES, + payload: { lineNumbers, contextLines, params, fileHash }, + }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('loadCollapsedDiff', () => { + it('should fetch data and call mutation with response and the give parameter', done => { + const file = { hash: 123, loadCollapsedDiffUrl: '/load/collapsed/diff/url' }; + const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] }; + const mock = new MockAdapter(axios); + mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); + + testAction( + actions.loadCollapsedDiff, + file, + {}, + [ + { + type: types.ADD_COLLAPSED_DIFFS, + payload: { file, data }, + }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('expandAllFiles', () => { + it('should change the collapsed prop from the diffFiles', done => { + testAction( + actions.expandAllFiles, + null, + {}, + [ + { + type: types.EXPAND_ALL_FILES, + }, + ], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js new file mode 100644 index 00000000000..7945ddea911 --- /dev/null +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -0,0 +1,24 @@ +import getters from '~/diffs/store/getters'; +import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; + +describe('DiffsStoreGetters', () => { + describe('isParallelView', () => { + it('should return true if view set to parallel view', () => { + expect(getters.isParallelView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeTruthy(); + }); + + it('should return false if view not to parallel view', () => { + expect(getters.isParallelView({ diffViewType: 'foo' })).toBeFalsy(); + }); + }); + + describe('isInlineView', () => { + it('should return true if view set to inline view', () => { + expect(getters.isInlineView({ diffViewType: INLINE_DIFF_VIEW_TYPE })).toBeTruthy(); + }); + + it('should return false if view not to inline view', () => { + expect(getters.isInlineView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js new file mode 100644 index 00000000000..5f1a6e9def7 --- /dev/null +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -0,0 +1,147 @@ +import mutations from '~/diffs/store/mutations'; +import * as types from '~/diffs/store/mutation_types'; +import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; + +describe('DiffsStoreMutations', () => { + describe('SET_ENDPOINT', () => { + it('should set endpoint', () => { + const state = {}; + const endpoint = '/diffs/endpoint'; + + mutations[types.SET_ENDPOINT](state, endpoint); + expect(state.endpoint).toEqual(endpoint); + }); + }); + + describe('SET_LOADING', () => { + it('should set loading state', () => { + const state = {}; + + mutations[types.SET_LOADING](state, false); + expect(state.isLoading).toEqual(false); + }); + }); + + describe('SET_DIFF_FILES', () => { + it('should set diff files to state', () => { + const filePath = '/first-diff-file-path'; + const state = {}; + const diffFiles = { + a_mode: 1, + highlighted_diff_lines: [{ file_path: filePath }], + }; + + mutations[types.SET_DIFF_FILES](state, diffFiles); + expect(state.diffFiles.aMode).toEqual(1); + expect(state.diffFiles.highlightedDiffLines[0].filePath).toEqual(filePath); + }); + }); + + describe('SET_DIFF_VIEW_TYPE', () => { + it('should set diff view type properly', () => { + const state = {}; + + mutations[types.SET_DIFF_VIEW_TYPE](state, INLINE_DIFF_VIEW_TYPE); + expect(state.diffViewType).toEqual(INLINE_DIFF_VIEW_TYPE); + }); + }); + + describe('ADD_COMMENT_FORM_LINE', () => { + it('should set a truthy reference for the given line code in diffLineCommentForms', () => { + const state = { diffLineCommentForms: {} }; + const lineCode = 'FDE'; + + mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); + expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); + }); + }); + + describe('REMOVE_COMMENT_FORM_LINE', () => { + it('should remove given reference from diffLineCommentForms', () => { + const state = { diffLineCommentForms: {} }; + const lineCode = 'FDE'; + + mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); + expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); + + mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }); + expect(state.diffLineCommentForms[lineCode]).toBeUndefined(); + }); + }); + + describe('EXPAND_ALL_FILES', () => { + it('should change the collapsed prop from diffFiles', () => { + const diffFile = { + collapsed: true, + }; + const state = { expandAllFiles: true, diffFiles: [diffFile] }; + + mutations[types.EXPAND_ALL_FILES](state); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + }); + + describe('ADD_CONTEXT_LINES', () => { + it('should call utils.addContextLines with proper params', () => { + const options = { + lineNumbers: { oldLineNumber: 1, newLineNumber: 2 }, + contextLines: [{ oldLine: 1 }], + fileHash: 'ff9200', + params: { + bottom: true, + }, + }; + const diffFile = { + fileHash: options.fileHash, + highlightedDiffLines: [], + parallelDiffLines: [], + }; + const state = { diffFiles: [diffFile] }; + const lines = [{ oldLine: 1 }]; + + const findDiffFileSpy = spyOnDependency(mutations, 'findDiffFile').and.returnValue(diffFile); + const removeMatchLineSpy = spyOnDependency(mutations, 'removeMatchLine'); + const lineRefSpy = spyOnDependency(mutations, 'addLineReferences').and.returnValue(lines); + const addContextLinesSpy = spyOnDependency(mutations, 'addContextLines'); + + mutations[types.ADD_CONTEXT_LINES](state, options); + + expect(findDiffFileSpy).toHaveBeenCalledWith(state.diffFiles, options.fileHash); + expect(removeMatchLineSpy).toHaveBeenCalledWith( + diffFile, + options.lineNumbers, + options.params.bottom, + ); + expect(lineRefSpy).toHaveBeenCalledWith( + options.contextLines, + options.lineNumbers, + options.params.bottom, + ); + expect(addContextLinesSpy).toHaveBeenCalledWith({ + inlineLines: diffFile.highlightedDiffLines, + parallelLines: diffFile.parallelDiffLines, + contextLines: options.contextLines, + bottom: options.params.bottom, + lineNumbers: options.lineNumbers, + }); + }); + }); + + describe('ADD_COLLAPSED_DIFFS', () => { + it('should update the state with the given data for the given file hash', () => { + const spy = spyOnDependency(mutations, 'convertObjectPropsToCamelCase').and.callThrough(); + + const fileHash = 123; + const state = { diffFiles: [{}, { fileHash, existingField: 0 }] }; + const file = { fileHash }; + const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] }; + + mutations[types.ADD_COLLAPSED_DIFFS](state, { file, data }); + expect(spy).toHaveBeenCalledWith(data, { deep: true }); + + expect(state.diffFiles[1].fileHash).toEqual(fileHash); + expect(state.diffFiles[1].existingField).toEqual(1); + expect(state.diffFiles[1].extraField).toEqual(1); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js new file mode 100644 index 00000000000..5a024a0f2ad --- /dev/null +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -0,0 +1,179 @@ +import * as utils from '~/diffs/store/utils'; +import { + LINE_POSITION_LEFT, + LINE_POSITION_RIGHT, + TEXT_DIFF_POSITION_TYPE, + DIFF_NOTE_TYPE, + NEW_LINE_TYPE, + OLD_LINE_TYPE, + MATCH_LINE_TYPE, + PARALLEL_DIFF_VIEW_TYPE, +} from '~/diffs/constants'; +import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants'; +import diffFileMockData from '../mock_data/diff_file'; +import { noteableDataMock } from '../../notes/mock_data'; + +const getDiffFileMock = () => Object.assign({}, diffFileMockData); + +describe('DiffsStoreUtils', () => { + describe('findDiffFile', () => { + const files = [{ fileHash: 1, name: 'one' }]; + + it('should return correct file', () => { + expect(utils.findDiffFile(files, 1).name).toEqual('one'); + expect(utils.findDiffFile(files, 2)).toBeUndefined(); + }); + }); + + describe('getReversePosition', () => { + it('should return correct line position name', () => { + expect(utils.getReversePosition(LINE_POSITION_RIGHT)).toEqual(LINE_POSITION_LEFT); + expect(utils.getReversePosition(LINE_POSITION_LEFT)).toEqual(LINE_POSITION_RIGHT); + }); + }); + + describe('findIndexInInlineLines and findIndexInParallelLines', () => { + const expectSet = (method, lines, invalidLines) => { + expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4); + expect(method(invalidLines || lines, { oldLineNumber: 32, newLineNumber: 53 })).toEqual(-1); + }; + + describe('findIndexInInlineLines', () => { + it('should return correct index for given line numbers', () => { + expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlightedDiffLines); + }); + }); + + describe('findIndexInParallelLines', () => { + it('should return correct index for given line numbers', () => { + expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallelDiffLines, {}); + }); + }); + }); + + describe('removeMatchLine', () => { + it('should remove match line properly by regarding the bottom parameter', () => { + const diffFile = getDiffFileMock(); + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers); + const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers); + const atInlineIndex = diffFile.highlightedDiffLines[inlineIndex]; + const atParallelIndex = diffFile.parallelDiffLines[parallelIndex]; + + utils.removeMatchLine(diffFile, lineNumbers, false); + expect(diffFile.highlightedDiffLines[inlineIndex]).not.toEqual(atInlineIndex); + expect(diffFile.parallelDiffLines[parallelIndex]).not.toEqual(atParallelIndex); + + utils.removeMatchLine(diffFile, lineNumbers, true); + expect(diffFile.highlightedDiffLines[inlineIndex + 1]).not.toEqual(atInlineIndex); + expect(diffFile.parallelDiffLines[parallelIndex + 1]).not.toEqual(atParallelIndex); + }); + }); + + describe('addContextLines', () => { + it('should add context lines properly with bottom parameter', () => { + const diffFile = getDiffFileMock(); + const inlineLines = diffFile.highlightedDiffLines; + const parallelLines = diffFile.parallelDiffLines; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const contextLines = [{ lineNumber: 42 }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true }; + const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers); + const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers); + const normalizedParallelLine = { + left: options.contextLines[0], + right: options.contextLines[0], + }; + + utils.addContextLines(options); + expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); + expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine); + + delete options.bottom; + utils.addContextLines(options); + expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); + expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + }); + }); + + describe('getNoteFormData', () => { + it('should properly create note form data', () => { + const diffFile = getDiffFileMock(); + noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE; + + const options = { + note: 'Hello world!', + noteableData: noteableDataMock, + noteableType: MERGE_REQUEST_NOTEABLE_TYPE, + diffFile, + noteTargetLine: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + metaData: null, + newLine: 3, + oldLine: 1, + }, + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + linePosition: LINE_POSITION_LEFT, + }; + + const position = JSON.stringify({ + base_sha: diffFile.diffRefs.baseSha, + start_sha: diffFile.diffRefs.startSha, + head_sha: diffFile.diffRefs.headSha, + old_path: diffFile.oldPath, + new_path: diffFile.newPath, + position_type: TEXT_DIFF_POSITION_TYPE, + old_line: options.noteTargetLine.oldLine, + new_line: options.noteTargetLine.newLine, + }); + + const postData = { + view: options.diffViewType, + line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, + merge_request_diff_head_sha: diffFile.diffRefs.headSha, + in_reply_to_discussion_id: '', + note_project_id: '', + target_type: options.noteableType, + target_id: options.noteableData.id, + note: { + noteable_type: options.noteableType, + noteable_id: options.noteableData.id, + commit_id: '', + type: DIFF_NOTE_TYPE, + line_code: options.noteTargetLine.lineCode, + note: options.note, + position, + }, + }; + + expect(utils.getNoteFormData(options)).toEqual({ + endpoint: options.noteableData.create_note_path, + data: postData, + }); + }); + }); + + describe('addLineReferences', () => { + const lineNumbers = { oldLineNumber: 3, newLineNumber: 4 }; + + it('should add correct line references when bottom set to true', () => { + const lines = [{ type: null }, { type: MATCH_LINE_TYPE }]; + const linesWithReferences = utils.addLineReferences(lines, lineNumbers, true); + + expect(linesWithReferences[0].oldLine).toEqual(lineNumbers.oldLineNumber + 1); + expect(linesWithReferences[0].newLine).toEqual(lineNumbers.newLineNumber + 1); + expect(linesWithReferences[1].metaData.oldPos).toEqual(4); + expect(linesWithReferences[1].metaData.newPos).toEqual(5); + }); + + it('should add correct line references when bottom falsy', () => { + const lines = [{ type: null }, { type: MATCH_LINE_TYPE }, { type: null }]; + const linesWithReferences = utils.addLineReferences(lines, lineNumbers); + + expect(linesWithReferences[0].oldLine).toEqual(0); + expect(linesWithReferences[0].newLine).toEqual(1); + expect(linesWithReferences[1].metaData.oldPos).toEqual(2); + expect(linesWithReferences[1].metaData.newPos).toEqual(3); + }); + }); +}); diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb new file mode 100644 index 00000000000..351db6ba184 --- /dev/null +++ b/spec/javascripts/fixtures/commit.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + let(:commit) { project.commit("master") } + + render_views + + before(:all) do + clean_frontend_fixtures('commit/') + end + + before do + project.add_master(user) + sign_in(user) + end + + it 'commit/show.html.raw' do |example| + params = { + namespace_id: project.namespace, + project_id: project, + id: commit.id + } + + get :show, params + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index fa97f352e31..38fc963caf7 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -7,6 +7,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) } + let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') } render_views diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 175f386b60e..af58dff7da7 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ +/* eslint-disable comma-dangle, no-param-reassign */ import $ from 'jquery'; import GLDropdown from '~/gl_dropdown'; diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js index 108e0064c47..2839020b2ca 100644 --- a/spec/javascripts/gl_field_errors_spec.js +++ b/spec/javascripts/gl_field_errors_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, arrow-body-style */ +/* eslint-disable arrow-body-style */ import $ from 'jquery'; import GlFieldErrors from '~/gl_field_errors'; diff --git a/spec/javascripts/helpers/index.js b/spec/javascripts/helpers/index.js new file mode 100644 index 00000000000..d2c5caf0bdb --- /dev/null +++ b/spec/javascripts/helpers/index.js @@ -0,0 +1,3 @@ +import mountComponent, { mountComponentWithStore } from './vue_mount_component_helper'; + +export { mountComponent, mountComponentWithStore }; diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js new file mode 100644 index 00000000000..921d42a0871 --- /dev/null +++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js @@ -0,0 +1,40 @@ +import initMRPage from '~/mr_notes/index'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; +import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data'; +import diffFileMockData from '../diffs/mock_data/diff_file'; + +export default function initVueMRPage() { + const diffsAppEndpoint = '/diffs/app/endpoint'; + const mrEl = document.createElement('div'); + mrEl.className = 'merge-request fixture-mr'; + mrEl.setAttribute('data-mr-action', 'diffs'); + document.body.appendChild(mrEl); + + const mrDiscussionsEl = document.createElement('div'); + mrDiscussionsEl.id = 'js-vue-mr-discussions'; + mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock)); + mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock)); + mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock)); + mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request'); + document.body.appendChild(mrDiscussionsEl); + + const discussionCounterEl = document.createElement('div'); + discussionCounterEl.id = 'js-vue-discussion-counter'; + document.body.appendChild(discussionCounterEl); + + const diffsAppEl = document.createElement('div'); + diffsAppEl.id = 'js-diffs-app'; + diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint); + diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock)); + document.body.appendChild(diffsAppEl); + + const mock = new MockAdapter(axios); + mock.onGet(diffsAppEndpoint).reply(200, { + branch_name: 'foo', + diff_files: [diffFileMockData], + }); + + initMRPage(); + return mock; +} diff --git a/spec/javascripts/helpers/vue_resource_helper.js b/spec/javascripts/helpers/vue_resource_helper.js index 0d1bf5e2e80..70b7ec4e574 100644 --- a/spec/javascripts/helpers/vue_resource_helper.js +++ b/spec/javascripts/helpers/vue_resource_helper.js @@ -5,7 +5,6 @@ export const headersInterceptor = (request, next) => { response.headers.forEach((value, key) => { headers[key] = value; }); - // eslint-disable-next-line no-param-reassign response.headers = headers; }); }; diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js index ba9040524b1..5add150f874 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js +++ b/spec/javascripts/issuable_time_tracker_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars, space-before-function-paren, func-call-spacing, no-spaced-func, semi, max-len, quotes, space-infix-ops, padded-blocks */ +/* eslint-disable no-unused-vars, func-call-spacing, no-spaced-func, semi, quotes, space-infix-ops, max-len */ import $ from 'jquery'; import Vue from 'vue'; diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 047ecab27db..e12419b835d 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ +/* eslint-disable one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index da00b615c9b..79e375aa02e 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -304,7 +304,6 @@ describe('Job', () => { describe('getBuildTrace', () => { it('should request build trace with state parameter', (done) => { spyOn(axios, 'get').and.callThrough(); - // eslint-disable-next-line no-new job = new Job(); setTimeout(() => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a9ec7f42a9d..41ff59949e5 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -556,5 +556,75 @@ describe('common_utils', () => { expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0); expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0); }); + + it('does not deep-convert by default', () => { + const obj = { + snake_key: { + child_snake_key: 'value', + }, + }; + + expect( + commonUtils.convertObjectPropsToCamelCase(obj), + ).toEqual({ + snakeKey: { + child_snake_key: 'value', + }, + }); + }); + + describe('deep: true', () => { + it('converts object with child objects', () => { + const obj = { + snake_key: { + child_snake_key: 'value', + }, + }; + + expect( + commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }), + ).toEqual({ + snakeKey: { + childSnakeKey: 'value', + }, + }); + }); + + it('converts array with child objects', () => { + const arr = [ + { + child_snake_key: 'value', + }, + ]; + + expect( + commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), + ).toEqual([ + { + childSnakeKey: 'value', + }, + ]); + }); + + it('converts array with child arrays', () => { + const arr = [ + [ + { + child_snake_key: 'value', + }, + ], + ]; + + expect( + commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), + ).toEqual([ + [ + { + childSnakeKey: 'value', + }, + ], + ]); + }); + }); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index eab5c24406a..33987574f00 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -96,4 +96,20 @@ describe('text_utility', () => { expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world'); }); }); + + describe('truncateSha', () => { + it('shortens SHAs to 8 characters', () => { + expect(textUtils.truncateSha('verylongsha')).toBe('verylong'); + }); + + it('leaves short SHAs as is', () => { + expect(textUtils.truncateSha('shortsha')).toBe('shortsha'); + }); + }); + + describe('splitCamelCase', () => { + it('separates a PascalCase word to two', () => { + expect(textUtils.splitCamelCase('HelloWorld')).toBe('Hello World'); + }); + }); }); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index d2bdc9e160c..8cf0017f4d8 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */ +/* eslint-disable no-var, quotes, prefer-template, no-else-return, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ import $ from 'jquery'; import LineHighlighter from '~/line_highlighter'; diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js index 7cc5e753c22..0d465510fd3 100644 --- a/spec/javascripts/matchers.js +++ b/spec/javascripts/matchers.js @@ -1,4 +1,16 @@ export default { + toContainText: () => ({ + compare(vm, text) { + if (!(vm.$el instanceof HTMLElement)) { + throw new Error('vm.$el is not a DOM element!'); + } + + const result = { + pass: vm.$el.innerText.includes(text), + }; + return result; + }, + }), toHaveSpriteIcon: () => ({ compare(element, iconName) { if (!iconName) { @@ -10,7 +22,9 @@ export default { } const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); - const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`)); + const matchingIcon = iconReferences.find(reference => + reference.getAttribute('xlink:href').endsWith(`#${iconName}`), + ); const result = { pass: !!matchingIcon, }; @@ -20,7 +34,7 @@ export default { } else { result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; - const existingIcons = iconReferences.map((reference) => { + const existingIcons = iconReferences.map(reference => { const iconUrl = reference.getAttribute('xlink:href'); return `"${iconUrl.replace(/^.+#/, '')}"`; }); @@ -32,4 +46,12 @@ export default { return result; }, }), + toRender: () => ({ + compare(vm) { + const result = { + pass: vm.$el.nodeType !== Node.COMMENT_NODE, + }; + return result; + }, + }), }; diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js deleted file mode 100644 index dc9dc4d4249..00000000000 --- a/spec/javascripts/merge_request_notes_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import $ from 'jquery'; -import _ from 'underscore'; -import 'autosize'; -import '~/gl_form'; -import '~/lib/utils/text_utility'; -import '~/behaviors/markdown/render_gfm'; -import Notes from '~/notes'; - -const upArrowKeyCode = 38; - -describe('Merge request notes', () => { - window.gon = window.gon || {}; - window.gl = window.gl || {}; - gl.utils = gl.utils || {}; - - const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; - const changesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; - preloadFixtures(discussionTabFixture, changesTabJsonFixture); - - describe('Discussion tab with diff comments', () => { - beforeEach(() => { - loadFixtures(discussionTabFixture); - gl.utils.disableButtonIfEmptyField = _.noop; - window.project_uploads_path = 'http://test.host/uploads'; - $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('authorId'); - - return new Notes('', []); - }); - - afterEach(() => { - // Undo what we did to the shared <body> - $('body').removeAttr('data-page'); - }); - - describe('up arrow', () => { - it('edits last comment when triggered in main form', () => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = upArrowKeyCode; - - spyOnEvent('.note:last .js-note-edit', 'click'); - - $('.js-note-text').trigger(upArrowEvent); - - expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); - }); - - it('edits last comment in discussion when triggered in discussion form', (done) => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = upArrowKeyCode; - - spyOnEvent('.note-discussion .js-note-edit', 'click'); - - $('.js-discussion-reply-button').click(); - - setTimeout(() => { - expect( - $('.note-discussion .js-note-text'), - ).toExist(); - - $('.note-discussion .js-note-text').trigger(upArrowEvent); - - expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); - - done(); - }); - }); - }); - }); - - describe('Changes tab with diff comments', () => { - beforeEach(() => { - const diffsResponse = getJSONFixture(changesTabJsonFixture); - const noteFormHtml = `<form class="js-new-note-form"> - <textarea class="js-note-text"></textarea> - </form>`; - setFixtures(diffsResponse.html + noteFormHtml); - $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('authorId'); - - return new Notes('', []); - }); - - afterEach(() => { - // Undo what we did to the shared <body> - $('body').removeAttr('data-page'); - }); - - describe('up arrow', () => { - it('edits last comment in discussion when triggered in discussion form', (done) => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = upArrowKeyCode; - - spyOnEvent('.note:last .js-note-edit', 'click'); - - $('.js-discussion-reply-button').trigger('click'); - - setTimeout(() => { - $('.js-note-text').trigger(upArrowEvent); - - expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); - - done(); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 74ceff76d37..22eb0ad7143 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-return-assign */ +/* eslint-disable no-return-assign */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 3dbd9756cd2..08928e13985 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var, comma-dangle, object-shorthand */ - +/* eslint-disable no-var, object-shorthand */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; @@ -7,480 +6,228 @@ import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/breakpoints'; import '~/lib/utils/common_utils'; -import Diff from '~/diff'; -import Notes from '~/notes'; import 'vendor/jquery.scrollTo'; - -(function () { - describe('MergeRequestTabs', function () { - var stubLocation = {}; - var setLocation = function (stubs) { - var defaults = { - pathname: '', - search: '', - hash: '' - }; - $.extend(stubLocation, defaults, stubs || {}); +import initMrPage from './helpers/init_vue_mr_page_helper'; + +describe('MergeRequestTabs', function() { + let mrPageMock; + var stubLocation = {}; + var setLocation = function(stubs) { + var defaults = { + pathname: '', + search: '', + hash: '', }; + $.extend(stubLocation, defaults, stubs || {}); + }; - const inlineChangesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; - const parallelChangesTabJsonFixture = 'merge_request_diffs/parallel_changes_tab_with_comments.json'; - preloadFixtures( - 'merge_requests/merge_request_with_task_list.html.raw', - 'merge_requests/diff_comment.html.raw', - inlineChangesTabJsonFixture, - parallelChangesTabJsonFixture - ); - - beforeEach(function () { - this.class = new MergeRequestTabs({ stubLocation: stubLocation }); - setLocation(); - - this.spies = { - history: spyOn(window.history, 'replaceState').and.callFake(function () {}) - }; - }); - - afterEach(function () { - this.class.unbindEvents(); - this.class.destroyPipelinesView(); - }); - - describe('activateTab', function () { - beforeEach(function () { - spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - this.subject = this.class.activateTab; - }); - it('shows the notes tab when action is show', function () { - this.subject('show'); - expect($('#notes')).toHaveClass('active'); - }); - it('shows the commits tab when action is commits', function () { - this.subject('commits'); - expect($('#commits')).toHaveClass('active'); - }); - it('shows the diffs tab when action is diffs', function () { - this.subject('diffs'); - expect($('#diffs')).toHaveClass('active'); - }); - }); - - describe('opensInNewTab', function () { - var tabUrl; - var windowTarget = '_blank'; - - beforeEach(function () { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - - tabUrl = $('.commits-tab a').attr('href'); + preloadFixtures( + 'merge_requests/merge_request_with_task_list.html.raw', + 'merge_requests/diff_comment.html.raw', + ); - spyOn($.fn, 'attr').and.returnValue(tabUrl); - }); - - describe('meta click', () => { - let metakeyEvent; - beforeEach(function () { - metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); - }); + beforeEach(function() { + mrPageMock = initMrPage(); + this.class = new MergeRequestTabs({ stubLocation: stubLocation }); + setLocation(); - it('opens page when commits link is clicked', function () { - spyOn(window, 'open').and.callFake(function (url, name) { - expect(url).toEqual(tabUrl); - expect(name).toEqual(windowTarget); - }); + this.spies = { + history: spyOn(window.history, 'replaceState').and.callFake(function() {}), + }; + }); - this.class.bindEvents(); - $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); - }); + afterEach(function() { + this.class.unbindEvents(); + this.class.destroyPipelinesView(); + mrPageMock.restore(); + }); - it('opens page when commits badge is clicked', function () { - spyOn(window, 'open').and.callFake(function (url, name) { - expect(url).toEqual(tabUrl); - expect(name).toEqual(windowTarget); - }); + describe('opensInNewTab', function() { + var tabUrl; + var windowTarget = '_blank'; - this.class.bindEvents(); - $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); - }); - }); + beforeEach(function() { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { - spyOn(window, 'open').and.callFake(function (url, name) { - expect(url).toEqual(tabUrl); - expect(name).toEqual(windowTarget); - }); + tabUrl = $('.commits-tab a').attr('href'); + }); - this.class.clickTab({ - metaKey: false, - ctrlKey: true, - which: 1, - stopImmediatePropagation: function () {} - }); + describe('meta click', () => { + let metakeyEvent; + beforeEach(function() { + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); - it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { - spyOn(window, 'open').and.callFake(function (url, name) { + it('opens page when commits link is clicked', function() { + spyOn(window, 'open').and.callFake(function(url, name) { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: true, - ctrlKey: false, - which: 1, - stopImmediatePropagation: function () {} - }); + this.class.bindEvents(); + $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); }); - it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { - spyOn(window, 'open').and.callFake(function (url, name) { + it('opens page when commits badge is clicked', function() { + spyOn(window, 'open').and.callFake(function(url, name) { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: false, - ctrlKey: false, - which: 2, - stopImmediatePropagation: function () {} - }); + this.class.bindEvents(); + $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); }); }); - describe('setCurrentAction', function () { - beforeEach(function () { - spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); - this.subject = this.class.setCurrentAction; - }); - - it('changes from commits', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1/commits' - }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); - expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); - }); - - it('changes from diffs', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1/diffs' - }); - - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); - expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function() { + spyOn(window, 'open').and.callFake(function(url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); }); - it('changes from diffs.html', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1/diffs.html' - }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); - expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + this.class.clickTab({ + metaKey: false, + ctrlKey: true, + which: 1, + stopImmediatePropagation: function() {}, }); + }); - it('changes from notes', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1' - }); - expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); - expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + it('opens page tab in a new browser tab with Cmd+Click - Mac', function() { + spyOn(window, 'open').and.callFake(function(url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); }); - it('includes search parameters and hash string', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1/diffs', - search: '?view=parallel', - hash: '#L15-35' - }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + this.class.clickTab({ + metaKey: true, + ctrlKey: false, + which: 1, + stopImmediatePropagation: function() {}, }); + }); - it('replaces the current history state', function () { - var newState; - setLocation({ - pathname: '/foo/bar/merge_requests/1' - }); - newState = this.subject('commits'); - expect(this.spies.history).toHaveBeenCalledWith({ - url: newState - }, document.title, newState); + it('opens page tab in a new browser tab with Middle-click - Mac/PC', function() { + spyOn(window, 'open').and.callFake(function(url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); }); - it('treats "show" like "notes"', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1/commits' - }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + this.class.clickTab({ + metaKey: false, + ctrlKey: false, + which: 2, + stopImmediatePropagation: function() {}, }); }); + }); - describe('tabShown', () => { - let mock; + describe('setCurrentAction', function() { + let mock; - beforeEach(function () { - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/diffs\.json/).reply(200, { - data: { html: '' }, - }); + beforeEach(function() { + mock = new MockAdapter(axios); + mock.onAny().reply({ data: {} }); + this.subject = this.class.setCurrentAction; + }); - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - }); + afterEach(() => { + mock.restore(); + }); - afterEach(() => { - mock.restore(); + it('changes from commits', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/commits', }); - describe('with "Side-by-side"/parallel diff view', () => { - beforeEach(function () { - this.class.diffViewType = () => 'parallel'; - Diff.prototype.diffViewType = () => 'parallel'; - }); - - it('maintains `container-limited` for pipelines tab', function (done) { - const asyncClick = function (selector) { - return new Promise((resolve) => { - setTimeout(() => { - document.querySelector(selector).click(); - resolve(); - }); - }); - }; - asyncClick('.merge-request-tabs .pipelines-tab a') - .then(() => asyncClick('.merge-request-tabs .diffs-tab a')) - .then(() => asyncClick('.merge-request-tabs .pipelines-tab a')) - .then(() => { - const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); - expect(hasContainerLimitedClass).toBe(true); - }) - .then(done) - .catch((err) => { - done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); - }); - }); - - it('maintains `container-limited` when switching from "Changes" tab before it loads', function (done) { - const asyncClick = function (selector) { - return new Promise((resolve) => { - setTimeout(() => { - document.querySelector(selector).click(); - resolve(); - }); - }); - }; - - asyncClick('.merge-request-tabs .diffs-tab a') - .then(() => asyncClick('.merge-request-tabs .notes-tab a')) - .then(() => { - const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); - expect(hasContainerLimitedClass).toBe(true); - }) - .then(done) - .catch((err) => { - done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); - }); - }); - }); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); }); - describe('loadDiff', function () { - beforeEach(() => { - loadFixtures('merge_requests/diff_comment.html.raw'); - $('body').attr('data-page', 'projects:merge_requests:show'); - window.gl.ImageFile = () => {}; - Notes.initialize('', []); - spyOn(Notes.instance, 'toggleDiffNote').and.callThrough(); + it('changes from diffs', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', }); - afterEach(() => { - delete window.gl.ImageFile; - delete window.notes; + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); - // Undo what we did to the shared <body> - $('body').removeAttr('data-page'); + it('changes from diffs.html', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/diffs.html', }); - it('triggers Ajax request to JSON endpoint', function (done) { - const url = '/foo/bar/merge_requests/1/diffs'; - - spyOn(axios, 'get').and.callFake((reqUrl) => { - expect(reqUrl).toBe(`${url}.json`); - - done(); - - return Promise.resolve({ data: {} }); - }); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); - this.class.loadDiff(url); + it('changes from notes', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1', }); - it('triggers scroll event when diff already loaded', function (done) { - spyOn(axios, 'get').and.callFake(done.fail); - spyOn(document, 'dispatchEvent'); - - this.class.diffsLoaded = true; - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); - expect( - document.dispatchEvent, - ).toHaveBeenCalledWith(new CustomEvent('scroll')); - done(); + it('includes search parameters and hash string', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', + search: '?view=parallel', + hash: '#L15-35', }); - describe('with inline diff', () => { - let noteId; - let noteLineNumId; - let mock; - - beforeEach(() => { - const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); - - const $html = $(diffsResponse.html); - noteId = $html.find('.note').attr('id'); - noteLineNumId = $html - .find('.note') - .closest('.notes_holder') - .prev('.line_holder') - .find('a[data-linenumber]') - .attr('href') - .replace('#', ''); - - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'old', - forceShow: true, - }); - - done(); - }); - }); - - it('should gracefully ignore non-existant fragment hash', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - - done(); - }); - }); - }); - - describe('with line number fragment hash', () => { - it('should gracefully ignore line number fragment hash', function () { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + }); - expect(noteLineNumId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - }); - }); + it('replaces the current history state', function() { + var newState; + setLocation({ + pathname: '/foo/bar/merge_requests/1', }); + newState = this.subject('commits'); - describe('with parallel diff', () => { - let noteId; - let noteLineNumId; - let mock; - - beforeEach(() => { - const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); - - const $html = $(diffsResponse.html); - noteId = $html.find('.note').attr('id'); - noteLineNumId = $html - .find('.note') - .closest('.notes_holder') - .prev('.line_holder') - .find('a[data-linenumber]') - .attr('href') - .replace('#', ''); - - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId); - - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'new', - forceShow: true, - }); - - done(); - }); - }); - - it('should gracefully ignore non-existant fragment hash', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - done(); - }); - }); - }); - - describe('with line number fragment hash', () => { - it('should gracefully ignore line number fragment hash', function () { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + expect(this.spies.history).toHaveBeenCalledWith( + { + url: newState, + }, + document.title, + newState, + ); + }); - expect(noteLineNumId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - }); - }); + it('treats "show" like "notes"', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/commits', }); + + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); + }); - describe('expandViewContainer', function () { - beforeEach(() => { - $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>'); - }); + describe('expandViewContainer', function() { + beforeEach(() => { + $('body').append( + '<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>', + ); + }); - afterEach(() => { - $('.content-wrapper').remove(); - }); + afterEach(() => { + $('.content-wrapper').remove(); + }); - it('removes container-limited from containers', function () { - this.class.expandViewContainer(); + it('removes container-limited from containers', function() { + this.class.expandViewContainer(); - expect($('.content-wrapper')).not.toContainElement('.container-limited'); - }); + expect($('.content-wrapper')).not.toContainElement('.container-limited'); + }); - it('does remove container-limited from breadcrumbs', function () { - $('.container-limited').addClass('breadcrumbs'); - this.class.expandViewContainer(); + it('does remove container-limited from breadcrumbs', function() { + $('.container-limited').addClass('breadcrumbs'); + this.class.expandViewContainer(); - expect($('.content-wrapper')).toContainElement('.container-limited'); - }); + expect($('.content-wrapper')).toContainElement('.container-limited'); }); }); -}).call(window); +}); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 009b3fd75b7..1879424c629 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable no-new */ - import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 50da6da2e07..799d03f6b57 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -1,5 +1,3 @@ -/* eslint-disable quote-props, indent, comma-dangle */ - export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`; export const metricsGroupsAPIResponse = { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 5e5d8f8f34f..122e5bc58b2 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ +/* eslint-disable one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index a7d1e4331eb..155c91dcc46 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -1,23 +1,27 @@ import $ from 'jquery'; import Vue from 'vue'; import Autosize from 'autosize'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import CommentForm from '~/notes/components/comment_form.vue'; +import * as constants from '~/notes/constants'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; describe('issue_comment_form component', () => { + let store; let vm; const Component = Vue.extend(CommentForm); let mountComponent; beforeEach(() => { - mountComponent = (noteableType = 'issue') => new Component({ - propsData: { - noteableType, - }, - store, - }).$mount(); + store = createStore(); + mountComponent = (noteableType = 'issue') => + new Component({ + propsData: { + noteableType, + }, + store, + }).$mount(); }); afterEach(() => { @@ -34,7 +38,9 @@ describe('issue_comment_form component', () => { }); it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual( + userDataMock.path, + ); }); describe('handleSave', () => { @@ -60,7 +66,7 @@ describe('issue_comment_form component', () => { expect(vm.toggleIssueState).toHaveBeenCalled(); }); - it('should disable action button whilst submitting', (done) => { + it('should disable action button whilst submitting', done => { const saveNotePromise = Promise.resolve(); vm.note = 'hello world'; spyOn(vm, 'saveNote').and.returnValue(saveNotePromise); @@ -87,16 +93,18 @@ describe('issue_comment_form component', () => { ).toEqual('Write a comment or drag your files here…'); }); - it('should make textarea disabled while requesting', (done) => { + it('should make textarea disabled while requesting', done => { const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); vm.note = 'hello world'; spyOn(vm, 'stopPolling'); spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); - vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. + vm.$nextTick(() => { + // Wait for vm.note change triggered. It should enable $submitButton. $submitButton.trigger('click'); - vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. + vm.$nextTick(() => { + // Wait for vm.isSubmitting triggered. It should disable textarea. expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); done(); }); @@ -105,21 +113,27 @@ describe('issue_comment_form component', () => { it('should support quick actions', () => { expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), + vm.$el + .querySelector('.js-main-target-form textarea') + .getAttribute('data-supports-quick-actions'), ).toEqual('true'); }); it('should link to markdown docs', () => { const { markdownDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( + 'Markdown', + ); }); it('should link to quick actions docs', () => { const { quickActionsDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + expect( + vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim(), + ).toEqual('quick actions'); }); - it('should resize textarea after note discarded', (done) => { + it('should resize textarea after note discarded', done => { spyOn(Autosize, 'update'); spyOn(vm, 'discard').and.callThrough(); @@ -136,7 +150,9 @@ describe('issue_comment_form component', () => { it('should enter edit mode when arrow up is pressed', () => { spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true)); + vm.$el + .querySelector('.js-main-target-form textarea') + .dispatchEvent(keyboardDownEvent(38, true)); expect(vm.editCurrentUserLastNote).toHaveBeenCalled(); }); @@ -151,7 +167,9 @@ describe('issue_comment_form component', () => { it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); + vm.$el + .querySelector('.js-main-target-form textarea') + .dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleSave).toHaveBeenCalled(); }); @@ -159,7 +177,9 @@ describe('issue_comment_form component', () => { it('should save note when ctrl+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + vm.$el + .querySelector('.js-main-target-form textarea') + .dispatchEvent(keyboardDownEvent(13, false, true)); expect(vm.handleSave).toHaveBeenCalled(); }); @@ -168,41 +188,51 @@ describe('issue_comment_form component', () => { describe('actions', () => { it('should be possible to close the issue', () => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue'); + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual( + 'Close issue', + ); }); it('should render comment button as disabled', () => { - expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled'); + expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual( + 'disabled', + ); }); - it('should enable comment button if it has note', (done) => { + it('should enable comment button if it has note', done => { vm.note = 'Foo'; Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null); + expect( + vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled'), + ).toEqual(null); done(); }); }); - it('should update buttons texts when it has note', (done) => { + it('should update buttons texts when it has note', done => { vm.note = 'Foo'; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue'); + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual( + 'Comment & close issue', + ); expect(vm.$el.querySelector('.js-note-discard')).toBeDefined(); done(); }); }); - it('updates button text with noteable type', (done) => { - vm.noteableType = 'merge_request'; + it('updates button text with noteable type', done => { + vm.noteableType = constants.MERGE_REQUEST_NOTEABLE_TYPE; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close merge request'); + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual( + 'Close merge request', + ); done(); }); }); describe('when clicking close/reopen button', () => { - it('should disable button and show a loading spinner', (done) => { + it('should disable button and show a loading spinner', done => { const toggleStateButton = vm.$el.querySelector('.js-action-button'); toggleStateButton.click(); @@ -217,7 +247,7 @@ describe('issue_comment_form component', () => { }); describe('issue is confidential', () => { - it('shows information warning', (done) => { + it('shows information warning', done => { store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true })); Vue.nextTick(() => { expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); @@ -237,7 +267,9 @@ describe('issue_comment_form component', () => { }); it('should render signed out widget', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual( + 'Please register or sign in to reply', + ); }); it('should not render submission form', () => { diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js deleted file mode 100644 index ef6d513444a..00000000000 --- a/spec/javascripts/notes/components/diff_file_header_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import Vue from 'vue'; -import DiffFileHeader from '~/notes/components/diff_file_header.vue'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -const discussionFixture = 'merge_requests/diff_discussion.json'; - -describe('diff_file_header', () => { - let vm; - const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; - const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file); - const props = { - diffFile, - }; - const Component = Vue.extend(DiffFileHeader); - const selectors = { - get copyButton() { - return vm.$el.querySelector('button[data-original-title="Copy file path to clipboard"]'); - }, - get fileName() { - return vm.$el.querySelector('.file-title-name'); - }, - get titleWrapper() { - return vm.$refs.titleWrapper; - }, - }; - - describe('submodule', () => { - beforeEach(() => { - props.diffFile.submodule = true; - props.diffFile.submoduleLink = '<a href="/bha">Submodule</a>'; - - vm = mountComponent(Component, props); - }); - - it('shows submoduleLink', () => { - expect(selectors.fileName.innerHTML).toBe(props.diffFile.submoduleLink); - }); - - it('has button to copy blob path', () => { - expect(selectors.copyButton).toExist(); - expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.submoduleLink); - }); - }); - - describe('changed file', () => { - beforeEach(() => { - props.diffFile.submodule = false; - props.diffFile.discussionPath = 'some/discussion/id'; - - vm = mountComponent(Component, props); - }); - - it('shows file type icon', () => { - expect(vm.$el.innerHTML).toContain('fa-file-text-o'); - }); - - it('links to discussion path', () => { - expect(selectors.titleWrapper).toExist(); - expect(selectors.titleWrapper.tagName).toBe('A'); - expect(selectors.titleWrapper.getAttribute('href')).toBe(props.diffFile.discussionPath); - }); - - it('shows plain title if no link given', () => { - props.diffFile.discussionPath = undefined; - vm = mountComponent(Component, props); - - expect(selectors.titleWrapper.tagName).not.toBe('A'); - expect(selectors.titleWrapper.href).toBeFalsy(); - }); - - it('has button to copy file path', () => { - expect(selectors.copyButton).toExist(); - expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.filePath); - }); - - it('shows file mode change', (done) => { - vm.diffFile = { - ...props.diffFile, - modeChanged: true, - aMode: '100755', - bMode: '100644', - }; - - Vue.nextTick(() => { - expect( - vm.$refs.fileMode.textContent.trim(), - ).toBe('100755 → 100644'); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js index f4ec7132dbd..239d7950907 100644 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ b/spec/javascripts/notes/components/diff_with_note_spec.js @@ -1,12 +1,14 @@ import Vue from 'vue'; import DiffWithNote from '~/notes/components/diff_with_note.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import createStore from '~/notes/stores'; +import { mountComponentWithStore } from 'spec/helpers'; const discussionFixture = 'merge_requests/diff_discussion.json'; const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; describe('diff_with_note', () => { + let store; let vm; const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; const diffDiscussion = convertObjectPropsToCamelCase(diffDiscussionMock); @@ -29,9 +31,21 @@ describe('diff_with_note', () => { }, }; + beforeEach(() => { + store = createStore(); + store.replaceState({ + ...store.state, + notes: { + noteableData: { + current_user: {}, + }, + }, + }); + }); + describe('text diff', () => { beforeEach(() => { - vm = mountComponent(Component, props); + vm = mountComponentWithStore(Component, { props, store }); }); it('shows text diff', () => { @@ -55,7 +69,7 @@ describe('diff_with_note', () => { }); it('shows image diff', () => { - vm = mountComponent(Component, props); + vm = mountComponentWithStore(Component, { props, store }); expect(selectors.container).toHaveClass('js-image-file'); expect(selectors.diffTable).not.toExist(); diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js new file mode 100644 index 00000000000..7b2302e6f47 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_counter_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import createStore from '~/notes/stores'; +import DiscussionCounter from '~/notes/components/discussion_counter.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; + +describe('DiscussionCounter component', () => { + let store; + let vm; + + beforeEach(() => { + window.mrTabs = {}; + + const Component = Vue.extend(DiscussionCounter); + + store = createStore(); + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = createComponentWithStore(Component, store); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('jumpToFirstUnresolvedDiscussion', () => { + it('expands unresolved discussion', () => { + spyOn(vm, 'expandDiscussion').and.stub(); + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + notes: [{ ...discussionMock.notes[0], resolved: true }], + }, + { + ...discussionMock, + id: discussionMock.id + 1, + notes: [{ ...discussionMock.notes[0], resolved: false }], + }, + ]; + const firstDiscussionId = discussionMock.id + 1; + store.replaceState({ + ...store.state, + discussions, + }); + setFixtures(` + <div data-discussion-id="${firstDiscussionId}"></div> + `); + + vm.jumpToFirstUnresolvedDiscussion(); + + expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId }); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index c9e549d2096..52cc42cb53d 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,14 +1,16 @@ import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; describe('issue_note_actions component', () => { let vm; + let store; let Component; beforeEach(() => { Component = Vue.extend(noteActions); + store = createStore(); }); afterEach(() => { @@ -27,7 +29,9 @@ describe('issue_note_actions component', () => { canAwardEmoji: true, canReportAsAbuse: true, noteId: 539, - reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', + reportAbusePath: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', }; store.dispatch('setUserData', userDataMock); @@ -74,7 +78,9 @@ describe('issue_note_actions component', () => { canAwardEmoji: false, canReportAsAbuse: false, noteId: 539, - reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', + reportAbusePath: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', }; vm = new Component({ store, diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index d494c63ff11..7eb4d3aed29 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -3,7 +3,9 @@ import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; +import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; +import { mountComponentWithStore } from 'spec/helpers'; import * as mockData from '../mock_data'; const vueMatchers = { @@ -22,6 +24,7 @@ const vueMatchers = { describe('note_app', () => { let mountComponent; let vm; + let store; beforeEach(() => { jasmine.addMatchers(vueMatchers); @@ -29,16 +32,18 @@ describe('note_app', () => { const IssueNotesApp = Vue.extend(notesApp); - mountComponent = (data) => { + store = createStore(); + mountComponent = data => { const props = data || { noteableData: mockData.noteableDataMock, notesData: mockData.notesDataMock, userData: mockData.userDataMock, }; - return new IssueNotesApp({ - propsData: props, - }).$mount(); + return mountComponentWithStore(IssueNotesApp, { + props, + store, + }); }; }); @@ -48,9 +53,11 @@ describe('note_app', () => { describe('set data', () => { const responseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); + next( + request.respondWith(JSON.stringify([]), { + status: 200, + }), + ); }; beforeEach(() => { @@ -74,8 +81,8 @@ describe('note_app', () => { expect(vm.$store.state.userData).toEqual(mockData.userDataMock); }); - it('should fetch notes', () => { - expect(vm.$store.state.notes).toEqual([]); + it('should fetch discussions', () => { + expect(vm.$store.state.discussions).toEqual([]); }); }); @@ -89,15 +96,20 @@ describe('note_app', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); - it('should render list of notes', (done) => { - const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0]; + it('should render list of notes', done => { + const note = + mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ + '/gitlab-org/gitlab-ce/issues/26/discussions.json' + ][0].notes[0]; setTimeout(() => { expect( vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(), ).toEqual(note.author.name); - expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html); + expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual( + note.note_html, + ); done(); }, 0); }); @@ -110,9 +122,9 @@ describe('note_app', () => { }); it('should render form comment button as disabled', () => { - expect( - vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'), - ).toEqual('disabled'); + expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( + 'disabled', + ); }); }); @@ -135,7 +147,7 @@ describe('note_app', () => { describe('update note', () => { describe('individual note', () => { - beforeEach((done) => { + beforeEach(done => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); @@ -156,7 +168,7 @@ describe('note_app', () => { expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); - it('calls the service to update the note', (done) => { + it('calls the service to update the note', done => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); @@ -169,7 +181,7 @@ describe('note_app', () => { }); describe('discussion note', () => { - beforeEach((done) => { + beforeEach(done => { Vue.http.interceptors.push(mockData.discussionNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); @@ -191,7 +203,7 @@ describe('note_app', () => { expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); - it('updates the note and resets the edit form', (done) => { + it('updates the note and resets the edit form', done => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); @@ -211,12 +223,16 @@ describe('note_app', () => { it('should render markdown docs url', () => { const { markdownDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( + 'Markdown', + ); }); it('should render quick action docs url', () => { const { quickActionsDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual( + 'quick actions', + ); }); }); @@ -230,7 +246,7 @@ describe('note_app', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); - it('should render markdown docs url', (done) => { + it('should render markdown docs url', done => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); const { markdownDocsPath } = mockData.notesDataMock; @@ -244,15 +260,15 @@ describe('note_app', () => { }, 0); }); - it('should not render quick actions docs url', (done) => { + it('should not render quick actions docs url', done => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); const { quickActionsDocsPath } = mockData.notesDataMock; Vue.nextTick(() => { - expect( - vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`), - ).toEqual(null); + expect(vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`)).toEqual( + null, + ); done(); }); }, 0); diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index 1c30d8691b1..9d98ba219da 100644 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -1,15 +1,17 @@ import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import awardsNote from '~/notes/components/note_awards_list.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; describe('note_awards_list component', () => { + let store; let vm; let awardsMock; beforeEach(() => { const Component = Vue.extend(awardsNote); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); awardsMock = [ @@ -41,7 +43,9 @@ describe('note_awards_list component', () => { it('should render awarded emojis', () => { expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); - expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined(); + expect( + vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'), + ).toBeDefined(); }); it('should be possible to remove awarded emoji', () => { diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js index 4e551496ff0..efad0785afe 100644 --- a/spec/javascripts/notes/components/note_body_spec.js +++ b/spec/javascripts/notes/components/note_body_spec.js @@ -1,15 +1,16 @@ - import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import noteBody from '~/notes/components/note_body.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note_body component', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(noteBody); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -37,7 +38,7 @@ describe('issue_note_body component', () => { }); describe('isEditing', () => { - beforeEach((done) => { + beforeEach(done => { vm.isEditing = true; Vue.nextTick(done); }); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 413d4f69434..95d400ab3df 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -1,16 +1,18 @@ import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import issueNoteForm from '~/notes/components/note_form.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; describe('issue_note_form component', () => { + let store; let vm; let props; beforeEach(() => { const Component = Vue.extend(issueNoteForm); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -31,14 +33,18 @@ describe('issue_note_form component', () => { }); describe('conflicts editing', () => { - it('should show conflict message if note changes outside the component', (done) => { + it('should show conflict message if note changes outside the component', done => { vm.isEditing = true; vm.noteBody = 'Foo'; - const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; + const message = + 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; Vue.nextTick(() => { expect( - vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(), + vm.$el + .querySelector('.js-conflict-edit-warning') + .textContent.replace(/\s+/g, ' ') + .trim(), ).toEqual(message); done(); }); @@ -47,14 +53,16 @@ describe('issue_note_form component', () => { describe('form', () => { it('should render text area with placeholder', () => { - expect( - vm.$el.querySelector('textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here…'); + expect(vm.$el.querySelector('textarea').getAttribute('placeholder')).toEqual( + 'Write a comment or drag your files here…', + ); }); it('should link to markdown docs', () => { const { markdownDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( + 'Markdown', + ); }); describe('keyboard events', () => { @@ -87,7 +95,7 @@ describe('issue_note_form component', () => { }); describe('actions', () => { - it('should be possible to cancel', (done) => { + it('should be possible to cancel', done => { spyOn(vm, 'cancelHandler').and.callThrough(); vm.isEditing = true; @@ -101,7 +109,7 @@ describe('issue_note_form component', () => { }); }); - it('should be possible to update the note', (done) => { + it('should be possible to update the note', done => { vm.isEditing = true; Vue.nextTick(() => { diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js index 5636f8d1a9f..a3c6bf78988 100644 --- a/spec/javascripts/notes/components/note_header_spec.js +++ b/spec/javascripts/notes/components/note_header_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import noteHeader from '~/notes/components/note_header.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; describe('note_header component', () => { + let store; let vm; let Component; beforeEach(() => { Component = Vue.extend(noteHeader); + store = createStore(); }); afterEach(() => { @@ -38,12 +40,8 @@ describe('note_header component', () => { }); it('should render user information', () => { - expect( - vm.$el.querySelector('.note-header-author-name').textContent.trim(), - ).toEqual('Root'); - expect( - vm.$el.querySelector('.note-header-info a').getAttribute('href'), - ).toEqual('/root'); + expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root'); + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root'); }); it('should render timestamp link', () => { @@ -78,7 +76,7 @@ describe('note_header component', () => { expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); }); - it('emits toggle event on click', (done) => { + it('emits toggle event on click', done => { spyOn(vm, '$emit'); vm.$el.querySelector('.js-vue-toggle-button').click(); @@ -89,24 +87,24 @@ describe('note_header component', () => { }); }); - it('renders up arrow when open', (done) => { + it('renders up arrow when open', done => { vm.expanded = true; Vue.nextTick(() => { - expect( - vm.$el.querySelector('.js-vue-toggle-button i').classList, - ).toContain('fa-chevron-up'); + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-up', + ); done(); }); }); - it('renders down arrow when closed', (done) => { + it('renders down arrow when closed', done => { vm.expanded = false; Vue.nextTick(() => { - expect( - vm.$el.querySelector('.js-vue-toggle-button i').classList, - ).toContain('fa-chevron-down'); + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-down', + ); done(); }); }); diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js index 6cba8053888..e217a2caa73 100644 --- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js +++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import noteSignedOut from '~/notes/components/note_signed_out_widget.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import { notesDataMock } from '../mock_data'; describe('note_signed_out_widget component', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(noteSignedOut); + store = createStore(); store.dispatch('setNotesData', notesDataMock); vm = new Component({ @@ -20,18 +22,20 @@ describe('note_signed_out_widget component', () => { }); it('should render sign in link provided in the store', () => { - expect( - vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent, - ).toEqual('sign in'); + expect(vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent).toEqual( + 'sign in', + ); }); it('should render register link provided in the store', () => { - expect( - vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent, - ).toEqual('register'); + expect(vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent).toEqual( + 'register', + ); }); it('should render information text', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual( + 'Please register or sign in to reply', + ); }); }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index cda550760fe..058ddb6202f 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -1,21 +1,24 @@ import Vue from 'vue'; -import store from '~/notes/stores'; -import issueDiscussion from '~/notes/components/noteable_discussion.vue'; +import createStore from '~/notes/stores'; +import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; +import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; -describe('issue_discussion component', () => { +describe('noteable_discussion component', () => { + let store; let vm; beforeEach(() => { - const Component = Vue.extend(issueDiscussion); + const Component = Vue.extend(noteableDiscussion); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); vm = new Component({ store, propsData: { - note: discussionMock, + discussion: discussionMock, }, }).$mount(); }); @@ -55,4 +58,74 @@ describe('issue_discussion component', () => { ).toBeNull(); }); }); + + describe('computed', () => { + describe('hasMultipleUnresolvedDiscussions', () => { + it('is false if there are no unresolved discussions', done => { + spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]); + + Vue.nextTick() + .then(() => { + expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('is false if there is one unresolved discussion', done => { + spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]); + + Vue.nextTick() + .then(() => { + expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('is true if there are two unresolved discussions', done => { + spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([{}, {}]); + + Vue.nextTick() + .then(() => { + expect(vm.hasMultipleUnresolvedDiscussions).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('methods', () => { + describe('jumpToNextDiscussion', () => { + it('expands next unresolved discussion', () => { + spyOn(vm, 'expandDiscussion').and.stub(); + const discussions = [ + discussionMock, + { + ...discussionMock, + id: discussionMock.id + 1, + notes: [{ ...discussionMock.notes[0], resolved: true }], + }, + { + ...discussionMock, + id: discussionMock.id + 2, + notes: [{ ...discussionMock.notes[0], resolved: false }], + }, + ]; + const nextDiscussionId = discussionMock.id + 2; + store.replaceState({ + ...store.state, + discussions, + }); + setFixtures(` + <div data-discussion-id="${nextDiscussionId}"></div> + `); + + vm.jumpToNextDiscussion(); + + expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId }); + }); + }); + }); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index cfd037633e9..a31d17cacbb 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,16 +1,18 @@ import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(issueNote); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -27,12 +29,14 @@ describe('issue_note', () => { }); it('should render user information', () => { - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( + note.author.avatar_url, + ); }); it('should render note header content', () => { - expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name); - expect(vm.$el.querySelector('.note-header .note-headline-meta').textContent.trim()).toContain('commented'); + const el = vm.$el.querySelector('.note-header .note-header-author-name'); + expect(el.textContent.trim()).toEqual(note.author.name); }); it('should render note actions', () => { @@ -43,7 +47,7 @@ describe('issue_note', () => { expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); }); - it('prevents note preview xss', (done) => { + it('prevents note preview xss', done => { const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; const alertSpy = spyOn(window, 'alert'); @@ -59,7 +63,7 @@ describe('issue_note', () => { }); describe('cancel edit', () => { - it('restores content of updated note', (done) => { + it('restores content of updated note', done => { const noteBody = 'updated note text'; vm.updateNote = () => Promise.resolve(); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index fa7adc32193..547efa32694 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -51,6 +51,7 @@ export const noteableDataMock = { time_estimate: 0, title: '14', total_time_spent: 0, + noteable_note_url: '/group/project/merge_requests/1#note_1', updated_at: '2017-08-04T09:53:01.226Z', updated_by_id: 1, web_url: '/gitlab-org/gitlab-ce/issues/26', @@ -99,6 +100,8 @@ export const individualNote = { { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, ], toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', + noteable_note_url: '/group/project/merge_requests/1#note_1', + note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', path: '/gitlab-org/gitlab-ce/notes/1390', @@ -157,6 +160,8 @@ export const note = { }, ], toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji', + note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', path: '/gitlab-org/gitlab-ce/notes/546', @@ -198,6 +203,7 @@ export const discussionMock = { discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', @@ -244,6 +250,7 @@ export const discussionMock = { emoji_awardable: true, award_emoji: [], toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', + noteable_note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', path: '/gitlab-org/gitlab-ce/notes/1396', @@ -288,6 +295,7 @@ export const discussionMock = { discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', @@ -335,6 +343,7 @@ export const loggedOutnoteableData = { can_create_note: false, can_update: false, }, + noteable_note_url: '/group/project/merge_requests/1#note_1', create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', @@ -469,6 +478,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { }, }, ], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1', @@ -513,6 +523,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1', @@ -567,6 +578,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', @@ -618,6 +630,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { emoji_awardable: true, award_emoji: [], toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji', + noteable_note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', path: '/gitlab-org/gitlab-ce/notes/1471', diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 520a25cc5c6..985c2f81ef3 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import _ from 'underscore'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -14,6 +14,12 @@ import { } from '../mock_data'; describe('Actions Notes Store', () => { + let store; + + beforeEach(() => { + store = createStore(); + }); + afterEach(() => { resetStore(store); }); @@ -76,7 +82,7 @@ describe('Actions Notes Store', () => { actions.setInitialNotes, [individualNote], { notes: [] }, - [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }], + [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }], [], done, ); @@ -109,6 +115,19 @@ describe('Actions Notes Store', () => { }); }); + describe('expandDiscussion', () => { + it('should expand discussion', done => { + testAction( + actions.expandDiscussion, + { discussionId: discussionMock.id }, + { notes: [discussionMock] }, + [{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }], + [], + done, + ); + }); + }); + describe('async methods', () => { const interceptor = (request, next) => { next( @@ -194,7 +213,14 @@ describe('Actions Notes Store', () => { }); it('sets issue state as reopened', done => { - testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done); + testAction( + actions.toggleIssueLocalState, + 'reopened', + {}, + [{ type: 'REOPEN_ISSUE' }], + [], + done, + ); }); }); @@ -239,13 +265,7 @@ describe('Actions Notes Store', () => { .dispatch('poll') .then(() => new Promise(resolve => requestAnimationFrame(resolve))) .then(() => { - expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), { - url: jasmine.anything(), - method: 'get', - headers: { - 'X-Last-Fetched-At': undefined, - }, - }); + expect(Vue.http.get).toHaveBeenCalled(); expect(store.state.lastFetchedAt).toBe('123456'); jasmine.clock().tick(1500); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index e5550580bf8..5501e50e97b 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -1,12 +1,18 @@ import * as getters from '~/notes/stores/getters'; -import { notesDataMock, userDataMock, noteableDataMock, individualNote, collapseNotesMock } from '../mock_data'; +import { + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, + collapseNotesMock, +} from '../mock_data'; describe('Getters Notes Store', () => { let state; beforeEach(() => { state = { - notes: [individualNote], + discussions: [individualNote], targetNoteHash: 'hash', lastFetchedAt: 'timestamp', @@ -15,15 +21,15 @@ describe('Getters Notes Store', () => { noteableData: noteableDataMock, }; }); - describe('notes', () => { - it('should return all notes in the store', () => { - expect(getters.notes(state)).toEqual([individualNote]); + describe('discussions', () => { + it('should return all discussions in the store', () => { + expect(getters.discussions(state)).toEqual([individualNote]); }); }); describe('Collapsed notes', () => { const stateCollapsedNotes = { - notes: collapseNotesMock, + discussions: collapseNotesMock, targetNoteHash: 'hash', lastFetchedAt: 'timestamp', @@ -33,7 +39,7 @@ describe('Getters Notes Store', () => { }; it('should return a single system note when a description was updated multiple times', () => { - expect(getters.notes(stateCollapsedNotes).length).toEqual(1); + expect(getters.discussions(stateCollapsedNotes).length).toEqual(1); }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 98f101d6bc5..556a1c244c0 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -1,5 +1,12 @@ import mutations from '~/notes/stores/mutations'; -import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; +import { + note, + discussionMock, + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, +} from '../mock_data'; describe('Notes Store mutations', () => { describe('ADD_NEW_NOTE', () => { @@ -7,7 +14,7 @@ describe('Notes Store mutations', () => { let noteData; beforeEach(() => { - state = { notes: [] }; + state = { discussions: [] }; noteData = { expanded: true, id: note.discussion_id, @@ -20,46 +27,60 @@ describe('Notes Store mutations', () => { it('should add a new note to an array of notes', () => { expect(state).toEqual({ - notes: [noteData], + discussions: [noteData], }); - expect(state.notes.length).toBe(1); + expect(state.discussions.length).toBe(1); }); it('should not add the same note to the notes array', () => { mutations.ADD_NEW_NOTE(state, note); - expect(state.notes.length).toBe(1); + expect(state.discussions.length).toBe(1); }); }); describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { it('should add a reply to a specific discussion', () => { - const state = { notes: [discussionMock] }; + const state = { discussions: [discussionMock] }; const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); - expect(state.notes[0].notes.length).toEqual(4); + expect(state.discussions[0].notes.length).toEqual(4); }); }); describe('DELETE_NOTE', () => { it('should delete a note ', () => { - const state = { notes: [discussionMock] }; + const state = { discussions: [discussionMock] }; const toDelete = discussionMock.notes[0]; const lengthBefore = discussionMock.notes.length; mutations.DELETE_NOTE(state, toDelete); - expect(state.notes[0].notes.length).toEqual(lengthBefore - 1); + expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1); + }); + }); + + describe('EXPAND_DISCUSSION', () => { + it('should expand a collapsed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + discussions: [discussion], + }; + + mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(true); }); }); describe('REMOVE_PLACEHOLDER_NOTES', () => { it('should remove all placeholder notes in indivudal notes and discussion', () => { const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); - const state = { notes: [placeholderNote] }; + const state = { discussions: [placeholderNote] }; mutations.REMOVE_PLACEHOLDER_NOTES(state); - expect(state.notes).toEqual([]); + expect(state.discussions).toEqual([]); }); }); @@ -96,26 +117,29 @@ describe('Notes Store mutations', () => { }); }); - describe('SET_INITIAL_NOTES', () => { + describe('SET_INITIAL_DISCUSSIONS', () => { it('should set the initial notes received', () => { const state = { - notes: [], + discussions: [], }; const legacyNote = { id: 2, individual_note: true, - notes: [{ - note: '1', - }, { - note: '2', - }], + notes: [ + { + note: '1', + }, + { + note: '2', + }, + ], }; - mutations.SET_INITIAL_NOTES(state, [note, legacyNote]); - expect(state.notes[0].id).toEqual(note.id); - expect(state.notes[1].notes[0].note).toBe(legacyNote.notes[0].note); - expect(state.notes[2].notes[0].note).toBe(legacyNote.notes[1].note); - expect(state.notes.length).toEqual(3); + mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); + expect(state.discussions[0].id).toEqual(note.id); + expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); + expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note); + expect(state.discussions.length).toEqual(3); }); }); @@ -144,17 +168,17 @@ describe('Notes Store mutations', () => { describe('SHOW_PLACEHOLDER_NOTE', () => { it('should set a placeholder note', () => { const state = { - notes: [], + discussions: [], }; mutations.SHOW_PLACEHOLDER_NOTE(state, note); - expect(state.notes[0].isPlaceholderNote).toEqual(true); + expect(state.discussions[0].isPlaceholderNote).toEqual(true); }); }); describe('TOGGLE_AWARD', () => { it('should add award if user has not reacted yet', () => { const state = { - notes: [note], + discussions: [note], userData: userDataMock, }; @@ -164,9 +188,9 @@ describe('Notes Store mutations', () => { }; mutations.TOGGLE_AWARD(state, data); - const lastIndex = state.notes[0].award_emoji.length - 1; + const lastIndex = state.discussions[0].award_emoji.length - 1; - expect(state.notes[0].award_emoji[lastIndex]).toEqual({ + expect(state.discussions[0].award_emoji[lastIndex]).toEqual({ name: 'cartwheel', user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, }); @@ -174,7 +198,7 @@ describe('Notes Store mutations', () => { it('should remove award if user already reacted', () => { const state = { - notes: [note], + discussions: [note], userData: { id: 1, name: 'Administrator', @@ -187,7 +211,7 @@ describe('Notes Store mutations', () => { awardName: 'bath_tone3', }; mutations.TOGGLE_AWARD(state, data); - expect(state.notes[0].award_emoji.length).toEqual(2); + expect(state.discussions[0].award_emoji.length).toEqual(2); }); }); @@ -196,43 +220,43 @@ describe('Notes Store mutations', () => { const discussion = Object.assign({}, discussionMock, { expanded: false }); const state = { - notes: [discussion], + discussions: [discussion], }; mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); - expect(state.notes[0].expanded).toEqual(true); + expect(state.discussions[0].expanded).toEqual(true); }); it('should close a opened discussion', () => { const state = { - notes: [discussionMock], + discussions: [discussionMock], }; mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); - expect(state.notes[0].expanded).toEqual(false); + expect(state.discussions[0].expanded).toEqual(false); }); }); describe('UPDATE_NOTE', () => { it('should update a note', () => { const state = { - notes: [individualNote], + discussions: [individualNote], }; const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); mutations.UPDATE_NOTE(state, updated); - expect(state.notes[0].notes[0].note).toEqual('Foo'); + expect(state.discussions[0].notes[0].note).toEqual('Foo'); }); }); describe('CLOSE_ISSUE', () => { it('should set issue as closed', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: false, @@ -249,7 +273,7 @@ describe('Notes Store mutations', () => { describe('REOPEN_ISSUE', () => { it('should set issue as closed', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: false, @@ -266,7 +290,7 @@ describe('Notes Store mutations', () => { describe('TOGGLE_STATE_BUTTON_LOADING', () => { it('should set isToggleStateButtonLoading as true', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: false, @@ -281,7 +305,7 @@ describe('Notes Store mutations', () => { it('should set isToggleStateButtonLoading as false', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: true, diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index acbf23e2007..faeedae40e9 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ +/* eslint-disable no-unused-expressions, no-var, object-shorthand */ import $ from 'jquery'; import _ from 'underscore'; import MockAdapter from 'axios-mock-adapter'; @@ -35,11 +35,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; const NOTES_POST_PATH = /(.*)\/notes\?html=true$/; - var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw'; - preloadFixtures(commentsTemplate); + var fixture = 'snippets/show.html.raw'; + preloadFixtures(fixture); beforeEach(function() { - loadFixtures(commentsTemplate); + loadFixtures(fixture); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; $('body').attr('data-page', 'projects:merge_requets:show'); @@ -65,16 +65,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; let mock; beforeEach(function() { - spyOn(axios, 'patch').and.callThrough(); + spyOn(axios, 'patch').and.callFake(() => new Promise(() => {})); mock = new MockAdapter(axios); - - mock - .onPatch( - `${ - gl.TEST_HOST - }/frontend-fixtures/merge-requests-project/merge_requests/1.json`, - ) - .reply(200, {}); + mock.onAny().reply(200, {}); $('.js-comment-button').on('click', function(e) { e.preventDefault(); @@ -90,26 +83,17 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); $('input[type=checkbox]') - .attr('checked', true)[1] + .attr('checked', true)[0] .dispatchEvent(changeEvent); - expect($('.js-task-list-field.original-task-list').val()).toBe( - '- [x] Task List Item', - ); + expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item'); }); it('submits an ajax request on tasklist:changed', function(done) { $('.js-task-list-container').trigger('tasklist:changed'); setTimeout(() => { - expect(axios.patch).toHaveBeenCalledWith( - `${ - gl.TEST_HOST - }/frontend-fixtures/merge-requests-project/merge_requests/1.json`, - { - note: { note: '' }, - }, - ); + expect(axios.patch).toHaveBeenCalled(); done(); }); }); @@ -200,9 +184,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; updatedNote.note = 'bar'; this.notes.updateNote(updatedNote, $targetNote); - expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith( - $targetNote, - ); + expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); expect(this.notes.setupNewNote).toHaveBeenCalled(); done(); @@ -282,10 +264,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; Notes.isNewNote.and.returnValue(true); Notes.prototype.renderNote.call(notes, note, null, $notesList); - expect(Notes.animateAppendNote).toHaveBeenCalledWith( - note.html, - $notesList, - ); + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList); }); }); @@ -300,10 +279,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; Notes.prototype.renderNote.call(notes, note, null, $notesList); - expect(Notes.animateUpdateNote).toHaveBeenCalledWith( - note.html, - $note, - ); + expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note); expect(notes.setupNewNote).toHaveBeenCalledWith($newNote); }); @@ -331,10 +307,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $notesList.find.and.returnValue($note); Notes.prototype.renderNote.call(notes, note, null, $notesList); - expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith( - note, - $note, - ); + expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note); }); }); }); @@ -400,10 +373,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $form.length = 1; row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']); - notes = jasmine.createSpyObj('notes', [ - 'isParallelView', - 'updateNotesCount', - ]); + notes = jasmine.createSpyObj('notes', ['isParallelView', 'updateNotesCount']); notes.note_ids = []; spyOn(Notes, 'isNewNote'); @@ -464,10 +434,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('should call Notes.animateAppendNote', () => { - expect(Notes.animateAppendNote).toHaveBeenCalledWith( - note.html, - discussionContainer, - ); + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer); }); }); }); @@ -571,9 +538,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; mockNotesPost(); $('.js-comment-button').click(); - expect($notesContainer.find('.note.being-posted').length > 0).toEqual( - true, - ); + expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true); }); it('should remove placeholder note when new comment is done posting', done => { @@ -617,9 +582,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $('.js-comment-button').click(); setTimeout(() => { - expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual( - true, - ); + expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); done(); }); @@ -734,14 +697,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); $('.js-comment-button').click(); - expect( - $notesContainer.find('.system-note.being-posted').length, - ).toEqual(1); // Placeholder shown + expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown setTimeout(() => { - expect( - $notesContainer.find('.system-note.being-posted').length, - ).toEqual(0); // Placeholder removed + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed done(); }); }); @@ -815,9 +774,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should return form metadata object from form reference', () => { $form.find('textarea.js-note-text').val(sampleComment); - const { formData, formContent, formAction } = this.notes.getFormData( - $form, - ); + const { formData, formContent, formAction } = this.notes.getFormData($form); expect(formData.indexOf(sampleComment) > -1).toBe(true); expect(formContent).toEqual(sampleComment); @@ -833,9 +790,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; const { formContent } = this.notes.getFormData($form); expect(_.escape).toHaveBeenCalledWith(sampleComment); - expect(formContent).toEqual( - '<script>alert("Boom!");</script>', - ); + expect(formContent).toEqual('<script>alert("Boom!");</script>'); }); }); @@ -845,8 +800,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('should return true when comment begins with a quick action', () => { - const sampleComment = - '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const hasQuickActions = this.notes.hasQuickActions(sampleComment); expect(hasQuickActions).toBeTruthy(); @@ -870,8 +824,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('stripQuickActions', () => { it('should strip quick actions from the comment which begins with a quick action', () => { this.notes = new Notes(); - const sampleComment = - '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe(''); @@ -879,8 +832,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should strip quick actions from the comment but leaves plain comment if it is present', () => { this.notes = new Notes(); - const sampleComment = - '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe('Merging this'); @@ -888,8 +840,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should NOT strip string that has slashes within', () => { this.notes = new Notes(); - const sampleComment = - 'http://127.0.0.1:3000/root/gitlab-shell/issues/1'; + const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1'; const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe(sampleComment); @@ -909,29 +860,21 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should return executing quick action description when note has single quick action', () => { const sampleComment = '/close'; - expect( - this.notes.getQuickActionDescription( - sampleComment, - availableQuickActions, - ), - ).toBe('Applying command to close this issue'); + expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe( + 'Applying command to close this issue', + ); }); it('should return generic multiple quick action description when note has multiple quick actions', () => { const sampleComment = '/close\n/title [Duplicate] Issue foobar'; - expect( - this.notes.getQuickActionDescription( - sampleComment, - availableQuickActions, - ), - ).toBe('Applying multiple commands'); + expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe( + 'Applying multiple commands', + ); }); it('should return generic quick action description when available quick actions list is not populated', () => { const sampleComment = '/close\n/title [Duplicate] Issue foobar'; - expect(this.notes.getQuickActionDescription(sampleComment)).toBe( - 'Applying command', - ); + expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command'); }); }); @@ -961,17 +904,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; expect($tempNote.attr('id')).toEqual(uniqueId); expect($tempNote.hasClass('being-posted')).toBeTruthy(); expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); - $tempNote - .find('.timeline-icon > a, .note-header-info > a') - .each(function() { - expect($(this).attr('href')).toEqual(`/${currentUsername}`); - }); - expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual( - currentUserAvatar, - ); - expect( - $tempNote.find('.timeline-content').hasClass('discussion'), - ).toBeFalsy(); + $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() { + expect($(this).attr('href')).toEqual(`/${currentUsername}`); + }); + expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect( $tempNoteHeader .find('.d-none.d-sm-inline-block') @@ -1002,9 +939,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); expect($tempNote.prop('nodeName')).toEqual('LI'); - expect( - $tempNote.find('.timeline-content').hasClass('discussion'), - ).toBeTruthy(); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); it('should return a escaped user name', () => { @@ -1061,11 +996,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('shows a flash message', () => { - this.notes.addFlash( - 'Error message', - FLASH_TYPE_ALERT, - this.notes.parentTimeline.get(0), - ); + this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0)); expect($('.flash-alert').is(':visible')).toBeTruthy(); }); @@ -1078,11 +1009,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('hides visible flash message', () => { - this.notes.addFlash( - 'Error message 1', - FLASH_TYPE_ALERT, - this.notes.parentTimeline.get(0), - ); + this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0)); this.notes.clearFlash(); diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js index bebed432f91..69230bb0937 100644 --- a/spec/javascripts/pdf/index_spec.js +++ b/spec/javascripts/pdf/index_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable import/no-unresolved */ - import Vue from 'vue'; import { PDFJS } from 'vendor/pdf'; import workerSrc from 'vendor/pdf.worker.min'; diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js index ac5b21e8f6c..9c686748c10 100644 --- a/spec/javascripts/pdf/page_spec.js +++ b/spec/javascripts/pdf/page_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable import/no-unresolved */ - import Vue from 'vue'; import pdfjsLib from 'vendor/pdf'; import workerSrc from 'vendor/pdf.worker.min'; diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 68043b91bd0..78d8e9e572e 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -4,7 +4,7 @@ import eventHub from '~/pipelines/event_hub'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; - const buildComponent = (pipeline) => { + const buildComponent = pipeline => { const PipelinesTableRowComponent = Vue.extend(tableRowComp); return new PipelinesTableRowComponent({ el: document.querySelector('.test-dom-element'), @@ -52,9 +52,9 @@ describe('Pipelines Table Row', () => { }); it('should render status text', () => { - expect( - component.$el.querySelector('.table-section.commit-link a').textContent, - ).toContain(pipeline.details.status.text); + expect(component.$el.querySelector('.table-section.commit-link a').textContent).toContain( + pipeline.details.status.text, + ); }); }); @@ -78,11 +78,15 @@ describe('Pipelines Table Row', () => { describe('when a user is provided', () => { it('should render user information', () => { expect( - component.$el.querySelector('.table-section:nth-child(2) a:nth-child(3)').getAttribute('href'), + component.$el + .querySelector('.table-section:nth-child(2) a:nth-child(3)') + .getAttribute('href'), ).toEqual(pipeline.user.path); expect( - component.$el.querySelector('.table-section:nth-child(2) img').getAttribute('data-original-title'), + component.$el + .querySelector('.table-section:nth-child(2) img') + .getAttribute('data-original-title'), ).toEqual(pipeline.user.name); }); }); @@ -105,7 +109,9 @@ describe('Pipelines Table Row', () => { } const commitAuthorLink = commitAuthorElement.getAttribute('href'); - const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title'); + const commitAuthorName = commitAuthorElement + .querySelector('img.avatar') + .getAttribute('data-original-title'); return { commitAuthorElement, commitAuthorLink, commitAuthorName }; }; @@ -145,7 +151,8 @@ describe('Pipelines Table Row', () => { it('should render an icon for each stage', () => { expect( - component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button').length, + component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button') + .length, ).toEqual(pipeline.details.stages.length); }); }); @@ -167,7 +174,7 @@ describe('Pipelines Table Row', () => { }); it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => { - eventHub.$on('retryPipeline', (endpoint) => { + eventHub.$on('retryPipeline', endpoint => { expect(endpoint).toEqual('/retry'); }); @@ -176,7 +183,7 @@ describe('Pipelines Table Row', () => { }); it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => { - eventHub.$on('openConfirmationModal', (data) => { + eventHub.$once('openConfirmationModal', data => { expect(data.endpoint).toEqual('/cancel'); expect(data.pipelineId).toEqual(pipeline.id); }); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index e264b16335f..6d49536a712 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ +/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-return-assign, vars-on-top, max-len */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 4f515f98a7e..86c001678c5 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ +/* eslint-disable max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, object-shorthand, prefer-template, vars-on-top, max-len */ import $ from 'jquery'; import '~/gl_dropdown'; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index b10d8be6781..a4753ab7cde 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -4,71 +4,102 @@ import ShortcutsIssuable from '~/shortcuts_issuable'; initCopyAsGFM(); -describe('ShortcutsIssuable', function () { - const fixtureName = 'merge_requests/diff_comment.html.raw'; +const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; + +describe('ShortcutsIssuable', function() { + const fixtureName = 'snippets/show.html.raw'; preloadFixtures(fixtureName); + beforeEach(() => { loadFixtures(fixtureName); + $('body').append( + `<div class="js-main-target-form"> + <textare class="js-vue-comment-form"></textare> + </div>`, + ); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); this.shortcut = new ShortcutsIssuable(true); }); + + afterEach(() => { + $(FORM_SELECTOR).remove(); + }); + describe('replyWithSelectedText', () => { // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. - const stubSelection = (html) => { + const stubSelection = html => { window.gl.utils.getSelectedFragment = () => { const node = document.createElement('div'); node.innerHTML = html; + return node; }; }; - beforeEach(() => { - this.selector = '.js-main-target-form #note_note'; - }); describe('with empty selection', () => { it('does not return an error', () => { - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe(''); + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe(''); }); + it('triggers `focus`', () => { - this.shortcut.replyWithSelectedText(true); - expect(document.activeElement).toBe(document.querySelector(this.selector)); + const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect(spy).toHaveBeenCalled(); }); }); + describe('with any selection', () => { beforeEach(() => { stubSelection('<p>Selected text.</p>'); }); + it('leaves existing input intact', () => { - $(this.selector).val('This text was already here.'); - expect($(this.selector).val()).toBe('This text was already here.'); - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); + $(FORM_SELECTOR).val('This text was already here.'); + expect($(FORM_SELECTOR).val()).toBe('This text was already here.'); + + ShortcutsIssuable.replyWithSelectedText(true); + expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); }); + it('triggers `input`', () => { let triggered = false; - $(this.selector).on('input', () => { + $(FORM_SELECTOR).on('input', () => { triggered = true; }); - this.shortcut.replyWithSelectedText(true); + + ShortcutsIssuable.replyWithSelectedText(true); expect(triggered).toBe(true); }); + it('triggers `focus`', () => { - this.shortcut.replyWithSelectedText(true); - expect(document.activeElement).toBe(document.querySelector(this.selector)); + const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect(spy).toHaveBeenCalled(); }); }); + describe('with a one-line selection', () => { it('quotes the selection', () => { stubSelection('<p>This text has been selected.</p>'); - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe('> This text has been selected.\n\n'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n'); }); }); + describe('with a multi-line selection', () => { it('quotes the selected lines as a group', () => { - stubSelection('<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>'); - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n'); + stubSelection( + '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>', + ); + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe( + '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', + ); }); }); }); diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index ee92295ef5e..94cded7ee37 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -2,10 +2,11 @@ import $ from 'jquery'; import Shortcuts from '~/shortcuts'; describe('Shortcuts', () => { - const fixtureName = 'merge_requests/diff_comment.html.raw'; - const createEvent = (type, target) => $.Event(type, { - target, - }); + const fixtureName = 'snippets/show.html.raw'; + const createEvent = (type, target) => + $.Event(type, { + target, + }); preloadFixtures(fixtureName); @@ -21,19 +22,19 @@ describe('Shortcuts', () => { it('focuses preview button in form', () => { Shortcuts.toggleMarkdownPreview( - createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'), - )); + 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) => { + it('focues preview button inside edit comment form', done => { document.querySelector('.js-note-edit').click(); setTimeout(() => { Shortcuts.toggleMarkdownPreview( - createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'), - )); + 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'); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index 423432c9e5d..9d3905fa1d8 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -45,6 +45,21 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer'; expect(fakeTab.click).toHaveBeenCalled(); }); + it('clicks the first tab if value in local storage is bad', () => { + createMemoizer().saveData('#bogus'); + const fakeTab = { + click: () => {}, + }; + spyOn(document, 'querySelector').and.callFake(selector => (selector === `${tabSelector} a[href="#bogus"]` ? null : fakeTab)); + spyOn(fakeTab, 'click'); + + memo.bootstrap(); + + // verify that triggers click on stored selector and fallback + expect(document.querySelector.calls.allArgs()).toEqual([['ul.new-session-tabs a[href="#bogus"]'], ['ul.new-session-tabs a']]); + expect(fakeTab.click).toHaveBeenCalled(); + }); + it('saves last selected tab on change', () => { createMemoizer(); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 0d1fa680e00..1c3dac3584e 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ +/* eslint-disable no-var, no-return-assign, quotes */ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 994011b262a..aeb936b0e3c 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -91,7 +91,8 @@ testsContext.keys().forEach(function(path) { try { testsContext(path); } catch (err) { - console.error('[ERROR] Unable to load spec: ', path); + console.log(err); + console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path); describe('Test bundle', function() { it(`includes '${path}'`, function() { expect(err).toBeNull(); @@ -135,7 +136,7 @@ if (process.env.BABEL_ENV === 'coverage') { // exempt these files from the coverage report const troubleMakers = [ './blob_edit/blob_bundle.js', - './boards/components/modal/empty_state.js', + './boards/components/modal/empty_state.vue', './boards/components/modal/footer.js', './boards/components/modal/header.js', './cycle_analytics/cycle_analytics_bundle.js', diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 8fec6ae3fa4..012a1cefbbf 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,5 +1,4 @@ -/* eslint-disable prefer-rest-params, wrap-iife, -no-unused-expressions, no-return-assign, no-param-reassign */ +/* eslint-disable wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign */ export default class MockU2FDevice { constructor() { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index 3e2fd71b5b8..efa5c878678 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -39,7 +39,8 @@ describe('MRWidgetMerged', () => { readableClosedAt: '', }, updatedAt: 'mergedUpdatedAt', - shortMergeCommitSha: 'asdf1234', + shortMergeCommitSha: '958c0475', + mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed', mergeCommitPath: 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d', sourceBranch: 'bar', targetBranch, @@ -153,7 +154,7 @@ describe('MRWidgetMerged', () => { it('shows button to copy commit SHA to clipboard', () => { expect(selectors.copyMergeShaButton).toExist(); - expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.shortMergeCommitSha); + expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.mergeCommitSha); }); it('shows merge commit SHA link', () => { diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js index af9693c48fd..98fee9a74a5 100644 --- a/spec/javascripts/vue_shared/components/expand_button_spec.js +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -19,7 +19,7 @@ describe('expand button', () => { }); it('renders a collpased button', () => { - expect(vm.$el.textContent.trim()).toEqual('...'); + expect(vm.$children[0].iconTestClass).toEqual('ic-ellipsis_h'); }); it('hides expander on click', done => { diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js index ba8ab0b2cd7..7e57c51bf29 100644 --- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import { userDataMock } from '../../../notes/mock_data'; describe('issue placeholder system note component', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(issuePlaceholderNote); + store = createStore(); store.dispatch('setUserData', userDataMock); vm = new Component({ store, @@ -21,15 +23,23 @@ describe('issue placeholder system note component', () => { describe('user information', () => { it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); + expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual( + userDataMock.path, + ); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( + userDataMock.avatar_url, + ); }); }); describe('note content', () => { it('should render note header information', () => { - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual( + userDataMock.path, + ); + expect( + vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim(), + ).toEqual(`@${userDataMock.username}`); }); it('should render note body', () => { diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js index 36aaf0a6c2e..aa4c9c4c88c 100644 --- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import issueSystemNote from '~/vue_shared/components/notes/system_note.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; -describe('issue system note', () => { +describe('system note component', () => { let vm; let props; @@ -24,6 +24,7 @@ describe('issue system note', () => { }, }; + const store = createStore(); store.dispatch('setTargetNoteHash', `note_${props.note.id}`); const Component = Vue.extend(issueSystemNote); @@ -49,9 +50,10 @@ describe('issue system note', () => { expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); }); - it('should render note header component', () => { - expect( - vm.$el.querySelector('.system-note-message').innerHTML, - ).toEqual(props.note.note_html); + // Redcarpet Markdown renderer wraps text in `<p>` tags + // we need to strip them because they break layout of commit lists in system notes: + // https://gitlab.com/gitlab-org/gitlab-ce/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png + it('removes wrapping paragraph from note HTML', () => { + expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>'); }); }); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 7fe3bd92049..bdeebe0de75 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,11 +1,12 @@ import $ from 'jquery'; -import Mousetrap from 'mousetrap'; import Dropzone from 'dropzone'; +import Mousetrap from 'mousetrap'; import ZenMode from '~/zen_mode'; describe('ZenMode', () => { let zen; - const fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; + let dropzoneForElementSpy; + const fixtureName = 'snippets/show.html.raw'; preloadFixtures(fixtureName); @@ -18,15 +19,17 @@ describe('ZenMode', () => { } function escapeKeydown() { - $('.notes-form textarea').trigger($.Event('keydown', { - keyCode: 27, - })); + $('.notes-form textarea').trigger( + $.Event('keydown', { + keyCode: 27, + }), + ); } beforeEach(() => { loadFixtures(fixtureName); - spyOn(Dropzone, 'forElement').and.callFake(() => ({ + dropzoneForElementSpy = spyOn(Dropzone, 'forElement').and.callFake(() => ({ enable: () => true, })); zen = new ZenMode(); @@ -35,11 +38,29 @@ describe('ZenMode', () => { zen.scroll_position = 456; }); + describe('enabling dropzone', () => { + beforeEach(() => { + enterZen(); + }); + + it('should not call dropzone if element is not dropzone valid', () => { + $('.div-dropzone').addClass('js-invalid-dropzone'); + exitZen(); + expect(dropzoneForElementSpy.calls.count()).toEqual(0); + }); + + it('should call dropzone if element is dropzone valid', () => { + $('.div-dropzone').removeClass('js-invalid-dropzone'); + exitZen(); + expect(dropzoneForElementSpy.calls.count()).toEqual(2); + }); + }); + describe('on enter', () => { it('pauses Mousetrap', () => { - spyOn(Mousetrap, 'pause'); + const mouseTrapPauseSpy = spyOn(Mousetrap, 'pause'); enterZen(); - expect(Mousetrap.pause).toHaveBeenCalled(); + expect(mouseTrapPauseSpy).toHaveBeenCalled(); }); it('removes textarea styling', () => { @@ -62,9 +83,9 @@ describe('ZenMode', () => { beforeEach(enterZen); it('unpauses Mousetrap', () => { - spyOn(Mousetrap, 'unpause'); + const mouseTrapUnpauseSpy = spyOn(Mousetrap, 'unpause'); exitZen(); - expect(Mousetrap.unpause).toHaveBeenCalled(); + expect(mouseTrapUnpauseSpy).toHaveBeenCalled(); }); it('restores the scroll position', () => { @@ -73,22 +94,4 @@ describe('ZenMode', () => { expect(zen.scrollTo).toHaveBeenCalled(); }); }); - - describe('enabling dropzone', () => { - beforeEach(() => { - enterZen(); - }); - - it('should not call dropzone if element is not dropzone valid', () => { - $('.div-dropzone').addClass('js-invalid-dropzone'); - exitZen(); - expect(Dropzone.forElement).not.toHaveBeenCalled(); - }); - - it('should call dropzone if element is dropzone valid', () => { - $('.div-dropzone').removeClass('js-invalid-dropzone'); - exitZen(); - expect(Dropzone.forElement).toHaveBeenCalled(); - }); - }); }); diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index 8224dc5a6b9..b645e49bd43 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -11,4 +11,8 @@ describe Banzai::Filter::BlockquoteFenceFilter do expect(output).to eq(expected) end + + it 'allows trailing whitespace on blockquote fence lines' do + expect(filter(">>> \ntest\n>>> ")).to eq("> test") + end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index b30f3661e70..00257ed7904 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -148,9 +148,11 @@ describe Banzai::Filter::LabelReferenceFilter do expect(doc.text).to eq 'See ?g.fm&' end - it 'links with adjacent text' do - doc = reference_filter("Label (#{reference}).") - expect(doc.to_html).to match(%r(\(<a.+><span.+>\?g\.fm&</span></a>\)\.)) + it 'does not include trailing punctuation', :aggregate_failures do + ['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation| + doc = filter("Label #{reference}#{trailing_punctuation}") + expect(doc.to_html).to match(%r(<a.+><span.+>\?g\.fm&</span></a>#{Regexp.escape(trailing_punctuation)})) + end end it 'ignores invalid label names' do diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb index 136646bd4ee..454ad1589b9 100644 --- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -99,7 +99,7 @@ describe Gitlab::Auth::UserAuthFinders do context 'when the request format is empty' do it 'the method call does not modify the original value' do - env['action_dispatch.request.formats'] = nil + env.delete('action_dispatch.request.formats') find_user_from_feed_token diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index c3f528dd6fc..ed6fa3d229f 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -25,7 +25,9 @@ describe Gitlab::BitbucketImport::ProjectCreator do end it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) + expect_next_instance_of(Project) do |project| + expect(project).to receive(:add_import_job) + end project_creator = described_class.new(repo, 'vim', namespace, user, access_params) project = project_creator.execute diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index a65012d2314..0e0788ce974 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -1,21 +1,17 @@ require 'spec_helper' describe Gitlab::Checks::ForcePush do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository.raw } + set(:project) { create(:project, :repository) } - context "exit code checking", :skip_gitaly_mock do - it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do - allow(repository).to receive(:popen).and_return(['normal output', 0]) + describe '.force_push?' do + it 'returns false if the repo is empty' do + allow(project).to receive(:empty_repo?).and_return(true) - expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error + expect(described_class.force_push?(project, 'HEAD', 'HEAD~')).to be(false) end - it "raises a GitError error if the `popen` call to git returns a non-zero exit code" do - allow(repository).to receive(:popen).and_return(['error', 1]) - - expect { described_class.force_push?(project, 'oldrev', 'newrev') } - .to raise_error(Gitlab::Git::Repository::GitError) + it 'checks if old rev is an anchestor' do + expect(described_class.force_push?(project, 'HEAD', 'HEAD~')).to be(true) end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index bc5a5e43103..2e204da307d 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -49,7 +49,7 @@ describe Gitlab::Ci::Config do describe '.new' do it 'raises error' do expect { config }.to raise_error( - Gitlab::Ci::Config::Loader::FormatError, + ::Gitlab::Ci::Config::Loader::FormatError, /Invalid configuration format/ ) end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index c5a4d9b4778..284aed91e29 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Chain::Populate do - set(:project) { create(:project) } + set(:project) { create(:project, :repository) } set(:user) { create(:user) } let(:pipeline) do @@ -174,7 +174,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end let(:pipeline) do - build(:ci_pipeline, ref: 'master', config: config) + build(:ci_pipeline, ref: 'master', project: project, config: config) end it_behaves_like 'a correct pipeline' diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb index c53294d091c..a8dc5356413 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Chain::Validate::Config do - set(:project) { create(:project) } + set(:project) { create(:project, :repository) } set(:user) { create(:user) } let(:command) do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index ecb16daec96..fa5327c26f0 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab module Ci - describe YamlProcessor, :lib do + describe YamlProcessor do subject { described_class.new(config) } describe 'our current .gitlab-ci.yml' do diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index f36111a4946..122dcd9634c 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -21,6 +21,21 @@ RSpec.describe Gitlab::Favicon, :request_store do create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png') expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/dk.png} end + + context 'asset host' do + before do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + end + + it 'returns a relative url when the asset host is not configured' do + expect(described_class.main).to match %r{^/assets/favicon-(?:\h+).png$} + end + + it 'returns a full url when the asset host is configured' do + allow(Gitlab::Application.config).to receive(:asset_host).and_return('http://assets.local') + expect(described_class.main).to match %r{^http://localhost/assets/favicon-(?:\h+).png$} + end + end end describe '.status_overlay' do diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb index d6d9e4001a3..b49c5817131 100644 --- a/spec/lib/gitlab/file_finder_spec.rb +++ b/spec/lib/gitlab/file_finder_spec.rb @@ -3,11 +3,29 @@ require 'spec_helper' describe Gitlab::FileFinder do describe '#find' do let(:project) { create(:project, :public, :repository) } + subject { described_class.new(project, project.default_branch) } it_behaves_like 'file finder' do - subject { described_class.new(project, project.default_branch) } let(:expected_file_by_name) { 'files/images/wm.svg' } let(:expected_file_by_content) { 'CHANGELOG' } end + + it 'filters by name' do + results = subject.find('files filename:wm.svg') + + expect(results.count).to eq(1) + end + + it 'filters by path' do + results = subject.find('white path:images') + + expect(results.count).to eq(1) + end + + it 'filters by extension' do + results = subject.find('files extension:svg') + + expect(results.count).to eq(1) + end end end diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb index d0dd8c6303f..c5e7ab959b2 100644 --- a/spec/lib/gitlab/git/lfs_changes_spec.rb +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -1,50 +1,19 @@ require 'spec_helper' describe Gitlab::Git::LfsChanges do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' } subject { described_class.new(project.repository, newrev) } describe '#new_pointers' do - shared_examples 'new pointers' do - it 'filters new objects to find lfs pointers' do - expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id) - end - - it 'limits new_objects using object_limit' do - expect(subject.new_pointers(object_limit: 1)).to eq([]) - end - end - - context 'with gitaly enabled' do - it_behaves_like 'new pointers' + it 'filters new objects to find lfs pointers' do + expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id) end - context 'with gitaly disabled', :skip_gitaly_mock do - it_behaves_like 'new pointers' - - it 'uses rev-list to find new objects' do - rev_list = double - allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) - - expect(rev_list).to receive(:new_objects).and_return([]) - - subject.new_pointers - end - end - end - - describe '#all_pointers', :skip_gitaly_mock do - it 'uses rev-list to find all objects' do - rev_list = double - allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) - allow(rev_list).to receive(:all_objects).and_yield([blob_object_id]) - - expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) - - subject.all_pointers + it 'limits new_objects using object_limit' do + expect(subject.new_pointers(object_limit: 1)).to eq([]) end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5bae99101e6..45f0006dc85 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1043,50 +1043,40 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#raw_changes_between' do - shared_examples 'raw changes' do - let(:old_rev) { } - let(:new_rev) { } - let(:changes) { repository.raw_changes_between(old_rev, new_rev) } + let(:old_rev) { } + let(:new_rev) { } + let(:changes) { repository.raw_changes_between(old_rev, new_rev) } - context 'initial commit' do - let(:old_rev) { Gitlab::Git::BLANK_SHA } - let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + context 'initial commit' do + let(:old_rev) { Gitlab::Git::BLANK_SHA } + let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } - it 'returns the changes' do - expect(changes).to be_present - expect(changes.size).to eq(3) - end - end - - context 'with an invalid rev' do - let(:old_rev) { 'foo' } - let(:new_rev) { 'bar' } - - it 'returns an error' do - expect { changes }.to raise_error(Gitlab::Git::Repository::GitError) - end + it 'returns the changes' do + expect(changes).to be_present + expect(changes.size).to eq(3) end + end - context 'with valid revs' do - let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } - let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + context 'with an invalid rev' do + let(:old_rev) { 'foo' } + let(:new_rev) { 'bar' } - it 'returns the changes' do - expect(changes.size).to eq(9) - expect(changes.first.operation).to eq(:modified) - expect(changes.first.new_path).to eq('.gitmodules') - expect(changes.last.operation).to eq(:added) - expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png') - end + it 'returns an error' do + expect { changes }.to raise_error(Gitlab::Git::Repository::GitError) end end - context 'when gitaly is enabled' do - it_behaves_like 'raw changes' - end + context 'with valid revs' do + let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } + let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } - context 'when gitaly is disabled', :disable_gitaly do - it_behaves_like 'raw changes' + it 'returns the changes' do + expect(changes.size).to eq(9) + expect(changes.first.operation).to eq(:modified) + expect(changes.first.new_path).to eq('.gitmodules') + expect(changes.last.operation).to eq(:added) + expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png') + end end end @@ -1114,7 +1104,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#count_commits' do - shared_examples 'extended commit counting' do + describe 'extended commit counting' do context 'with after timestamp' do it 'returns the number of commits after timestamp' do options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') } @@ -1199,14 +1189,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end end - - context 'when Gitaly count_commits feature is enabled' do - it_behaves_like 'extended commit counting' - end - - context 'when Gitaly count_commits feature is disabled', :disable_gitaly do - it_behaves_like 'extended commit counting' - end end describe '#autocrlf' do @@ -1688,70 +1670,52 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#languages' do - shared_examples 'languages' do - it 'returns exactly the expected results' do - languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') - expected_languages = [ - { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, - { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, - { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, - { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } - ] + it 'returns exactly the expected results' do + languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') + expected_languages = [ + { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, + { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, + { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, + { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } + ] - expect(languages.size).to eq(expected_languages.size) + expect(languages.size).to eq(expected_languages.size) - expected_languages.size.times do |i| - a = expected_languages[i] - b = languages[i] + expected_languages.size.times do |i| + a = expected_languages[i] + b = languages[i] - expect(a.keys.sort).to eq(b.keys.sort) - expect(a[:value]).to be_within(0.1).of(b[:value]) + expect(a.keys.sort).to eq(b.keys.sort) + expect(a[:value]).to be_within(0.1).of(b[:value]) - non_float_keys = a.keys - [:value] - expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys)) - end - end - - it "uses the repository's HEAD when no ref is passed" do - lang = repository.languages.first - - expect(lang[:label]).to eq('Ruby') + non_float_keys = a.keys - [:value] + expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys)) end end - it_behaves_like 'languages' + it "uses the repository's HEAD when no ref is passed" do + lang = repository.languages.first - context 'with rugged', :skip_gitaly_mock do - it_behaves_like 'languages' + expect(lang[:label]).to eq('Ruby') end end describe '#license_short_name' do - shared_examples 'acquiring the Licensee license key' do - subject { repository.license_short_name } - - context 'when no license file can be found' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository.raw_repository } - - before do - project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master') - end + subject { repository.license_short_name } - it { is_expected.to be_nil } - end + context 'when no license file can be found' do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw_repository } - context 'when an mit license is found' do - it { is_expected.to eq('mit') } + before do + project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master') end - end - context 'when gitaly is enabled' do - it_behaves_like 'acquiring the Licensee license key' + it { is_expected.to be_nil } end - context 'when gitaly is disabled', :disable_gitaly do - it_behaves_like 'acquiring the Licensee license key' + context 'when an mit license is found' do + it { is_expected.to eq('mit') } end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index 95dc47e2a00..b752c3e8341 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -93,14 +93,4 @@ describe Gitlab::Git::RevList do expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2]) end end - - context "#missed_ref" do - let(:rev_list) { described_class.new(repository, oldrev: 'oldrev', newrev: 'newrev') } - - it 'calls out to `popen`' do - stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', with_lazy_block: false, output: "sha1\nsha2") - - 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 0d5f6a0b576..ff32025253a 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -934,6 +934,22 @@ describe Gitlab::GitAccess do expect(project.repository).to receive(:clean_stale_repository_files).and_call_original expect { push_access_check }.not_to raise_error end + + it 'avoids N+1 queries', :request_store do + # Run this once to establish a baseline. Cached queries should get + # cached, so that when we introduce another change we shouldn't see + # additional queries. + access.check('git-receive-pack', changes) + + control_count = ActiveRecord::QueryRecorder.new do + access.check('git-receive-pack', changes) + end + + changes = ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] + + # There is still an N+1 query with protected branches + expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1) + end end end diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb index 5ea086e4abd..b814f5fc76c 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -21,7 +21,9 @@ describe Gitlab::GitlabImport::ProjectCreator do end it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) + expect_next_instance_of(Project) do |project| + expect(project).to receive(:add_import_job) + end project_creator = described_class.new(repo, namespace, user, access_params) project = project_creator.execute diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb index 24cd518c77b..b959e006292 100644 --- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb +++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb @@ -16,7 +16,9 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do end it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) + expect_next_instance_of(Project) do |project| + expect(project).to receive(:add_import_job) + end project_creator = described_class.new(repo, namespace, user) project = project_creator.execute diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb index ab71d6454a9..a399517cc04 100644 --- a/spec/lib/gitlab/i18n/metadata_entry_spec.rb +++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::I18n::MetadataEntry do - describe '#expected_plurals' do + describe '#expected_forms' do it 'returns the number of plurals' do data = { msgid: "", @@ -22,7 +22,7 @@ describe Gitlab::I18n::MetadataEntry do } entry = described_class.new(data) - expect(entry.expected_plurals).to eq(2) + expect(entry.expected_forms).to eq(2) end it 'returns 0 for the POT-metadata' do @@ -45,7 +45,7 @@ describe Gitlab::I18n::MetadataEntry do } entry = described_class.new(data) - expect(entry.expected_plurals).to eq(0) + expect(entry.expected_forms).to eq(0) end end end diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index 3a962ba7f22..3dbc23d2aaf 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -1,10 +1,31 @@ require 'spec_helper' require 'simple_po_parser' +# Disabling this cop to allow for multi-language examples in comments +# rubocop:disable Style/AsciiComments describe Gitlab::I18n::PoLinter do let(:linter) { described_class.new(po_path) } let(:po_path) { 'spec/fixtures/valid.po' } + def fake_translation(msgid:, translation:, plural_id: nil, plurals: []) + data = { msgid: msgid, msgid_plural: plural_id } + + if plural_id + [translation, *plurals].each_with_index do |plural, index| + allow(FastGettext::Translation).to receive(:n_).with(msgid, plural_id, index).and_return(plural) + data.merge!("msgstr[#{index}]" => plural) + end + else + allow(FastGettext::Translation).to receive(:_).with(msgid).and_return(translation) + data[:msgstr] = translation + end + + Gitlab::I18n::TranslationEntry.new( + data, + plurals.size + 1 + ) + end + describe '#errors' do it 'only calls validation once' do expect(linter).to receive(:validate_po).once.and_call_original @@ -155,9 +176,8 @@ describe Gitlab::I18n::PoLinter do describe '#validate_entries' do it 'keeps track of errors for entries' do - fake_invalid_entry = Gitlab::I18n::TranslationEntry.new( - { msgid: "Hello %{world}", msgstr: "Bonjour %{monde}" }, 2 - ) + fake_invalid_entry = fake_translation(msgid: "Hello %{world}", + translation: "Bonjour %{monde}") allow(linter).to receive(:translation_entries) { [fake_invalid_entry] } expect(linter).to receive(:validate_entry) @@ -177,6 +197,7 @@ describe Gitlab::I18n::PoLinter do expect(linter).to receive(:validate_newlines).with([], fake_entry) expect(linter).to receive(:validate_number_of_plurals).with([], fake_entry) expect(linter).to receive(:validate_unescaped_chars).with([], fake_entry) + expect(linter).to receive(:validate_translation).with([], fake_entry) linter.validate_entry(fake_entry) end @@ -185,7 +206,7 @@ describe Gitlab::I18n::PoLinter do describe '#validate_number_of_plurals' do it 'validates when there are an incorrect number of translations' do fake_metadata = double - allow(fake_metadata).to receive(:expected_plurals).and_return(2) + allow(fake_metadata).to receive(:expected_forms).and_return(2) allow(linter).to receive(:metadata_entry).and_return(fake_metadata) fake_entry = Gitlab::I18n::TranslationEntry.new( @@ -201,13 +222,16 @@ describe Gitlab::I18n::PoLinter do end describe '#validate_variables' do - it 'validates both signular and plural in a pluralized string when the entry has a singular' do - pluralized_entry = Gitlab::I18n::TranslationEntry.new( - { msgid: 'Hello %{world}', - msgid_plural: 'Hello all %{world}', - 'msgstr[0]' => 'Bonjour %{world}', - 'msgstr[1]' => 'Bonjour tous %{world}' }, - 2 + before do + allow(linter).to receive(:validate_variables_in_message).and_call_original + end + + it 'validates both singular and plural in a pluralized string when the entry has a singular' do + pluralized_entry = fake_translation( + msgid: 'Hello %{world}', + translation: 'Bonjour %{world}', + plural_id: 'Hello all %{world}', + plurals: ['Bonjour tous %{world}'] ) expect(linter).to receive(:validate_variables_in_message) @@ -221,11 +245,10 @@ describe Gitlab::I18n::PoLinter do end it 'only validates plural when there is no separate singular' do - pluralized_entry = Gitlab::I18n::TranslationEntry.new( - { msgid: 'Hello %{world}', - msgid_plural: 'Hello all %{world}', - 'msgstr[0]' => 'Bonjour %{world}' }, - 1 + pluralized_entry = fake_translation( + msgid: 'Hello %{world}', + translation: 'Bonjour %{world}', + plural_id: 'Hello all %{world}' ) expect(linter).to receive(:validate_variables_in_message) @@ -235,37 +258,65 @@ describe Gitlab::I18n::PoLinter do end it 'validates the message variables' do - entry = Gitlab::I18n::TranslationEntry.new( - { msgid: 'Hello', msgstr: 'Bonjour' }, - 2 - ) + entry = fake_translation(msgid: 'Hello', translation: 'Bonjour') expect(linter).to receive(:validate_variables_in_message) .with([], 'Hello', 'Bonjour') linter.validate_variables([], entry) end + + it 'validates variable usage in message ids' do + entry = fake_translation( + msgid: 'Hello %{world}', + translation: 'Bonjour %{world}', + plural_id: 'Hello all %{world}', + plurals: ['Bonjour tous %{world}'] + ) + + expect(linter).to receive(:validate_variables_in_message) + .with([], 'Hello %{world}', 'Hello %{world}') + .and_call_original + expect(linter).to receive(:validate_variables_in_message) + .with([], 'Hello all %{world}', 'Hello all %{world}') + .and_call_original + + linter.validate_variables([], entry) + end end describe '#validate_variables_in_message' do it 'detects when a variables are used incorrectly' do errors = [] - expected_errors = ['<hello %{world} %d> is missing: [%{hello}]', - '<hello %{world} %d> is using unknown variables: [%{world}]', - 'is combining multiple unnamed variables'] + expected_errors = ['<%d hello %{world} %s> is missing: [%{hello}]', + '<%d hello %{world} %s> is using unknown variables: [%{world}]', + 'is combining multiple unnamed variables', + 'is combining named variables with unnamed variables'] - linter.validate_variables_in_message(errors, '%{hello} world %d', 'hello %{world} %d') + linter.validate_variables_in_message(errors, '%d %{hello} world %s', '%d hello %{world} %s') expect(errors).to include(*expected_errors) end + + it 'does not allow combining 1 `%d` unnamed variable with named variables' do + errors = [] + + linter.validate_variables_in_message(errors, + '%{type} detected %d vulnerability', + '%{type} detecteerde %d kwetsbaarheid') + + expect(errors).not_to be_empty + end end describe '#validate_translation' do + let(:entry) { fake_translation(msgid: 'Hello %{world}', translation: 'Bonjour %{world}') } + it 'succeeds with valid variables' do errors = [] - linter.validate_translation(errors, 'Hello %{world}', ['%{world}']) + linter.validate_translation(errors, entry) expect(errors).to be_empty end @@ -275,43 +326,80 @@ describe Gitlab::I18n::PoLinter do expect(FastGettext::Translation).to receive(:_) { raise 'broken' } - linter.validate_translation(errors, 'Hello', []) + linter.validate_translation(errors, entry) - expect(errors).to include('Failure translating to en with []: broken') + expect(errors).to include('Failure translating to en: broken') end it 'adds an error message when translating fails when translating with context' do + entry = fake_translation(msgid: 'Tests|Hello', translation: 'broken') errors = [] expect(FastGettext::Translation).to receive(:s_) { raise 'broken' } - linter.validate_translation(errors, 'Tests|Hello', []) + linter.validate_translation(errors, entry) - expect(errors).to include('Failure translating to en with []: broken') + expect(errors).to include('Failure translating to en: broken') end it "adds an error when trying to translate with incorrect variables when using unnamed variables" do + entry = fake_translation(msgid: 'Hello %s', translation: 'Hello %d') errors = [] - linter.validate_translation(errors, 'Hello %d', ['%s']) + linter.validate_translation(errors, entry) - expect(errors.first).to start_with("Failure translating to en with") + expect(errors.first).to start_with("Failure translating to en") end it "adds an error when trying to translate with named variables when unnamed variables are expected" do + entry = fake_translation(msgid: 'Hello %s', translation: 'Hello %{thing}') errors = [] - linter.validate_translation(errors, 'Hello %d', ['%{world}']) + linter.validate_translation(errors, entry) - expect(errors.first).to start_with("Failure translating to en with") + expect(errors.first).to start_with("Failure translating to en") end - it 'adds an error when translated with incorrect variables using named variables' do - errors = [] + it 'tests translation for all given forms' do + # Fake a language that has 3 forms to translate + fake_metadata = double + allow(fake_metadata).to receive(:forms_to_test).and_return(3) + allow(linter).to receive(:metadata_entry).and_return(fake_metadata) + entry = fake_translation( + msgid: '%d exception', + translation: '%d uitzondering', + plural_id: '%d exceptions', + plurals: ['%d uitzonderingen', '%d uitzonderingetjes'] + ) + + # Make each count use a different index + allow(linter).to receive(:index_for_pluralization).with(0).and_return(0) + allow(linter).to receive(:index_for_pluralization).with(1).and_return(1) + allow(linter).to receive(:index_for_pluralization).with(2).and_return(2) + + expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 0).and_call_original + expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 1).and_call_original + expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 2).and_call_original + + linter.validate_translation([], entry) + end + end + + describe '#numbers_covering_all_plurals' do + it 'can correctly find all required numbers to translate to Polish' do + # Polish used as an example with 3 different forms: + # 0, all plurals except the ones ending in 2,3,4: Kotów + # 1: Kot + # 2-3-4: Koty + # So translating with [0, 1, 2] will give us all different posibilities + fake_metadata = double + allow(fake_metadata).to receive(:forms_to_test).and_return(4) + allow(linter).to receive(:metadata_entry).and_return(fake_metadata) + allow(linter).to receive(:locale).and_return('pl_PL') - linter.validate_translation(errors, 'Hello %{thing}', ['%d']) + numbers = linter.numbers_covering_all_plurals - expect(errors.first).to start_with("Failure translating to en with") + expect(numbers).to contain_exactly(0, 1, 2) end end @@ -336,3 +424,4 @@ describe Gitlab::I18n::PoLinter do end end end +# rubocop:enable Style/AsciiComments diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb index f68bc8feff9..b301e6ea443 100644 --- a/spec/lib/gitlab/i18n/translation_entry_spec.rb +++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb @@ -109,7 +109,7 @@ describe Gitlab::I18n::TranslationEntry do data = { msgid: %w(hello world) } entry = described_class.new(data, 2) - expect(entry.msgid_contains_newlines?).to be_truthy + expect(entry.msgid_has_multiple_lines?).to be_truthy end end @@ -118,7 +118,7 @@ describe Gitlab::I18n::TranslationEntry do data = { msgid_plural: %w(hello world) } entry = described_class.new(data, 2) - expect(entry.plural_id_contains_newlines?).to be_truthy + expect(entry.plural_id_has_multiple_lines?).to be_truthy end end @@ -127,7 +127,7 @@ describe Gitlab::I18n::TranslationEntry do data = { msgstr: %w(hello world) } entry = described_class.new(data, 2) - expect(entry.translations_contain_newlines?).to be_truthy + expect(entry.translations_have_multiple_lines?).to be_truthy end end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 740466ea5cb..aa7e43dfb16 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -7,13 +7,7 @@ describe Gitlab::Kubernetes::Helm::Api do let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) } let(:application) { create(:clusters_applications_prometheus) } - let(:command) do - Gitlab::Kubernetes::Helm::InstallCommand.new( - application.name, - chart: application.chart, - values: application.values - ) - end + let(:command) { application.install_command } subject { helm } diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 547f3f1752c..25c6fa3b9a3 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -3,44 +3,60 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do let(:application) { create(:clusters_applications_prometheus) } let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } - - let(:install_command) do - described_class.new( - application.name, - chart: application.chart, - values: application.values - ) - end + let(:install_command) { application.install_command } subject { install_command } - it_behaves_like 'helm commands' do - let(:commands) do - <<~EOS + context 'for ingress' do + let(:application) { create(:clusters_applications_ingress) } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS helm init --client-only >/dev/null helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null - EOS + EOS + end + end + end + + context 'for prometheus' do + let(:application) { create(:clusters_applications_prometheus) } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm install #{application.chart} --name #{application.name} --version #{application.version} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + EOS + end end end - context 'with an application with a repository' do + context 'for runner' do let(:ci_runner) { create(:ci_runner) } let(:application) { create(:clusters_applications_runner, runner: ci_runner) } - let(:install_command) do - described_class.new( - application.name, - chart: application.chart, - values: application.values, - repository: application.repository - ) + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm repo add #{application.name} #{application.repository} + helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + EOS + end end + end + + context 'for jupyter' do + let(:application) { create(:clusters_applications_jupyter) } it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm repo add #{application.name} #{application.repository} - helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + helm init --client-only >/dev/null + helm repo add #{application.name} #{application.repository} + helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null EOS end end diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 972b17d5b12..3d4240fa4ba 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -17,7 +17,10 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do before do namespace.add_owner(user) - allow_any_instance_of(Project).to receive(:add_import_job) + + expect_next_instance_of(Project) do |project| + expect(project).to receive(:add_import_job) + end end describe '#execute' do diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb index f66451c5188..81954fcf8c5 100644 --- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb @@ -3,10 +3,6 @@ require 'spec_helper' describe Gitlab::Metrics::Samplers::InfluxSampler do let(:sampler) { described_class.new(5) } - after do - Allocations.stop if Gitlab::Metrics.mri? - end - describe '#start' do it 'runs once and gathers a sample at a given interval' do expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 54781dd52fc..7972ff253fe 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -8,10 +8,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) end - after do - Allocations.stop if Gitlab::Metrics.mri? - end - describe '#sample' do it 'samples various statistics' do expect(Gitlab::Metrics::System).to receive(:memory_usage) @@ -49,7 +45,7 @@ describe Gitlab::Metrics::Samplers::RubySampler do it 'adds a metric containing garbage collection time statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) - expect(sampler.metrics[:total_time]).to receive(:set).with({}, 240) + expect(sampler.metrics[:total_time]).to receive(:increment).with({}, 0.24) sampler.sample end diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index 6eb0600f49e..0b3b23e930f 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -194,7 +194,7 @@ describe Gitlab::Metrics::WebTransaction do expect(transaction.action).to eq('TestController#show') end - context 'when the response content type is not :html' do + context 'when the request content type is not :html' do let(:request) { double(:request, format: double(:format, ref: :json)) } it 'appends the mime type to the transaction action' do @@ -202,6 +202,15 @@ describe Gitlab::Metrics::WebTransaction do expect(transaction.action).to eq('TestController#show.json') end end + + context 'when the request content type is not' do + let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) } + + it 'does not append the MIME type to the transaction action' do + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' }) + expect(transaction.action).to eq('TestController#show') + end + end end it 'returns no labels when no route information is present in env' do diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 548eb28fe4d..4059188fba1 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -135,6 +135,51 @@ describe Gitlab::Profiler do end end + describe '.clean_backtrace' do + it 'uses the Rails backtrace cleaner' do + backtrace = [] + + expect(Rails.backtrace_cleaner).to receive(:clean).with(backtrace) + + described_class.clean_backtrace(backtrace) + end + + it 'removes lines from IGNORE_BACKTRACES' do + backtrace = [ + "lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'", + "lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'", + "lib/gitlab/gitaly_client.rb:280:in `block in migrate'", + "lib/gitlab/metrics/influx_db.rb:103:in `measure'", + "lib/gitlab/gitaly_client.rb:278:in `migrate'", + "lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'", + "lib/gitlab/git/commit.rb:66:in `find'", + "app/models/repository.rb:1047:in `find_commit'", + "lib/gitlab/metrics/instrumentation.rb:159:in `block in find_commit'", + "lib/gitlab/metrics/method_call.rb:36:in `measure'", + "lib/gitlab/metrics/instrumentation.rb:159:in `find_commit'", + "app/models/repository.rb:113:in `commit'", + "lib/gitlab/i18n.rb:50:in `with_locale'", + "lib/gitlab/middleware/multipart.rb:95:in `call'", + "lib/gitlab/request_profiler/middleware.rb:14:in `call'", + "ee/lib/gitlab/database/load_balancing/rack_middleware.rb:37:in `call'", + "ee/lib/gitlab/jira/middleware.rb:15:in `call'" + ] + + expect(described_class.clean_backtrace(backtrace)) + .to eq([ + "lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'", + "lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'", + "lib/gitlab/gitaly_client.rb:280:in `block in migrate'", + "lib/gitlab/gitaly_client.rb:278:in `migrate'", + "lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'", + "lib/gitlab/git/commit.rb:66:in `find'", + "app/models/repository.rb:1047:in `find_commit'", + "app/models/repository.rb:113:in `commit'", + "ee/lib/gitlab/jira/middleware.rb:15:in `call'" + ]) + end + end + describe '.with_custom_logger' do context 'when the logger is set' do it 'uses the replacement logger for the duration of the block' do diff --git a/spec/lib/gitlab/search/query_spec.rb b/spec/lib/gitlab/search/query_spec.rb new file mode 100644 index 00000000000..2d00428fffa --- /dev/null +++ b/spec/lib/gitlab/search/query_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Search::Query do + let(:query) { 'base filter:wow anotherfilter:noway name:maybe other:mmm leftover' } + let(:subject) do + described_class.new(query) do + filter :filter + filter :name, parser: :upcase.to_proc + filter :other + end + end + + it { expect(described_class).to be < SimpleDelegator } + + it 'leaves undefined filters in the main query' do + expect(subject.term).to eq('base anotherfilter:noway leftover') + end + + it 'parses filters' do + expect(subject.filters.count).to eq(3) + expect(subject.filters.map { |f| f[:value] }).to match_array(%w[wow MAYBE mmm]) + end + + context 'with an empty filter' do + let(:query) { 'some bar name: baz' } + + it 'ignores empty filters' do + expect(subject.term).to eq('some bar name: baz') + end + end + + context 'with a pipe' do + let(:query) { 'base | nofilter' } + + it 'does not escape the pipe' do + expect(subject.term).to eq(query) + end + end +end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 155e1663298..c435f988cdd 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -498,34 +498,18 @@ describe Gitlab::Shell do ) end - context 'with gitaly' do - it 'returns true when the command succeeds' do - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository) - .with(repository.raw_repository) { :gitaly_response_object } - - is_expected.to be_truthy - end - - it 'return false when the command fails' do - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository) - .with(repository.raw_repository) { raise GRPC::BadStatus, 'bla' } + it 'returns true when the command succeeds' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository) + .with(repository.raw_repository) { :gitaly_response_object } - is_expected.to be_falsy - end + is_expected.to be_truthy end - context 'without gitaly', :disable_gitaly do - it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { true } - - is_expected.to be_truthy - end - - it 'return false when the command fails' do - expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { false } + it 'return false when the command fails' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository) + .with(repository.raw_repository) { raise GRPC::BadStatus, 'bla' } - is_expected.to be_falsy - end + is_expected.to be_falsy end end @@ -665,7 +649,7 @@ describe Gitlab::Shell do subject do gitlab_shell.fetch_remote(repository.raw_repository, remote_name, - forced: true, no_tags: true, ssh_auth: ssh_auth) + forced: true, no_tags: true, ssh_auth: ssh_auth) end it 'passes the correct params to the gitaly service' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 22d921716aa..20def4fefe2 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -29,20 +29,20 @@ describe Gitlab::UsageData do active_user_count counts recorded_at - mattermost_enabled edition version installation_type uuid hostname - signup - ldap - gravatar - omniauth - reply_by_email - container_registry + mattermost_enabled + signup_enabled + ldap_enabled + gravatar_enabled + omniauth_enabled + reply_by_email_enabled + container_registry_enabled + gitlab_shared_runners_enabled gitlab_pages - gitlab_shared_runners git database avg_cycle_analytics @@ -129,13 +129,14 @@ describe Gitlab::UsageData do subject { described_class.features_usage_data_ce } it 'gathers feature usage data' do - expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?) - expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled) - expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?) - expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled) - expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?) - expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled) - expect(subject[:gitlab_shared_runners]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled) + expect(subject[:mattermost_enabled]).to eq(Gitlab.config.mattermost.enabled) + expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?) + expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled) + expect(subject[:gravatar_enabled]).to eq(Gitlab::CurrentSettings.gravatar_enabled?) + expect(subject[:omniauth_enabled]).to eq(Gitlab.config.omniauth.enabled) + expect(subject[:reply_by_email_enabled]).to eq(Gitlab::IncomingEmail.enabled?) + expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled) + expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled) end end diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb index 296866d3319..38c30fab1ba 100644 --- a/spec/lib/gitlab/verify/uploads_spec.rb +++ b/spec/lib/gitlab/verify/uploads_spec.rb @@ -47,20 +47,49 @@ describe Gitlab::Verify::Uploads do before do stub_uploads_object_storage(AvatarUploader) upload.update!(store: ObjectStorage::Store::REMOTE) - expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file) end - it 'passes uploads in object storage that exist' do - expect(file).to receive(:exists?).and_return(true) + describe 'returned hash object' do + before do + expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file) + end - expect(failures).to eq({}) + it 'passes uploads in object storage that exist' do + expect(file).to receive(:exists?).and_return(true) + + expect(failures).to eq({}) + end + + it 'fails uploads in object storage that do not exist' do + expect(file).to receive(:exists?).and_return(false) + + expect(failures.keys).to contain_exactly(upload) + expect(failure).to include('Remote object does not exist') + end end - it 'fails uploads in object storage that do not exist' do - expect(file).to receive(:exists?).and_return(false) + describe 'performance' do + before do + allow(file).to receive(:exists?) + allow(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file) + end + + it "avoids N+1 queries" do + control_count = ActiveRecord::QueryRecorder.new { perform_task } + + # Create additional uploads in object storage + projects = create_list(:project, 3, :with_avatar) + uploads = projects.flat_map(&:uploads) + uploads.each do |upload| + upload.update!(store: ObjectStorage::Store::REMOTE) + end + + expect { perform_task }.not_to exceed_query_limit(control_count) + end - expect(failures.keys).to contain_exactly(upload) - expect(failure).to include('Remote object does not exist') + def perform_task + described_class.new(batch_size: 100).run_batches { } + end end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 775ca4ba0eb..a9a45367b4a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -416,16 +416,10 @@ describe Notify do end it 'has the correct subject and body' do - reasons = %w[foo bar] - - allow_any_instance_of(MergeRequestPresenter).to receive(:unmergeable_reasons).and_return(reasons) aggregate_failures do is_expected.to have_referable_subject(merge_request, reply: true) is_expected.to have_body_text(project_merge_request_path(project, merge_request)) - is_expected.to have_body_text('following reasons:') - reasons.each do |reason| - is_expected.to have_body_text(reason) - end + is_expected.to have_body_text('due to conflict.') end end end diff --git a/spec/migrations/remove_soft_removed_objects_spec.rb b/spec/migrations/remove_soft_removed_objects_spec.rb index fb70c284f5e..d0bde98b80e 100644 --- a/spec/migrations/remove_soft_removed_objects_spec.rb +++ b/spec/migrations/remove_soft_removed_objects_spec.rb @@ -3,6 +3,18 @@ require Rails.root.join('db', 'post_migrate', '20171207150343_remove_soft_remove describe RemoveSoftRemovedObjects, :migration do describe '#up' do + let!(:groups) do + table(:namespaces).tap do |t| + t.inheritance_column = nil + end + end + + let!(:routes) do + table(:routes).tap do |t| + t.inheritance_column = nil + end + end + it 'removes various soft removed objects' do 5.times do create_with_deleted_at(:issue) @@ -28,19 +40,20 @@ describe RemoveSoftRemovedObjects, :migration do it 'removes routes of soft removed personal namespaces' do namespace = create_with_deleted_at(:namespace) - group = create(:group) # rubocop:disable RSpec/FactoriesInMigrationSpecs + group = groups.create!(name: 'group', path: 'group_path', type: 'Group') + routes.create!(source_id: group.id, source_type: 'Group', name: 'group', path: 'group_path') - expect(Route.where(source: namespace).exists?).to eq(true) - expect(Route.where(source: group).exists?).to eq(true) + expect(routes.where(source_id: namespace.id).exists?).to eq(true) + expect(routes.where(source_id: group.id).exists?).to eq(true) run_migration - expect(Route.where(source: namespace).exists?).to eq(false) - expect(Route.where(source: group).exists?).to eq(true) + expect(routes.where(source_id: namespace.id).exists?).to eq(false) + expect(routes.where(source_id: group.id).exists?).to eq(true) end it 'schedules the removal of soft removed groups' do - group = create_with_deleted_at(:group) + group = create_deleted_group admin = create(:user, admin: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs expect_any_instance_of(GroupDestroyWorker) @@ -51,7 +64,7 @@ describe RemoveSoftRemovedObjects, :migration do end it 'does not remove soft removed groups when no admin user could be found' do - create_with_deleted_at(:group) + create_deleted_group expect_any_instance_of(GroupDestroyWorker) .not_to receive(:perform) @@ -74,4 +87,13 @@ describe RemoveSoftRemovedObjects, :migration do row end + + def create_deleted_group + group = groups.create!(name: 'group', path: 'group_path', type: 'Group') + routes.create!(source_id: group.id, source_type: 'Group', name: 'group', path: 'group_path') + + groups.where(id: group.id).update_all(deleted_at: 1.year.ago) + + group + end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index a47a07d908d..bb5b2ef3a47 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -73,6 +73,7 @@ describe Clusters::Applications::Ingress do it 'should be initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') + expect(subject.version).to be_nil expect(subject.values).to eq(ingress.values) end end diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index ca48a1d8072..65750141e65 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -36,6 +36,7 @@ describe Clusters::Applications::Jupyter do it 'should be initialized with 4 arguments' do expect(subject.name).to eq('jupyter') expect(subject.chart).to eq('jupyter/jupyterhub') + expect(subject.version).to be_nil expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/') expect(subject.values).to eq(jupyter.values) end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index d2302583ac8..efd57040005 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -109,6 +109,7 @@ describe Clusters::Applications::Prometheus do it 'should be initialized with 3 arguments' do expect(subject.name).to eq('prometheus') expect(subject.chart).to eq('stable/prometheus') + expect(subject.version).to eq('6.7.3') expect(subject.values).to eq(prometheus.values) end end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 3ef59457c5f..b12500d0acd 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -31,6 +31,7 @@ describe Clusters::Applications::Runner do it 'should be initialized with 4 arguments' do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') + expect(subject.version).to be_nil expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.values).to eq(gitlab_runner.values) end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 3f028b3bd5c..7ae70c3afb4 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1324,6 +1324,7 @@ describe MergeRequest do context 'when broken' do before do allow(subject).to receive(:broken?) { true } + allow(project.repository).to receive(:can_be_merged?).and_return(false) end it 'becomes unmergeable' do @@ -2150,9 +2151,11 @@ describe MergeRequest do before do allow(NotificationService).to receive(:new).and_return(notification_service) allow(TodoService).to receive(:new).and_return(todo_service) + + allow(subject.project.repository).to receive(:can_be_merged?).and_return(false) end - it 'notifies, but does not notify again if rechecking still results in cannot_be_merged' do + it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once @@ -2161,7 +2164,7 @@ describe MergeRequest do subject.mark_as_unmergeable end - it 'notifies whenever merge request is newly unmergeable' do + it 'notifies conflict, whenever newly unmergeable' do expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice @@ -2171,6 +2174,15 @@ describe MergeRequest do subject.mark_as_unchecked subject.mark_as_unmergeable end + + it 'does not notify whenever merge request is newly unmergeable due to other reasons' do + allow(subject.project.repository).to receive(:can_be_merged?).and_return(true) + + expect(notification_service).not_to receive(:merge_request_unmergeable) + expect(todo_service).not_to receive(:merge_request_became_unmergeable) + + subject.mark_as_unmergeable + end end describe 'check_state?' do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 6a6c71e6c82..a2cb716cb93 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -828,5 +828,15 @@ describe Note do note.destroy! end + + context 'when issuable etag caching is disabled' do + it 'does not store cache key' do + allow(note.noteable).to receive(:etag_caching_enabled?).and_return(false) + + expect_any_instance_of(Gitlab::EtagCaching::Store).not_to receive(:touch) + + note.save! + end + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bc9cce6b0c3..a2f8fac2f38 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2339,6 +2339,22 @@ describe Project do end end + describe '#any_lfs_file_locks?', :request_store do + set(:project) { create(:project) } + + it 'returns false when there are no LFS file locks' do + expect(project.any_lfs_file_locks?).to be_falsey + end + + it 'returns a cached true when there are LFS file locks' do + create(:lfs_file_lock, project: project) + + expect(project.lfs_file_locks).to receive(:any?).once.and_call_original + + 2.times { expect(project.any_lfs_file_locks?).to be_truthy } + end + end + describe '#protected_for?' do let(:project) { create(:project) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b6df048d4ca..d817a8376f4 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -434,44 +434,34 @@ describe Repository do end describe '#can_be_merged?' do - shared_examples 'can be merged' do - context 'mergeable branches' do - subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') } + context 'mergeable branches' do + subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') } - it { is_expected.to be_truthy } - end - - context 'non-mergeable branches without conflict sides missing' do - subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') } - - it { is_expected.to be_falsey } - end + it { is_expected.to be_truthy } + end - context 'non-mergeable branches with conflict sides missing' do - subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') } + context 'non-mergeable branches without conflict sides missing' do + subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') } - it { is_expected.to be_falsey } - end + it { is_expected.to be_falsey } + end - context 'non merged branch' do - subject { repository.merged_to_root_ref?('fix') } + context 'non-mergeable branches with conflict sides missing' do + subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') } - it { is_expected.to be_falsey } - end + it { is_expected.to be_falsey } + end - context 'non existent branch' do - subject { repository.merged_to_root_ref?('non_existent_branch') } + context 'non merged branch' do + subject { repository.merged_to_root_ref?('fix') } - it { is_expected.to be_nil } - end + it { is_expected.to be_falsey } end - context 'when Gitaly can_be_merged feature is enabled' do - it_behaves_like 'can be merged' - end + context 'non existent branch' do + subject { repository.merged_to_root_ref?('non_existent_branch') } - context 'when Gitaly can_be_merged feature is disabled', :disable_gitaly do - it_behaves_like 'can be merged' + it { is_expected.to be_nil } end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 6ac151f92f3..6d4676c25a5 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -151,6 +151,44 @@ describe ProjectPolicy do end end + context 'builds feature' do + subject { described_class.new(owner, project) } + + it 'disallows all permissions when the feature is disabled' do + project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) + + builds_permissions = [ + :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline, + :create_build, :read_build, :update_build, :admin_build, :destroy_build, + :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, + :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, + :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster, + :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment + ] + + expect_disallowed(*builds_permissions) + end + end + + context 'repository feature' do + subject { described_class.new(owner, project) } + + it 'disallows all permissions when the feature is disabled' do + project.project_feature.update(repository_access_level: ProjectFeature::DISABLED) + + repository_permissions = [ + :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline, + :create_build, :read_build, :update_build, :admin_build, :destroy_build, + :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, + :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, + :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster, + :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment + ] + + expect_disallowed(*repository_permissions) + end + end + shared_examples 'archived project policies' do let(:feature_write_abilities) do described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index d5fb4a7742c..e3b37739e8e 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -70,41 +70,6 @@ describe MergeRequestPresenter do end end - describe "#unmergeable_reasons" do - let(:presenter) { described_class.new(resource, current_user: user) } - - before do - # Mergeable base state - allow(resource).to receive(:has_no_commits?).and_return(false) - allow(resource).to receive(:source_branch_exists?).and_return(true) - allow(resource).to receive(:target_branch_exists?).and_return(true) - allow(resource.project.repository).to receive(:can_be_merged?).and_return(true) - end - - it "handles mergeable request" do - expect(presenter.unmergeable_reasons).to eq([]) - end - - it "handles no commit" do - allow(resource).to receive(:has_no_commits?).and_return(true) - - expect(presenter.unmergeable_reasons).to eq(["no commits"]) - end - - it "handles branches missing" do - allow(resource).to receive(:source_branch_exists?).and_return(false) - allow(resource).to receive(:target_branch_exists?).and_return(false) - - expect(presenter.unmergeable_reasons).to eq(["source branch is missing", "target branch is missing"]) - end - - it "handles merge conflict" do - allow(resource.project.repository).to receive(:can_be_merged?).and_return(false) - - expect(presenter.unmergeable_reasons).to eq(["has merge conflicts"]) - end - end - describe '#conflict_resolution_path' do let(:project) { create :project } let(:user) { create :user } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 7d923932309..da23fdd7dca 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -138,10 +138,15 @@ describe API::Groups do context "when using sorting" do let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") } + let(:group4) { create(:group, name: "same-name", path: "y#{group1.path}") } + let(:group5) { create(:group, name: "same-name") } let(:response_groups) { json_response.map { |group| group['name'] } } + let(:response_groups_ids) { json_response.map { |group| group['id'] } } before do group3.add_owner(user1) + group4.add_owner(user1) + group5.add_owner(user1) end it "sorts by name ascending by default" do @@ -150,7 +155,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq([group3.name, group1.name]) + expect(response_groups).to eq(Group.visible_to_user(user1).order(:name).pluck(:name)) end it "sorts in descending order when passed" do @@ -159,16 +164,52 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq([group1.name, group3.name]) + expect(response_groups).to eq(Group.visible_to_user(user1).order(name: :desc).pluck(:name)) end - it "sorts by the order_by param" do + it "sorts by path in order_by param" do get api("/groups", user1), order_by: "path" expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq([group1.name, group3.name]) + expect(response_groups).to eq(Group.visible_to_user(user1).order(:path).pluck(:name)) + end + + it "sorts by id in the order_by param" do + get api("/groups", user1), order_by: "id" + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(response_groups).to eq(Group.visible_to_user(user1).order(:id).pluck(:name)) + end + + it "sorts also by descending id with pagination fix" do + get api("/groups", user1), order_by: "id", sort: "desc" + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(response_groups).to eq(Group.visible_to_user(user1).order(id: :desc).pluck(:name)) + end + + it "sorts identical keys by id for good pagination" do + get api("/groups", user1), search: "same-name", order_by: "name" + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort) + end + + it "sorts descending identical keys by id for good pagination" do + get api("/groups", user1), search: "same-name", order_by: "name", sort: "desc" + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort) end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index aca4aa40027..f8e468be170 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -312,6 +312,30 @@ describe API::Search do end it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2 + + context 'filters' do + it 'by filename' do + get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'mon filename:PROCESS.md' + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.first['filename']).to eq('PROCESS.md') + end + + it 'by path' do + get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'mon path:markdown' + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(8) + end + + it 'by extension' do + get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'mon extension:md' + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(11) + end + end end end end diff --git a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb new file mode 100644 index 00000000000..7f689b196c5 --- /dev/null +++ b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/gitlab/finder_with_find_by' + +describe RuboCop::Cop::Gitlab::FinderWithFindBy do + include CopHelper + + subject(:cop) { described_class.new } + + context 'when calling execute.find' do + let(:source) do + <<~SRC + DummyFinder.new(some_args) + .execute + .find_by!(1) + SRC + end + let(:corrected_source) do + <<~SRC + DummyFinder.new(some_args) + .find_by!(1) + SRC + end + + it 'registers an offence' do + inspect_source(source) + + expect(cop.offenses.size).to eq(1) + end + + it 'can autocorrect the source' do + expect(autocorrect_source(source)).to eq(corrected_source) + end + + context 'when called within the `FinderMethods` module' do + let(:source) do + <<~SRC + module FinderMethods + def find_by!(*args) + execute.find_by!(args) + end + end + SRC + end + + it 'does not register an offence' do + inspect_source(source) + + expect(cop.offenses).to be_empty + end + end + end +end diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb index ef724fc8bad..5e08eb4f772 100644 --- a/spec/rubocop/cop/migration/update_large_table_spec.rb +++ b/spec/rubocop/cop/migration/update_large_table_spec.rb @@ -32,6 +32,14 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do include_examples 'large tables', 'add_column_with_default' end + context 'for the change_column_type_concurrently method' do + include_examples 'large tables', 'change_column_type_concurrently' + end + + context 'for the rename_column_concurrently method' do + include_examples 'large tables', 'rename_column_concurrently' + end + context 'for the update_column_in_batches method' do include_examples 'large tables', 'update_column_in_batches' end @@ -60,6 +68,18 @@ describe RuboCop::Cop::Migration::UpdateLargeTable do expect(cop.offenses).to be_empty end + it 'registers no offense for change_column_type_concurrently' do + inspect_source("change_column_type_concurrently :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + + it 'registers no offense for update_column_in_batches' do + inspect_source("rename_column_concurrently :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + it 'registers no offense for update_column_in_batches' do inspect_source("add_column_with_default :#{table}, :column, default: true") diff --git a/spec/serializers/blob_entity_spec.rb b/spec/serializers/blob_entity_spec.rb new file mode 100644 index 00000000000..dde59ff72df --- /dev/null +++ b/spec/serializers/blob_entity_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe BlobEntity do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:blob) { project.commit('master').diffs.diff_files.first.blob } + let(:request) { EntityRequest.new(project: project, ref: 'master') } + + let(:entity) do + described_class.new(blob, request: request) + end + + context 'as json' do + subject { entity.as_json } + + it 'exposes needed attributes' do + expect(subject).to include(:readable_text, :url) + end + end +end diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb index 45d7c703df3..c4a6c117b76 100644 --- a/spec/serializers/diff_file_entity_spec.rb +++ b/spec/serializers/diff_file_entity_spec.rb @@ -9,16 +9,48 @@ describe DiffFileEntity do let(:diff_refs) { commit.diff_refs } let(:diff) { commit.raw_diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } - let(:entity) { described_class.new(diff_file) } + let(:entity) { described_class.new(diff_file, request: {}) } subject { entity.as_json } - it 'exposes correct attributes' do - expect(subject).to include( - :submodule, :submodule_link, :file_path, - :deleted_file, :old_path, :new_path, :mode_changed, - :a_mode, :b_mode, :text, :old_path_html, - :new_path_html - ) + shared_examples 'diff file entity' do + it 'exposes correct attributes' do + expect(subject).to include( + :submodule, :submodule_link, :submodule_tree_url, :file_path, + :deleted_file, :old_path, :new_path, :mode_changed, + :a_mode, :b_mode, :text, :old_path_html, + :new_path_html, :highlighted_diff_lines, :parallel_diff_lines, + :blob, :file_hash, :added_lines, :removed_lines, :diff_refs, :content_sha, + :stored_externally, :external_storage, :too_large, :collapsed, :new_file, + :context_lines_path + ) + end + end + + context 'when there is no merge request' do + it_behaves_like 'diff file entity' + end + + context 'when there is a merge request' do + let(:user) { create(:user) } + let(:request) { EntityRequest.new(project: project, current_user: user) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) } + let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) } + + it_behaves_like 'diff file entity' + + it 'exposes additional attributes' do + expect(subject).to include(*exposed_urls) + expect(subject).to include(:replaced_view_path) + end + + it 'points all urls to merge request target project' do + response = subject + + exposed_urls.each do |attribute| + expect(response[attribute]).to include(merge_request.target_project.to_param) + end + end end end diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb new file mode 100644 index 00000000000..19a843b0cb7 --- /dev/null +++ b/spec/serializers/diffs_entity_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe DiffsEntity do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:request) { EntityRequest.new(project: project, current_user: user) } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + let(:merge_request_diffs) { merge_request.merge_request_diffs } + + let(:entity) do + described_class.new(merge_request_diffs.first.diffs, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs) + end + + context 'as json' do + subject { entity.as_json } + + it 'contains needed attributes' do + expect(subject).to include( + :real_size, :size, :branch_name, + :target_branch_name, :commit, :merge_request_diff, + :start_version, :latest_diff, :latest_version_path, + :added_lines, :removed_lines, :render_overflow_warning, + :email_patch_path, :plain_diff_path, :diff_files, + :merge_request_diffs + ) + end + end +end diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb index 7e19e74ca00..44d8cc69d9b 100644 --- a/spec/serializers/discussion_entity_spec.rb +++ b/spec/serializers/discussion_entity_spec.rb @@ -19,10 +19,20 @@ describe DiscussionEntity do end it 'exposes correct attributes' do - expect(subject).to include( - :id, :expanded, :notes, :individual_note, - :resolvable, :resolved, :resolve_path, - :resolve_with_issue_path, :diff_discussion + expect(subject.keys.sort).to include( + :diff_discussion, + :expanded, + :id, + :individual_note, + :notes, + :resolvable, + :resolve_path, + :resolve_with_issue_path, + :resolved, + :discussion_path, + :resolved_at, + :for_commit, + :commit_id ) end @@ -30,7 +40,21 @@ describe DiscussionEntity do let(:note) { create(:diff_note_on_merge_request) } it 'exposes diff file attributes' do - expect(subject).to include(:diff_file, :truncated_diff_lines, :image_diff_html) + expect(subject.keys.sort).to include( + :diff_file, + :truncated_diff_lines, + :position, + :line_code, + :active + ) + end + + context 'when diff file is a image' do + it 'exposes image attributes' do + allow(discussion).to receive(:on_image?).and_return(true) + + expect(subject.keys).to include(:image_diff_html) + end end end end diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb new file mode 100644 index 00000000000..84f6833d88a --- /dev/null +++ b/spec/serializers/merge_request_diff_entity_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe MergeRequestDiffEntity do + let(:project) { create(:project, :repository) } + let(:request) { EntityRequest.new(project: project) } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + let(:merge_request_diffs) { merge_request.merge_request_diffs } + + let(:entity) do + described_class.new(merge_request_diffs.first, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs) + end + + context 'as json' do + subject { entity.as_json } + + it 'exposes needed attributes' do + expect(subject).to include( + :version_index, :created_at, :commits_count, + :latest, :short_commit_sha, :version_path, + :compare_path + ) + end + end +end diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb new file mode 100644 index 00000000000..c91ea4aa681 --- /dev/null +++ b/spec/serializers/merge_request_user_entity_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe MergeRequestUserEntity do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:request) { EntityRequest.new(project: project, current_user: user) } + + let(:entity) do + described_class.new(user, request: request) + end + + context 'as json' do + subject { entity.as_json } + + it 'exposes needed attributes' do + expect(subject).to include(:can_fork, :can_create_merge_request, :fork_path) + end + end +end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index da8e660c16b..fce73e0ac1f 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -21,6 +21,11 @@ describe Auth::ContainerRegistryAuthenticationService do allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) end + shared_examples 'an authenticated' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } + end + shared_examples 'a valid token' do it { is_expected.to include(:token) } it { expect(payload).to include('access') } @@ -380,6 +385,14 @@ describe Auth::ContainerRegistryAuthenticationService do current_project.add_developer(current_user) end + context 'allow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'an authenticated' + end + it_behaves_like 'a valid token' context 'allow to pull and push images' do diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index 13accc6ae1b..b6cfc09da65 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -31,10 +31,8 @@ describe Issues::ResolveDiscussions do it "only queries for the merge request once" do fake_finder = double - fake_results = double - expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1) - expect(fake_results).to receive(:find_by).exactly(1) + expect(fake_finder).to receive(:find_by).exactly(1) expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1) 2.times { service.merge_request_to_resolve_discussions_of } diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 76f1e625fda..f82d4b483e7 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -19,7 +19,9 @@ describe Users::DestroyService do end it 'will delete the project' do - expect_any_instance_of(Projects::DestroyService).to receive(:execute).once + expect_next_instance_of(Projects::DestroyService) do |destroy_service| + expect(destroy_service).to receive(:execute).once + end service.execute(user) end @@ -32,7 +34,9 @@ describe Users::DestroyService do end it 'destroys a project in pending_delete' do - expect_any_instance_of(Projects::DestroyService).to receive(:execute).once + expect_next_instance_of(Projects::DestroyService) do |destroy_service| + expect(destroy_service).to receive(:execute).once + end service.execute(user) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dac609e2545..fdce8e84620 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -69,6 +69,7 @@ RSpec.configure do |config| config.include StubFeatureFlags config.include StubGitlabCalls config.include StubGitlabData + config.include ExpectNextInstanceOf config.include TestEnv config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::IntegrationHelpers, type: :feature diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index b4c71d69119..89a5518239d 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -22,7 +22,7 @@ shared_examples 'reportable note' do |type| expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) - if type == 'issue' + if type == 'issue' || type == 'merge_request' expect(dropdown).to have_button('Delete comment') else expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) diff --git a/spec/support/helpers/expect_next_instance_of.rb b/spec/support/helpers/expect_next_instance_of.rb new file mode 100644 index 00000000000..b95046b2b42 --- /dev/null +++ b/spec/support/helpers/expect_next_instance_of.rb @@ -0,0 +1,13 @@ +module ExpectNextInstanceOf + def expect_next_instance_of(klass, *new_args) + receive_new = receive(:new) + receive_new.with(*new_args) if new_args.any? + + expect(klass).to receive_new + .and_wrap_original do |method, *original_args| + method.call(*original_args).tap do |instance| + yield(instance) + end + end + end +end diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb index 1a1d5853a7a..2b9f8b30c60 100644 --- a/spec/support/helpers/features/notes_helpers.rb +++ b/spec/support/helpers/features/notes_helpers.rb @@ -13,7 +13,7 @@ module Spec module Features module NotesHelpers def add_note(text) - Sidekiq::Testing.fake! do + perform_enqueued_jobs do page.within(".js-main-target-form") do fill_in("note[note]", with: text) find(".js-comment-submit-button").click diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index f7b71bf42e3..329f18cd288 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -152,8 +152,13 @@ module LoginHelpers end def stub_saml_authorize_path_helpers - allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml') - allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') + allow_any_instance_of(ActionDispatch::Routing::RoutesProxy) + .to receive(:user_saml_omniauth_authorize_path) + .and_return('/users/auth/saml') + allow(Devise::OmniAuth::UrlHelpers) + .to receive(:omniauth_authorize_path) + .with(:user, "saml") + .and_return('/users/auth/saml') end def stub_omniauth_config(messages) diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb index c98aa503ed1..3b49d0b3319 100644 --- a/spec/support/helpers/merge_request_diff_helpers.rb +++ b/spec/support/helpers/merge_request_diff_helpers.rb @@ -2,7 +2,7 @@ module MergeRequestDiffHelpers def click_diff_line(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) line[:content].hover - line[:num].find('.add-diff-note', visible: false).send_keys(:return) + line[:num].find('.js-add-diff-note-button', visible: false).send_keys(:return) end def get_line_components(line_holder, diff_side = nil) diff --git a/spec/support/matchers/match_ids.rb b/spec/support/matchers/match_ids.rb index d8424405b96..1cb6b74acac 100644 --- a/spec/support/matchers/match_ids.rb +++ b/spec/support/matchers/match_ids.rb @@ -10,6 +10,13 @@ RSpec::Matchers.define :match_ids do |*expected| 'matches elements by ids' end + failure_message do + actual_ids = map_ids(actual) + expected_ids = map_ids(expected) + + "expected IDs #{actual_ids} in:\n\n #{actual.inspect}\n\nto match IDs #{expected_ids} in:\n\n #{expected.inspect}" + end + def map_ids(elements) elements = elements.flatten if elements.respond_to?(:flatten) diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index 8676f895a83..e650a176041 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -65,6 +65,14 @@ RSpec.shared_examples "redis_shared_examples" do end describe '.url' do + it 'withstands mutation' do + url1 = described_class.url + url2 = described_class.url + url1 << 'foobar' unless url1.frozen? + + expect(url2).not_to end_with('foobar') + end + context 'when yml file with env variable' do let(:config_file_name) { config_with_environment_variable_inside } @@ -101,7 +109,6 @@ RSpec.shared_examples "redis_shared_examples" do before do clear_pool end - after do clear_pool end diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb index 639b0924197..64c3b80136d 100644 --- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb @@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass| before do _ = issuable - gitlab_sign_in(user) if user + sign_in(user) if user visit path end diff --git a/spec/support/shared_examples/serializers/note_entity_examples.rb b/spec/support/shared_examples/serializers/note_entity_examples.rb index 9097c8e5513..ec208aba2a9 100644 --- a/spec/support/shared_examples/serializers/note_entity_examples.rb +++ b/spec/support/shared_examples/serializers/note_entity_examples.rb @@ -3,8 +3,8 @@ shared_examples 'note entity' do context 'basic note' do it 'exposes correct elements' do - expect(subject).to include(:type, :author, :note, :note_html, :current_user, - :discussion_id, :emoji_awardable, :award_emoji, :report_abuse_path, :attachment) + expect(subject).to include(:type, :author, :note, :note_html, :current_user, :discussion_id, + :emoji_awardable, :award_emoji, :report_abuse_path, :attachment, :noteable_note_url, :resolvable) end it 'does not expose elements for specific notes cases' do diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb index 2228e872926..7c34c7b4977 100644 --- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb @@ -245,6 +245,70 @@ RSpec.shared_examples 'slack or mattermost notifications' do end end + describe 'Push events' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, creator: user) } + + before do + allow(chat_service).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + context 'only notify for the default branch' do + context 'when enabled' do + before do + chat_service.notify_only_default_branch = true + end + + it 'does not notify push events if they are not for the default branch' do + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" + push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + + chat_service.execute(push_sample_data) + + expect(WebMock).not_to have_requested(:post, webhook_url) + end + + it 'notifies about push events for the default branch' do + push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) + + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it 'still notifies about pushed tags' do + ref = "#{Gitlab::Git::TAG_REF_PREFIX}test" + push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when disabled' do + before do + chat_service.notify_only_default_branch = false + end + + it 'notifies about all push events' do + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" + push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + end + end + describe "Note events" do let(:user) { create(:user) } let(:project) { create(:project, :repository, creator: user) } @@ -394,23 +458,6 @@ RSpec.shared_examples 'slack or mattermost notifications' do expect(result).to be_falsy end - - it 'does not notify push events if they are not for the default branch' do - ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) - - chat_service.execute(push_sample_data) - - expect(WebMock).not_to have_requested(:post, webhook_url) - end - - it 'notifies about push events for the default branch' do - push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) - - chat_service.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end end context 'when disabled' do diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index c7f5694ff43..7e673681c31 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -191,6 +191,18 @@ describe ObjectStorage do it "calls a cache path" do expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache]) end + + it "cleans up the cached file" do + cached_path = '' + + uploader.use_file do |path| + cached_path = path + + expect(File.exist?(cached_path)).to be_truthy + end + + expect(File.exist?(cached_path)).to be_falsey + end end end diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb index 0870b8f09f9..66c064e3fba 100644 --- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb +++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb @@ -6,6 +6,7 @@ describe 'devise/shared/_signin_box' do stub_devise assign(:ldap_servers, []) allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) + allow(view).to receive(:captcha_enabled?).and_return(false) end it 'is shown when Crowd is enabled' do diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 5d83397e8df..ac8716ecfb1 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -92,16 +92,6 @@ describe RepositoryForkWorker do end it_behaves_like 'RepositoryForkWorker performing' - - it 'logs a message about forking with old-style arguments' do - allow(subject).to receive(:gitlab_shell).and_return(shell) - expect(shell).to receive(:fork_repository) { true } - - allow(Rails.logger).to receive(:info).with(anything) # To compensate for other logs - expect(Rails.logger).to receive(:info).with("Project #{fork_project.id} is being forked using old-style arguments.") - - perform! - end end end end diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb index 80137815d2b..0b553db0ca4 100644 --- a/spec/workers/update_merge_requests_worker_spec.rb +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -18,13 +18,9 @@ describe UpdateMergeRequestsWorker do end it 'executes MergeRequests::RefreshService with expected values' do - expect(MergeRequests::RefreshService).to receive(:new) - .with(project, user).and_wrap_original do |method, *args| - method.call(*args).tap do |refresh_service| - expect(refresh_service) - .to receive(:execute).with(oldrev, newrev, ref) - end - end + expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service| + expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref) + end perform end |