From e540c0d71e00c4ce031b94cf11ec3de905e87da7 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Thu, 4 Apr 2019 13:08:34 +0000 Subject: [PATCH] Fixed test specs - added suggestions to mock data - fixed props to be not required --- .../diffs/components/diff_line_note_form.vue | 6 +- .../notes/components/note_form.vue | 44 ++++++- .../components/lib/utils/diff_utils.js | 20 ++++ .../vue_shared/components/markdown/field.vue | 5 +- .../vue_shared/components/markdown/header.vue | 2 +- .../components/markdown/suggestion_diff.vue | 42 ++----- .../markdown/suggestion_diff_row.vue | 32 +++++ .../components/markdown/suggestions.vue | 38 +----- app/controllers/concerns/preview_markdown.rb | 2 +- app/serializers/issue_entity.rb | 2 +- .../merge_request_widget_entity.rb | 2 +- app/serializers/suggestion_entity.rb | 2 + app/serializers/suggestion_serializer.rb | 9 ++ app/services/concerns/suggestible.rb | 7 ++ app/services/preview_markdown_service.rb | 28 +++-- .../form_elements/_description.html.haml | 2 +- app/views/shared/notes/_form.html.haml | 2 +- .../osw-support-multi-line-suggestions.yml | 5 + .../img/multi-line-suggestion-preview.png | Bin 0 -> 61692 bytes .../img/multi-line-suggestion-syntax.png | Bin 0 -> 29753 bytes doc/user/discussions/index.md | 18 +++ lib/banzai/suggestions_parser.rb | 16 --- spec/controllers/projects_controller_spec.rb | 10 ++ .../user_suggests_changes_on_diff_spec.rb | 111 ++++++++++++++++-- .../markdown/suggestion_diff_row_spec.js | 98 ++++++++++++++++ spec/javascripts/notes/mock_data.js | 6 +- .../components/markdown/header_spec.js | 2 +- .../markdown/suggestion_diff_spec.js | 66 +++++++---- .../components/markdown/suggestions_spec.js | 109 +++++++---------- spec/lib/banzai/suggestions_parser_spec.rb | 32 ----- spec/lib/gitlab/diff/suggestion_spec.rb | 87 ++++++++++++-- .../gitlab/diff/suggestions_parser_spec.rb | 61 ++++++++++ spec/models/suggestion_spec.rb | 16 +++ spec/serializers/suggestion_entity_spec.rb | 3 +- .../services/preview_markdown_service_spec.rb | 73 ++++++++++-- .../suggestions/apply_service_spec.rb | 64 ++++++++-- 36 files changed, 755 insertions(+), 267 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js create mode 100644 app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue create mode 100644 app/serializers/suggestion_serializer.rb create mode 100644 changelogs/unreleased/osw-support-multi-line-suggestions.yml create mode 100644 doc/user/discussions/img/multi-line-suggestion-preview.png create mode 100644 doc/user/discussions/img/multi-line-suggestion-syntax.png delete mode 100644 lib/banzai/suggestions_parser.rb create mode 100644 spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js delete mode 100644 spec/lib/banzai/suggestions_parser_spec.rb diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index bb66ab36283..41670b45798 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -48,10 +48,13 @@ export default { noteableType: this.noteableType, noteTargetLine: this.noteTargetLine, diffViewType: this.diffViewType, - diffFile: this.getDiffFileByHash(this.diffFileHash), + diffFile: this.diffFile, linePosition: this.linePosition, }; }, + diffFile() { + return this.getDiffFileByHash(this.diffFileHash); + }, }, mounted() { if (this.isLoggedIn) { @@ -102,6 +105,7 @@ export default { :line-code="line.line_code" :line="line" :help-page-path="helpPagePath" + :diff-file="diffFile" save-button-title="Comment" class="diff-comment-form" @handleFormUpdateAddToReview="addToReview" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 57d6b181bd7..471323bfc83 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -61,6 +61,11 @@ export default { required: false, default: null, }, + diffFile: { + type: Object, + required: false, + default: null, + }, helpPagePath: { type: String, required: false, @@ -102,9 +107,42 @@ export default { } return '#'; }, + diffParams() { + if (this.diffFile) { + return { + filePath: this.diffFile.file_path, + refs: this.diffFile.diff_refs, + }; + } else if (this.note && this.note.position) { + return { + filePath: this.note.position.new_path, + refs: this.note.position, + }; + } else if (this.discussion && this.discussion.diff_file) { + return { + filePath: this.discussion.diff_file.file_path, + refs: this.discussion.diff_file.diff_refs, + }; + } + + return null; + }, markdownPreviewPath() { const notable = this.getNoteableDataByProp('preview_note_path'); - return mergeUrlParams({ preview_suggestions: true }, notable); + + const previewSuggestions = this.line && this.diffParams; + const params = previewSuggestions + ? { + preview_suggestions: previewSuggestions, + line: this.line.new_line, + file_path: this.diffParams.filePath, + base_sha: this.diffParams.refs.base_sha, + start_sha: this.diffParams.refs.start_sha, + head_sha: this.diffParams.refs.head_sha, + } + : {}; + + return mergeUrlParams(params, notable); }, markdownDocsPath() { return this.getNotesDataByProp('markdownDocsPath'); @@ -234,8 +272,8 @@ export default { placeholder="Write a comment or drag your files here…" @keydown.meta.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()" - @keydown.up="editMyLastNote()" - @keydown.esc="cancelHandler(true)" + @keydown.exact.up="editMyLastNote()" + @keydown.exact.esc="cancelHandler(true)" @input="onInput" > diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js new file mode 100644 index 00000000000..d1aba99ac22 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js @@ -0,0 +1,20 @@ +/* eslint-disable import/prefer-default-export */ + +function trimFirstCharOfLineContent(text) { + if (!text) { + return text; + } + + return text.replace(/^( |\+|-)/, ''); +} + +function cleanSuggestionLine(line = {}) { + return { + ...line, + text: trimFirstCharOfLineContent(line.text), + }; +} + +export function selectDiffLines(lines) { + return lines.filter(line => line.type !== 'match').map(line => cleanSuggestionLine(line)); +} diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index eccf73e227c..0f3b3568414 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -76,6 +76,7 @@ export default { hasSuggestion: false, markdownPreviewLoading: false, previewMarkdown: false, + suggestions: this.note.suggestions || [], }; }, computed: { @@ -109,9 +110,6 @@ export default { } return lineNumber; }, - suggestions() { - return this.note.suggestions || []; - }, lineType() { return this.line ? this.line.type : ''; }, @@ -175,6 +173,7 @@ export default { this.referencedCommands = data.references.commands; this.referencedUsers = data.references.users; this.hasSuggestion = data.references.suggestions && data.references.suggestions.length; + this.suggestions = data.references.suggestions; } this.$nextTick() diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index cc6ecdb0395..a5a5b2ef415 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -38,7 +38,7 @@ export default { ].join('\n'); }, mdSuggestion() { - return ['```suggestion', `{text}`, '```'].join('\n'); + return ['```suggestion:-0+0', `{text}`, '```'].join('\n'); }, }, mounted() { diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue index a351ca62c94..2eb4ec12a4a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue @@ -1,24 +1,14 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 177d78cb904..8d3705e1e4a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -6,16 +6,6 @@ import Flash from '~/flash'; export default { components: { SuggestionDiff }, props: { - fromLine: { - type: Number, - required: false, - default: 0, - }, - fromContent: { - type: String, - required: false, - default: '', - }, lineType: { type: String, required: false, @@ -71,41 +61,19 @@ export default { suggestionElements.forEach((suggestionEl, i) => { const suggestionParentEl = suggestionEl.parentElement; - const newLines = this.extractNewLines(suggestionParentEl); - const diffComponent = this.generateDiff(newLines, i); + const diffComponent = this.generateDiff(i); diffComponent.$mount(suggestionParentEl); }); this.isRendered = true; }, - extractNewLines(suggestionEl) { - // extracts the suggested lines from the markdown - // calculates a line number for each line - - const newLines = suggestionEl.querySelectorAll('.line'); - const fromLine = this.suggestions.length ? this.suggestions[0].from_line : this.fromLine; - const lines = []; - - newLines.forEach((line, i) => { - const content = `${line.innerText}\n`; - const lineNumber = fromLine + i; - lines.push({ content, lineNumber }); - }); - - return lines; - }, - generateDiff(newLines, suggestionIndex) { - // generates the diff component - // all `suggestion` markdown will be swapped out by this component - + generateDiff(suggestionIndex) { const { suggestions, disabled, helpPagePath } = this; const suggestion = suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {}; - const fromContent = suggestion.from_content || this.fromContent; - const fromLine = suggestion.from_line || this.fromLine; const SuggestionDiffComponent = Vue.extend(SuggestionDiff); const suggestionDiff = new SuggestionDiffComponent({ - propsData: { newLines, fromLine, fromContent, disabled, suggestion, helpPagePath }, + propsData: { disabled, suggestion, helpPagePath }, }); suggestionDiff.$on('apply', ({ suggestionId, callback }) => { diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index f72d25fc54c..2a9729b6ffd 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -20,7 +20,7 @@ module PreviewMarkdown body: view_context.markdown(result[:text], markdown_params), references: { users: result[:users], - suggestions: result[:suggestions], + suggestions: SuggestionSerializer.new.represent_diff(result[:suggestions]), commands: view_context.markdown(result[:commands]) } } diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index c3f7d4651fb..914ad628a99 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -42,6 +42,6 @@ class IssueEntity < IssuableEntity end expose :preview_note_path do |issue| - preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.iid) + preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid) end end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index d673f8ae896..4831eb32c96 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -235,7 +235,7 @@ class MergeRequestWidgetEntity < IssuableEntity end expose :preview_note_path do |merge_request| - preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.iid) + preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid) end expose :merge_commit_path do |merge_request| diff --git a/app/serializers/suggestion_entity.rb b/app/serializers/suggestion_entity.rb index 4d0d4da10be..2dd62e19e29 100644 --- a/app/serializers/suggestion_entity.rb +++ b/app/serializers/suggestion_entity.rb @@ -3,6 +3,8 @@ class SuggestionEntity < API::Entities::Suggestion include RequestAwareEntity + unexpose :from_line, :to_line, :from_content, :to_content + expose :diff_lines, using: DiffLineEntity expose :current_user do expose :can_apply do |suggestion| Ability.allowed?(current_user, :apply_suggestion, suggestion) diff --git a/app/serializers/suggestion_serializer.rb b/app/serializers/suggestion_serializer.rb new file mode 100644 index 00000000000..010344f9fcd --- /dev/null +++ b/app/serializers/suggestion_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SuggestionSerializer < BaseSerializer + entity SuggestionEntity + + def represent_diff(resource) + represent(resource, { only: [:diff_lines] }) + end +end diff --git a/app/services/concerns/suggestible.rb b/app/services/concerns/suggestible.rb index 0b9822b1909..0cba9bf1b8a 100644 --- a/app/services/concerns/suggestible.rb +++ b/app/services/concerns/suggestible.rb @@ -2,10 +2,17 @@ module Suggestible extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize # This translates into limiting suggestion changes to `suggestion:-100+100`. MAX_LINES_CONTEXT = 100.freeze + def diff_lines + strong_memoize(:diff_lines) do + Gitlab::Diff::SuggestionDiff.new(self).diff_lines + end + end + def fetch_from_content diff_file.new_blob_lines_between(from_line, to_line).join end diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index c1655c38095..7386530f45f 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -17,7 +17,7 @@ class PreviewMarkdownService < BaseService private def explain_quick_actions(text) - return text, [] unless %w(Issue MergeRequest Commit).include?(commands_target_type) + return text, [] unless %w(Issue MergeRequest Commit).include?(target_type) quick_actions_service = QuickActions::InterpretService.new(project, current_user) quick_actions_service.explain(text, find_commands_target) @@ -30,22 +30,34 @@ class PreviewMarkdownService < BaseService end def find_suggestions(text) - return [] unless params[:preview_suggestions] + return [] unless preview_sugestions? - Banzai::SuggestionsParser.parse(text) + position = Gitlab::Diff::Position.new(new_path: params[:file_path], + new_line: params[:line].to_i, + base_sha: params[:base_sha], + head_sha: params[:head_sha], + start_sha: params[:start_sha]) + + Gitlab::Diff::SuggestionsParser.parse(text, position: position, project: project) + end + + def preview_sugestions? + params[:preview_suggestions] && + target_type == 'MergeRequest' && + Ability.allowed?(current_user, :download_code, project) end def find_commands_target QuickActions::TargetService .new(project, current_user) - .execute(commands_target_type, commands_target_id) + .execute(target_type, target_id) end - def commands_target_type - params[:quick_actions_target_type] + def target_type + params[:target_type] end - def commands_target_id - params[:quick_actions_target_id] + def target_id + params[:target_id] end end diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 25df2fe5cd6..b11cb8a3076 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -5,7 +5,7 @@ - supports_quick_actions = model.new_record? - if supports_quick_actions - - preview_url = preview_markdown_path(project, quick_actions_target_type: model.class.name) + - preview_url = preview_markdown_path(project, target_type: model.class.name) - else - preview_url = preview_markdown_path(project) diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 6a1eea85fde..d91bc6e57c9 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -1,7 +1,7 @@ - supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) - supports_quick_actions = note_supports_quick_actions?(@note) - if supports_quick_actions - - preview_url = preview_markdown_path(@project, quick_actions_target_type: @note.noteable_type, quick_actions_target_id: @note.noteable_id) + - preview_url = preview_markdown_path(@project, target_type: @note.noteable_type, target_id: @note.noteable_id) - else - preview_url = preview_markdown_path(@project) diff --git a/changelogs/unreleased/osw-support-multi-line-suggestions.yml b/changelogs/unreleased/osw-support-multi-line-suggestions.yml new file mode 100644 index 00000000000..8c8206c3822 --- /dev/null +++ b/changelogs/unreleased/osw-support-multi-line-suggestions.yml @@ -0,0 +1,5 @@ +--- +title: Support multi-line suggestions +merge_request: 25211 +author: +type: added diff --git a/doc/user/discussions/img/multi-line-suggestion-preview.png b/doc/user/discussions/img/multi-line-suggestion-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..4288d0ba034b29fec5c2ebb03f905aa7cede7bd2 GIT binary patch literal 61692 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 oLCK)mZ{KMC$KoIBkzNJP=q7n+a literal 0 HcmV?d00001 diff --git a/doc/user/discussions/img/multi-line-suggestion-syntax.png b/doc/user/discussions/img/multi-line-suggestion-syntax.png new file mode 100644 index 0000000000000000000000000000000000000000..df0c99b84ef48aaa60b1f8dbdaa7afb574116efc GIT binary patch literal 29753 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 N [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310) in GitLab 11.10. + +Reviewers can also suggest changes to +multiple lines with a single suggestion within Merge Request diff discussions. + +![Multi-line suggestion syntax](img/multi-line-suggestion-syntax.png) + +In the example above, the suggestion covers three lines above and four lines below the commented diff line. +It'd change from 3 lines _above_ to 4 lines _below_ the commented Diff line. + +![Multi-line suggestion preview](img/multi-line-suggestion-preview.png) + +NOTE: **Note:** +Suggestions covering multiple lines are limited to 100 lines _above_ and 100 lines _below_ +the commented diff line, allowing up to 200 changed lines per suggestion. + ## Start a discussion by replying to a standard comment > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30299) in GitLab 11.9 diff --git a/lib/banzai/suggestions_parser.rb b/lib/banzai/suggestions_parser.rb deleted file mode 100644 index 0d7f751bfc1..00000000000 --- a/lib/banzai/suggestions_parser.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# TODO: Delete when https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26107 -# exchange this parser by `Gitlab::Diff::SuggestionsParser`. -module Banzai - module SuggestionsParser - # Returns the content of each suggestion code block. - # - def self.parse(text) - html = Banzai.render(text, project: nil, no_original_data: true) - doc = Nokogiri::HTML(html) - - doc.search('pre.suggestion').map { |node| node.text } - end - end -end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 356d606d5c5..56d38b9475e 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -703,6 +703,16 @@ describe ProjectsController do expect(JSON.parse(response.body).keys).to match_array(%w(body references)) end + context 'when not authorized' do + let(:private_project) { create(:project, :private) } + + it 'returns 404' do + post :preview_markdown, params: { namespace_id: private_project.namespace, id: private_project, text: '*Markdown* text' } + + expect(response).to have_gitlab_http_status(404) + end + end + context 'state filter on references' do let(:issue) { create(:issue, :closed, project: public_project) } let(:merge_request) { create(:merge_request, :closed, target_project: public_project) } diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb index c19e299097e..1b5dd6945e0 100644 --- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb +++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb @@ -6,6 +6,14 @@ describe 'User comments on a diff', :js do include MergeRequestDiffHelpers include RepoHelpers + def expect_suggestion_has_content(element, expected_changing_content, expected_suggested_content) + changing_content = element.all(:css, '.line_holder.old').map(&:text) + suggested_content = element.all(:css, '.line_holder.new').map(&:text) + + expect(changing_content).to eq(expected_changing_content) + expect(suggested_content).to eq(expected_suggested_content) + end + let(:project) { create(:project, :repository) } let(:merge_request) do create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') @@ -33,8 +41,18 @@ describe 'User comments on a diff', :js do page.within('.diff-discussions') do expect(page).to have_button('Apply suggestion') expect(page).to have_content('Suggested change') - expect(page).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git') - expect(page).to have_content('# change to a comment') + end + + page.within('.md-suggestion-diff') do + expected_changing_content = [ + "6 url = https://github.com/gitlabhq/gitlab-shell.git" + ] + + expected_suggested_content = [ + "6 # change to a comment" + ] + + expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content) end end @@ -64,7 +82,7 @@ describe 'User comments on a diff', :js do click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) page.within('.js-discussion-note-form') do - fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion\n# or that\n```") + fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion:-2\n# or that\n# heh\n```") click_button('Comment') end @@ -74,11 +92,90 @@ describe 'User comments on a diff', :js do suggestion_1 = page.all(:css, '.md-suggestion-diff')[0] suggestion_2 = page.all(:css, '.md-suggestion-diff')[1] - expect(suggestion_1).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git') - expect(suggestion_1).to have_content('# change to a comment') + suggestion_1_expected_changing_content = [ + "6 url = https://github.com/gitlabhq/gitlab-shell.git" + ] + suggestion_1_expected_suggested_content = [ + "6 # change to a comment" + ] - expect(suggestion_2).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git') - expect(suggestion_2).to have_content('# or that') + suggestion_2_expected_changing_content = [ + "4 [submodule \"gitlab-shell\"]", + "5 path = gitlab-shell", + "6 url = https://github.com/gitlabhq/gitlab-shell.git" + ] + suggestion_2_expected_suggested_content = [ + "4 # or that", + "5 # heh" + ] + + expect_suggestion_has_content(suggestion_1, + suggestion_1_expected_changing_content, + suggestion_1_expected_suggested_content) + + expect_suggestion_has_content(suggestion_2, + suggestion_2_expected_changing_content, + suggestion_2_expected_suggested_content) + end + end + end + + context 'multi-line suggestions' do + it 'suggestion is presented' do + click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```") + click_button('Comment') + end + + wait_for_requests + + page.within('.diff-discussions') do + expect(page).to have_button('Apply suggestion') + expect(page).to have_content('Suggested change') + end + + page.within('.md-suggestion-diff') do + expected_changing_content = [ + "3 url = git://github.com/randx/six.git", + "4 [submodule \"gitlab-shell\"]", + "5 path = gitlab-shell", + "6 url = https://github.com/gitlabhq/gitlab-shell.git", + "7 [submodule \"gitlab-grack\"]", + "8 path = gitlab-grack", + "9 url = https://gitlab.com/gitlab-org/gitlab-grack.git" + ] + + expected_suggested_content = [ + "3 # change to a", + "4 # comment", + "5 # with", + "6 # broken", + "7 # lines" + ] + + expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content) + end + end + + it 'suggestion is appliable' do + click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```") + click_button('Comment') + end + + wait_for_requests + + page.within('.diff-discussions') do + expect(page).not_to have_content('Applied') + + click_button('Apply suggestion') + wait_for_requests + + expect(page).to have_content('Applied') end end end diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js new file mode 100644 index 00000000000..866d6eb05c6 --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js @@ -0,0 +1,98 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import SuggestionDiffRow from '~/vue_shared/components/markdown/suggestion_diff_row.vue'; + +const oldLine = { + can_receive_suggestion: false, + line_code: null, + meta_data: null, + new_line: null, + old_line: 5, + rich_text: '-oldtext', + text: '-oldtext', + type: 'old', +}; + +const newLine = { + can_receive_suggestion: false, + line_code: null, + meta_data: null, + new_line: 6, + old_line: null, + rich_text: '-newtext', + text: '-newtext', + type: 'new', +}; + +describe(SuggestionDiffRow.name, () => { + let wrapper; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(SuggestionDiffRow, { + localVue, + ...options, + }); + }; + + const findOldLineWrapper = () => wrapper.find('.old_line'); + const findNewLineWrapper = () => wrapper.find('.new_line'); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders correctly', () => { + factory({ + propsData: { + line: oldLine, + }, + }); + + expect(wrapper.is('.line_holder')).toBe(true); + }); + + describe('when passed line has type old', () => { + beforeEach(() => { + factory({ + propsData: { + line: oldLine, + }, + }); + }); + + it('has old class when line has type old', () => { + expect(wrapper.find('td').classes()).toContain('old'); + }); + + it('has old line number rendered', () => { + expect(findOldLineWrapper().text()).toBe('5'); + }); + + it('has no new line number rendered', () => { + expect(findNewLineWrapper().text()).toBe(''); + }); + }); + + describe('when passed line has type new', () => { + beforeEach(() => { + factory({ + propsData: { + line: newLine, + }, + }); + }); + + it('has new class when line has type new', () => { + expect(wrapper.find('td').classes()).toContain('new'); + }); + + it('has no old line number rendered', () => { + expect(findOldLineWrapper().text()).toBe(''); + }); + + it('has no new line number rendered', () => { + expect(findNewLineWrapper().text()).toBe('6'); + }); + }); +}); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 348743081eb..1df5cf9ef68 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -44,8 +44,7 @@ export const noteableDataMock = { milestone: null, milestone_id: null, moved_to_id: null, - preview_note_path: - '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', + preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue', project_id: 2, state: 'opened', time_estimate: 0, @@ -347,8 +346,7 @@ export const loggedOutnoteableData = { }, 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', + preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue', }; export const collapseNotesMock = [ diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js index e733a95288e..d4be2451f0b 100644 --- a/spec/javascripts/vue_shared/components/markdown/header_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js @@ -98,7 +98,7 @@ describe('Markdown field header component', () => { it('renders suggestion template', () => { vm.lineContent = 'Some content'; - expect(vm.mdSuggestion).toEqual('```suggestion\n{text}\n```'); + expect(vm.mdSuggestion).toEqual('```suggestion:-0+0\n{text}\n```'); }); it('does not render suggestion button if `canSuggest` is set to false', () => { diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js index f87c2a92f47..ea74cb9eb21 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js @@ -1,21 +1,50 @@ import Vue from 'vue'; import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue'; +import { selectDiffLines } from '~/vue_shared/components/lib/utils/diff_utils'; const MOCK_DATA = { canApply: true, - newLines: [ - { content: 'Line 1\n', lineNumber: 1 }, - { content: 'Line 2\n', lineNumber: 2 }, - { content: 'Line 3\n', lineNumber: 3 }, - ], - fromLine: 1, - fromContent: 'Old content', suggestion: { id: 1, + diff_lines: [ + { + can_receive_suggestion: false, + line_code: null, + meta_data: null, + new_line: null, + old_line: 5, + rich_text: '-test', + text: '-test', + type: 'old', + }, + { + can_receive_suggestion: true, + line_code: null, + meta_data: null, + new_line: 5, + old_line: null, + rich_text: '+new test', + text: '+new test', + type: 'new', + }, + { + can_receive_suggestion: true, + line_code: null, + meta_data: null, + new_line: 5, + old_line: null, + rich_text: '+new test2', + text: '+new test2', + type: 'new', + }, + ], }, helpPagePath: 'path_to_docs', }; +const lines = selectDiffLines(MOCK_DATA.suggestion.diff_lines); +const newLines = lines.filter(line => line.type === 'new'); + describe('Suggestion Diff component', () => { let vm; @@ -39,30 +68,23 @@ describe('Suggestion Diff component', () => { }); it('renders the oldLineNumber', () => { - const fromLine = vm.$el.querySelector('.qa-old-diff-line-number').innerHTML; + const fromLine = vm.$el.querySelector('.old_line').innerHTML; - expect(parseInt(fromLine, 10)).toBe(vm.fromLine); + expect(parseInt(fromLine, 10)).toBe(lines[0].old_line); }); it('renders the oldLineContent', () => { const fromContent = vm.$el.querySelector('.line_content.old').innerHTML; - expect(fromContent.includes(vm.fromContent)).toBe(true); + expect(fromContent.includes(lines[0].text)).toBe(true); }); - it('renders the contents of newLines', () => { - const newLines = vm.$el.querySelectorAll('.line_holder.new'); + it('renders new lines', () => { + const newLinesElements = vm.$el.querySelectorAll('.line_holder.new'); - newLines.forEach((line, i) => { - expect(newLines[i].innerHTML.includes(vm.newLines[i].content)).toBe(true); - }); - }); - - it('renders a line number for each line', () => { - const newLineNumbers = vm.$el.querySelectorAll('.qa-new-diff-line-number'); - - newLineNumbers.forEach((line, i) => { - expect(newLineNumbers[i].innerHTML.includes(vm.newLines[i].lineNumber)).toBe(true); + newLinesElements.forEach((line, i) => { + expect(newLinesElements[i].innerHTML.includes(newLines[i].new_line)).toBe(true); + expect(newLinesElements[i].innerHTML.includes(newLines[i].text)).toBe(true); }); }); }); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js index 33be63a3a1e..b7de40b4831 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js @@ -2,46 +2,52 @@ import Vue from 'vue'; import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue'; const MOCK_DATA = { - fromLine: 1, - fromContent: 'Old content', - suggestions: [], + suggestions: [ + { + id: 1, + appliable: true, + applied: false, + current_user: { + can_apply: true, + }, + diff_lines: [ + { + can_receive_suggestion: false, + line_code: null, + meta_data: null, + new_line: null, + old_line: 5, + rich_text: '-test', + text: '-test', + type: 'old', + }, + { + can_receive_suggestion: true, + line_code: null, + meta_data: null, + new_line: 5, + old_line: null, + rich_text: '+new test', + text: '+new test', + type: 'new', + }, + ], + }, + ], noteHtml: ` +
+
-oldtest
+
-
Suggestion 1
+
+newtest
- -
-
Suggestion 2
-
`, isApplied: false, helpPagePath: 'path_to_docs', }; -const generateLine = content => { - const line = document.createElement('div'); - line.className = 'line'; - line.innerHTML = content; - - return line; -}; - -const generateMockLines = () => { - const line1 = generateLine('Line 1'); - const line2 = generateLine('Line 2'); - const line3 = generateLine('- Line 3'); - const container = document.createElement('div'); - - container.appendChild(line1); - container.appendChild(line2); - container.appendChild(line3); - - return container; -}; - describe('Suggestion component', () => { let vm; - let extractedLines; let diffTable; beforeEach(done => { @@ -51,8 +57,7 @@ describe('Suggestion component', () => { propsData: MOCK_DATA, }).$mount(); - extractedLines = vm.extractNewLines(generateMockLines()); - diffTable = vm.generateDiff(extractedLines).$mount().$el; + diffTable = vm.generateDiff(0).$mount().$el; spyOn(vm, 'renderSuggestions'); vm.renderSuggestions(); @@ -70,32 +75,8 @@ describe('Suggestion component', () => { it('renders suggestions', () => { expect(vm.renderSuggestions).toHaveBeenCalled(); - expect(vm.$el.innerHTML.includes('Suggestion 1')).toBe(true); - expect(vm.$el.innerHTML.includes('Suggestion 2')).toBe(true); - }); - }); - - describe('extractNewLines', () => { - it('extracts suggested lines', () => { - const expectedReturn = [ - { content: 'Line 1\n', lineNumber: 1 }, - { content: 'Line 2\n', lineNumber: 2 }, - { content: '- Line 3\n', lineNumber: 3 }, - ]; - - expect(vm.extractNewLines(generateMockLines())).toEqual(expectedReturn); - }); - - it('increments line number for each extracted line', () => { - expect(extractedLines[0].lineNumber).toEqual(1); - expect(extractedLines[1].lineNumber).toEqual(2); - expect(extractedLines[2].lineNumber).toEqual(3); - }); - - it('returns empty array if no lines are found', () => { - const el = document.createElement('div'); - - expect(vm.extractNewLines(el)).toEqual([]); + expect(vm.$el.innerHTML.includes('oldtest')).toBe(true); + expect(vm.$el.innerHTML.includes('newtest')).toBe(true); }); }); @@ -109,17 +90,17 @@ describe('Suggestion component', () => { }); it('generates a diff table that contains contents the suggested lines', () => { - extractedLines.forEach((line, i) => { - expect(diffTable.innerHTML.includes(extractedLines[i].content)).toBe(true); + MOCK_DATA.suggestions[0].diff_lines.forEach(line => { + const text = line.text.substring(1); + + expect(diffTable.innerHTML.includes(text)).toBe(true); }); }); it('generates a diff table with the correct line number for each suggested line', () => { - const lines = diffTable.getElementsByClassName('qa-new-diff-line-number'); + const lines = diffTable.querySelectorAll('.old_line'); - expect([...lines][0].innerHTML).toBe('1'); - expect([...lines][1].innerHTML).toBe('2'); - expect([...lines][2].innerHTML).toBe('3'); + expect(parseInt([...lines][0].innerHTML, 10)).toBe(5); }); }); }); diff --git a/spec/lib/banzai/suggestions_parser_spec.rb b/spec/lib/banzai/suggestions_parser_spec.rb deleted file mode 100644 index 79658d710ce..00000000000 --- a/spec/lib/banzai/suggestions_parser_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Banzai::SuggestionsParser do - describe '.parse' do - it 'returns a list of suggestion contents' do - markdown = <<-MARKDOWN.strip_heredoc - ```suggestion - foo - bar - ``` - - ``` - nothing - ``` - - ```suggestion - xpto - baz - ``` - - ```thing - this is not a suggestion, it's a thing - ``` - MARKDOWN - - expect(described_class.parse(markdown)).to eq([" foo\n bar", - " xpto\n baz"]) - end - end -end diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb index 71fd25df698..d7ca0e0a522 100644 --- a/spec/lib/gitlab/diff/suggestion_spec.rb +++ b/spec/lib/gitlab/diff/suggestion_spec.rb @@ -10,6 +10,16 @@ describe Gitlab::Diff::Suggestion do lines_above: above, lines_below: below) end + + it 'returns diff lines with correct line numbers' do + diff_lines = suggestion.diff_lines + + expect(diff_lines).to all(be_a(Gitlab::Diff::Line)) + + expected_diff_lines.each_with_index do |expected_line, index| + expect(diff_lines[index].to_hash).to include(expected_line) + end + end end let(:merge_request) { create(:merge_request) } @@ -48,6 +58,18 @@ describe Gitlab::Diff::Suggestion do let(:expected_above) { line - 1 } let(:expected_below) { below } let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + let(:expected_diff_lines) do + [ + { old_pos: 1, new_pos: 1, type: 'old', text: "-require 'fileutils'" }, + { old_pos: 2, new_pos: 1, type: 'old', text: "-require 'open3'" }, + { old_pos: 3, new_pos: 1, type: 'old', text: "-" }, + { old_pos: 4, new_pos: 1, type: 'old', text: "-module Popen" }, + { old_pos: 5, new_pos: 1, type: 'old', text: "- extend self" }, + { old_pos: 6, new_pos: 1, type: 'old', text: "-" }, + { old_pos: 7, new_pos: 1, type: 'new', text: "+# parsed suggestion content" }, + { old_pos: 7, new_pos: 2, type: 'new', text: "+# with comments" } + ] + end it_behaves_like 'correct suggestion raw content' end @@ -59,6 +81,47 @@ describe Gitlab::Diff::Suggestion do let(:expected_below) { below } let(:expected_above) { above } let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + let(:expected_diff_lines) do + [ + { old_pos: 4, new_pos: 4, type: "match", text: "@@ -4 +4" }, + { old_pos: 4, new_pos: 4, type: "old", text: "-module Popen" }, + { old_pos: 5, new_pos: 4, type: "old", text: "- extend self" }, + { old_pos: 6, new_pos: 4, type: "old", text: "-" }, + { old_pos: 7, new_pos: 4, type: "old", text: "- def popen(cmd, path=nil)" }, + { old_pos: 8, new_pos: 4, type: "old", text: "- unless cmd.is_a?(Array)" }, + { old_pos: 9, new_pos: 4, type: "old", text: "- raise RuntimeError, \"System commands must be given as an array of strings\"" }, + { old_pos: 10, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 11, new_pos: 4, type: "old", text: "-" }, + { old_pos: 12, new_pos: 4, type: "old", text: "- path ||= Dir.pwd" }, + { old_pos: 13, new_pos: 4, type: "old", text: "-" }, + { old_pos: 14, new_pos: 4, type: "old", text: "- vars = {" }, + { old_pos: 15, new_pos: 4, type: "old", text: "- \"PWD\" => path" }, + { old_pos: 16, new_pos: 4, type: "old", text: "- }" }, + { old_pos: 17, new_pos: 4, type: "old", text: "-" }, + { old_pos: 18, new_pos: 4, type: "old", text: "- options = {" }, + { old_pos: 19, new_pos: 4, type: "old", text: "- chdir: path" }, + { old_pos: 20, new_pos: 4, type: "old", text: "- }" }, + { old_pos: 21, new_pos: 4, type: "old", text: "-" }, + { old_pos: 22, new_pos: 4, type: "old", text: "- unless File.directory?(path)" }, + { old_pos: 23, new_pos: 4, type: "old", text: "- FileUtils.mkdir_p(path)" }, + { old_pos: 24, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 25, new_pos: 4, type: "old", text: "-" }, + { old_pos: 26, new_pos: 4, type: "old", text: "- @cmd_output = \"\"" }, + { old_pos: 27, new_pos: 4, type: "old", text: "- @cmd_status = 0" }, + { old_pos: 28, new_pos: 4, type: "old", text: "-" }, + { old_pos: 29, new_pos: 4, type: "old", text: "- Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|" }, + { old_pos: 30, new_pos: 4, type: "old", text: "- @cmd_output << stdout.read" }, + { old_pos: 31, new_pos: 4, type: "old", text: "- @cmd_output << stderr.read" }, + { old_pos: 32, new_pos: 4, type: "old", text: "- @cmd_status = wait_thr.value.exitstatus" }, + { old_pos: 33, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 34, new_pos: 4, type: "old", text: "-" }, + { old_pos: 35, new_pos: 4, type: "old", text: "- return @cmd_output, @cmd_status" }, + { old_pos: 36, new_pos: 4, type: "old", text: "- end" }, + { old_pos: 37, new_pos: 4, type: "old", text: "-end" }, + { old_pos: 38, new_pos: 4, type: "new", text: "+# parsed suggestion content" }, + { old_pos: 38, new_pos: 5, type: "new", text: "+# with comments" } + ] + end it_behaves_like 'correct suggestion raw content' end @@ -70,17 +133,19 @@ describe Gitlab::Diff::Suggestion do let(:expected_below) { below } let(:expected_above) { above } let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } - - it_behaves_like 'correct suggestion raw content' - end - - context 'when no extra lines (single-line suggestion)' do - let(:line) { 5 } - let(:above) { 0 } - let(:below) { 0 } - let(:expected_below) { below } - let(:expected_above) { above } - let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + let(:expected_diff_lines) do + [ + { old_pos: 3, new_pos: 3, type: "match", text: "@@ -3 +3" }, + { old_pos: 3, new_pos: 3, type: "old", text: "-" }, + { old_pos: 4, new_pos: 3, type: "old", text: "-module Popen" }, + { old_pos: 5, new_pos: 3, type: "old", text: "- extend self" }, + { old_pos: 6, new_pos: 3, type: "old", text: "-" }, + { old_pos: 7, new_pos: 3, type: "old", text: "- def popen(cmd, path=nil)" }, + { old_pos: 8, new_pos: 3, type: "old", text: "- unless cmd.is_a?(Array)" }, + { old_pos: 9, new_pos: 3, type: "new", text: "+# parsed suggestion content" }, + { old_pos: 9, new_pos: 4, type: "new", text: "+# with comments" } + ] + end it_behaves_like 'correct suggestion raw content' end diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb index 1119ea04995..1f2af42f6e7 100644 --- a/spec/lib/gitlab/diff/suggestions_parser_spec.rb +++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb @@ -69,5 +69,66 @@ describe Gitlab::Diff::SuggestionsParser do lines_below: 0) end end + + context 'multi-line suggestions' do + let(:markdown) do + <<-MARKDOWN.strip_heredoc + ```suggestion:-2+1 + # above and below + ``` + + ``` + nothing + ``` + + ```suggestion:-3 + # only above + ``` + + ```suggestion:+3 + # only below + ``` + + ```thing + this is not a suggestion, it's a thing + ``` + MARKDOWN + end + + it 'returns a list of Gitlab::Diff::Suggestion' do + expect(subject).to all(be_a(Gitlab::Diff::Suggestion)) + expect(subject.size).to eq(3) + end + + it 'suggestion with above and below param has correct data' do + from_line = position.new_line - 2 + to_line = position.new_line + 1 + + expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " # above and below\n", + lines_above: 2, + lines_below: 1) + end + + it 'suggestion with above param has correct data' do + from_line = position.new_line - 3 + to_line = position.new_line + + expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line), + to_content: " # only above\n", + lines_above: 3, + lines_below: 0) + end + + it 'suggestion with below param has correct data' do + from_line = position.new_line + to_line = position.new_line + 3 + + expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line), + to_content: " # only below\n", + lines_above: 0, + lines_below: 3) + end + end end end diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb index cafc725dddb..8d4e9070b19 100644 --- a/spec/models/suggestion_spec.rb +++ b/spec/models/suggestion_spec.rb @@ -21,6 +21,22 @@ describe Suggestion do end end + describe '#diff_lines' do + let(:suggestion) { create(:suggestion, :content_from_repo) } + + it 'returns parsed diff lines' do + expected_diff_lines = Gitlab::Diff::SuggestionDiff.new(suggestion).diff_lines + diff_lines = suggestion.diff_lines + + expect(diff_lines.size).to eq(expected_diff_lines.size) + expect(diff_lines).to all(be_a(Gitlab::Diff::Line)) + + expected_diff_lines.each_with_index do |expected_line, index| + expect(diff_lines[index].to_hash).to eq(expected_line.to_hash) + end + end + end + describe '#appliable?' do context 'when note does not support suggestions' do it 'returns false' do diff --git a/spec/serializers/suggestion_entity_spec.rb b/spec/serializers/suggestion_entity_spec.rb index d38fc2b132b..d282a7f9c7a 100644 --- a/spec/serializers/suggestion_entity_spec.rb +++ b/spec/serializers/suggestion_entity_spec.rb @@ -13,8 +13,7 @@ describe SuggestionEntity do subject { entity.as_json } it 'exposes correct attributes' do - expect(subject).to include(:id, :from_line, :to_line, :appliable, - :applied, :from_content, :to_content) + expect(subject.keys).to match_array([:id, :appliable, :applied, :diff_lines, :current_user]) end it 'exposes current user abilities' do diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index 85515d548a7..a1d31464e07 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe PreviewMarkdownService do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } before do project.add_developer(user) @@ -20,23 +20,72 @@ describe PreviewMarkdownService do end describe 'suggestions' do - let(:params) { { text: "```suggestion\nfoo\n```", preview_suggestions: preview_suggestions } } + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project) + end + let(:text) { "```suggestion\nfoo\n```" } + let(:params) do + suggestion_params.merge(text: text, + target_type: 'MergeRequest', + target_id: merge_request.iid) + end let(:service) { described_class.new(project, user, params) } context 'when preview markdown param is present' do - let(:preview_suggestions) { true } + let(:path) { "files/ruby/popen.rb" } + let(:line) { 10 } + let(:diff_refs) { merge_request.diff_refs } + + let(:suggestion_params) do + { + preview_suggestions: true, + file_path: path, + line: line, + base_sha: diff_refs.base_sha, + start_sha: diff_refs.start_sha, + head_sha: diff_refs.head_sha + } + end + + it 'returns suggestions referenced in text' do + position = Gitlab::Diff::Position.new(new_path: path, + new_line: line, + diff_refs: diff_refs) + + expect(Gitlab::Diff::SuggestionsParser) + .to receive(:parse) + .with(text, position: position, project: merge_request.project) + .and_call_original - it 'returns users referenced in text' do result = service.execute - expect(result[:suggestions]).to eq(['foo']) + expect(result[:suggestions]).to all(be_a(Gitlab::Diff::Suggestion)) + end + + context 'when user is not authorized' do + let(:another_user) { create(:user) } + let(:service) { described_class.new(project, another_user, params) } + + before do + project.add_guest(another_user) + end + + it 'returns no suggestions' do + result = service.execute + + expect(result[:suggestions]).to be_empty + end end end context 'when preview markdown param is not present' do - let(:preview_suggestions) { false } + let(:suggestion_params) do + { + preview_suggestions: false + } + end - it 'returns users referenced in text' do + it 'returns suggestions referenced in text' do result = service.execute expect(result[:suggestions]).to eq([]) @@ -49,8 +98,8 @@ describe PreviewMarkdownService do let(:params) do { text: "Please do it\n/assign #{user.to_reference}", - quick_actions_target_type: 'Issue', - quick_actions_target_id: issue.id + target_type: 'Issue', + target_id: issue.id } end let(:service) { described_class.new(project, user, params) } @@ -72,7 +121,7 @@ describe PreviewMarkdownService do let(:params) do { text: "My work\n/estimate 2y", - quick_actions_target_type: 'MergeRequest' + target_type: 'MergeRequest' } end let(:service) { described_class.new(project, user, params) } @@ -96,8 +145,8 @@ describe PreviewMarkdownService do let(:params) do { text: "My work\n/tag v1.2.3 Stable release", - quick_actions_target_type: 'Commit', - quick_actions_target_id: commit.id + target_type: 'Commit', + target_id: commit.id } end let(:service) { described_class.new(project, user, params) } diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 80b5dcac6c7..7732767137c 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -51,6 +51,10 @@ describe Suggestions::ApplyService do diff_refs: merge_request.diff_refs) end + let(:diff_note) do + create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project) + end + let(:suggestion) do create(:suggestion, :content_from_repo, note: diff_note, to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") @@ -108,12 +112,6 @@ describe Suggestions::ApplyService do target_project: project) end - let!(:diff_note) do - create(:diff_note_on_merge_request, noteable: merge_request, - position: position, - project: project) - end - before do project.add_maintainer(user) end @@ -192,11 +190,6 @@ describe Suggestions::ApplyService do CONTENT end - let(:merge_request) do - create(:merge_request, source_project: project, - target_project: project) - end - def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:) position = Gitlab::Diff::Position.new(old_path: path, new_path: path, @@ -291,6 +284,55 @@ describe Suggestions::ApplyService do expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip) end end + + context 'multi-line suggestion' do + let(:expected_content) do + <<~CONTENT + require 'fileutils' + require 'open3' + + module Popen + extend self + + # multi + # line + + vars = { + "PWD" => path + } + + options = { + chdir: path + } + + unless File.directory?(path) + FileUtils.mkdir_p(path) + end + + @cmd_output = "" + @cmd_status = 0 + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read + @cmd_status = wait_thr.value.exitstatus + end + + return @cmd_output, @cmd_status + end + end + CONTENT + end + + let(:suggestion) do + create(:suggestion, :content_from_repo, note: diff_note, + lines_above: 2, + lines_below: 3, + to_content: "# multi\n# line\n") + end + + it_behaves_like 'successfully creates commit and updates suggestion' + end end context 'fork-project' do