diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2017-09-02 11:55:18 +0000 |
---|---|---|
committer | Jacob Schatz <jschatz@gitlab.com> | 2017-09-02 11:55:18 +0000 |
commit | 81002745184df28fc9d969afc524986279c653bb (patch) | |
tree | 386724b936531148dfe98110aa214b6deb0aa84a /spec | |
parent | 1e60725174cf8cfac1b54bbcdb1453d74bfdd37e (diff) | |
parent | 92edb3edab89dcb7b87bad3ac0fe7fed404fea85 (diff) | |
download | gitlab-ce-81002745184df28fc9d969afc524986279c653bb.tar.gz |
Merge branch 'issue-discussions-refactor' into 'master'
Issue discussions Vue refactor
See merge request !12069
Diffstat (limited to 'spec')
52 files changed, 2335 insertions, 296 deletions
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index da8f9e8376e..65f4d09cfce 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -879,4 +879,19 @@ describe Projects::IssuesController do format: :json end end + + describe 'GET #discussions' do + let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + + before do + project.add_developer(user) + sign_in(user) + end + + it 'returns discussion json' do + get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid + + expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note]) + end + end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index f280c55059c..6ffe41b8608 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -46,10 +46,13 @@ describe Projects::NotesController do end context 'for a discussion note' do - let!(:note) { create(:discussion_note_on_issue, noteable: issue, project: project) } + 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) } it 'responds with the expected attributes' do - get :index, request_params + get :index, params expect(note_json[:id]).to eq(note.id) expect(note_json[:discussion_html]).not_to be_nil @@ -104,10 +107,12 @@ describe Projects::NotesController do end context 'for a regular note' do - let!(:note) { create(:note, noteable: issue, project: project) } + let!(:note) { create(:note_on_merge_request, project: project) } + + let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } it 'responds with the expected attributes' do - get :index, request_params + get :index, params expect(note_json[:id]).to eq(note.id) expect(note_json[:html]).not_to be_nil @@ -125,7 +130,9 @@ describe Projects::NotesController do note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, namespace_id: project.namespace, project_id: project, - merge_request_diff_head_sha: 'sha' + merge_request_diff_head_sha: 'sha', + target_type: 'merge_request', + target_id: merge_request.id } end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 134e618feac..a29acb30163 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -70,13 +70,13 @@ describe 'Awards Emoji' do it 'toggles the smiley emoji on a note', js: true do toggle_smiley_emoji(true) - within('.note-awards') do + within('.note-body') do expect(find(emoji_counter)).to have_text("1") end toggle_smiley_emoji(false) - within('.note-awards') do + within('.note-body') do expect(page).not_to have_selector(emoji_counter) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index b84635c5134..c6cf6265645 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -28,8 +28,8 @@ feature 'GFM autocomplete', js: true do it 'opens autocomplete menu when field starts with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys('@') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys('@') end expect(page).to have_selector('.atwho-container') @@ -37,8 +37,8 @@ feature 'GFM autocomplete', js: true do it 'doesnt open autocomplete menu character is prefixed with text' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('testing') - find('#note_note').native.send_keys('@') + find('#note-body').native.send_keys('testing') + find('#note-body').native.send_keys('@') end expect(page).not_to have_selector('.atwho-view') @@ -46,8 +46,8 @@ feature 'GFM autocomplete', js: true do it 'doesnt select the first item for non-assignee dropdowns' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys(':') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys(':') end expect(page).to have_selector('.atwho-container') @@ -58,7 +58,7 @@ feature 'GFM autocomplete', js: true do end it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do - note = find('#note_note') + note = find('#note-body') # Number. page.within '.timeline-content-form' do @@ -86,8 +86,8 @@ feature 'GFM autocomplete', js: true do it 'selects the first item for assignee dropdowns' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys('@') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys('@') end expect(page).to have_selector('.atwho-container') @@ -99,8 +99,8 @@ feature 'GFM autocomplete', js: true do it 'includes items for assignee dropdowns with non-ASCII characters in name' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys("@#{user.name[0...8]}") + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys("@#{user.name[0...8]}") end expect(page).to have_selector('.atwho-container') @@ -112,8 +112,8 @@ feature 'GFM autocomplete', js: true do it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys('') - find('#note_note').native.send_keys(':1') + find('#note-body').native.send_keys('') + find('#note-body').native.send_keys(':1') end expect(page).to have_selector('.atwho-container') @@ -125,7 +125,7 @@ feature 'GFM autocomplete', js: true do context 'if a selected value has special characters' do it 'wraps the result in double quotes' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("~#{label.title[0]}") @@ -138,7 +138,7 @@ feature 'GFM autocomplete', js: true do end it "shows dropdown after a new line" do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('test') note.native.send_keys(:enter) @@ -150,7 +150,7 @@ feature 'GFM autocomplete', js: true do end it "does not show dropdown when preceded with a special character" do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("@") @@ -168,7 +168,7 @@ feature 'GFM autocomplete', js: true do end it "does not throw an error if no labels exist" do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys('~') @@ -179,7 +179,7 @@ feature 'GFM autocomplete', js: true do end it 'doesn\'t wrap for assignee values' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("@#{user.username[0]}") @@ -192,7 +192,7 @@ feature 'GFM autocomplete', js: true do end it 'doesn\'t wrap for emoji values' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys(":cartwheel") @@ -206,7 +206,7 @@ feature 'GFM autocomplete', js: true do it 'doesn\'t open autocomplete after non-word character' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys("@#{user.username[0..2]}!") + find('#note-body').native.send_keys("@#{user.username[0..2]}!") end expect(page).not_to have_selector('.atwho-view') @@ -214,14 +214,14 @@ feature 'GFM autocomplete', js: true do it 'doesn\'t open autocomplete if there is no space before' do page.within '.timeline-content-form' do - find('#note_note').native.send_keys("hello:#{user.username[0..2]}") + find('#note-body').native.send_keys("hello:#{user.username[0..2]}") end expect(page).not_to have_selector('.atwho-view') end it 'triggers autocomplete after selecting a quick action' do - note = find('#note_note') + note = find('#note-body') page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys('/as') diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb index 8c23fcd483b..634ea111dc1 100644 --- a/spec/features/issues/markdown_toolbar_spec.rb +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -12,26 +12,26 @@ feature 'Issue markdown toolbar', js: true do end it "doesn't include first new line when adding bold" do - find('#note_note').native.send_keys('test') - find('#note_note').native.send_key(:enter) - find('#note_note').native.send_keys('bold') + find('#note-body').native.send_keys('test') + find('#note-body').native.send_key(:enter) + find('#note-body').native.send_keys('bold') - page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)') + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 9)') first('.toolbar-btn').click - expect(find('#note_note')[:value]).to eq("test\n**bold**\n") + expect(find('#note-body')[:value]).to eq("test\n**bold**\n") end it "doesn't include first new line when adding underline" do - find('#note_note').native.send_keys('test') - find('#note_note').native.send_key(:enter) - find('#note_note').native.send_keys('underline') + find('#note-body').native.send_keys('test') + find('#note-body').native.send_key(:enter) + find('#note-body').native.send_keys('underline') - page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)') + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 50)') find('.toolbar-btn:nth-child(2)').click - expect(find('#note_note')[:value]).to eq("test\n*underline*\n") + expect(find('#note-body')[:value]).to eq("test\n*underline*\n") end end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 62dbc3efb01..793572851da 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -13,7 +13,7 @@ feature 'Issue notes polling', :js do it 'displays the new comment' do note = create(:note, noteable: issue, project: project, note: 'Looks good!') - page.execute_script('notes.refresh();') + wait_for_requests expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end @@ -31,16 +31,6 @@ feature 'Issue notes polling', :js do visit project_issue_path(project, issue) end - it 'has .original-note-content to compare against' do - expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) - - update_note(existing_note, updated_text) - - expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) - end - it 'displays the updated content' do expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) @@ -49,24 +39,14 @@ feature 'Issue notes polling', :js do expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) end - it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do + it 'when editing but have not changed anything, and an update comes in, show warning and does not update the note' do click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) update_note(existing_note, updated_text) - expect(page).to have_field("note[note]", with: updated_text) - end - - it 'when editing but you changed some things, and an update comes in, show a warning' do - click_edit_action(existing_note) - - expect(page).to have_field("note[note]", with: note_text) - - find("#note_#{existing_note.id} .js-note-text").set('something random') - update_note(existing_note, updated_text) - + expect(page).not_to have_field("note[note]", with: updated_text) expect(page).to have_selector(".alert") end @@ -75,8 +55,6 @@ feature 'Issue notes polling', :js do expect(page).to have_field("note[note]", with: note_text) - find("#note_#{existing_note.id} .js-note-text").set('something random') - update_note(existing_note, updated_text) find("#note_#{existing_note.id} .note-edit-cancel").click @@ -97,14 +75,12 @@ feature 'Issue notes polling', :js do visit project_issue_path(project, issue) end - it 'has .original-note-content to compare against' do + it 'displays the updated content' do expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) update_note(existing_note, updated_text) expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) - expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) end end @@ -118,16 +94,15 @@ feature 'Issue notes polling', :js do visit project_issue_path(project, issue) end - it 'has .original-note-content to compare against' do + it 'shows the system note' do expect(page).to have_selector("#note_#{system_note.id}", text: note_text) - expect(page).to have_selector("#note_#{system_note.id} .original-note-content", count: 1, visible: false) end end end def update_note(note, new_text) note.update(note: new_text) - page.execute_script('notes.refresh();') + wait_for_requests end def click_edit_action(note) diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index a22d548eef3..96f6df587e1 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -11,10 +11,14 @@ feature 'Member autocomplete', :js do sign_in(user) end - shared_examples "open suggestions when typing @" do + shared_examples "open suggestions when typing @" do |resource_name| before do page.within('.new-note') do - find('#note_note').send_keys('@') + if resource_name == 'issue' + find('#note-body').send_keys('@') + else + find('#note_note').send_keys('@') + end end end @@ -32,7 +36,7 @@ feature 'Member autocomplete', :js do visit project_issue_path(project, noteable) end - include_examples "open suggestions when typing @" + include_examples "open suggestions when typing @", 'issue' end context 'adding a new note on a Merge Request' do @@ -45,7 +49,7 @@ feature 'Member autocomplete', :js do visit project_merge_request_path(project, noteable) end - include_examples "open suggestions when typing @" + include_examples "open suggestions when typing @", 'merge_request' end context 'adding a new note on a Commit' do @@ -60,6 +64,6 @@ feature 'Member autocomplete', :js do visit project_commit_path(project, noteable) end - include_examples "open suggestions when typing @" + include_examples "open suggestions when typing @", 'commit' end end diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb index 3bf25221e36..9b6864eb90f 100644 --- a/spec/features/reportable_note/commit_spec.rb +++ b/spec/features/reportable_note/commit_spec.rb @@ -18,7 +18,7 @@ describe 'Reportable note on commit', :js do visit project_commit_path(project, sample_commit.id) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'commit' end context 'a diff note' do @@ -28,6 +28,6 @@ describe 'Reportable note on commit', :js do visit project_commit_path(project, sample_commit.id) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'commit' end end diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb index 21e96f6f103..f5a1950e48e 100644 --- a/spec/features/reportable_note/issue_spec.rb +++ b/spec/features/reportable_note/issue_spec.rb @@ -13,5 +13,5 @@ describe 'Reportable note on issue', :js do visit project_issue_path(project, issue) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'issue' end diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb index bb296546e06..1f69257f7ed 100644 --- a/spec/features/reportable_note/merge_request_spec.rb +++ b/spec/features/reportable_note/merge_request_spec.rb @@ -15,12 +15,12 @@ describe 'Reportable note on merge request', :js do context 'a normal note' do let!(:note) { create(:note_on_merge_request, noteable: merge_request, project: project) } - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'merge_request' end context 'a diff note' do let!(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'merge_request' end end diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb index f1e48ed46be..98ef50b78de 100644 --- a/spec/features/reportable_note/snippets_spec.rb +++ b/spec/features/reportable_note/snippets_spec.rb @@ -17,6 +17,6 @@ describe 'Reportable note on snippets', :js do visit project_snippet_path(project, snippet) end - it_behaves_like 'reportable note' + it_behaves_like 'reportable note', 'snippet' end end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 580258f77eb..ff6f71d7528 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -181,7 +181,7 @@ feature 'Task Lists' do project: project, author: user) end - it 'renders for note body' do + it 'renders for note body', :js do visit_issue(project, issue) expect(page).to have_selector('.note ul.task-list', count: 1) @@ -189,15 +189,14 @@ feature 'Task Lists' do expect(page).to have_selector('.note ul input[checked]', count: 2) end - it 'contains the required selectors' do + it 'contains the required selectors', :js do visit_issue(project, issue) expect(page).to have_selector('.note .js-task-list-container') expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') - expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') end - it 'is only editable by author' do + it 'is only editable by author', :js do visit_issue(project, issue) expect(page).to have_selector('.js-task-list-container') @@ -215,7 +214,7 @@ feature 'Task Lists' do project: project, author: user) end - it 'renders for note body' do + it 'renders for note body', :js do visit_issue(project, issue) expect(page).to have_selector('.note ul.task-list', count: 1) @@ -230,7 +229,7 @@ feature 'Task Lists' do project: project, author: user) end - it 'renders for note body' do + it 'renders for note body', :js do visit_issue(project, issue) expect(page).to have_selector('.note ul.task-list', count: 1) diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb index 53cad623a35..e1c95590af1 100644 --- a/spec/features/uploads/user_uploads_file_to_note_spec.rb +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -10,6 +10,7 @@ feature 'User uploads file to note' do before do sign_in(user) visit project_issue_path(project, issue) + wait_for_requests end context 'before uploading' do diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 2f12b671dec..1030f323a1f 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -18,6 +18,8 @@ "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["integer", "null"] }, "human_total_time_spent": { "type": ["integer", "null"] }, + "milestone": { "type": ["object", "null"] }, + "labels": { "type": ["array", "null"] }, "in_progress_merge_commit_sha": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] }, "merge_commit_sha": { "type": ["string", "null"] }, diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 8e056882108..a22b71fd1dc 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -25,9 +25,10 @@ import '~/lib/utils/common_utils'; }; describe('AwardsHandler', function() { - preloadFixtures('issues/issue_with_comment.html.raw'); + preloadFixtures('merge_requests/diff_comment.html.raw'); beforeEach(function(done) { - loadFixtures('issues/issue_with_comment.html.raw'); + loadFixtures('merge_requests/diff_comment.html.raw'); + $('body').data('page', 'projects:merge_requests:show'); loadAwardsHandler(true).then((obj) => { awardsHandler = obj; spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); @@ -139,7 +140,7 @@ import '~/lib/utils/common_utils'; }); describe('::getAwardUrl', function() { return it('returns the url for request', function() { - return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); + return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji'); }); }); describe('::addAward and ::checkMutuality', function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 6dc48f9a293..f62bf43adb9 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,119 +1,111 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */ - import '~/behaviors/quick_submit'; -(function() { - describe('Quick Submit behavior', function() { - var keydownEvent; - preloadFixtures('issues/open-issue.html.raw'); - beforeEach(function() { - loadFixtures('issues/open-issue.html.raw'); - $('form').submit(function(e) { - // Prevent a form submit from moving us off the testing page - return e.preventDefault(); - }); - this.spies = { - submit: spyOnEvent('form', 'submit') - }; +describe('Quick Submit behavior', () => { + const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options); - this.textarea = $('.js-quick-submit textarea').first(); - }); - it('does not respond to other keyCodes', function() { - this.textarea.trigger(keydownEvent({ - keyCode: 32 - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - it('does not respond to Enter alone', function() { - this.textarea.trigger(keydownEvent({ - ctrlKey: false, - metaKey: false - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - it('does not respond to repeated events', function() { - this.textarea.trigger(keydownEvent({ - repeat: true - })); - return expect(this.spies.submit).not.toHaveBeenTriggered(); - }); - it('disables input of type submit', function() { - const submitButton = $('.js-quick-submit input[type=submit]'); - this.textarea.trigger(keydownEvent()); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - expect(submitButton).toBeDisabled(); + beforeEach(() => { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + $('body').attr('data-page', 'projects:merge_requests:show'); + $('form').submit((e) => { + // Prevent a form submit from moving us off the testing page + e.preventDefault(); }); - it('disables button of type submit', function() { - const submitButton = $('.js-quick-submit input[type=submit]'); - this.textarea.trigger(keydownEvent()); + this.spies = { + submit: spyOnEvent('form', 'submit'), + }; - expect(submitButton).toBeDisabled(); - }); - it('only clicks one submit', function() { - const existingSubmit = $('.js-quick-submit input[type=submit]'); - // Add an extra submit button - const newSubmit = $('<button type="submit">Submit it</button>'); - newSubmit.insertAfter(this.textarea); + this.textarea = $('.js-quick-submit textarea').first(); + }); - const oldClick = spyOnEvent(existingSubmit, 'click'); - const newClick = spyOnEvent(newSubmit, 'click'); + it('does not respond to other keyCodes', () => { + this.textarea.trigger(keydownEvent({ + keyCode: 32, + })); + expect(this.spies.submit).not.toHaveBeenTriggered(); + }); - this.textarea.trigger(keydownEvent()); + it('does not respond to Enter alone', () => { + this.textarea.trigger(keydownEvent({ + ctrlKey: false, + metaKey: false, + })); + expect(this.spies.submit).not.toHaveBeenTriggered(); + }); - expect(oldClick).not.toHaveBeenTriggered(); - expect(newClick).toHaveBeenTriggered(); - }); - // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll - // only run the tests that apply to the current platform - if (navigator.userAgent.match(/Macintosh/)) { - it('responds to Meta+Enter', function() { - this.textarea.trigger(keydownEvent()); - return expect(this.spies.submit).toHaveBeenTriggered(); - }); - it('excludes other modifier keys', function() { - 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(); - }); - } else { - it('responds to Ctrl+Enter', function() { + it('does not respond to repeated events', () => { + this.textarea.trigger(keydownEvent({ + repeat: true, + })); + expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + + it('disables input of type submit', () => { + const submitButton = $('.js-quick-submit input[type=submit]'); + this.textarea.trigger(keydownEvent()); + + expect(submitButton).toBeDisabled(); + }); + it('disables button of type submit', () => { + const submitButton = $('.js-quick-submit input[type=submit]'); + this.textarea.trigger(keydownEvent()); + + expect(submitButton).toBeDisabled(); + }); + it('only clicks one submit', () => { + const existingSubmit = $('.js-quick-submit input[type=submit]'); + // Add an extra submit button + const newSubmit = $('<button type="submit">Submit it</button>'); + newSubmit.insertAfter(this.textarea); + + const oldClick = spyOnEvent(existingSubmit, 'click'); + const newClick = spyOnEvent(newSubmit, 'click'); + + this.textarea.trigger(keydownEvent()); + + expect(oldClick).not.toHaveBeenTriggered(); + expect(newClick).toHaveBeenTriggered(); + }); + // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll + // only run the tests that apply to the current platform + if (navigator.userAgent.match(/Macintosh/)) { + describe('In Macintosh', () => { + it('responds to Meta+Enter', () => { this.textarea.trigger(keydownEvent()); return expect(this.spies.submit).toHaveBeenTriggered(); }); - it('excludes other modifier keys', function() { + + it('excludes other modifier keys', () => { this.textarea.trigger(keydownEvent({ - altKey: true + altKey: true, })); this.textarea.trigger(keydownEvent({ - metaKey: true + ctrlKey: true, })); this.textarea.trigger(keydownEvent({ - shiftKey: true + shiftKey: true, })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); - } - return keydownEvent = function(options) { - var defaults; - if (navigator.userAgent.match(/Macintosh/)) { - defaults = { - keyCode: 13, - metaKey: true - }; - } else { - defaults = { - keyCode: 13, - ctrlKey: true - }; - } - return $.Event('keydown', $.extend({}, defaults, options)); - }; - }); -}).call(window); + }); + } else { + it('responds to Ctrl+Enter', () => { + this.textarea.trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + + it('excludes other modifier keys', () => { + 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/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index f97a5d2b5de..41700458aae 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,6 +55,11 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end + it 'merge_requests/merge_request_with_comment.html.raw' do |example| + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item') + render_merge_request(example.description, merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 81ce18bf2fb..3af26e2f28f 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -41,9 +41,9 @@ describe('Issuable output', () => { initialTitleText: '', initialDescriptionHtml: '', initialDescriptionText: '', - markdownPreviewUrl: '/', - markdownDocs: '/', - projectsAutocompleteUrl: '/', + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectsAutocompletePath: '/', isConfidential: false, projectNamespace: '/', projectPath: '/', diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js index df8189d9290..299f88e7778 100644 --- a/spec/javascripts/issue_show/components/fields/description_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_spec.js @@ -25,8 +25,8 @@ describe('Description field component', () => { vm = new Component({ el, propsData: { - markdownPreviewUrl: '/', - markdownDocs: '/', + markdownPreviewPath: '/', + markdownDocsPath: '/', formState: store.formState, }, }).$mount(); diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js index 86d35c33ff4..8b6ed6a03a9 100644 --- a/spec/javascripts/issue_show/components/fields/project_move_spec.js +++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js @@ -15,7 +15,7 @@ describe('Project move field component', () => { vm = new Component({ propsData: { formState, - projectsAutocompleteUrl: '/autocomplete', + projectsAutocompletePath: '/autocomplete', }, }).$mount(); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 9a85223208c..d8af5287431 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -18,9 +18,9 @@ describe('Inline edit form component', () => { description: 'a', lockedWarningVisible: false, }, - markdownPreviewUrl: '/', - markdownDocs: '/', - projectsAutocompleteUrl: '/', + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectsAutocompletePath: '/', projectPath: '/', projectNamespace: '/', }, diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js new file mode 100644 index 00000000000..cca5ec887a3 --- /dev/null +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -0,0 +1,134 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueCommentForm from '~/notes/components/issue_comment_form.vue'; +import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data'; +import { keyboardDownEvent } from '../../issue_show/helpers'; + +describe('issue_comment_form component', () => { + let vm; + const Component = Vue.extend(issueCommentForm); + let mountComponent; + + beforeEach(() => { + mountComponent = () => new Component({ + store, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user is logged in', () => { + beforeEach(() => { + store.dispatch('setUserData', userDataMock); + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = mountComponent(); + }); + + it('should render user avatar with link', () => { + expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + }); + + describe('textarea', () => { + it('should render textarea with placeholder', () => { + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + + it('should support quick actions', () => { + expect( + 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'); + }); + + it('should link to quick actions docs', () => { + const { quickActionsDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + }); + + describe('edit mode', () => { + 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)); + + expect(vm.editCurrentUserLastNote).toHaveBeenCalled(); + }); + }); + + describe('event enter', () => { + it('should save note when cmd/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, true)); + + expect(vm.handleSave).toHaveBeenCalled(); + }); + }); + }); + + describe('actions', () => { + it('should be possible to close the 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'); + }); + + 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); + 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('.js-note-discard')).toBeDefined(); + done(); + }); + }); + }); + + describe('issue is confidential', () => { + it('shows information warning', (done) => { + store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true })); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); + done(); + }); + }); + }); + }); + + describe('user is not logged in', () => { + beforeEach(() => { + store.dispatch('setUserData', null); + store.dispatch('setIssueData', loggedOutIssueData); + store.dispatch('setNotesData', notesDataMock); + + vm = mountComponent(); + }); + + it('should render signed out widget', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + }); + + it('should not render submission form', () => { + expect(vm.$el.querySelector('textarea')).toEqual(null); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js new file mode 100644 index 00000000000..05c6b57f93e --- /dev/null +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueDiscussion from '~/notes/components/issue_discussion.vue'; +import { issueDataMock, discussionMock, notesDataMock } from '../mock_data'; + +describe('issue_discussion component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueDiscussion); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note: discussionMock, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render user avatar', () => { + expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined(); + }); + + it('should render discussion header', () => { + expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); + expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length); + }); + + describe('actions', () => { + it('should render reply button', () => { + expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...'); + }); + + it('should toggle reply form', (done) => { + vm.$el.querySelector('.js-vue-discussion-reply').click(); + Vue.nextTick(() => { + expect(vm.$refs.noteForm).toBeDefined(); + expect(vm.isReplying).toEqual(true); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/issue_note_actions_spec.js new file mode 100644 index 00000000000..7bcc061f167 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_actions_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueActions from '~/notes/components/issue_note_actions.vue'; +import { userDataMock } from '../mock_data'; + +describe('issse_note_actions component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueActions); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user is logged in', () => { + let props; + + beforeEach(() => { + props = { + accessLevel: 'Master', + authorId: 26, + canDelete: true, + canEdit: 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', + }; + + store.dispatch('setUserData', userDataMock); + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + it('should render access level badge', () => { + expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel); + }); + + it('should render emoji link', () => { + expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + }); + + describe('actions dropdown', () => { + it('should be possible to edit the comment', () => { + expect(vm.$el.querySelector('.js-note-edit')).toBeDefined(); + }); + + it('should be possible to report as abuse', () => { + expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); + }); + + it('should be possible to delete comment', () => { + expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); + }); + }); + }); + + describe('user is not logged in', () => { + let props; + + beforeEach(() => { + store.dispatch('setUserData', {}); + props = { + accessLevel: 'Master', + authorId: 26, + canDelete: false, + canEdit: 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', + }; + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + it('should not render emoji link', () => { + expect(vm.$el.querySelector('.js-add-award')).toEqual(null); + }); + + it('should not render actions dropdown', () => { + expect(vm.$el.querySelector('.more-actions')).toEqual(null); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js new file mode 100644 index 00000000000..22e91c4c40f --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -0,0 +1,255 @@ +import Vue from 'vue'; +import issueNotesApp from '~/notes/components/issue_notes_app.vue'; +import service from '~/notes/services/issue_notes_service'; +import * as mockData from '../mock_data'; + +describe('issue_note_app', () => { + let mountComponent; + let vm; + + const individualNoteInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), { + status: 200, + })); + }; + + const discussionNoteInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + const IssueNotesApp = Vue.extend(issueNotesApp); + + mountComponent = (data) => { + const props = data || { + issueData: mockData.issueDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, + }; + + return new IssueNotesApp({ + propsData: props, + }).$mount(); + }; + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('set data', () => { + const responseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(responseInterceptor); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + }); + + it('should set notes data', () => { + expect(vm.$store.state.notesData).toEqual(mockData.notesDataMock); + }); + + it('should set issue data', () => { + expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock); + }); + + it('should set user data', () => { + expect(vm.$store.state.userData).toEqual(mockData.userDataMock); + }); + + it('should fetch notes', () => { + expect(vm.$store.state.notes).toEqual([]); + }); + }); + + describe('render', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + }); + + it('should render list of notes', (done) => { + const note = mockData.individualNoteServerResponse[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); + done(); + }, 0); + }); + + it('should render form', () => { + expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + + it('should render form comment button as disabled', () => { + expect( + vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'), + ).toEqual('disabled'); + }); + }); + + describe('while fetching data', () => { + beforeEach(() => { + vm = mountComponent(); + }); + + it('should render loading icon', () => { + expect(vm.$el.querySelector('.js-loading')).toBeDefined(); + }); + + it('should render form', () => { + expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + }); + + describe('update note', () => { + describe('individual note', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + }); + + it('renders edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + done(); + }); + }, 0); + }); + + it('calls the service to update the note', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + done(); + }); + }, 0); + }); + }); + + describe('dicussion note', () => { + beforeEach(() => { + Vue.http.interceptors.push(discussionNoteInterceptor); + spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor); + }); + + it('renders edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + done(); + }); + }, 0); + }); + + it('updates the note and resets the edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + done(); + }); + }, 0); + }); + }); + }); + + describe('new note form', () => { + beforeEach(() => { + vm = mountComponent(); + }); + + it('should render markdown docs url', () => { + const { markdownDocsPath } = mockData.notesDataMock; + 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'); + }); + }); + + describe('edit form', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + }); + + it('should render markdown docs url', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + const { markdownDocsPath } = mockData.notesDataMock; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector(`.edit-note a[href="${markdownDocsPath}"]`).textContent.trim(), + ).toEqual('Markdown is supported'); + done(); + }); + }, 0); + }); + + 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); + done(); + }); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_attachment_spec.js b/spec/javascripts/notes/components/issue_note_attachment_spec.js new file mode 100644 index 00000000000..8f33b874ad6 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_attachment_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import issueNoteAttachment from '~/notes/components/issue_note_attachment.vue'; + +describe('issue note attachment', () => { + it('should render properly', () => { + const props = { + attachment: { + filename: 'dk.png', + image: true, + url: '/dk.png', + }, + }; + + const Component = Vue.extend(issueNoteAttachment); + const vm = new Component({ + propsData: props, + }).$mount(); + + expect(vm.$el.classList.contains('note-attachment')).toBeTruthy(); + expect(vm.$el.querySelector('img').src).toContain(props.attachment.url); + expect(vm.$el.querySelector('a').href).toContain(props.attachment.url); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/issue_note_awards_list_spec.js new file mode 100644 index 00000000000..3b6c34f1494 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_awards_list_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import awardsNote from '~/notes/components/issue_note_awards_list.vue'; +import { issueDataMock, notesDataMock } from '../mock_data'; + +describe('issue_note_awards_list component', () => { + let vm; + let awardsMock; + + beforeEach(() => { + const Component = Vue.extend(awardsNote); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + awardsMock = [ + { + name: 'flag_tz', + user: { id: 1, name: 'Administrator', username: 'root' }, + }, + { + name: 'cartwheel_tone3', + user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, + }, + ]; + + vm = new Component({ + store, + propsData: { + awards: awardsMock, + noteAuthorId: 2, + noteId: 545, + toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + 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(); + }); + + it('should be possible to remove awareded emoji', () => { + spyOn(vm, 'handleAward').and.callThrough(); + vm.$el.querySelector('.js-awards-block button').click(); + + expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); + }); + + it('should be possible to add new emoji', () => { + expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js new file mode 100644 index 00000000000..81f07ed47cc --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_body_spec.js @@ -0,0 +1,46 @@ + +import Vue from 'vue'; +import store from '~/notes/stores'; +import noteBody from '~/notes/components/issue_note_body.vue'; +import { issueDataMock, notesDataMock, note } from '../mock_data'; + +describe('issue_note_body component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(noteBody); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note, + canEdit: true, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render the note', () => { + expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); + }); + + it('should be render form if user is editing', (done) => { + vm.isEditing = true; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('textarea.js-task-list-field')).toBeDefined(); + done(); + }); + }); + + it('should render awards list', () => { + expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).toBeDefined(); + expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).toBeDefined(); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/issue_note_edited_text_spec.js new file mode 100644 index 00000000000..6603241eb64 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_edited_text_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import issueNoteEditedText from '~/notes/components/issue_note_edited_text.vue'; + +describe('issue_note_edited_text', () => { + let vm; + let props; + + beforeEach(() => { + const Component = Vue.extend(issueNoteEditedText); + props = { + actionText: 'Edited', + className: 'foo-bar', + editedAt: '2017-08-04T09:52:31.062Z', + editedBy: { + avatar_url: 'path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + }; + + vm = new Component({ + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render block with provided className', () => { + expect(vm.$el.className).toEqual(props.className); + }); + + it('should render provided actionText', () => { + expect(vm.$el.textContent).toContain(props.actionText); + }); + + it('should render provided user information', () => { + const authorLink = vm.$el.querySelector('.js-vue-author'); + + expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path); + expect(authorLink.textContent.trim()).toEqual(props.editedBy.name); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js new file mode 100644 index 00000000000..a90dbcb72b5 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueNoteForm from '~/notes/components/issue_note_form.vue'; +import { issueDataMock, notesDataMock } from '../mock_data'; +import { keyboardDownEvent } from '../../issue_show/helpers'; + +describe('issue_note_form component', () => { + let vm; + let props; + + beforeEach(() => { + const Component = Vue.extend(issueNoteForm); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + props = { + isEditing: false, + noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', + noteId: 545, + }; + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('conflicts editing', () => { + 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.'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual(message); + done(); + }); + }); + }); + + 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...'); + }); + + it('should link to markdown docs', () => { + const { markdownDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + }); + + describe('keyboard events', () => { + describe('up', () => { + it('should ender edit mode', () => { + spyOn(vm, 'editMyLastNote').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(38, true)); + + expect(vm.editMyLastNote).toHaveBeenCalled(); + }); + }); + + describe('enter', () => { + it('should submit note', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); + + expect(vm.handleUpdate).toHaveBeenCalled(); + }); + }); + }); + + describe('actions', () => { + it('should be possible to cancel', (done) => { + spyOn(vm, 'cancelHandler').and.callThrough(); + vm.isEditing = true; + + Vue.nextTick(() => { + vm.$el.querySelector('.note-edit-cancel').click(); + + Vue.nextTick(() => { + expect(vm.cancelHandler).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should be possible to update the note', (done) => { + vm.isEditing = true; + + Vue.nextTick(() => { + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + Vue.nextTick(() => { + expect(vm.isSubmitting).toEqual(true); + done(); + }); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/issue_note_header_spec.js new file mode 100644 index 00000000000..83ea18508ae --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_header_spec.js @@ -0,0 +1,94 @@ +import Vue from 'vue'; +import issueNoteHeader from '~/notes/components/issue_note_header.vue'; +import store from '~/notes/stores'; + +describe('issue_note_header component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueNoteHeader); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('individual note', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'commented', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: false, + noteId: 1394, + }, + }).$mount(); + }); + + 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'); + }); + + it('should render timestamp link', () => { + expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); + }); + }); + + describe('discussion', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'started a discussion', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: true, + noteId: 1395, + }, + }).$mount(); + }); + + it('should render toggle button', () => { + expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); + }); + + it('should toggle the disucssion icon', (done) => { + expect( + vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-up'), + ).toEqual(true); + + vm.$el.querySelector('.js-vue-toggle-button').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-down'), + ).toEqual(true); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js new file mode 100644 index 00000000000..f20d9ce9268 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import issueNoteSignedOut from '~/notes/components/issue_note_signed_out_widget.vue'; +import store from '~/notes/stores'; +import { notesDataMock } from '../mock_data'; + +describe('issue_note_signed_out_widget component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueNoteSignedOut); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render sign in link provided in the store', () => { + 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'); + }); + + it('should render information text', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + }); +}); diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js new file mode 100644 index 00000000000..7ef85d5b4f0 --- /dev/null +++ b/spec/javascripts/notes/components/issue_note_spec.js @@ -0,0 +1,44 @@ + +import Vue from 'vue'; +import store from '~/notes/stores'; +import issueNote from '~/notes/components/issue_note.vue'; +import { issueDataMock, notesDataMock, note } from '../mock_data'; + +describe('issue_note', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueNote); + + store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render user information', () => { + 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'); + }); + + it('should render note actions', () => { + expect(vm.$el.querySelector('.note-actions')).toBeDefined(); + }); + + it('should render issue body', () => { + expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); + }); +}); diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js new file mode 100644 index 00000000000..6e5275087f3 --- /dev/null +++ b/spec/javascripts/notes/components/issue_placeholder_note_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue'; +import store from '~/notes/stores'; +import { userDataMock } from '../mock_data'; + +describe('issue placeholder system note component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issuePlaceholderNote); + store.dispatch('setUserData', userDataMock); + vm = new Component({ + store, + propsData: { note: { body: 'Foo' } }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + 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); + }); + }); + + 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}`); + }); + + it('should render note body', () => { + expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); + }); + }); +}); diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js new file mode 100644 index 00000000000..d508a49f710 --- /dev/null +++ b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue'; + +describe('issue placeholder system note component', () => { + let mountComponent; + beforeEach(() => { + const PlaceholderSystemNote = Vue.extend(placeholderSystemNote); + + mountComponent = props => new PlaceholderSystemNote({ + propsData: { + note: { + body: props, + }, + }, + }).$mount(); + }); + + it('should render system note placeholder with plain text', () => { + const vm = mountComponent('This is a placeholder'); + + expect(vm.$el.tagName).toEqual('LI'); + expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); + }); +}); diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/notes/components/issue_system_note_spec.js new file mode 100644 index 00000000000..c317ce32716 --- /dev/null +++ b/spec/javascripts/notes/components/issue_system_note_spec.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; +import issueSystemNote from '~/notes/components/issue_system_note.vue'; +import store from '~/notes/stores'; + +describe('issue system note', () => { + let vm; + let props; + + beforeEach(() => { + props = { + note: { + id: 1424, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'path', + path: '/root', + }, + note_html: '<p dir="auto">closed</p>', + system_note_icon_name: 'icon_status_closed', + created_at: '2017-08-02T10:51:58.559Z', + }, + }; + + store.dispatch('setTargetNoteHash', `note_${props.note.id}`); + + const Component = Vue.extend(issueSystemNote); + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + it('should render a list item with correct id', () => { + expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); + }); + + it('should render target class is note is target note', () => { + expect(vm.$el.classList).toContain('target'); + }); + + it('should render svg icon', () => { + 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); + }); +}); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js new file mode 100644 index 00000000000..89ba3a002b7 --- /dev/null +++ b/spec/javascripts/notes/mock_data.js @@ -0,0 +1,449 @@ +/* eslint-disable */ +export const notesDataMock = { + discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', + lastFetchedAt: '1501862675', + markdownDocsPath: '/help/user/markdown', + newSessionPath: '/users/sign_in?redirect_to_referer=yes', + notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes', + quickActionsDocsPath: '/help/user/project/quick_actions', + registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane', +}; + +export const userDataMock = { + avatar_url: 'mock_path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', +}; + +export const issueDataMock = { + assignees: [], + author_id: 1, + branch_name: null, + confidential: false, + create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', + created_at: '2017-02-07T10:11:18.395Z', + current_user: { + can_create_note: true, + can_update: true, + }, + deleted_at: null, + description: '', + due_date: null, + human_time_estimate: null, + human_total_time_spent: null, + id: 98, + iid: 26, + labels: [], + lock_version: null, + 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', + project_id: 2, + state: 'opened', + time_estimate: 0, + title: '14', + total_time_spent: 0, + updated_at: '2017-08-04T09:53:01.226Z', + updated_by_id: 1, + web_url: '/gitlab-org/gitlab-ce/issues/26', +}; + +export const lastFetchedAt = '1501862675'; + +export const individualNote = { + expanded: true, + id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + individual_note: true, + notes: [{ + id: 1390, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'test', + path: '/root', + }, + created_at: '2017-08-01T17: 09: 33.762Z', + updated_at: '2017-08-01T17: 09: 33.762Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'sdfdsaf', + note_html: '<p dir=\'auto\'>sdfdsaf</p>', + current_user: { can_edit: true }, + discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + emoji_awardable: true, + award_emoji: [ + { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } }, + { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, + ], + 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&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1390', + }], + reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', +}; + +export const note = { + "id": 546, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "path": "/root" + }, + "created_at": "2017-08-10T15:24:03.087Z", + "updated_at": "2017-08-10T15:24:03.087Z", + "system": false, + "noteable_id": 67, + "noteable_type": "Issue", + "noteable_iid": 7, + "type": null, + "human_access": "Owner", + "note": "Vel id placeat reprehenderit sit numquam.", + "note_html": "<p dir=\"auto\">Vel id placeat reprehenderit sit numquam.</p>", + "current_user": { + "can_edit": true + }, + "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Administrator", + "username": "root" + } + }, { + "name": "bath_tone3", + "user": { + "id": 1, + "name": "Administrator", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji", + "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" + } + +export const discussionMock = { + id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + expanded: true, + notes: [{ + id: 1395, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:51:58.559Z', + updated_at: '2017-08-02T10:51:58.559Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'THIS IS A DICUSSSION!', + note_html: '<p dir=\'auto\'>THIS IS A DICUSSSION!</p>', + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + 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', + path: '/gitlab-org/gitlab-ce/notes/1395', + }, { + id: 1396, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:56:50.980Z', + updated_at: '2017-08-03T14:19:35.691Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'sadfasdsdgdsf', + note_html: '<p dir=\'auto\'>sadfasdsdgdsf</p>', + last_edited_at: '2017-08-03T14:19:35.691Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', + 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', + }, { + id: 1437, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-03T18:11:18.780Z', + updated_at: '2017-08-04T09:52:31.062Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'adsfasf Should disappear', + note_html: '<p dir=\'auto\'>adsfasf Should disappear</p>', + last_edited_at: '2017-08-04T09:52:31.062Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + 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', + path: '/gitlab-org/gitlab-ce/notes/1437', + }], + individual_note: false, +}; + +export const loggedOutIssueData = { + "id": 98, + "iid": 26, + "author_id": 1, + "description": "", + "lock_version": 1, + "milestone_id": null, + "state": "opened", + "title": "asdsa", + "updated_by_id": 1, + "created_at": "2017-02-07T10:11:18.395Z", + "updated_at": "2017-08-08T10:22:51.564Z", + "deleted_at": null, + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null, + "milestone": null, + "labels": [], + "branch_name": null, + "confidential": false, + "assignees": [{ + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "web_url": "http://localhost:3000/root" + }], + "due_date": null, + "moved_to_id": null, + "project_id": 2, + "web_url": "/gitlab-org/gitlab-ce/issues/26", + "current_user": { + "can_create_note": false, + "can_update": false + }, + "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" +} + +export const individualNoteServerResponse = [{ + "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "expanded": true, + "notes": [{ + "id": 1390, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-01T17:09:33.762Z", + "updated_at": "2017-08-01T17:09:33.762Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "sdfdsaf", + "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }, { + "name": "art", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }], + "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", + "path": "/gitlab-org/gitlab-ce/notes/1390" + }], + "individual_note": true + }, { + "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "expanded": true, + "notes": [{ + "id": 1391, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-02T10:51:38.685Z", + "updated_at": "2017-08-02T10:51:38.685Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "New note!", + "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "emoji_awardable": true, + "award_emoji": [], + "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", + "path": "/gitlab-org/gitlab-ce/notes/1391" + }], + "individual_note": true +}]; + +export const discussionNoteServerResponse = [{ + "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "expanded": true, + "notes": [{ + "id": 1471, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-08-08T16:53:00.666Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "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", + "path": "/gitlab-org/gitlab-ce/notes/1471" + }], + "individual_note": false +}]; diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js new file mode 100644 index 00000000000..72d362acb2f --- /dev/null +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -0,0 +1,62 @@ + +import * as actions from '~/notes/stores/actions'; +import testAction from './helpers'; +import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; + +describe('Actions Notes Store', () => { + describe('setNotesData', () => { + it('should set received notes data', (done) => { + testAction(actions.setNotesData, null, { notesData: {} }, [ + { type: 'SET_NOTES_DATA', payload: notesDataMock }, + ], done); + }); + }); + + describe('setIssueData', () => { + it('should set received issue data', (done) => { + testAction(actions.setIssueData, null, { issueData: {} }, [ + { type: 'SET_ISSUE_DATA', payload: issueDataMock }, + ], done); + }); + }); + + describe('setUserData', () => { + it('should set received user data', (done) => { + testAction(actions.setUserData, null, { userData: {} }, [ + { type: 'SET_USER_DATA', payload: userDataMock }, + ], done); + }); + }); + + describe('setLastFetchedAt', () => { + it('should set received timestamp', (done) => { + testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [ + { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }, + ], done); + }); + }); + + describe('setInitialNotes', () => { + it('should set initial notes', (done) => { + testAction(actions.setInitialNotes, null, { notes: [] }, [ + { type: 'SET_INITIAL_NOTES', payload: [individualNote] }, + ], done); + }); + }); + + describe('setTargetNoteHash', () => { + it('should set target note hash', (done) => { + testAction(actions.setTargetNoteHash, null, { notes: [] }, [ + { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }, + ], done); + }); + }); + + describe('toggleDiscussion', () => { + it('should toggle discussion', (done) => { + testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [ + { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }, + ], done); + }); + }); +}); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js new file mode 100644 index 00000000000..48ee1bf9a52 --- /dev/null +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -0,0 +1,58 @@ +import * as getters from '~/notes/stores/getters'; +import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; + +describe('Getters Notes Store', () => { + let state; + beforeEach(() => { + state = { + notes: [individualNote], + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + + notesData: notesDataMock, + userData: userDataMock, + issueData: issueDataMock, + }; + }); + describe('notes', () => { + it('should return all notes in the store', () => { + expect(getters.notes(state)).toEqual([individualNote]); + }); + }); + + describe('targetNoteHash', () => { + it('should return `targetNoteHash`', () => { + expect(getters.targetNoteHash(state)).toEqual('hash'); + }); + }); + + describe('getNotesData', () => { + it('should return all data in `notesData`', () => { + expect(getters.getNotesData(state)).toEqual(notesDataMock); + }); + }); + + describe('getIssueData', () => { + it('should return all data in `issueData`', () => { + expect(getters.getIssueData(state)).toEqual(issueDataMock); + }); + }); + + describe('getUserData', () => { + it('should return all data in `userData`', () => { + expect(getters.getUserData(state)).toEqual(userDataMock); + }); + }); + + describe('notesById', () => { + it('should return the note for the given id', () => { + expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); + }); + }); + + describe('getCurrentUserLastNote', () => { + it('should return the last note of the current user', () => { + expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); + }); + }); +}); diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/notes/stores/helpers.js new file mode 100644 index 00000000000..2d386fe1da5 --- /dev/null +++ b/spec/javascripts/notes/stores/helpers.js @@ -0,0 +1,37 @@ +/* eslint-disable */ + +/** + * helper for testing action with expected mutations + * https://vuex.vuejs.org/en/testing.html + */ +export default (action, payload, state, expectedMutations, done) => { + let count = 0; + + // mock commit + const commit = (type, payload) => { + const mutation = expectedMutations[count]; + + try { + expect(mutation.type).to.equal(type); + if (payload) { + expect(mutation.payload).to.deep.equal(payload); + } + } catch (error) { + done(error); + } + + count++; + if (count >= expectedMutations.length) { + done(); + } + }; + + // call the action with mocked store and arguments + action({ commit, state }, payload); + + // check if no mutations should have been dispatched + if (expectedMutations.length === 0) { + expect(count).to.equal(0); + done(); + } +}; diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js new file mode 100644 index 00000000000..a38f29c1e39 --- /dev/null +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -0,0 +1,207 @@ +import mutations from '~/notes/stores/mutations'; +import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; + +describe('Mutation Notes Store', () => { + describe('ADD_NEW_NOTE', () => { + it('should add a new note to an array of notes', () => { + const state = { notes: [] }; + mutations.ADD_NEW_NOTE(state, note); + + expect(state).toEqual({ + notes: [{ + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }], + }); + }); + }); + + describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { + it('should add a reply to a specific discussion', () => { + const state = { notes: [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); + }); + }); + + describe('DELETE_NOTE', () => { + it('should delete a note ', () => { + const state = { notes: [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); + }); + }); + + 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] }; + mutations.REMOVE_PLACEHOLDER_NOTES(state); + + expect(state.notes).toEqual([]); + }); + }); + + describe('SET_NOTES_DATA', () => { + it('should set an object with notesData', () => { + const state = { + notesData: {}, + }; + + mutations.SET_NOTES_DATA(state, notesDataMock); + expect(state.notesData).toEqual(notesDataMock); + }); + }); + + describe('SET_ISSUE_DATA', () => { + it('should set the issue data', () => { + const state = { + issueData: {}, + }; + + mutations.SET_ISSUE_DATA(state, issueDataMock); + expect(state.issueData).toEqual(issueDataMock); + }); + }); + + describe('SET_USER_DATA', () => { + it('should set the user data', () => { + const state = { + userData: {}, + }; + + mutations.SET_USER_DATA(state, userDataMock); + expect(state.userData).toEqual(userDataMock); + }); + }); + + describe('SET_INITIAL_NOTES', () => { + it('should set the initial notes received', () => { + const state = { + notes: [], + }; + + mutations.SET_INITIAL_NOTES(state, [note]); + expect(state.notes).toEqual([note]); + }); + }); + + describe('SET_LAST_FETCHED_AT', () => { + it('should set timestamp', () => { + const state = { + lastFetchedAt: [], + }; + + mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); + expect(state.lastFetchedAt).toEqual('timestamp'); + }); + }); + + describe('SET_TARGET_NOTE_HASH', () => { + it('should set the note hash', () => { + const state = { + targetNoteHash: [], + }; + + mutations.SET_TARGET_NOTE_HASH(state, 'hash'); + expect(state.targetNoteHash).toEqual('hash'); + }); + }); + + describe('SHOW_PLACEHOLDER_NOTE', () => { + it('should set a placeholder note', () => { + const state = { + notes: [], + }; + mutations.SHOW_PLACEHOLDER_NOTE(state, note); + expect(state.notes[0].isPlaceholderNote).toEqual(true); + }); + }); + + describe('TOGGLE_AWARD', () => { + it('should add award if user has not reacted yet', () => { + const state = { + notes: [note], + userData: userDataMock, + }; + + const data = { + note, + awardName: 'cartwheel', + }; + + mutations.TOGGLE_AWARD(state, data); + const lastIndex = state.notes[0].award_emoji.length - 1; + + expect(state.notes[0].award_emoji[lastIndex]).toEqual({ + name: 'cartwheel', + user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, + }); + }); + + it('should remove award if user already reacted', () => { + const state = { + notes: [note], + userData: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }; + + const data = { + note, + awardName: 'bath_tone3', + }; + mutations.TOGGLE_AWARD(state, data); + expect(state.notes[0].award_emoji.length).toEqual(2); + }); + }); + + describe('TOGGLE_DISCUSSION', () => { + it('should open a closed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + notes: [discussion], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.notes[0].expanded).toEqual(true); + }); + + it('should close a opened discussion', () => { + const state = { + notes: [discussionMock], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); + + expect(state.notes[0].expanded).toEqual(false); + }); + }); + + describe('UPDATE_NOTE', () => { + it('should update a note', () => { + const state = { + notes: [individualNote], + }; + + const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); + + mutations.UPDATE_NOTE(state, updated); + + expect(state.notes[0].notes[0].note).toEqual('Foo'); + }); + }); +}); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2c096ed08a8..8c5ad8914b0 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -32,14 +32,14 @@ import '~/notes'; describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; - var commentsTemplate = 'issues/issue_with_comment.html.raw'; + var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw'; preloadFixtures(commentsTemplate); beforeEach(function () { loadFixtures(commentsTemplate); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; - $('body').data('page', 'projects:issues:show'); + $('body').data('page', 'projects:merge_requets:show'); }); describe('task lists', function() { @@ -53,17 +53,19 @@ import '~/notes'; it('modifies the Markdown field', function() { const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); - $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + $('input[type=checkbox]').attr('checked', true)[1].dispatchEvent(changeEvent); + + expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item'); }); it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1'); + expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json'); return expect(req.data.note).not.toBe(null); }); - $('.js-task-list-field').trigger('tasklist:changed'); + + $('.js-task-list-field.js-note-text').trigger('tasklist:changed'); }); }); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 3515dfbc60b..a912e150e9b 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,78 +1,74 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */ /* global ShortcutsIssuable */ import '~/copy_as_gfm'; import '~/shortcuts_issuable'; -(function() { - describe('ShortcutsIssuable', function() { - var fixtureName = 'issues/open-issue.html.raw'; - preloadFixtures(fixtureName); - beforeEach(function() { - loadFixtures(fixtureName); - document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - this.shortcut = new ShortcutsIssuable(); - }); - describe('replyWithSelectedText', function() { - var stubSelection; - // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. - stubSelection = function(html) { - window.gl.utils.getSelectedFragment = function() { - var node = document.createElement('div'); - node.innerHTML = html; - return node; - }; +describe('ShortcutsIssuable', () => { + const fixtureName = 'merge_requests/diff_comment.html.raw'; + preloadFixtures(fixtureName); + beforeEach(() => { + loadFixtures(fixtureName); + document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); + this.shortcut = new ShortcutsIssuable(true); + }); + describe('replyWithSelectedText', () => { + // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. + const stubSelection = (html) => { + window.gl.utils.getSelectedFragment = () => { + const node = document.createElement('div'); + node.innerHTML = html; + return node; }; - beforeEach(function() { - this.selector = 'form.js-main-target-form textarea#note_note'; + }; + 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(''); }); - describe('with empty selection', function() { - it('does not return an error', function() { - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe(''); - }); - it('triggers `focus`', function() { - this.shortcut.replyWithSelectedText(); - expect(document.activeElement).toBe(document.querySelector(this.selector)); - }); + it('triggers `focus`', () => { + this.shortcut.replyWithSelectedText(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); - describe('with any selection', function() { - beforeEach(function() { - stubSelection('<p>Selected text.</p>'); - }); - it('leaves existing input intact', function() { - $(this.selector).val('This text was already here.'); - expect($(this.selector).val()).toBe('This text was already here.'); - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); - }); - it('triggers `input`', function() { - var triggered = false; - $(this.selector).on('input', function() { - triggered = true; - }); - this.shortcut.replyWithSelectedText(); - expect(triggered).toBe(true); - }); - it('triggers `focus`', function() { - this.shortcut.replyWithSelectedText(); - expect(document.activeElement).toBe(document.querySelector(this.selector)); - }); + }); + describe('with any selection', () => { + beforeEach(() => { + stubSelection('<p>Selected text.</p>'); }); - describe('with a one-line selection', function() { - it('quotes the selection', function() { - stubSelection('<p>This text has been selected.</p>'); - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); - }); + 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'); }); - describe('with a multi-line selection', function() { - it('quotes the selected lines as a group', function() { - stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>"); - this.shortcut.replyWithSelectedText(); - expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); + it('triggers `input`', () => { + let triggered = false; + $(this.selector).on('input', () => { + triggered = true; }); + this.shortcut.replyWithSelectedText(true); + expect(triggered).toBe(true); + }); + it('triggers `focus`', () => { + this.shortcut.replyWithSelectedText(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); + }); + }); + 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'); + }); + }); + describe('with a multi-line selection', () => { + it('quotes the selected lines as a group', () => { + stubSelection('<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\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'); }); }); }); -}).call(window); +}); diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index 9b8373df29e..53e4c68beb3 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -1,6 +1,6 @@ /* global Shortcuts */ describe('Shortcuts', () => { - const fixtureName = 'issues/issue_with_comment.html.raw'; + const fixtureName = 'merge_requests/diff_comment.html.raw'; const createEvent = (type, target) => $.Event(type, { target, }); diff --git a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js new file mode 100644 index 00000000000..6df08f3ebe7 --- /dev/null +++ b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue'; + +describe('Confidential Issue Warning Component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(confidentialIssue); + vm = new Component().$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render confidential issue warning information', () => { + expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash'); + expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 291e19c9f3c..60a5c2ae74e 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -16,8 +16,8 @@ describe('Markdown field component', () => { }, template: ` <field-component - marodown-preview-url="/preview" - markdown-docs="/docs" + markdown-preview-path="/preview" + markdown-docs-path="/docs" > <textarea slot="textarea" @@ -92,6 +92,7 @@ describe('Markdown field component', () => { it('renders GFM with jQuery', (done) => { spyOn($.fn, 'renderGFM'); + previewLink.click(); setTimeout(() => { @@ -100,7 +101,7 @@ describe('Markdown field component', () => { ).toHaveBeenCalled(); done(); - }); + }, 0); }); }); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index a225b04c47e..bd18f79cea7 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -8,7 +8,7 @@ import ZenMode from '~/zen_mode'; var enterZen, escapeKeydown, exitZen; describe('ZenMode', function() { - var fixtureName = 'issues/open-issue.html.raw'; + var fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; preloadFixtures(fixtureName); beforeEach(function() { loadFixtures(fixtureName); diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index 87e60d9c16b..b909e04dfc3 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -41,4 +41,40 @@ describe AwardEmoji do end end end + + describe 'expiring ETag cache' do + context 'on a note' do + let(:note) { create(:note_on_issue) } + let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: note) } + + it 'calls expire_etag_cache on the note when saved' do + expect(note).to receive(:expire_etag_cache) + + award_emoji.save! + end + + it 'calls expire_etag_cache on the note when destroyed' do + expect(note).to receive(:expire_etag_cache) + + award_emoji.destroy! + end + end + + context 'on another awardable' do + let(:issue) { create(:issue) } + let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: issue) } + + it 'does not call expire_etag_cache on the issue when saved' do + expect(issue).not_to receive(:expire_etag_cache) + + award_emoji.save! + end + + it 'does not call expire_etag_cache on the issue when destroyed' do + expect(issue).not_to receive(:expire_etag_cache) + + award_emoji.destroy! + end + end + end end diff --git a/spec/serializers/note_entity_spec.rb b/spec/serializers/note_entity_spec.rb new file mode 100644 index 00000000000..3459cc72063 --- /dev/null +++ b/spec/serializers/note_entity_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe NoteEntity do + include Gitlab::Routing + + let(:request) { double('request', current_user: user, noteable: note.noteable) } + + let(:entity) { described_class.new(note, request: request) } + let(:note) { create(:note) } + let(:user) { create(:user) } + subject { entity.as_json } + + context 'basic note' do + it 'exposes correct elements' do + expect(subject).to include(:type, :author, :human_access, :note, :note_html, :current_user, + :discussion_id, :emoji_awardable, :award_emoji, :toggle_award_path, :report_abuse_path, :path, :attachment) + end + + it 'does not expose elements for specific notes cases' do + expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name) + end + + it 'exposes author correctly' do + expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path) + end + + it 'does not expose web_url for author' do + expect(subject[:author]).not_to include(:web_url) + end + end + + context 'when note was edited' do + before do + note.update(updated_at: 1.minute.from_now, updated_by: user) + end + + it 'exposes last_edited_at and last_edited_by elements' do + expect(subject).to include(:last_edited_at, :last_edited_by) + end + end + + context 'when note is a system note' do + before do + note.update(system: true) + end + + it 'exposes system_note_icon_name element' do + expect(subject).to include(:system_note_icon_name) + end + end +end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index bb4542b1683..81cb94ab8c4 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -14,6 +14,8 @@ shared_examples 'discussion comments' do |resource_name| find(submit_selector).click + wait_for_requests + find(comments_selector, match: :first) new_comment = all(comments_selector).last @@ -26,6 +28,7 @@ shared_examples 'discussion comments' do |resource_name| find("#{form_selector} .note-textarea").send_keys('a') find(close_selector).click + wait_for_requests find(comments_selector, match: :first) find("#{comments_selector}.system-note") @@ -76,12 +79,22 @@ shared_examples 'discussion comments' do |resource_name| it 'clicking the ul padding or divider should not change the text' do find(menu_selector).trigger 'click' - expect(page).to have_selector menu_selector - expect(find(dropdown_selector)).to have_content 'Comment' + if resource_name == 'issue' + expect(find(dropdown_selector)).to have_content 'Comment' + + find(toggle_selector).click + find("#{menu_selector} .divider").trigger 'click' + else + find(menu_selector).trigger 'click' - find("#{menu_selector} .divider").trigger 'click' + expect(page).to have_selector menu_selector + expect(find(dropdown_selector)).to have_content 'Comment' + + find("#{menu_selector} .divider").trigger 'click' + + expect(page).to have_selector menu_selector + end - expect(page).to have_selector menu_selector expect(find(dropdown_selector)).to have_content 'Comment' end @@ -91,9 +104,8 @@ shared_examples 'discussion comments' do |resource_name| all("#{menu_selector} li").last.click end - it 'updates the submit button text, note_type input and closes the dropdown' do + it 'updates the submit button text and closes the dropdown' do expect(find(dropdown_selector)).to have_content 'Start discussion' - expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote') expect(page).not_to have_selector menu_selector end @@ -157,9 +169,8 @@ shared_examples 'discussion comments' do |resource_name| find("#{menu_selector} li", match: :first).click end - it 'updates the submit button text, clears the note_type input and closes the dropdown' do + it 'updates the submit button text and closes the dropdown' do expect(find(dropdown_selector)).to have_content 'Comment' - expect(find("#{form_selector} #note_type", visible: false).value).to eq('') expect(page).not_to have_selector menu_selector end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 68f0ce8afb3..8282ba7e536 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -21,7 +21,7 @@ shared_examples 'issuable record that supports quick actions in its description before do project.team << [master, :master] - sign_in(master) + gitlab_sign_in(master) end after do @@ -119,16 +119,15 @@ shared_examples 'issuable record that supports quick actions in its description guest = create(:user) project.add_guest(guest) - sign_out(:user) - sign_in(guest) - + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end it "does not close the #{issuable_type}" do write_note("/close") - expect(page).not_to have_content '/close' + expect(page).to have_content '/close' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_open @@ -158,16 +157,15 @@ shared_examples 'issuable record that supports quick actions in its description guest = create(:user) project.add_guest(guest) - sign_out(:user) - sign_in(guest) - + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end it "does not reopen the #{issuable_type}" do write_note("/reopen") - expect(page).not_to have_content '/reopen' + expect(page).to have_content '/reopen' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_closed @@ -192,15 +190,15 @@ shared_examples 'issuable record that supports quick actions in its description guest = create(:user) project.add_guest(guest) - sign_out(:user) - sign_in(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end it "does not reopen the #{issuable_type}" do write_note("/title Awesome new title") - expect(page).not_to have_content '/title' + expect(page).to have_content '/title' expect(page).not_to have_content 'Commands applied' expect(issuable.reload.title).not_to eq 'Awesome new title' @@ -292,7 +290,7 @@ shared_examples 'issuable record that supports quick actions in its description end end - describe "preview of note on #{issuable_type}" do + describe "preview of note on #{issuable_type}", js: true do it 'removes quick actions from note and explains them' do create(:user, username: 'bob') diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 5a0e7c3d099..192a2fed0a8 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -1,6 +1,6 @@ require 'spec_helper' -shared_examples 'reportable note' do +shared_examples 'reportable note' do |type| include NotesHelper let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } @@ -20,7 +20,12 @@ shared_examples 'reportable note' do open_dropdown(dropdown) expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) - expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) + + if type == 'issue' + expect(dropdown).to have_button('Delete comment') + else + expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) + end end it 'Report button links to a report page' do |