diff options
Diffstat (limited to 'spec')
113 files changed, 2688 insertions, 933 deletions
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index 299d2c981d3..ad15e3942a5 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do    end    describe 'GET index' do -    context 'with valid list id' do -      it 'returns issues that have the list label applied' do -        johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) -        issue = create(:labeled_issue, project: project, labels: [planning]) -        create(:labeled_issue, project: project, labels: [planning]) -        create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) -        create(:labeled_issue, project: project, labels: [development], assignee: johndoe) -        issue.subscribe(johndoe, project) - -        list_issues user: user, board: board, list: list2 - -        parsed_response = JSON.parse(response.body) - -        expect(response).to match_response_schema('issues') -        expect(parsed_response.length).to eq 2 -      end -    end +    let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }      context 'with invalid board id' do        it 'returns a not found 404 response' do @@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do        end      end -    context 'with invalid list id' do -      it 'returns a not found 404 response' do -        list_issues user: user, board: board, list: 999 +    context 'when list id is present' do +      context 'with valid list id' do +        it 'returns issues that have the list label applied' do +          issue = create(:labeled_issue, project: project, labels: [planning]) +          create(:labeled_issue, project: project, labels: [planning]) +          create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) +          create(:labeled_issue, project: project, labels: [development], assignee: johndoe) +          issue.subscribe(johndoe, project) -        expect(response).to have_http_status(404) +          list_issues user: user, board: board, list: list2 + +          parsed_response = JSON.parse(response.body) + +          expect(response).to match_response_schema('issues') +          expect(parsed_response.length).to eq 2 +        end +      end + +      context 'with invalid list id' do +        it 'returns a not found 404 response' do +          list_issues user: user, board: board, list: 999 + +          expect(response).to have_http_status(404) +        end +      end +    end + +    context 'when list id is missing' do +      it 'returns opened issues without board labels applied' do +        bug = create(:label, project: project, name: 'Bug') +        create(:issue, project: project) +        create(:labeled_issue, project: project, labels: [planning]) +        create(:labeled_issue, project: project, labels: [development]) +        create(:labeled_issue, project: project, labels: [bug]) + +        list_issues user: user, board: board + +        parsed_response = JSON.parse(response.body) + +        expect(response).to match_response_schema('issues') +        expect(parsed_response.length).to eq 2        end      end @@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do        end      end -    def list_issues(user:, board:, list:) +    def list_issues(user:, board:, list: nil)        sign_in(user) -      get :index, namespace_id: project.namespace.to_param, -                  project_id: project.to_param, -                  board_id: board.to_param, -                  list_id: list.to_param +      params = { +        namespace_id: project.namespace.to_param, +        project_id: project.to_param, +        board_id: board.to_param, +        list_id: list.try(:to_param) +      } + +      get :index, params.compact      end    end diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 34d6119429d..b3f9f76a50c 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do        parsed_response = JSON.parse(response.body)        expect(response).to match_response_schema('lists') -      expect(parsed_response.length).to eq 3 +      expect(parsed_response.length).to eq 2      end      context 'with unauthorized user' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 9323f723bdb..e7aa8745b99 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -213,6 +213,17 @@ describe ProjectsController do          expect(response.status).to eq 404        end      end + +    context "redirection from http://someproject.git" do +      it 'redirects to project page (format.html)' do +        project = create(:project, :public) + +        get :show, namespace_id: project.namespace.path, id: project.path, format: :git + +        expect(response).to have_http_status(302) +        expect(response).to redirect_to(namespace_project_path) +      end +    end    end    describe "#update" do diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb index ec46146d9b5..a581725245a 100644 --- a/spec/factories/boards.rb +++ b/spec/factories/boards.rb @@ -3,7 +3,6 @@ FactoryGirl.define do      project factory: :empty_project      after(:create) do |board| -      board.lists.create(list_type: :backlog)        board.lists.create(list_type: :done)      end    end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index bfe41f71b57..55727d6b62c 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -3,6 +3,18 @@ FactoryGirl.define do      project factory: :empty_project      author factory: :user +    trait(:created)   { action Event::CREATED } +    trait(:updated)   { action Event::UPDATED } +    trait(:closed)    { action Event::CLOSED } +    trait(:reopened)  { action Event::REOPENED } +    trait(:pushed)    { action Event::PUSHED } +    trait(:commented) { action Event::COMMENTED } +    trait(:merged)    { action Event::MERGED } +    trait(:joined)    { action Event::JOINED } +    trait(:left)      { action Event::LEFT } +    trait(:destroyed) { action Event::DESTROYED } +    trait(:expired)   { action Event::EXPIRED } +      factory :closed_issue_event do        action { Event::CLOSED }        target factory: :closed_issue diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index 9e3f06c682c..2a2f3cca91c 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,12 +6,6 @@ FactoryGirl.define do      sequence(:position)    end -  factory :backlog_list, parent: :list do -    list_type :backlog -    label nil -    position nil -  end -    factory :done_list, parent: :list do      list_type :done      label nil diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb new file mode 100644 index 00000000000..2875fc1e533 --- /dev/null +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -0,0 +1,233 @@ +require 'rails_helper' + +describe 'Issue Boards add issue modal', :feature, :js do +  include WaitForAjax +  include WaitForVueResource + +  let(:project) { create(:empty_project, :public) } +  let(:board) { create(:board, project: project) } +  let(:user) { create(:user) } +  let!(:planning) { create(:label, project: project, name: 'Planning') } +  let!(:label) { create(:label, project: project) } +  let!(:list1) { create(:list, board: board, label: planning, position: 0) } +  let!(:list2) { create(:list, board: board, label: label, position: 1) } +  let!(:issue) { create(:issue, project: project) } +  let!(:issue2) { create(:issue, project: project) } + +  before do +    project.team << [user, :master] + +    login_as(user) + +    visit namespace_project_board_path(project.namespace, project, board) +    wait_for_vue_resource +  end + +  context 'modal interaction' do +    it 'opens modal' do +      click_button('Add issues') + +      expect(page).to have_selector('.add-issues-modal') +    end + +    it 'closes modal' do +      click_button('Add issues') + +      page.within('.add-issues-modal') do +        find('.close').click +      end + +      expect(page).not_to have_selector('.add-issues-modal') +    end + +    it 'closes modal if cancel button clicked' do +      click_button('Add issues') + +      page.within('.add-issues-modal') do +        click_button 'Cancel' +      end + +      expect(page).not_to have_selector('.add-issues-modal') +    end +  end + +  context 'issues list' do +    before do +      click_button('Add issues') + +      wait_for_vue_resource +    end + +    it 'loads issues' do +      page.within('.add-issues-modal') do +        page.within('.nav-links') do +          expect(page).to have_content('2') +        end + +        expect(page).to have_selector('.card', count: 2) +      end +    end + +    it 'shows selected issues' do +      page.within('.add-issues-modal') do +        click_link 'Selected issues' + +        expect(page).not_to have_selector('.card') +      end +    end + +    context 'list dropdown' do +      it 'resets after deleting list' do +        page.within('.add-issues-modal') do +          expect(find('.add-issues-footer')).to have_button(planning.title) + +          click_button 'Cancel' +        end + +        first('.board-delete').click + +        click_button('Add issues') + +        wait_for_vue_resource + +        page.within('.add-issues-modal') do +          expect(find('.add-issues-footer')).not_to have_button(planning.title) +          expect(find('.add-issues-footer')).to have_button(label.title) +        end +      end +    end + +    context 'search' do +      it 'returns issues' do +        page.within('.add-issues-modal') do +          find('.form-control').native.send_keys(issue.title) + +          expect(page).to have_selector('.card', count: 1) +        end +      end + +      it 'returns no issues' do +        page.within('.add-issues-modal') do +          find('.form-control').native.send_keys('testing search') + +          expect(page).not_to have_selector('.card') +          expect(page).not_to have_content("You haven't added any issues to your project yet") +        end +      end +    end + +    context 'selecing issues' do +      it 'selects single issue' do +        page.within('.add-issues-modal') do +          first('.card').click + +          page.within('.nav-links') do +            expect(page).to have_content('Selected issues 1') +          end +        end +      end + +      it 'changes button text' do +        page.within('.add-issues-modal') do +          first('.card').click + +          expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue') +        end +      end + +      it 'changes button text with plural' do +        page.within('.add-issues-modal') do +          all('.card').each do |el| +            el.click +          end + +          expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues') +        end +      end + +      it 'shows only selected issues on selected tab' do +        page.within('.add-issues-modal') do +          first('.card').click + +          click_link 'Selected issues' + +          expect(page).to have_selector('.card', count: 1) +        end +      end + +      it 'selects all issues' do +        page.within('.add-issues-modal') do +          click_button 'Select all' + +          expect(page).to have_selector('.is-active', count: 2) +        end +      end + +      it 'deselects all issues' do +        page.within('.add-issues-modal') do +          click_button 'Select all' + +          expect(page).to have_selector('.is-active', count: 2) + +          click_button 'Deselect all' + +          expect(page).not_to have_selector('.is-active') +        end +      end + +      it 'selects all that arent already selected' do +        page.within('.add-issues-modal') do +          first('.card').click + +          expect(page).to have_selector('.is-active', count: 1) + +          click_button 'Select all' + +          expect(page).to have_selector('.is-active', count: 2) +        end +      end + +      it 'unselects from selected tab' do +        page.within('.add-issues-modal') do +          first('.card').click + +          click_link 'Selected issues' + +          first('.card').click + +          expect(page).not_to have_selector('.is-active') +        end +      end +    end + +    context 'adding issues' do +      it 'adds to board' do +        page.within('.add-issues-modal') do +          first('.card').click + +          click_button 'Add 1 issue' +        end + +        page.within(first('.board')) do +          expect(page).to have_selector('.card') +        end +      end + +      it 'adds to second list' do +        page.within('.add-issues-modal') do +          first('.card').click + +          click_button planning.title + +          click_link label.title + +          click_button 'Add 1 issue' +        end + +        page.within(find('.board:nth-child(2)')) do +          expect(page).to have_selector('.card') +        end +      end +    end +  end +end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index bfac5a1b8ab..34f47daf0e5 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do      before do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_vue_resource -      expect(page).to have_selector('.board', count: 3) +      expect(page).to have_selector('.board', count: 2)      end      it 'shows blank state' do @@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do        page.within(find('.board-blank-state')) do          click_button("Nevermind, I'll use my own")        end -      expect(page).to have_selector('.board', count: 2) +      expect(page).to have_selector('.board', count: 1)      end      it 'creates default lists' do -      lists = ['Backlog', 'To Do', 'Doing', 'Done'] +      lists = ['To Do', 'Doing', 'Done']        page.within(find('.board-blank-state')) do          click_button('Add default lists')        end        wait_for_vue_resource -      expect(page).to have_selector('.board', count: 4) +      expect(page).to have_selector('.board', count: 3)        page.all('.board').each_with_index do |list, i|          expect(list.find('.board-title')).to have_content(lists[i]) @@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do      let!(:list1) { create(:list, board: board, label: planning, position: 0) }      let!(:list2) { create(:list, board: board, label: development, position: 1) } -    let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } -    let!(:issue1) { create(:issue, project: project, assignee: user) } -    let!(:issue2) { create(:issue, project: project, author: user2) } -    let!(:issue3) { create(:issue, project: project) } -    let!(:issue4) { create(:issue, project: project) } +    let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) } +    let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) } +    let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) } +    let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) } +    let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }      let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }      let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }      let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }      let!(:issue8) { create(:closed_issue, project: project) } -    let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) } +    let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }      before do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_vue_resource -      expect(page).to have_selector('.board', count: 4) +      expect(page).to have_selector('.board', count: 3)        expect(find('.board:nth-child(1)')).to have_selector('.card')        expect(find('.board:nth-child(2)')).to have_selector('.card')        expect(find('.board:nth-child(3)')).to have_selector('.card') -      expect(find('.board:nth-child(4)')).to have_selector('.card')      end      it 'shows lists' do -      expect(page).to have_selector('.board', count: 4) +      expect(page).to have_selector('.board', count: 3)      end      it 'shows description tooltip on list title' do -      page.within('.board:nth-child(2)') do +      page.within('.board:nth-child(1)') do          expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')        end      end      it 'shows issues in lists' do +      wait_for_board_cards(1, 8)        wait_for_board_cards(2, 2) -      wait_for_board_cards(3, 2)      end      it 'shows confidential issues with icon' do @@ -108,19 +107,6 @@ describe 'Issue Boards', feature: true, js: true do        end      end -    it 'search backlog list' do -      page.within('#js-boards-search') do -        find('.form-control').set(issue1.title) -      end - -      wait_for_vue_resource - -      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) -      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) -    end -      it 'search done list' do        page.within('#js-boards-search') do          find('.form-control').set(issue8.title) @@ -130,8 +116,7 @@ describe 'Issue Boards', feature: true, js: true do        expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)        expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) +      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)      end      it 'search list' do @@ -141,157 +126,135 @@ describe 'Issue Boards', feature: true, js: true do        wait_for_vue_resource -      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) +      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) +      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) -      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)      end      it 'allows user to delete board' do -      page.within(find('.board:nth-child(2)')) do +      page.within(find('.board:nth-child(1)')) do          find('.board-delete').click        end        wait_for_vue_resource -      expect(page).to have_selector('.board', count: 3) +      expect(page).to have_selector('.board', count: 2)      end      it 'removes checkmark in new list dropdown after deleting' do        click_button 'Add list'        wait_for_ajax -      page.within(find('.board:nth-child(2)')) do +      page.within(find('.board:nth-child(1)')) do          find('.board-delete').click        end        wait_for_vue_resource -      expect(page).to have_selector('.board', count: 3) -      expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active') +      expect(page).to have_selector('.board', count: 2)      end      it 'infinite scrolls list' do        50.times do -        create(:issue, project: project) +        create(:labeled_issue, project: project, labels: [planning])        end        visit namespace_project_board_path(project.namespace, project, board)        wait_for_vue_resource        page.within(find('.board', match: :first)) do -        expect(page.find('.board-header')).to have_content('56') +        expect(page.find('.board-header')).to have_content('58')          expect(page).to have_selector('.card', count: 20) -        expect(page).to have_content('Showing 20 of 56 issues') +        expect(page).to have_content('Showing 20 of 58 issues')          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")          wait_for_vue_resource          expect(page).to have_selector('.card', count: 40) -        expect(page).to have_content('Showing 40 of 56 issues') +        expect(page).to have_content('Showing 40 of 58 issues')          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")          wait_for_vue_resource -        expect(page).to have_selector('.card', count: 56) +        expect(page).to have_selector('.card', count: 58)          expect(page).to have_content('Showing all issues')        end      end -    context 'backlog' do -      it 'shows issues in backlog with no labels' do -        wait_for_board_cards(1, 6) -      end - -      it 'moves issue from backlog into list' do -        drag_to(list_to_index: 1) - -        wait_for_vue_resource -        wait_for_board_cards(1, 5) -        wait_for_board_cards(2, 3) -      end -    end -      context 'done' do        it 'shows list of done issues' do -        wait_for_board_cards(4, 1) +        wait_for_board_cards(3, 1)          wait_for_ajax        end        it 'moves issue to done' do -        drag_to(list_from_index: 0, list_to_index: 3) +        drag_to(list_from_index: 0, list_to_index: 2) -        wait_for_board_cards(1, 5) +        wait_for_board_cards(1, 7)          wait_for_board_cards(2, 2)          wait_for_board_cards(3, 2) -        wait_for_board_cards(4, 2)          expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) -        expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) -        expect(find('.board:nth-child(4)')).to have_content(issue9.title) -        expect(find('.board:nth-child(4)')).not_to have_content(planning.title) +        expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2) +        expect(find('.board:nth-child(3)')).to have_content(issue9.title) +        expect(find('.board:nth-child(3)')).not_to have_content(planning.title)        end        it 'removes all of the same issue to done' do -        drag_to(list_from_index: 1, list_to_index: 3) +        drag_to(list_from_index: 0, list_to_index: 2) -        wait_for_board_cards(1, 6) -        wait_for_board_cards(2, 1) -        wait_for_board_cards(3, 1) -        wait_for_board_cards(4, 2) +        wait_for_board_cards(1, 7) +        wait_for_board_cards(2, 2) +        wait_for_board_cards(3, 2) -        expect(find('.board:nth-child(2)')).not_to have_content(issue6.title) -        expect(find('.board:nth-child(4)')).to have_content(issue6.title) -        expect(find('.board:nth-child(4)')).not_to have_content(planning.title) +        expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) +        expect(find('.board:nth-child(3)')).to have_content(issue9.title) +        expect(find('.board:nth-child(3)')).not_to have_content(planning.title)        end      end      context 'lists' do        it 'changes position of list' do -        drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header') +        drag_to(list_from_index: 1, list_to_index: 0, selector: '.board-header') -        wait_for_board_cards(1, 6) -        wait_for_board_cards(2, 2) -        wait_for_board_cards(3, 2) -        wait_for_board_cards(4, 1) +        wait_for_board_cards(1, 2) +        wait_for_board_cards(2, 8) +        wait_for_board_cards(3, 1) -        expect(find('.board:nth-child(2)')).to have_content(development.title) -        expect(find('.board:nth-child(2)')).to have_content(planning.title) +        expect(find('.board:nth-child(1)')).to have_content(development.title) +        expect(find('.board:nth-child(1)')).to have_content(planning.title)        end        it 'issue moves between lists' do -        drag_to(list_from_index: 1, card_index: 1, list_to_index: 2) +        drag_to(list_from_index: 0, card_index: 1, list_to_index: 1) -        wait_for_board_cards(1, 6) -        wait_for_board_cards(2, 1) -        wait_for_board_cards(3, 3) -        wait_for_board_cards(4, 1) +        wait_for_board_cards(1, 7) +        wait_for_board_cards(2, 2) +        wait_for_board_cards(3, 1) -        expect(find('.board:nth-child(3)')).to have_content(issue6.title) -        expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title) +        expect(find('.board:nth-child(2)')).to have_content(issue6.title) +        expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title)        end        it 'issue moves between lists' do -        drag_to(list_from_index: 2, list_to_index: 1) +        drag_to(list_from_index: 1, list_to_index: 0) -        wait_for_board_cards(1, 6) -        wait_for_board_cards(2, 3) +        wait_for_board_cards(1, 9) +        wait_for_board_cards(2, 1)          wait_for_board_cards(3, 1) -        wait_for_board_cards(4, 1) -        expect(find('.board:nth-child(2)')).to have_content(issue7.title) -        expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title) +        expect(find('.board:nth-child(1)')).to have_content(issue7.title) +        expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title)        end        it 'issue moves from done' do -        drag_to(list_from_index: 3, list_to_index: 1) +        drag_to(list_from_index: 2, list_to_index: 1)          expect(find('.board:nth-child(2)')).to have_content(issue8.title) -        wait_for_board_cards(1, 6) +        wait_for_board_cards(1, 8)          wait_for_board_cards(2, 3) -        wait_for_board_cards(3, 2) -        wait_for_board_cards(4, 0) +        wait_for_board_cards(3, 0)        end        context 'issue card' do @@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_vue_resource -          expect(page).to have_selector('.board', count: 5) +          expect(page).to have_selector('.board', count: 4)          end          it 'creates new list for Backlog label' do @@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_vue_resource -          expect(page).to have_selector('.board', count: 5) +          expect(page).to have_selector('.board', count: 4)          end          it 'creates new list for Done label' do @@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_vue_resource -          expect(page).to have_selector('.board', count: 5) +          expect(page).to have_selector('.board', count: 4)          end          it 'keeps dropdown open after adding new list' do @@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do            expect(find('.issue-boards-search')).to have_selector('.open')          end -        it 'moves issues from backlog into new list' do -          wait_for_board_cards(1, 6) - -          click_button 'Add list' -          wait_for_ajax - -          page.within('.dropdown-menu-issues-board-new') do -            click_link testing.title -          end - -          wait_for_vue_resource - -          wait_for_board_cards(1, 5) -        end -          it 'creates new list from a new label' do            click_button 'Add list' @@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do            wait_for_ajax            wait_for_vue_resource -          expect(page).to have_selector('.board', count: 5) +          expect(page).to have_selector('.board', count: 4)          end        end      end @@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_vue_resource          wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..4)) +        wait_for_empty_boards((2..3))        end        it 'filters by assignee' do @@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_vue_resource          wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..4)) +        wait_for_empty_boards((2..3))        end        it 'filters by milestone' do @@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do          end          wait_for_vue_resource -        wait_for_board_cards(1, 0) -        wait_for_board_cards(2, 1) +        wait_for_board_cards(1, 1) +        wait_for_board_cards(2, 0)          wait_for_board_cards(3, 0) -        wait_for_board_cards(4, 0)        end        it 'filters by label' do @@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_vue_resource          wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..4)) +        wait_for_empty_boards((2..3))        end        it 'filters by label with space after reload' do @@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do        it 'infinite scrolls list with label filter' do          50.times do -          create(:labeled_issue, project: project, labels: [testing]) +          create(:labeled_issue, project: project, labels: [planning, testing])          end          page.within '.issues-filters' do @@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_vue_resource          wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..4)) -      end - -      it 'filters by no label' do -        page.within '.issues-filters' do -          click_button('Label') -          wait_for_ajax - -          page.within '.dropdown-menu-labels' do -            click_link("No Label") -            wait_for_vue_resource -            find('.dropdown-menu-close').click -          end -        end - -        wait_for_vue_resource - -        wait_for_board_cards(1, 5) -        wait_for_board_cards(2, 0) -        wait_for_board_cards(3, 0) -        wait_for_board_cards(4, 1) +        wait_for_empty_boards((2..3))        end        it 'filters by clicking label button on issue' do          page.within(find('.board', match: :first)) do -          expect(page).to have_selector('.card', count: 6) +          expect(page).to have_selector('.card', count: 8)            expect(find('.card', match: :first)).to have_content(bug.title)            click_button(bug.title)            wait_for_vue_resource @@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_vue_resource          wait_for_board_cards(1, 1) -        wait_for_empty_boards((2..4)) +        wait_for_empty_boards((2..3))          page.within('.labels-filter') do            expect(find('.dropdown-toggle-text')).to have_content(bug.title) diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index a03cd6fbf2d..6d14a8cf483 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do    let(:project) { create(:empty_project, :public) }    let(:board)   { create(:board, project: project) } +  let!(:list)   { create(:list, board: board, position: 0) }    let(:user)    { create(:user) }    context 'authorized user' do @@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do        visit namespace_project_board_path(project.namespace, project, board)        wait_for_vue_resource -      expect(page).to have_selector('.board', count: 3) +      expect(page).to have_selector('.board', count: 2)      end      it 'displays new issue button' do @@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do      end      it 'does not display new issue button in done list' do -      page.within('.board:nth-child(3)') do +      page.within('.board:nth-child(2)') do          expect(page).not_to have_selector('.board-issue-count-holder .btn')        end      end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c28bb0dcdae..9cc50167395 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do    include WaitForAjax    include WaitForVueResource -  let(:project)     { create(:empty_project, :public) } -  let(:board)       { create(:board, project: project) } -  let(:user)        { create(:user) } -  let!(:label)      { create(:label, project: project) } -  let!(:label2)     { create(:label, project: project) } -  let!(:milestone)  { create(:milestone, project: project) } -  let!(:issue2)     { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) } -  let!(:issue)      { create(:issue, project: project) } +  let(:user)         { create(:user) } +  let(:project)      { create(:empty_project, :public) } +  let!(:milestone)   { create(:milestone, project: project) } +  let!(:development) { create(:label, project: project, name: 'Development') } +  let!(:bug)         { create(:label, project: project, name: 'Bug') } +  let!(:regression)  { create(:label, project: project, name: 'Regression') } +  let!(:stretch)     { create(:label, project: project, name: 'Stretch') } +  let!(:issue1)      { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) } +  let!(:issue2)      { create(:labeled_issue, project: project, labels: [development, stretch]) } +  let(:board)        { create(:board, project: project) } +  let!(:list)        { create(:list, board: board, label: development, position: 0) }    before do      project.team << [user, :master] @@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do      end      page.within('.issue-boards-sidebar') do -      expect(page).to have_content(issue.title) -      expect(page).to have_content(issue.to_reference) +      expect(page).to have_content(issue2.title) +      expect(page).to have_content(issue2.to_reference) +    end +  end + +  it 'removes card from board when clicking remove button' do +    page.within(first('.board')) do +      first('.card').click +    end + +    page.within('.issue-boards-sidebar') do +      click_button 'Remove from board' +    end + +    page.within(first('.board')) do +      expect(page).to have_selector('.card', count: 1)      end    end @@ -244,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_ajax -        click_link label.title +        click_link bug.title          wait_for_vue_resource          find('.dropdown-menu-close-icon').click          page.within('.value') do -          expect(page).to have_selector('.label', count: 1) -          expect(page).to have_content(label.title) +          expect(page).to have_selector('.label', count: 3) +          expect(page).to have_content(bug.title)          end        end        page.within(first('.board')) do          page.within(first('.card')) do -          expect(page).to have_selector('.label', count: 1) -          expect(page).to have_content(label.title) +          expect(page).to have_selector('.label', count: 2) +          expect(page).to have_content(bug.title)          end        end      end @@ -274,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_ajax -        click_link label.title -        click_link label2.title +        click_link bug.title +        click_link regression.title          wait_for_vue_resource          find('.dropdown-menu-close-icon').click          page.within('.value') do -          expect(page).to have_selector('.label', count: 2) -          expect(page).to have_content(label.title) -          expect(page).to have_content(label2.title) +          expect(page).to have_selector('.label', count: 4) +          expect(page).to have_content(bug.title) +          expect(page).to have_content(regression.title)          end        end        page.within(first('.board')) do          page.within(first('.card')) do -          expect(page).to have_selector('.label', count: 2) -          expect(page).to have_content(label.title) -          expect(page).to have_content(label2.title) +          expect(page).to have_selector('.label', count: 3) +          expect(page).to have_content(bug.title) +          expect(page).to have_content(regression.title)          end        end      end      it 'removes a label' do        page.within(first('.board')) do -        find('.card:nth-child(2)').click +        first('.card').click        end        page.within('.labels') do @@ -307,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do          wait_for_ajax -        click_link label.title +        click_link stretch.title          wait_for_vue_resource          find('.dropdown-menu-close-icon').click          page.within('.value') do -          expect(page).to have_selector('.label', count: 0) -          expect(page).not_to have_content(label.title) +          expect(page).to have_selector('.label', count: 1) +          expect(page).not_to have_content(stretch.title)          end        end        page.within(first('.board')) do -        page.within(find('.card:nth-child(2)')) do -          expect(page).not_to have_selector('.label', count: 1) -          expect(page).not_to have_content(label.title) +        page.within(first('.card')) do +          expect(page).not_to have_selector('.label') +          expect(page).not_to have_content(stretch.title)          end        end      end diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 44a9b545ff8..a2cf9b18bf2 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -10,8 +10,8 @@ feature 'toggler_behavior', js: true, feature: true do    before do      login_as :admin      project = merge_request.source_project -    visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"      page.current_window.resize_to(1000, 300) +    visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"    end    describe 'scroll position' do diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 2582a540240..2f3c3e45ae6 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -120,5 +120,81 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do          expect(page).not_to have_content '/due 2016-08-28'        end      end + +    describe '/target_branch command in merge request' do +      let(:another_project) { create(:project, :public) } +      let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + +      before do +        logout +        another_project.team << [user, :master] +        login_with(user) +      end + +      it 'changes target_branch in new merge_request' do +        visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) +        click_button "Compare branches and continue" + +        fill_in "merge_request_title", with: 'My brand new feature' +        fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" +        click_button "Submit merge request" + +        merge_request = another_project.merge_requests.first +        expect(merge_request.description).to eq "le feature \nFeature description:" +        expect(merge_request.target_branch).to eq 'fix' +      end + +      it 'does not change target branch when merge request is edited' do +        new_merge_request = create(:merge_request, source_project: another_project) + +        visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request) +        fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n" +        click_button "Save changes" + +        new_merge_request = another_project.merge_requests.first +        expect(new_merge_request.description).to include('/target_branch') +        expect(new_merge_request.target_branch).not_to eq('fix') +      end +    end + +    describe '/target_branch command from note' do +      context 'when the current user can change target branch' do +        it 'changes target branch from a note' do +          write_note("message start \n/target_branch merge-test\n message end.") + +          expect(page).not_to have_content('/target_branch') +          expect(page).to have_content('message start') +          expect(page).to have_content('message end.') + +          expect(merge_request.reload.target_branch).to eq 'merge-test' +        end + +        it 'does not fail when target branch does not exists' do +          write_note('/target_branch totally_not_existing_branch') + +          expect(page).not_to have_content('/target_branch') + +          expect(merge_request.target_branch).to eq 'feature' +        end +      end + +      context 'when current user can not change target branch' do +        let(:guest) { create(:user) } +        before do +          project.team << [guest, :guest] +          logout +          login_with(guest) +          visit namespace_project_merge_request_path(project.namespace, project, merge_request) +        end + +        it 'does not change target branch' do +          write_note('/target_branch merge-test') + +          expect(page).not_to have_content '/target_branch merge-test' + +          expect(merge_request.target_branch).to eq 'feature' +        end +      end +    end    end  end diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index ad2d456529a..34f665826b6 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -10,15 +10,12 @@ describe ContributedProjectsFinder do    let!(:private_project) { create(:empty_project, :private) }    before do -    private_project.team << [source_user, Gitlab::Access::MASTER] -    private_project.team << [current_user, Gitlab::Access::DEVELOPER] -    public_project.team << [source_user, Gitlab::Access::MASTER] +    private_project.add_master(source_user) +    private_project.add_developer(current_user) +    public_project.add_master(source_user) -    create(:event, action: Event::PUSHED, project: public_project, -                   target: public_project, author: source_user) - -    create(:event, action: Event::PUSHED, project: private_project, -                   target: private_project, author: source_user) +    create(:event, :pushed, project: public_project, target: public_project, author: source_user) +    create(:event, :pushed, project: private_project, target: private_project, author: source_user)    end    describe 'without a current user' do diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 77f2bcee1f3..8e19cee5440 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -6,6 +6,7 @@      "confidential"    ],    "properties" : { +    "id": { "type": "integer" },      "iid": { "type": "integer" },      "title": { "type": "string" },      "confidential": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 8d94cf26ecb..819287bf919 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@      "id": { "type": "integer" },      "list_type": {        "type": "string", -      "enum": ["backlog", "label", "done"] +      "enum": ["label", "done"]      },      "label": {        "type": ["object", "null"], diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index a2d57824585..76b370b345b 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,5 +1,5 @@ -/*= require lib/utils/text_utility */ -/*= require abuse_reports */ +require('~/lib/utils/text_utility'); +require('~/abuse_reports');  ((global) => {    describe('Abuse Reports', () => { diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 7bc5b3268a0..e6a6fc36ca1 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,9 +1,8 @@  /* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */ -/*= require js.cookie.js */ -/*= require jquery.endless-scroll.js */ -/*= require pager */ -/*= require activities */ +require('vendor/jquery.endless-scroll.js'); +require('~/pager'); +require('~/activities');  (() => {    window.gon || (window.gon = {}); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index f1bfd529983..001cd8d6325 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,10 +1,8 @@  /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */  /* global AwardsHandler */ -/*= require awards_handler */ -/*= require jquery */ -/*= require js.cookie */ -/*= require ./fixtures/emoji_menu */ +require('~/awards_handler'); +require('./fixtures/emoji_menu');  (function() {    var awardsHandler, lazyAssert, urlRoot; diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 51d911792ba..4a3da9e318b 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,6 +1,6 @@  /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */ -/*= require behaviors/autosize */ +require('~/behaviors/autosize');  (function() {    describe('Autosize behavior', function() { @@ -15,7 +15,7 @@        });      });      return load = function() { -      return $(document).trigger('page:load'); +      return $(document).trigger('load');      };    });  }).call(this); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 0f046c2d83a..b84126c0e3d 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,6 @@  /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */ -/*= require behaviors/quick_submit */ +require('~/behaviors/quick_submit');  (function() {    describe('Quick Submit behavior', function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 9467056f04c..a958ac76e66 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,6 @@  /* eslint-disable space-before-function-paren, no-var */ -/*= require behaviors/requires_input */ +require('~/behaviors/requires_input');  (function() {    describe('requiresInput', function() { @@ -34,11 +34,5 @@        $('#required5').val('1').change();        return expect($('.submit')).not.toBeDisabled();      }); -    return it('is called on page:load event', function() { -      var spy; -      spy = spyOn($.fn, 'requiresInput'); -      $(document).trigger('page:load'); -      return expect(spy).toHaveBeenCalled(); -    });    });  }).call(this); diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 7c5850111cb..9dd741a680b 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,24 +6,19 @@  /* global listObj */  /* global listObjDuplicate */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); +require('./mock_data');  describe('Store', () => {    beforeEach(() => {      Vue.http.interceptors.push(boardsMockInterceptor); -    gl.boardService = new BoardService('/test/issue-boards/board', '1'); +    gl.boardService = new BoardService('/test/issue-boards/board', '', '1');      gl.issueBoards.BoardsStore.create();      Cookies.set('issue_board_welcome_hidden', 'false', { @@ -61,18 +56,6 @@ describe('Store', () => {        expect(list).toBeDefined();      }); -    it('finds list limited by type', () => { -      gl.issueBoards.BoardsStore.addList({ -        id: 1, -        position: 0, -        title: 'Test', -        list_type: 'backlog' -      }); -      const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog'); - -      expect(list).toBeDefined(); -    }); -      it('gets issue when new list added', (done) => {        gl.issueBoards.BoardsStore.addList(listObj);        const list = gl.issueBoards.BoardsStore.findList('id', 1); @@ -117,10 +100,7 @@ describe('Store', () => {        expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);      }); -    it('check for blank state adding when backlog & done list exist', () => { -      gl.issueBoards.BoardsStore.addList({ -        list_type: 'backlog' -      }); +    it('check for blank state adding when done list exist', () => {        gl.issueBoards.BoardsStore.addList({          list_type: 'done'        }); diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6 new file mode 100644 index 00000000000..4340a571017 --- /dev/null +++ b/spec/javascripts/boards/issue_card_spec.js.es6 @@ -0,0 +1,191 @@ +/* global Vue */ +/* global ListUser */ +/* global ListLabel */ +/* global listObj */ +/* global ListIssue */ + +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/stores/boards_store'); +require('~/boards/components/issue_card_inner'); +require('./mock_data'); + +describe('Issue card component', () => { +  const user = new ListUser({ +    id: 1, +    name: 'testing 123', +    username: 'test', +    avatar: 'test_image', +  }); +  const label1 = new ListLabel({ +    id: 3, +    title: 'testing 123', +    color: 'blue', +    text_color: 'white', +    description: 'test', +  }); +  let component; +  let issue; +  let list; + +  beforeEach(() => { +    setFixtures('<div class="test-container"></div>'); + +    list = listObj; +    issue = new ListIssue({ +      title: 'Testing', +      iid: 1, +      confidential: false, +      labels: [list.label], +    }); + +    component = new Vue({ +      el: document.querySelector('.test-container'), +      data() { +        return { +          list, +          issue, +          issueLinkBase: '/test', +          rootPath: '/', +        }; +      }, +      components: { +        'issue-card': gl.issueBoards.IssueCardInner, +      }, +      template: ` +        <issue-card +          :issue="issue" +          :list="list" +          :issue-link-base="issueLinkBase" +          :root-path="rootPath"></issue-card> +      `, +    }); +  }); + +  it('renders issue title', () => { +    expect( +      component.$el.querySelector('.card-title').textContent, +    ).toContain(issue.title); +  }); + +  it('includes issue base in link', () => { +    expect( +      component.$el.querySelector('.card-title a').getAttribute('href'), +    ).toContain('/test'); +  }); + +  it('includes issue title on link', () => { +    expect( +      component.$el.querySelector('.card-title a').getAttribute('title'), +    ).toBe(issue.title); +  }); + +  it('does not render confidential icon', () => { +    expect( +      component.$el.querySelector('.fa-eye-flash'), +    ).toBeNull(); +  }); + +  it('renders confidential icon', (done) => { +    component.issue.confidential = true; + +    setTimeout(() => { +      expect( +        component.$el.querySelector('.confidential-icon'), +      ).not.toBeNull(); +      done(); +    }, 0); +  }); + +  it('renders issue ID with #', () => { +    expect( +      component.$el.querySelector('.card-number').textContent, +    ).toContain(`#${issue.id}`); +  }); + +  describe('assignee', () => { +    it('does not render assignee', () => { +      expect( +        component.$el.querySelector('.card-assignee'), +      ).toBeNull(); +    }); + +    describe('exists', () => { +      beforeEach((done) => { +        component.issue.assignee = user; + +        setTimeout(() => { +          done(); +        }, 0); +      }); + +      it('renders assignee', () => { +        expect( +          component.$el.querySelector('.card-assignee'), +        ).not.toBeNull(); +      }); + +      it('sets title', () => { +        expect( +          component.$el.querySelector('.card-assignee').getAttribute('title'), +        ).toContain(`Assigned to ${user.name}`); +      }); + +      it('sets users path', () => { +        expect( +          component.$el.querySelector('.card-assignee').getAttribute('href'), +        ).toBe('/test'); +      }); + +      it('renders avatar', () => { +        expect( +          component.$el.querySelector('.card-assignee img'), +        ).not.toBeNull(); +      }); +    }); +  }); + +  describe('labels', () => { +    it('does not render any', () => { +      expect( +        component.$el.querySelector('.label'), +      ).toBeNull(); +    }); + +    describe('exists', () => { +      beforeEach((done) => { +        component.issue.addLabel(label1); + +        setTimeout(() => { +          done(); +        }, 0); +      }); + +      it('does not render list label', () => { +        expect( +          component.$el.querySelectorAll('.label').length, +        ).toBe(1); +      }); + +      it('renders label', () => { +        expect( +          component.$el.querySelector('.label').textContent, +        ).toContain(label1.title); +      }); + +      it('sets label description as title', () => { +        expect( +          component.$el.querySelector('.label').getAttribute('title'), +        ).toContain(label1.description); +      }); + +      it('sets background color of button', () => { +        expect( +          component.$el.querySelector('.label').style.backgroundColor, +        ).toContain(label1.color); +      }); +    }); +  }); +}); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index c8a61a0a9b5..aab4d9c501e 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,25 +2,20 @@  /* global BoardService */  /* global ListIssue */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); +require('./mock_data');  describe('Issue model', () => {    let issue;    beforeEach(() => { -    gl.boardService = new BoardService('/test/issue-boards/board', '1'); +    gl.boardService = new BoardService('/test/issue-boards/board', '', '1');      gl.issueBoards.BoardsStore.create();      issue = new ListIssue({ diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 7d942ec3d65..4397a32fedc 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,26 +5,21 @@  /* global List */  /* global listObj */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); +require('./mock_data');  describe('List model', () => {    let list;    beforeEach(() => {      Vue.http.interceptors.push(boardsMockInterceptor); -    gl.boardService = new BoardService('/test/issue-boards/board', '1'); +    gl.boardService = new BoardService('/test/issue-boards/board', '', '1');      gl.issueBoards.BoardsStore.create();      list = new List(listObj); diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js.es6 new file mode 100644 index 00000000000..1815847f3fa --- /dev/null +++ b/spec/javascripts/boards/modal_store_spec.js.es6 @@ -0,0 +1,132 @@ +/* global Vue */ +/* global ListIssue */ + +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/stores/modal_store'); + +describe('Modal store', () => { +  let issue; +  let issue2; +  const Store = gl.issueBoards.ModalStore; + +  beforeEach(() => { +    // Setup default state +    Store.store.issues = []; +    Store.store.selectedIssues = []; + +    issue = new ListIssue({ +      title: 'Testing', +      iid: 1, +      confidential: false, +      labels: [], +    }); +    issue2 = new ListIssue({ +      title: 'Testing', +      iid: 2, +      confidential: false, +      labels: [], +    }); +    Store.store.issues.push(issue); +    Store.store.issues.push(issue2); +  }); + +  it('returns selected count', () => { +    expect(Store.selectedCount()).toBe(0); +  }); + +  it('toggles the issue as selected', () => { +    Store.toggleIssue(issue); + +    expect(issue.selected).toBe(true); +    expect(Store.selectedCount()).toBe(1); +  }); + +  it('toggles the issue as un-selected', () => { +    Store.toggleIssue(issue); +    Store.toggleIssue(issue); + +    expect(issue.selected).toBe(false); +    expect(Store.selectedCount()).toBe(0); +  }); + +  it('toggles all issues as selected', () => { +    Store.toggleAll(); + +    expect(issue.selected).toBe(true); +    expect(issue2.selected).toBe(true); +    expect(Store.selectedCount()).toBe(2); +  }); + +  it('toggles all issues as un-selected', () => { +    Store.toggleAll(); +    Store.toggleAll(); + +    expect(issue.selected).toBe(false); +    expect(issue2.selected).toBe(false); +    expect(Store.selectedCount()).toBe(0); +  }); + +  it('toggles all if a single issue is selected', () => { +    Store.toggleIssue(issue); +    Store.toggleAll(); + +    expect(issue.selected).toBe(true); +    expect(issue2.selected).toBe(true); +    expect(Store.selectedCount()).toBe(2); +  }); + +  it('adds issue to selected array', () => { +    issue.selected = true; +    Store.addSelectedIssue(issue); + +    expect(Store.selectedCount()).toBe(1); +  }); + +  it('removes issue from selected array', () => { +    Store.addSelectedIssue(issue); +    Store.removeSelectedIssue(issue); + +    expect(Store.selectedCount()).toBe(0); +  }); + +  it('returns selected issue index if present', () => { +    Store.toggleIssue(issue); + +    expect(Store.selectedIssueIndex(issue)).toBe(0); +  }); + +  it('returns -1 if issue is not selected', () => { +    expect(Store.selectedIssueIndex(issue)).toBe(-1); +  }); + +  it('finds the selected issue', () => { +    Store.toggleIssue(issue); + +    expect(Store.findSelectedIssue(issue)).toBe(issue); +  }); + +  it('does not find a selected issue', () => { +    expect(Store.findSelectedIssue(issue)).toBe(undefined); +  }); + +  it('does not remove from selected issue if tab is not all', () => { +    Store.store.activeTab = 'selected'; + +    Store.toggleIssue(issue); +    Store.toggleIssue(issue); + +    expect(Store.store.selectedIssues.length).toBe(1); +    expect(Store.selectedCount()).toBe(0); +  }); + +  it('gets selected issue array with only selected issues', () => { +    Store.toggleIssue(issue); +    Store.toggleIssue(issue2); +    Store.toggleIssue(issue2); + +    expect(Store.getSelectedIssues().length).toBe(1); +  }); +}); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index ea953d0f5a5..fa9f95e16cd 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -1,6 +1,15 @@ -//= require lib/utils/bootstrap_linked_tabs +require('~/lib/utils/bootstrap_linked_tabs');  (() => { +  // TODO: remove this hack! +  // PhantomJS causes spyOn to panic because replaceState isn't "writable" +  let phantomjs; +  try { +    phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; +  } catch (err) { +    phantomjs = false; +  } +    describe('Linked Tabs', () => {      preloadFixtures('static/linked_tabs.html.raw'); @@ -10,7 +19,9 @@      describe('when is initialized', () => {        beforeEach(() => { -        spyOn(window.history, 'replaceState').and.callFake(function () {}); +        if (!phantomjs) { +          spyOn(window.history, 'replaceState').and.callFake(function () {}); +        }        });        it('should activate the tab correspondent to the given action', () => { @@ -36,7 +47,7 @@      describe('on click', () => {        it('should change the url according to the clicked tab', () => { -        const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); +        const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});          const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line            action: 'show', @@ -49,10 +60,11 @@          secondTab.click(); -        expect(historySpy).toHaveBeenCalledWith({ -          turbolinks: true, -          url: newState, -        }, document.title, newState); +        if (historySpy) { +          expect(historySpy).toHaveBeenCalledWith({ +            url: newState, +          }, document.title, newState); +        }        });      });    }); diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 0c556382980..0bd50588f5a 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -1,12 +1,11 @@  /* eslint-disable no-new */  /* global Build */ -/* global Turbolinks */ -//= require lib/utils/datetime_utility -//= require build -//= require breakpoints -//= require jquery.nicescroll -//= require turbolinks +require('~/lib/utils/datetime_utility'); +require('~/lib/utils/url_utility'); +require('~/build'); +require('~/breakpoints'); +require('vendor/jquery.nicescroll');  describe('Build', () => {    const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; @@ -167,7 +166,7 @@ describe('Build', () => {        });        it('reloads the page when the build is done', () => { -        spyOn(Turbolinks, 'visit'); +        spyOn(gl.utils, 'visitUrl');          jasmine.clock().tick(4001);          const [{ success, context }] = $.ajax.calls.argsFor(1); @@ -177,7 +176,7 @@ describe('Build', () => {            append: true,          }); -        expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL); +        expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);        });      });    }); diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js.es6 index bb9a9072f3a..05260760c43 100644 --- a/spec/javascripts/commits_spec.js.es6 +++ b/spec/javascripts/commits_spec.js.es6 @@ -1,10 +1,19 @@  /* global CommitsList */ -//= require jquery.endless-scroll -//= require pager -//= require commits +require('vendor/jquery.endless-scroll'); +require('~/pager'); +require('~/commits');  (() => { +  // TODO: remove this hack! +  // PhantomJS causes spyOn to panic because replaceState isn't "writable" +  let phantomjs; +  try { +    phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; +  } catch (err) { +    phantomjs = false; +  } +    describe('Commits List', () => {      beforeEach(() => {        setFixtures(` @@ -25,7 +34,10 @@        beforeEach(() => {          CommitsList.init(25);          CommitsList.searchField.val(''); -        spyOn(history, 'replaceState').and.stub(); + +        if (!phantomjs) { +          spyOn(history, 'replaceState').and.stub(); +        }          ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {            req.success({              data: '<li>Result</li>', diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 4d851b2d320..c0bdb89ed63 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,9 +1,7 @@  /* eslint-disable no-new */ -/*= require sidebar */ -/*= require jquery */ -/*= require js.cookie */ -/*= require lib/utils/text_utility */ +require('~/sidebar'); +require('~/lib/utils/text_utility');  ((global) => {    describe('Dashboard', () => { diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 8ece24555c5..d5eec10be42 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/datetime_utility +require('~/lib/utils/datetime_utility');  (() => {    describe('Date time utils', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index fbfa34a5da7..f956394ef53 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,10 +1,9 @@  /* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */  /* global CommentsStore */ -//= require vue -//= require diff_notes/models/discussion -//= require diff_notes/models/note -//= require diff_notes/stores/comments +require('~/diff_notes/models/discussion'); +require('~/diff_notes/models/note'); +require('~/diff_notes/stores/comments');  (() => {    function createDiscussion(noteId = 1, resolved = true) { diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 056e4d41e93..b1838045a06 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,5 +1,4 @@ -//= require vue -//= require environments/components/environment_actions +require('~/environments/components/environment_actions');  describe('Actions Component', () => {    preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 950a5d53fad..a6a587e69f5 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,5 +1,4 @@ -//= require vue -//= require environments/components/environment_external_url +require('~/environments/components/environment_external_url');  describe('External URL Component', () => {    preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index c178b9cc1ec..9858f346c83 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,6 +1,5 @@ -//= require vue -//= require timeago -//= require environments/components/environment_item +window.timeago = require('vendor/timeago'); +require('~/environments/components/environment_item');  describe('Environment item', () => {    preloadFixtures('static/environments/table.html.raw'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 95796f23894..043b8708a6e 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,5 +1,5 @@ -//= require vue -//= require environments/components/environment_rollback +require('~/environments/components/environment_rollback'); +  describe('Rollback Component', () => {    preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 239cd69dd3a..87eda136122 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -1,11 +1,9 @@  /* global Vue, environment */ -//= require vue -//= require vue-resource -//= require flash -//= require environments/stores/environments_store -//= require environments/components/environment -//= require ./mock_data +require('~/flash'); +require('~/environments/stores/environments_store'); +require('~/environments/components/environment'); +require('./mock_data');  describe('Environment', () => {    preloadFixtures('static/environments/environments.html.raw'); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index bb998a32f32..2dfce5ba824 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,5 +1,5 @@ -//= require vue -//= require environments/components/environment_stop +require('~/environments/components/environment_stop'); +  describe('Stop Component', () => {    preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 17c00acf63e..9a8300d3832 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,8 +1,7 @@  /* global environmentsList */ -//= require vue -//= require environments/stores/environments_store -//= require ./mock_data +require('~/environments/stores/environments_store'); +require('./mock_data');  (() => {    describe('Store', () => { diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index 3949c5615d5..ba5eb81defc 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,6 +1,6 @@  /* eslint-disable space-before-function-paren, no-var */ -/*= require extensions/array */ +require('~/extensions/array');  (function() {    describe('Array extensions', function() { diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 index c5b86d35204..2d8a128ed33 100644 --- a/spec/javascripts/extensions/element_spec.js.es6 +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require extensions/element */ +require('~/extensions/element');  (() => {    describe('Element extensions', function () { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 5cd0e5ab0f0..c0bb0419814 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,6 +1,6 @@  /* eslint-disable space-before-function-paren, no-var */ -/*= require extensions/jquery */ +require('~/extensions/jquery');  (function() {    describe('jQuery extensions', function() { diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 index 3b71c255b30..2467ed78459 100644 --- a/spec/javascripts/extensions/object_spec.js.es6 +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require extensions/object */ +require('~/extensions/object');  describe('Object extensions', () => {    describe('assign', () => { diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 index 5eba4343a1d..10a316f31b4 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -1,7 +1,7 @@ -//= require filtered_search/dropdown_utils -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown -//= require filtered_search/dropdown_user +require('~/filtered_search/dropdown_utils'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown'); +require('~/filtered_search/dropdown_user');  (() => {    describe('Dropdown User', () => { diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index 89e49b7c511..1e2d7582d5b 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -1,7 +1,7 @@ -//= require extensions/array -//= require filtered_search/dropdown_utils -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager +require('~/extensions/array'); +require('~/filtered_search/dropdown_utils'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager');  (() => {    describe('Dropdown Utils', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index 4bd45eb457d..ed0b0196ec4 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -1,6 +1,6 @@ -//= require extensions/array -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager +require('~/extensions/array'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager');  (() => {    describe('Filtered Search Dropdown Manager', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index a508dacf7f0..98959dda242 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -1,11 +1,9 @@ -/* global Turbolinks */ - -//= require turbolinks -//= require lib/utils/common_utils -//= require filtered_search/filtered_search_token_keys -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager -//= require filtered_search/filtered_search_manager +require('~/lib/utils/url_utility'); +require('~/lib/utils/common_utils'); +require('~/filtered_search/filtered_search_token_keys'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); +require('~/filtered_search/filtered_search_manager');  (() => {    describe('Filtered Search Manager', () => { @@ -38,7 +36,7 @@        it('should search with a single word', () => {          getInput().value = 'searchTerm'; -        spyOn(Turbolinks, 'visit').and.callFake((url) => { +        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {            expect(url).toEqual(`${defaultParams}&search=searchTerm`);          }); @@ -48,7 +46,7 @@        it('should search with multiple words', () => {          getInput().value = 'awesome search terms'; -        spyOn(Turbolinks, 'visit').and.callFake((url) => { +        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {            expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);          }); @@ -58,7 +56,7 @@        it('should search with special characters', () => {          getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; -        spyOn(Turbolinks, 'visit').and.callFake((url) => { +        spyOn(gl.utils, 'visitUrl').and.callFake((url) => {            expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);          }); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 index 9d9097419ea..cf409a7e509 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -1,5 +1,5 @@ -//= require extensions/array -//= require filtered_search/filtered_search_token_keys +require('~/extensions/array'); +require('~/filtered_search/filtered_search_token_keys');  (() => {    describe('Filtered Search Token Keys', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index ac7f8e9cbcd..84c0e9cbfe2 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -1,6 +1,6 @@ -//= require extensions/array -//= require filtered_search/filtered_search_token_keys -//= require filtered_search/filtered_search_tokenizer +require('~/extensions/array'); +require('~/filtered_search/filtered_search_token_keys'); +require('~/filtered_search/filtered_search_tokenizer');  (() => {    describe('Filtered Search Tokenizer', () => { diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 index 99cebb32a8b..c61c32f8a13 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js.es6 +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -1,6 +1,6 @@ -//= require gfm_auto_complete -//= require jquery -//= require jquery.atwho +require('~/gfm_auto_complete'); +require('vendor/jquery.caret'); +require('vendor/jquery.atwho');  const global = window.gl || (window.gl = {});  const GfmAutoComplete = global.GfmAutoComplete; diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 4e7eed2767c..317f38c5888 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,11 +1,9 @@  /* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ -/* global Turbolinks */ -/*= require jquery */ -/*= require gl_dropdown */ -/*= require turbolinks */ -/*= require lib/utils/common_utils */ -/*= require lib/utils/type_utility */ +require('~/gl_dropdown'); +require('~/lib/utils/common_utils'); +require('~/lib/utils/type_utility'); +require('~/lib/utils/url_utility');  (() => {    const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; @@ -113,13 +111,13 @@          expect(this.dropdownContainerElement).toHaveClass('open');          const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;          navigateWithKeys('down', randomIndex, () => { -          spyOn(Turbolinks, 'visit').and.stub(); +          spyOn(gl.utils, 'visitUrl').and.stub();            navigateWithKeys('enter', null, () => {              expect(this.dropdownContainerElement).not.toHaveClass('open');              const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);              expect(link).toHaveClass('is-active');              const linkedLocation = link.attr('href'); -            if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation); +            if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);            });          });        }); diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index f68fd9e00d7..733023481f5 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,7 +1,6 @@  /* eslint-disable space-before-function-paren, arrow-body-style */ -//= require jquery -//= require gl_field_errors +require('~/gl_field_errors');  ((global) => {    preloadFixtures('static/gl_field_errors.html.raw'); diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js.es6 index b5f99483bfb..71d6e2a7e22 100644 --- a/spec/javascripts/gl_form_spec.js.es6 +++ b/spec/javascripts/gl_form_spec.js.es6 @@ -1,8 +1,9 @@  /* global autosize */ -/*= require gl_form */ -/*= require autosize */ -/*= require lib/utils/text_utility */ -/*= require lib/utils/common_utils */ + +window.autosize = require('vendor/autosize'); +require('~/gl_form'); +require('~/lib/utils/text_utility'); +require('~/lib/utils/common_utils');  describe('GLForm', () => {    const global = window.gl || (window.gl = {}); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index d76fcc5206a..a954bb60560 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -3,7 +3,7 @@  /* global ContributorsGraph */  /* global ContributorsMasterGraph */ -//= require graphs/stat_graph_contributors_graph +require('~/graphs/stat_graph_contributors_graph');  describe("ContributorsGraph", function () {    describe("#set_x_domain", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 63f28dfb8ad..b15764abe8c 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,7 @@  /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */  /* global ContributorsStatGraphUtil */ -//= require graphs/stat_graph_contributors_util +require('~/graphs/stat_graph_contributors_util');  describe("ContributorsStatGraphUtil", function () {    describe("#parse_log", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 71b589e6b83..876c23361bc 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,7 +1,7 @@  /* eslint-disable quotes */  /* global StatGraph */ -//= require graphs/stat_graph +require('~/graphs/stat_graph');  describe("StatGraph", function () {    describe("#get_log", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index b846c5ab00b..cecebb0b038 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,7 +1,7 @@  /* eslint-disable space-before-function-paren, no-var */ -/*= require header */ -/*= require lib/utils/text_utility */ -/*= require jquery */ + +require('~/header'); +require('~/lib/utils/text_utility');  (function() {    describe('Header', function() { diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js.es6 index d1155f1bd1e..0a61e561640 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper_spec.js.es6 @@ -1,5 +1,6 @@  /* global ClassSpecHelper */ -//= require ./class_spec_helper + +require('./class_spec_helper');  describe('ClassSpecHelper', () => {    describe('.itShouldBeAStaticMethod', function () { diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index 917a6267b92..26d87cc5931 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,8 +1,7 @@  /* global Issuable */ -/* global Turbolinks */ -//= require issuable -//= require turbolinks +require('~/lib/utils/url_utility'); +require('~/issuable');  (() => {    const BASE_URL = '/user/project/issues?scope=all&state=closed'; @@ -42,39 +41,39 @@        });        it('should contain only the default parameters', () => { -        spyOn(Turbolinks, 'visit'); +        spyOn(gl.utils, 'visitUrl');          Issuable.filterResults($filtersForm); -        expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); +        expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);        });        it('should filter for the phrase "broken"', () => { -        spyOn(Turbolinks, 'visit'); +        spyOn(gl.utils, 'visitUrl');          updateForm({ search: 'broken' }, $filtersForm);          Issuable.filterResults($filtersForm);          const params = `${DEFAULT_PARAMS}&search=broken`; -        expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); +        expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);        });        it('should keep query parameters after modifying filter', () => { -        spyOn(Turbolinks, 'visit'); +        spyOn(gl.utils, 'visitUrl');          // initial filter          updateForm({ milestone_title: 'v1.0' }, $filtersForm);          Issuable.filterResults($filtersForm);          let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; -        expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); +        expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);          // update filter          updateForm({ label_name: 'Frontend' }, $filtersForm);          Issuable.filterResults($filtersForm);          params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; -        expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); +        expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);        });      });    }); diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 index c5671af235e..cb068a4f879 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js.es6 +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -1,7 +1,8 @@  /* eslint-disable */ -//= require jquery -//= require vue -//= require issuable/time_tracking/components/time_tracker + +require('jquery'); +require('vue'); +require('~/issuable/time_tracking/components/time_tracker');  function initTimeTrackingComponent(opts) {    setFixtures(` diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 673a4b3c07a..5b0b7aa7903 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,8 +1,8 @@  /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */  /* global Issue */ -/*= require lib/utils/text_utility */ -/*= require issue */ +require('~/lib/utils/text_utility'); +require('~/issue');  (function() {    var INVALID_URL = 'http://goesnowhere.nothing/whereami'; diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 0d19b4a25b9..37e038c16da 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -2,17 +2,15 @@  /* global IssuableContext */  /* global LabelsSelect */ -//= require lib/utils/type_utility -//= require jquery -//= require bootstrap -//= require gl_dropdown -//= require select2 -//= require jquery.nicescroll -//= require api -//= require create_label -//= require issuable_context -//= require users_select -//= require labels_select +require('~/lib/utils/type_utility'); +require('~/gl_dropdown'); +require('select2'); +require('vendor/jquery.nicescroll'); +require('~/api'); +require('~/create_label'); +require('~/issuable_context'); +require('~/users_select'); +require('~/labels_select');  (() => {    let saveLabelCount = 0; diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 32c96e2a088..fbb06f3948b 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/common_utils +require('~/lib/utils/common_utils');  (() => {    describe('common_utils', () => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index e97356b65d5..86ade66ec29 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/text_utility +require('~/lib/utils/text_utility');  (() => {    describe('text_utility', () => { @@ -21,5 +21,19 @@          expect(largeFont > regular).toBe(true);        });      }); + +    describe('gl.text.pluralize', () => { +      it('returns pluralized', () => { +        expect(gl.text.pluralize('test', 2)).toBe('tests'); +      }); + +      it('returns pluralized when count is 0', () => { +        expect(gl.text.pluralize('test', 0)).toBe('tests'); +      }); + +      it('does not return pluralized', () => { +        expect(gl.text.pluralize('test', 1)).toBe('test'); +      }); +    });    });  })(); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 6605986c33a..8b196f7720f 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,7 @@  /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */  /* global LineHighlighter */ -/*= require line_highlighter */ +require('~/line_highlighter');  (function() {    describe('LineHighlighter', function() { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index f644d39b1c7..25cfa9e9479 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,7 @@  /* eslint-disable space-before-function-paren, no-return-assign */  /* global MergeRequest */ -/*= require merge_request */ +require('~/merge_request');  (function() {    describe('MergeRequest', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 98201fb98ed..d20a59df041 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,11 +1,20 @@  /* eslint-disable no-var, comma-dangle, object-shorthand */ -/*= require merge_request_tabs */ -//= require breakpoints -//= require lib/utils/common_utils -//= require jquery.scrollTo +require('~/merge_request_tabs'); +require('~/breakpoints'); +require('~/lib/utils/common_utils'); +require('vendor/jquery.scrollTo');  (function () { +  // TODO: remove this hack! +  // PhantomJS causes spyOn to panic because replaceState isn't "writable" +  var phantomjs; +  try { +    phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; +  } catch (err) { +    phantomjs = false; +  } +    describe('MergeRequestTabs', function () {      var stubLocation = {};      var setLocation = function (stubs) { @@ -22,9 +31,11 @@        this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });        setLocation(); -      this.spies = { -        history: spyOn(window.history, 'replaceState').and.callFake(function () {}) -      }; +      if (!phantomjs) { +        this.spies = { +          history: spyOn(window.history, 'replaceState').and.callFake(function () {}) +        }; +      }      });      describe('#activateTab', function () { @@ -98,10 +109,11 @@            pathname: '/foo/bar/merge_requests/1'          });          newState = this.subject('commits'); -        expect(this.spies.history).toHaveBeenCalledWith({ -          turbolinks: true, -          url: newState -        }, document.title, newState); +        if (!phantomjs) { +          expect(this.spies.history).toHaveBeenCalledWith({ +            url: newState +          }, document.title, newState); +        }        });        it('treats "show" like "notes"', function () {          setLocation({ diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 6f1d6406897..8cefdd2409d 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,8 +1,8 @@  /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ -/*= require merge_request_widget */ -/*= require smart_interval */ -/*= require lib/utils/datetime_utility */ +require('~/merge_request_widget'); +require('~/smart_interval'); +require('~/lib/utils/datetime_utility');  (function() {    describe('MergeRequestWidget', function() { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index a1c2fe3df37..a6994f6edf4 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -1,7 +1,7 @@  /* eslint-disable no-new */ -//= require flash -//= require mini_pipeline_graph_dropdown +require('~/flash'); +require('~/mini_pipeline_graph_dropdown');  (() => {    describe('Mini Pipeline Graph Dropdown', () => { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8259d553f1b..9b657868523 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,8 +1,8 @@  /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */  /* global NewBranchForm */ -/*= require jquery-ui/autocomplete */ -/*= require new_branch_form */ +require('jquery-ui/ui/autocomplete'); +require('~/new_branch_form');  (function() {    describe('Branch', function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 015c35dfca7..af495787c54 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,10 +1,10 @@  /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */  /* global Notes */ -/*= require notes */ -/*= require autosize */ -/*= require gl_form */ -/*= require lib/utils/text_utility */ +require('~/notes'); +require('vendor/autosize'); +require('~/gl_form'); +require('~/lib/utils/text_utility');  (function() {    window.gon || (window.gon = {}); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index f0f9ad7430d..72770a702d3 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,4 +1,9 @@ -//= require pipelines +require('~/pipelines'); + +// Fix for phantomJS +if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) { +  Element.prototype.matches = Element.prototype.webkitMatchesSelector; +}  (() => {    describe('Pipelines', () => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 7a04fba5f7f..a4662cfb557 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/pretty_time +require('~/lib/utils/pretty_time');  (() => {    const prettyTime = gl.utils.prettyTime; diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index e562385a6c6..e0b52f767e4 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,14 +1,12 @@  /* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */ -  /* global Project */ -/*= require bootstrap */ -/*= require select2 */ -/*= require lib/utils/type_utility */ -/*= require gl_dropdown */ -/*= require api */ -/*= require project_select */ -/*= require project */ +require('select2/select2.js'); +require('~/lib/utils/type_utility'); +require('~/gl_dropdown'); +require('~/api'); +require('~/project_select'); +require('~/project');  (function() {    window.gon || (window.gon = {}); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 3a01a534557..f7636865aa1 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,11 +1,8 @@  /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */  /* global Sidebar */ -/*= require right_sidebar */ -/*= require jquery */ -/*= require js.cookie */ - -/*= require extensions/jquery.js */ +require('~/right_sidebar'); +require('~/extensions/jquery.js');  (function() {    var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 7ac9710654f..c79e30e9481 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,13 +1,10 @@  /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ -/*= require gl_dropdown */ -/*= require search_autocomplete */ -/*= require jquery */ -/*= require lib/utils/common_utils */ -/*= require lib/utils/type_utility */ -/*= require fuzzaldrin-plus */ -/*= require turbolinks */ -/*= require jquery.turbolinks */ +require('~/gl_dropdown'); +require('~/search_autocomplete'); +require('~/lib/utils/common_utils'); +require('~/lib/utils/type_utility'); +require('vendor/fuzzaldrin-plus');  (function() {    var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; @@ -117,13 +114,15 @@      preloadFixtures('static/search_autocomplete.html.raw');      beforeEach(function() {        loadFixtures('static/search_autocomplete.html.raw'); -      return widget = new gl.SearchAutocomplete; +      widget = new gl.SearchAutocomplete; +      // Prevent turbolinks from triggering within gl_dropdown +      spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);      });      it('should show Dashboard specific dropdown menu', function() {        var list;        addBodyAttributes();        mockDashboardOptions(); -      widget.searchInput.focus(); +      widget.searchInput.triggerHandler('focus');        list = widget.wrap.find('.dropdown-menu').find('ul');        return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);      }); @@ -131,7 +130,7 @@        var list;        addBodyAttributes('group');        mockGroupOptions(); -      widget.searchInput.focus(); +      widget.searchInput.triggerHandler('focus');        list = widget.wrap.find('.dropdown-menu').find('ul');        return assertLinks(list, groupIssuesPath, groupMRsPath);      }); @@ -139,7 +138,7 @@        var list;        addBodyAttributes('project');        mockProjectOptions(); -      widget.searchInput.focus(); +      widget.searchInput.triggerHandler('focus');        list = widget.wrap.find('.dropdown-menu').find('ul');        return assertLinks(list, projectIssuesPath, projectMRsPath);      }); @@ -148,7 +147,7 @@        addBodyAttributes('project');        mockProjectOptions();        widget.searchInput.val('help'); -      widget.searchInput.focus(); +      widget.searchInput.triggerHandler('focus');        list = widget.wrap.find('.dropdown-menu').find('ul');        link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";        return expect(list.find(link).length).toBe(0); @@ -159,7 +158,7 @@        addBodyAttributes();        mockDashboardOptions(true);        var submitSpy = spyOnEvent('form', 'submit'); -      widget.searchInput.focus(); +      widget.searchInput.triggerHandler('focus');        widget.wrap.trigger($.Event('keydown', { which: DOWN }));        var enterKeyEvent = $.Event('keydown', { which: ENTER });        widget.searchInput.trigger(enterKeyEvent); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index e0a5a7927bb..602ac01aec3 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,8 +1,8 @@  /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */  /* global ShortcutsIssuable */ -/*= require copy_as_gfm */ -/*= require shortcuts_issuable */ +require('~/copy_as_gfm'); +require('~/shortcuts_issuable');  (function() {    describe('ShortcutsIssuable', function() { diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index c274b9c45f4..d83d9a57b42 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require signin_tabs_memoizer */ +require('~/signin_tabs_memoizer');  ((global) => {    describe('SigninTabsMemoizer', () => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 39d236986b9..4366ec2a5b8 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -1,5 +1,4 @@ -//= require jquery -//= require smart_interval +require('~/smart_interval');  (() => {    const DEFAULT_MAX_INTERVAL = 100; @@ -164,7 +163,7 @@          const interval = this.smartInterval;          setTimeout(() => { -          $(document).trigger('page:before-unload'); +          $(document).triggerHandler('beforeunload');            expect(interval.state.intervalId).toBeUndefined();            expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);            done(); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js deleted file mode 100644 index f8e3aca29fa..00000000000 --- a/spec/javascripts/spec_helper.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable space-before-function-paren */ -// PhantomJS (Teaspoons default driver) doesn't have support for -// Function.prototype.bind, which has caused confusion.  Use this polyfill to -// avoid the confusion. -/*= require support/bind-poly */ - -// You can require your own javascript files here. By default this will include -// everything in application, however you may get better load performance if you -// require the specific files that are being used in the spec that tests them. -/*= require jquery */ -/*= require jquery.turbolinks */ -/*= require bootstrap */ -/*= require underscore */ - -// Teaspoon includes some support files, but you can use anything from your own -// support path too. -// require support/jasmine-jquery-1.7.0 -// require support/jasmine-jquery-2.0.0 -/*= require support/jasmine-jquery-2.1.0 */ - -// require support/sinon -// require support/your-support-file -// Deferring execution -// If you're using CommonJS, RequireJS or some other asynchronous library you can -// defer execution. Call Teaspoon.execute() after everything has been loaded. -// Simple example of a timeout: -// Teaspoon.defer = true -// setTimeout(Teaspoon.execute, 1000) -// Matching files -// By default Teaspoon will look for files that match -// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path -// and it'll be included in the default suite automatically. If you want to -// customize suites, check out the configuration in teaspoon_env.rb -// Manifest -// If you'd rather require your spec files manually (to control order for -// instance) you can disable the suite matcher in the configuration and use this -// file as a manifest. -// For more information: http://github.com/modeset/teaspoon - -// set our fixtures path -jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures'; -jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures'; - -// defined in ActionDispatch::TestRequest -// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 -window.gl = window.gl || {}; -window.gl.TEST_HOST = 'http://test.host'; -window.gon = window.gon || {}; diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index 99f45850ea3..454386697f5 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,9 +1,6 @@  /* eslint-disable max-len, arrow-parens, comma-dangle */ -//= vue -//= vue-resource -//= require jquery -//= require subbable_resource +require('~/subbable_resource');  /*  * Test that each rest verb calls the publish and subscribe function and passes the correct value back diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 436f7064a69..c0c3837d1f4 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,6 +1,6 @@  /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ -/*= require syntax_highlight */ +require('~/syntax_highlight');  (function() {    describe('Syntax Highlighter', function() { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js new file mode 100644 index 00000000000..bf11ddbbea8 --- /dev/null +++ b/spec/javascripts/test_bundle.js @@ -0,0 +1,40 @@ +// enable test fixtures +require('jasmine-jquery'); + +jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; +jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; + +// include common libraries +window.$ = window.jQuery = require('jquery'); +window._ = require('underscore'); +window.Cookies = require('vendor/js.cookie'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('jquery-ujs'); +require('bootstrap/js/affix'); +require('bootstrap/js/alert'); +require('bootstrap/js/button'); +require('bootstrap/js/collapse'); +require('bootstrap/js/dropdown'); +require('bootstrap/js/modal'); +require('bootstrap/js/scrollspy'); +require('bootstrap/js/tab'); +require('bootstrap/js/transition'); +require('bootstrap/js/tooltip'); +require('bootstrap/js/popover'); + +// stub expected globals +window.gl = window.gl || {}; +window.gl.TEST_HOST = 'http://test.host'; +window.gon = window.gon || {}; + +// render all of our tests +const testsContext = require.context('.', true, /_spec$/); +testsContext.keys().forEach(function (path) { +  try { +    testsContext(path); +  } catch (err) { +    console.error('[ERROR] WITH SPEC FILE: ', path); +    console.error(err); +  } +}); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 0e2fb07ba7f..cba1af4daa4 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,11 +2,11 @@  /* global MockU2FDevice */  /* global U2FAuthenticate */ -/*= require u2f/authenticate */ -/*= require u2f/util */ -/*= require u2f/error */ -/*= require u2f */ -/*= require ./mock_u2f_device */ +require('~/u2f/authenticate'); +require('~/u2f/util'); +require('~/u2f/error'); +require('vendor/u2f'); +require('./mock_u2f_device');  (function() {    describe('U2FAuthenticate', function() { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0790553b67e..10578c2c4b5 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,11 +2,11 @@  /* global MockU2FDevice */  /* global U2FRegister */ -/*= require u2f/register */ -/*= require u2f/util */ -/*= require u2f/error */ -/*= require u2f */ -/*= require ./mock_u2f_device */ +require('~/u2f/register'); +require('~/u2f/util'); +require('~/u2f/error'); +require('vendor/u2f'); +require('./mock_u2f_device');  (function() {    describe('U2FRegister', function() { diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6 index b21f6912e06..9727c03c91e 100644 --- a/spec/javascripts/visibility_select_spec.js.es6 +++ b/spec/javascripts/visibility_select_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require visibility_select */ +require('~/visibility_select');  (() => {    const VisibilitySelect = gl.VisibilitySelect; diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d6c6f786fb1..bbd914de4ea 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -//= require vue_common_component/commit +require('~/vue_common_component/commit');  describe('Commit component', () => {    let props; diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 index efb11211ce2..8935c474ee5 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -1,6 +1,5 @@ -//= require vue -//= require lib/utils/common_utils -//= require vue_pagination/index +require('~/lib/utils/common_utils'); +require('~/vue_pagination/index');  describe('Pagination component', () => {    let component; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index be706ca304f..ce33a6814aa 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -3,7 +3,7 @@  /* global Mousetrap */  /* global ZenMode */ -/*= require zen_mode */ +require('~/zen_mode');  (function() {    var enterZen, escapeKeydown, exitZen; diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index e3066311b7d..d70690f589d 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -5,15 +5,15 @@ describe EventFilter, lib: true do      let(:source_user) { create(:user) }      let!(:public_project) { create(:empty_project, :public) } -    let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) } -    let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) } -    let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) } -    let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) } -    let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) } -    let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) } -    let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) } -    let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) } -    let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) } +    let!(:push_event)     { create(:event, :pushed,    project: public_project, target: public_project, author: source_user) } +    let!(:merged_event)   { create(:event, :merged,    project: public_project, target: public_project, author: source_user) } +    let!(:created_event)  { create(:event, :created,   project: public_project, target: public_project, author: source_user) } +    let!(:updated_event)  { create(:event, :updated,   project: public_project, target: public_project, author: source_user) } +    let!(:closed_event)   { create(:event, :closed,    project: public_project, target: public_project, author: source_user) } +    let!(:reopened_event) { create(:event, :reopened,  project: public_project, target: public_project, author: source_user) } +    let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) } +    let!(:joined_event)   { create(:event, :joined,    project: public_project, target: public_project, author: source_user) } +    let!(:left_event)     { create(:event, :left,      project: public_project, target: public_project, author: source_user) }      it 'applies push filter' do        events = EventFilter.new(EventFilter.push).apply_filter(Event.all) diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index d480c3821ec..1d65b24c2c9 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -182,7 +182,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do             project: project,             commit_id: ci_pipeline.sha) -    create(:event, target: milestone, project: project, action: Event::CREATED, author: user) +    create(:event, :created, target: milestone, project: project, author: user)      create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')      project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7dd4d76d1a3..a32c6131030 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -42,7 +42,8 @@ describe Gitlab::Workhorse, lib: true do        out = {          subprotocols: ['foo'],          url: 'wss://example.com/terminal.ws', -        headers: { 'Authorization' => ['Token x'] } +        headers: { 'Authorization' => ['Token x'] }, +        max_session_time: 600        }        out[:ca_pem] = ca_pem if ca_pem        out @@ -53,7 +54,8 @@ describe Gitlab::Workhorse, lib: true do          'Terminal' => {            'Subprotocols' => ['foo'],            'Url' => 'wss://example.com/terminal.ws', -          'Header' => { 'Authorization' => ['Token x'] } +          'Header' => { 'Authorization' => ['Token x'] }, +          'MaxSessionTime' => 600          }        }        out['Terminal']['CAPem'] = ca_pem if ca_pem diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 349474bb656..8c90a538f57 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -19,7 +19,7 @@ describe Event, models: true do        let(:project) { create(:empty_project) }        it 'calls the reset_project_activity method' do -        expect_any_instance_of(Event).to receive(:reset_project_activity) +        expect_any_instance_of(described_class).to receive(:reset_project_activity)          create_event(project, project.owner)        end @@ -43,33 +43,33 @@ describe Event, models: true do    describe '#membership_changed?' do      context "created" do -      subject { build(:event, action: Event::CREATED).membership_changed? } +      subject { build(:event, :created).membership_changed? }        it { is_expected.to be_falsey }      end      context "updated" do -      subject { build(:event, action: Event::UPDATED).membership_changed? } +      subject { build(:event, :updated).membership_changed? }        it { is_expected.to be_falsey }      end      context "expired" do -      subject { build(:event, action: Event::EXPIRED).membership_changed? } +      subject { build(:event, :expired).membership_changed? }        it { is_expected.to be_truthy }      end      context "left" do -      subject { build(:event, action: Event::LEFT).membership_changed? } +      subject { build(:event, :left).membership_changed? }        it { is_expected.to be_truthy }      end      context "joined" do -      subject { build(:event, action: Event::JOINED).membership_changed? } +      subject { build(:event, :joined).membership_changed? }        it { is_expected.to be_truthy }      end    end    describe '#note?' do -    subject { Event.new(project: target.project, target: target) } +    subject { described_class.new(project: target.project, target: target) }      context 'issue note event' do        let(:target) { create(:note_on_issue) } @@ -97,7 +97,7 @@ describe Event, models: true do      let(:note_on_commit) { create(:note_on_commit, project: project) }      let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }      let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) } -    let(:event) { Event.new(project: project, target: target, author_id: author.id) } +    let(:event) { described_class.new(project: project, target: target, author_id: author.id) }      before do        project.team << [member, :developer] @@ -221,13 +221,13 @@ describe Event, models: true do      let!(:event2) { create(:closed_issue_event) }      describe 'without an explicit limit' do -      subject { Event.limit_recent } +      subject { described_class.limit_recent }        it { is_expected.to eq([event2, event1]) }      end      describe 'with an explicit limit' do -      subject { Event.limit_recent(1) } +      subject { described_class.limit_recent(1) }        it { is_expected.to eq([event2]) }      end @@ -294,9 +294,9 @@ describe Event, models: true do        }      } -    Event.create({ +    described_class.create({        project: project, -      action: Event::PUSHED, +      action: described_class::PUSHED,        data: data,        author_id: user.id      }.merge!(attrs)) diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 9e1a52011c3..e6ca4853873 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -19,13 +19,6 @@ describe List do        expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)      end -    context 'when list_type is set to backlog' do -      subject { described_class.new(list_type: :backlog) } - -      it { is_expected.not_to validate_presence_of(:label) } -      it { is_expected.not_to validate_presence_of(:position) } -    end -      context 'when list_type is set to done' do        subject { described_class.new(list_type: :done) } @@ -41,12 +34,6 @@ describe List do        expect(subject.destroy).to be_truthy      end -    it 'can not be destroyed when list_type is set to backlog' do -      subject = create(:backlog_list) - -      expect(subject.destroy).to be_falsey -    end -      it 'can not be destroyed when when list_type is set to done' do        subject = create(:done_list) @@ -55,19 +42,13 @@ describe List do    end    describe '#destroyable?' do -    it 'retruns true when list_type is set to label' do +    it 'returns true when list_type is set to label' do        subject.list_type = :label        expect(subject).to be_destroyable      end -    it 'retruns false when list_type is set to backlog' do -      subject.list_type = :backlog - -      expect(subject).not_to be_destroyable -    end - -    it 'retruns false when list_type is set to done' do +    it 'returns false when list_type is set to done' do        subject.list_type = :done        expect(subject).not_to be_destroyable @@ -75,19 +56,13 @@ describe List do    end    describe '#movable?' do -    it 'retruns true when list_type is set to label' do +    it 'returns true when list_type is set to label' do        subject.list_type = :label        expect(subject).to be_movable      end -    it 'retruns false when list_type is set to backlog' do -      subject.list_type = :backlog - -      expect(subject).not_to be_movable -    end - -    it 'retruns false when list_type is set to done' do +    it 'returns false when list_type is set to done' do        subject.list_type = :done        expect(subject).not_to be_movable @@ -102,12 +77,6 @@ describe List do        expect(subject.title).to eq 'Development'      end -    it 'returns Backlog when list_type is set to backlog' do -      subject.list_type = :backlog - -      expect(subject.title).to eq 'Backlog' -    end -      it 'returns Done when list_type is set to done' do        subject.list_type = :done diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 4f3cd14e941..9052479d35e 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -181,11 +181,23 @@ describe KubernetesService, models: true, caching: true do        let(:pod) { kube_pod(app: environment.slug) }        let(:terminals) { kube_terminals(service, pod) } -      it 'returns terminals' do -        stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]) +      before do +        stub_reactive_cache( +          service, +          pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ] +        ) +      end +      it 'returns terminals' do          is_expected.to eq(terminals + terminals)        end + +      it 'uses max session time from settings' do +        stub_application_setting(terminal_max_session_time: 600) + +        times = subject.map { |terminal| terminal[:max_session_time] } +        expect(times).to eq [600, 600, 600, 600] +      end      end    end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6ca5ad747d1..6d58b1455c4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1013,8 +1013,8 @@ describe User, models: true do      let!(:project2) { create(:empty_project, forked_from_project: project3) }      let!(:project3) { create(:empty_project) }      let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } -    let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) } -    let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) } +    let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) } +    let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }      before do        project1.team << [subject, :master] @@ -1058,7 +1058,7 @@ describe User, models: true do      let!(:push_data) do        Gitlab::DataBuilder::Push.build_sample(project2, subject)      end -    let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) } +    let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }      before do        project1.team << [subject, :master] @@ -1086,7 +1086,7 @@ describe User, models: true do        expect(subject.recent_push(project2)).to eq(push_event)        push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject) -      push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1) +      push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)        expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest      end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index f197fadebab..834c4e52693 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -188,6 +188,7 @@ describe API::Builds, api: true do          it 'returns specific job artifacts' do            expect(response).to have_http_status(200)            expect(response.headers).to include(download_headers) +          expect(response.body).to match_file(build.artifacts_file.file.file)          end        end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 753dde0ca3a..225e2e005df 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1085,52 +1085,6 @@ describe API::Projects, api: true  do      end    end -  describe 'GET /projects/search/:query' do -    let!(:query)            { 'query'} -    let!(:search)           { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } -    let!(:pre)              { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } -    let!(:post)             { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } -    let!(:pre_post)         { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } -    let!(:unfound)          { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } -    let!(:internal)         { create(:empty_project, :internal, name: "internal #{query}") } -    let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } -    let!(:public)           { create(:empty_project, :public, name: "public #{query}") } -    let!(:unfound_public)   { create(:empty_project, :public, name: 'unfound public') } -    let!(:one_dot_two)      { create(:empty_project, :public, name: "one.dot.two") } - -    shared_examples_for 'project search response' do |args = {}| -      it 'returns project search responses' do -        get api("/projects/search/#{args[:query]}", current_user) - -        expect(response).to have_http_status(200) -        expect(json_response).to be_an Array -        expect(json_response.size).to eq(args[:results]) -        json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } -      end -    end - -    context 'when unauthenticated' do -      it_behaves_like 'project search response', query: 'query', results: 1 do -        let(:current_user) { nil } -      end -    end - -    context 'when authenticated' do -      it_behaves_like 'project search response', query: 'query', results: 6 do -        let(:current_user) { user } -      end -      it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do -        let(:current_user) { user } -      end -    end - -    context 'when authenticated as a different user' do -      it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do -        let(:current_user) { user2 } -      end -    end -  end -    describe 'PUT /projects/:id' do      before { project }      before { user } diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb new file mode 100644 index 00000000000..a495122bba7 --- /dev/null +++ b/spec/requests/api/v3/projects_spec.rb @@ -0,0 +1,1424 @@ +require 'spec_helper' + +describe API::V3::Projects, api: true do +  include ApiHelpers +  include Gitlab::CurrentSettings + +  let(:user) { create(:user) } +  let(:user2) { create(:user) } +  let(:user3) { create(:user) } +  let(:admin) { create(:admin) } +  let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } +  let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) } +  let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } +  let(:project_member) { create(:project_member, :master, user: user, project: project) } +  let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } +  let(:user4) { create(:user) } +  let(:project3) do +    create(:project, +    :private, +    :repository, +    name: 'second_project', +    path: 'second_project', +    creator_id: user.id, +    namespace: user.namespace, +    merge_requests_enabled: false, +    issues_enabled: false, wiki_enabled: false, +    snippets_enabled: false) +  end +  let(:project_member3) do +    create(:project_member, +    user: user4, +    project: project3, +    access_level: ProjectMember::MASTER) +  end +  let(:project4) do +    create(:empty_project, +    name: 'third_project', +    path: 'third_project', +    creator_id: user4.id, +    namespace: user4.namespace) +  end + +  describe 'GET /projects' do +    before { project } + +    context 'when unauthenticated' do +      it 'returns authentication error' do +        get v3_api('/projects') +        expect(response).to have_http_status(401) +      end +    end + +    context 'when authenticated as regular user' do +      it 'returns an array of projects' do +        get v3_api('/projects', user) +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.first['name']).to eq(project.name) +        expect(json_response.first['owner']['username']).to eq(user.username) +      end + +      it 'includes the project labels as the tag_list' do +        get v3_api('/projects', user) +        expect(response.status).to eq 200 +        expect(json_response).to be_an Array +        expect(json_response.first.keys).to include('tag_list') +      end + +      it 'includes open_issues_count' do +        get v3_api('/projects', user) +        expect(response.status).to eq 200 +        expect(json_response).to be_an Array +        expect(json_response.first.keys).to include('open_issues_count') +      end + +      it 'does not include open_issues_count' do +        project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + +        get v3_api('/projects', user) +        expect(response.status).to eq 200 +        expect(json_response).to be_an Array +        expect(json_response.first.keys).not_to include('open_issues_count') +      end + +      context 'GET /projects?simple=true' do +        it 'returns a simplified version of all the projects' do +          expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] + +          get v3_api('/projects?simple=true', user) + +          expect(response).to have_http_status(200) +          expect(json_response).to be_an Array +          expect(json_response.first.keys).to match_array expected_keys +        end +      end + +      context 'and using search' do +        it 'returns searched project' do +          get v3_api('/projects', user), { search: project.name } +          expect(response).to have_http_status(200) +          expect(json_response).to be_an Array +          expect(json_response.length).to eq(1) +        end +      end + +      context 'and using the visibility filter' do +        it 'filters based on private visibility param' do +          get v3_api('/projects', user), { visibility: 'private' } +          expect(response).to have_http_status(200) +          expect(json_response).to be_an Array +          expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) +        end + +        it 'filters based on internal visibility param' do +          get v3_api('/projects', user), { visibility: 'internal' } +          expect(response).to have_http_status(200) +          expect(json_response).to be_an Array +          expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) +        end + +        it 'filters based on public visibility param' do +          get v3_api('/projects', user), { visibility: 'public' } +          expect(response).to have_http_status(200) +          expect(json_response).to be_an Array +          expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) +        end +      end + +      context 'and using sorting' do +        before do +          project2 +          project3 +        end + +        it 'returns the correct order when sorted by id' do +          get v3_api('/projects', user), { order_by: 'id', sort: 'desc' } +          expect(response).to have_http_status(200) +          expect(json_response).to be_an Array +          expect(json_response.first['id']).to eq(project3.id) +        end +      end +    end +  end + +  describe 'GET /projects/all' do +    before { project } + +    context 'when unauthenticated' do +      it 'returns authentication error' do +        get v3_api('/projects/all') +        expect(response).to have_http_status(401) +      end +    end + +    context 'when authenticated as regular user' do +      it 'returns authentication error' do +        get v3_api('/projects/all', user) +        expect(response).to have_http_status(403) +      end +    end + +    context 'when authenticated as admin' do +      it 'returns an array of all projects' do +        get v3_api('/projects/all', admin) +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array + +        expect(json_response).to satisfy do |response| +          response.one? do |entry| +            entry.has_key?('permissions') && +              entry['name'] == project.name && +              entry['owner']['username'] == user.username +          end +        end +      end + +      it "does not include statistics by default" do +        get v3_api('/projects/all', admin) + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.first).not_to include('statistics') +      end + +      it "includes statistics if requested" do +        get v3_api('/projects/all', admin), statistics: true + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.first).to include 'statistics' +      end +    end +  end + +  describe 'GET /projects/owned' do +    before do +      project3 +      project4 +    end + +    context 'when unauthenticated' do +      it 'returns authentication error' do +        get v3_api('/projects/owned') +        expect(response).to have_http_status(401) +      end +    end + +    context 'when authenticated as project owner' do +      it 'returns an array of projects the user owns' do +        get v3_api('/projects/owned', user4) +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.first['name']).to eq(project4.name) +        expect(json_response.first['owner']['username']).to eq(user4.username) +      end + +      it "does not include statistics by default" do +        get v3_api('/projects/owned', user4) + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.first).not_to include('statistics') +      end + +      it "includes statistics if requested" do +        attributes = { +          commit_count: 23, +          storage_size: 702, +          repository_size: 123, +          lfs_objects_size: 234, +          build_artifacts_size: 345, +        } + +        project4.statistics.update!(attributes) + +        get v3_api('/projects/owned', user4), statistics: true + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.first['statistics']).to eq attributes.stringify_keys +      end +    end +  end + +  describe 'GET /projects/visible' do +    shared_examples_for 'visible projects response' do +      it 'returns the visible projects' do +        get v3_api('/projects/visible', current_user) + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) +      end +    end + +    let!(:public_project) { create(:empty_project, :public) } +    before do +      project +      project2 +      project3 +      project4 +    end + +    context 'when unauthenticated' do +      it_behaves_like 'visible projects response' do +        let(:current_user) { nil } +        let(:projects) { [public_project] } +      end +    end + +    context 'when authenticated' do +      it_behaves_like 'visible projects response' do +        let(:current_user) { user } +        let(:projects) { [public_project, project, project2, project3] } +      end +    end + +    context 'when authenticated as a different user' do +      it_behaves_like 'visible projects response' do +        let(:current_user) { user2 } +        let(:projects) { [public_project] } +      end +    end +  end + +  describe 'GET /projects/starred' do +    let(:public_project) { create(:empty_project, :public) } + +    before do +      project_member2 +      user3.update_attributes(starred_projects: [project, project2, project3, public_project]) +    end + +    it 'returns the starred projects viewable by the user' do +      get v3_api('/projects/starred', user3) +      expect(response).to have_http_status(200) +      expect(json_response).to be_an Array +      expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) +    end +  end + +  describe 'POST /projects' do +    context 'maximum number of projects reached' do +      it 'does not create new project and respond with 403' do +        allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) +        expect { post v3_api('/projects', user2), name: 'foo' }. +          to change {Project.count}.by(0) +        expect(response).to have_http_status(403) +      end +    end + +    it 'creates new project without path and return 201' do +      expect { post v3_api('/projects', user), name: 'foo' }. +        to change { Project.count }.by(1) +      expect(response).to have_http_status(201) +    end + +    it 'creates last project before reaching project limit' do +      allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) +      post v3_api('/projects', user2), name: 'foo' +      expect(response).to have_http_status(201) +    end + +    it 'does not create new project without name and return 400' do +      expect { post v3_api('/projects', user) }.not_to change { Project.count } +      expect(response).to have_http_status(400) +    end + +    it "assigns attributes to project" do +      project = attributes_for(:project, { +        path: 'camelCasePath', +        description: FFaker::Lorem.sentence, +        issues_enabled: false, +        merge_requests_enabled: false, +        wiki_enabled: false, +        only_allow_merge_if_build_succeeds: false, +        request_access_enabled: true, +        only_allow_merge_if_all_discussions_are_resolved: false +      }) + +      post v3_api('/projects', user), project + +      project.each_pair do |k, v| +        next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) +        expect(json_response[k.to_s]).to eq(v) +      end + +      # Check feature permissions attributes +      project = Project.find_by_path(project[:path]) +      expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) +      expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) +      expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) +    end + +    it 'sets a project as public' do +      project = attributes_for(:project, :public) +      post v3_api('/projects', user), project +      expect(json_response['public']).to be_truthy +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) +    end + +    it 'sets a project as public using :public' do +      project = attributes_for(:project, { public: true }) +      post v3_api('/projects', user), project +      expect(json_response['public']).to be_truthy +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) +    end + +    it 'sets a project as internal' do +      project = attributes_for(:project, :internal) +      post v3_api('/projects', user), project +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) +    end + +    it 'sets a project as internal overriding :public' do +      project = attributes_for(:project, :internal, { public: true }) +      post v3_api('/projects', user), project +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) +    end + +    it 'sets a project as private' do +      project = attributes_for(:project, :private) +      post v3_api('/projects', user), project +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) +    end + +    it 'sets a project as private using :public' do +      project = attributes_for(:project, { public: false }) +      post v3_api('/projects', user), project +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) +    end + +    it 'sets a project as allowing merge even if build fails' do +      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) +      post v3_api('/projects', user), project +      expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey +    end + +    it 'sets a project as allowing merge only if build succeeds' do +      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) +      post v3_api('/projects', user), project +      expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy +    end + +    it 'sets a project as allowing merge even if discussions are unresolved' do +      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + +      post v3_api('/projects', user), project + +      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey +    end + +    it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do +      project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil) + +      post v3_api('/projects', user), project + +      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey +    end + +    it 'sets a project as allowing merge only if all discussions are resolved' do +      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + +      post v3_api('/projects', user), project + +      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy +    end + +    context 'when a visibility level is restricted' do +      before do +        @project = attributes_for(:project, { public: true }) +        stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) +      end + +      it 'does not allow a non-admin to use a restricted visibility level' do +        post v3_api('/projects', user), @project + +        expect(response).to have_http_status(400) +        expect(json_response['message']['visibility_level'].first).to( +          match('restricted by your GitLab administrator') +        ) +      end + +      it 'allows an admin to override restricted visibility settings' do +        post v3_api('/projects', admin), @project +        expect(json_response['public']).to be_truthy +        expect(json_response['visibility_level']).to( +          eq(Gitlab::VisibilityLevel::PUBLIC) +        ) +      end +    end +  end + +  describe 'POST /projects/user/:id' do +    before { project } +    before { admin } + +    it 'should create new project without path and return 201' do +      expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) +      expect(response).to have_http_status(201) +    end + +    it 'responds with 400 on failure and not project' do +      expect { post v3_api("/projects/user/#{user.id}", admin) }. +        not_to change { Project.count } + +      expect(response).to have_http_status(400) +      expect(json_response['error']).to eq('name is missing') +    end + +    it 'assigns attributes to project' do +      project = attributes_for(:project, { +        description: FFaker::Lorem.sentence, +        issues_enabled: false, +        merge_requests_enabled: false, +        wiki_enabled: false, +        request_access_enabled: true +      }) + +      post v3_api("/projects/user/#{user.id}", admin), project + +      expect(response).to have_http_status(201) +      project.each_pair do |k, v| +        next if %i[has_external_issue_tracker path].include?(k) +        expect(json_response[k.to_s]).to eq(v) +      end +    end + +    it 'sets a project as public' do +      project = attributes_for(:project, :public) +      post v3_api("/projects/user/#{user.id}", admin), project + +      expect(response).to have_http_status(201) +      expect(json_response['public']).to be_truthy +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) +    end + +    it 'sets a project as public using :public' do +      project = attributes_for(:project, { public: true }) +      post v3_api("/projects/user/#{user.id}", admin), project + +      expect(response).to have_http_status(201) +      expect(json_response['public']).to be_truthy +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) +    end + +    it 'sets a project as internal' do +      project = attributes_for(:project, :internal) +      post v3_api("/projects/user/#{user.id}", admin), project + +      expect(response).to have_http_status(201) +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) +    end + +    it 'sets a project as internal overriding :public' do +      project = attributes_for(:project, :internal, { public: true }) +      post v3_api("/projects/user/#{user.id}", admin), project +      expect(response).to have_http_status(201) +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) +    end + +    it 'sets a project as private' do +      project = attributes_for(:project, :private) +      post v3_api("/projects/user/#{user.id}", admin), project +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) +    end + +    it 'sets a project as private using :public' do +      project = attributes_for(:project, { public: false }) +      post v3_api("/projects/user/#{user.id}", admin), project +      expect(json_response['public']).to be_falsey +      expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) +    end + +    it 'sets a project as allowing merge even if build fails' do +      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) +      post v3_api("/projects/user/#{user.id}", admin), project +      expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey +    end + +    it 'sets a project as allowing merge only if build succeeds' do +      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) +      post v3_api("/projects/user/#{user.id}", admin), project +      expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy +    end + +    it 'sets a project as allowing merge even if discussions are unresolved' do +      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + +      post v3_api("/projects/user/#{user.id}", admin), project + +      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey +    end + +    it 'sets a project as allowing merge only if all discussions are resolved' do +      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + +      post v3_api("/projects/user/#{user.id}", admin), project + +      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy +    end +  end + +  describe "POST /projects/:id/uploads" do +    before { project } + +    it "uploads the file and returns its info" do +      post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + +      expect(response).to have_http_status(201) +      expect(json_response['alt']).to eq("dk") +      expect(json_response['url']).to start_with("/uploads/") +      expect(json_response['url']).to end_with("/dk.png") +    end +  end + +  describe 'GET /projects/:id' do +    context 'when unauthenticated' do +      it 'returns the public projects' do +        public_project = create(:empty_project, :public) + +        get v3_api("/projects/#{public_project.id}") + +        expect(response).to have_http_status(200) +        expect(json_response['id']).to eq(public_project.id) +        expect(json_response['description']).to eq(public_project.description) +        expect(json_response.keys).not_to include('permissions') +      end +    end + +    context 'when authenticated' do +      before do +        project +        project_member +      end + +      it 'returns a project by id' do +        group = create(:group) +        link = create(:project_group_link, project: project, group: group) + +        get v3_api("/projects/#{project.id}", user) + +        expect(response).to have_http_status(200) +        expect(json_response['id']).to eq(project.id) +        expect(json_response['description']).to eq(project.description) +        expect(json_response['default_branch']).to eq(project.default_branch) +        expect(json_response['tag_list']).to be_an Array +        expect(json_response['public']).to be_falsey +        expect(json_response['archived']).to be_falsey +        expect(json_response['visibility_level']).to be_present +        expect(json_response['ssh_url_to_repo']).to be_present +        expect(json_response['http_url_to_repo']).to be_present +        expect(json_response['web_url']).to be_present +        expect(json_response['owner']).to be_a Hash +        expect(json_response['owner']).to be_a Hash +        expect(json_response['name']).to eq(project.name) +        expect(json_response['path']).to be_present +        expect(json_response['issues_enabled']).to be_present +        expect(json_response['merge_requests_enabled']).to be_present +        expect(json_response['wiki_enabled']).to be_present +        expect(json_response['builds_enabled']).to be_present +        expect(json_response['snippets_enabled']).to be_present +        expect(json_response['container_registry_enabled']).to be_present +        expect(json_response['created_at']).to be_present +        expect(json_response['last_activity_at']).to be_present +        expect(json_response['shared_runners_enabled']).to be_present +        expect(json_response['creator_id']).to be_present +        expect(json_response['namespace']).to be_present +        expect(json_response['avatar_url']).to be_nil +        expect(json_response['star_count']).to be_present +        expect(json_response['forks_count']).to be_present +        expect(json_response['public_builds']).to be_present +        expect(json_response['shared_with_groups']).to be_an Array +        expect(json_response['shared_with_groups'].length).to eq(1) +        expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) +        expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) +        expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) +        expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) +        expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) +      end + +      it 'returns a project by path name' do +        get v3_api("/projects/#{project.id}", user) +        expect(response).to have_http_status(200) +        expect(json_response['name']).to eq(project.name) +      end + +      it 'returns a 404 error if not found' do +        get v3_api('/projects/42', user) +        expect(response).to have_http_status(404) +        expect(json_response['message']).to eq('404 Project Not Found') +      end + +      it 'returns a 404 error if user is not a member' do +        other_user = create(:user) +        get v3_api("/projects/#{project.id}", other_user) +        expect(response).to have_http_status(404) +      end + +      it 'handles users with dots' do +        dot_user = create(:user, username: 'dot.user') +        project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) + +        get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) +        expect(response).to have_http_status(200) +        expect(json_response['name']).to eq(project.name) +      end + +      it 'exposes namespace fields' do +        get v3_api("/projects/#{project.id}", user) + +        expect(response).to have_http_status(200) +        expect(json_response['namespace']).to eq({ +          'id' => user.namespace.id, +          'name' => user.namespace.name, +          'path' => user.namespace.path, +          'kind' => user.namespace.kind, +        }) +      end + +      describe 'permissions' do +        context 'all projects' do +          before { project.team << [user, :master] } + +          it 'contains permission information' do +            get v3_api("/projects", user) + +            expect(response).to have_http_status(200) +            expect(json_response.first['permissions']['project_access']['access_level']). +            to eq(Gitlab::Access::MASTER) +            expect(json_response.first['permissions']['group_access']).to be_nil +          end +        end + +        context 'personal project' do +          it 'sets project access and returns 200' do +            project.team << [user, :master] +            get v3_api("/projects/#{project.id}", user) + +            expect(response).to have_http_status(200) +            expect(json_response['permissions']['project_access']['access_level']). +            to eq(Gitlab::Access::MASTER) +            expect(json_response['permissions']['group_access']).to be_nil +          end +        end + +        context 'group project' do +          let(:project2) { create(:empty_project, group: create(:group)) } + +          before { project2.group.add_owner(user) } + +          it 'sets the owner and return 200' do +            get v3_api("/projects/#{project2.id}", user) + +            expect(response).to have_http_status(200) +            expect(json_response['permissions']['project_access']).to be_nil +            expect(json_response['permissions']['group_access']['access_level']). +            to eq(Gitlab::Access::OWNER) +          end +        end +      end +    end +  end + +  describe 'GET /projects/:id/events' do +    shared_examples_for 'project events response' do +      it 'returns the project events' do +        member = create(:user) +        create(:project_member, :developer, user: member, project: project) +        note = create(:note_on_issue, note: 'What an awesome day!', project: project) +        EventCreateService.new.leave_note(note, note.author) + +        get v3_api("/projects/#{project.id}/events", current_user) + +        expect(response).to have_http_status(200) + +        first_event = json_response.first + +        expect(first_event['action_name']).to eq('commented on') +        expect(first_event['note']['body']).to eq('What an awesome day!') + +        last_event = json_response.last + +        expect(last_event['action_name']).to eq('joined') +        expect(last_event['project_id'].to_i).to eq(project.id) +        expect(last_event['author_username']).to eq(member.username) +        expect(last_event['author']['name']).to eq(member.name) +      end +    end + +    context 'when unauthenticated' do +      it_behaves_like 'project events response' do +        let(:project) { create(:empty_project, :public) } +        let(:current_user) { nil } +      end +    end + +    context 'when authenticated' do +      context 'valid request' do +        it_behaves_like 'project events response' do +          let(:current_user) { user } +        end +      end + +      it 'returns a 404 error if not found' do +        get v3_api('/projects/42/events', user) + +        expect(response).to have_http_status(404) +        expect(json_response['message']).to eq('404 Project Not Found') +      end + +      it 'returns a 404 error if user is not a member' do +        other_user = create(:user) + +        get v3_api("/projects/#{project.id}/events", other_user) + +        expect(response).to have_http_status(404) +      end +    end +  end + +  describe 'GET /projects/:id/users' do +    shared_examples_for 'project users response' do +      it 'returns the project users' do +        member = create(:user) +        create(:project_member, :developer, user: member, project: project) + +        get v3_api("/projects/#{project.id}/users", current_user) + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.size).to eq(1) + +        first_user = json_response.first + +        expect(first_user['username']).to eq(member.username) +        expect(first_user['name']).to eq(member.name) +        expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) +      end +    end + +    context 'when unauthenticated' do +      it_behaves_like 'project users response' do +        let(:project) { create(:empty_project, :public) } +        let(:current_user) { nil } +      end +    end + +    context 'when authenticated' do +      context 'valid request' do +        it_behaves_like 'project users response' do +          let(:current_user) { user } +        end +      end + +      it 'returns a 404 error if not found' do +        get v3_api('/projects/42/users', user) + +        expect(response).to have_http_status(404) +        expect(json_response['message']).to eq('404 Project Not Found') +      end + +      it 'returns a 404 error if user is not a member' do +        other_user = create(:user) + +        get v3_api("/projects/#{project.id}/users", other_user) + +        expect(response).to have_http_status(404) +      end +    end +  end + +  describe 'GET /projects/:id/snippets' do +    before { snippet } + +    it 'returns an array of project snippets' do +      get v3_api("/projects/#{project.id}/snippets", user) +      expect(response).to have_http_status(200) +      expect(json_response).to be_an Array +      expect(json_response.first['title']).to eq(snippet.title) +    end +  end + +  describe 'GET /projects/:id/snippets/:snippet_id' do +    it 'returns a project snippet' do +      get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) +      expect(response).to have_http_status(200) +      expect(json_response['title']).to eq(snippet.title) +    end + +    it 'returns a 404 error if snippet id not found' do +      get v3_api("/projects/#{project.id}/snippets/1234", user) +      expect(response).to have_http_status(404) +    end +  end + +  describe 'POST /projects/:id/snippets' do +    it 'creates a new project snippet' do +      post v3_api("/projects/#{project.id}/snippets", user), +        title: 'v3_api test', file_name: 'sample.rb', code: 'test', +        visibility_level: '0' +      expect(response).to have_http_status(201) +      expect(json_response['title']).to eq('v3_api test') +    end + +    it 'returns a 400 error if invalid snippet is given' do +      post v3_api("/projects/#{project.id}/snippets", user) +      expect(status).to eq(400) +    end +  end + +  describe 'PUT /projects/:id/snippets/:snippet_id' do +    it 'updates an existing project snippet' do +      put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), +        code: 'updated code' +      expect(response).to have_http_status(200) +      expect(json_response['title']).to eq('example') +      expect(snippet.reload.content).to eq('updated code') +    end + +    it 'updates an existing project snippet with new title' do +      put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), +        title: 'other v3_api test' +      expect(response).to have_http_status(200) +      expect(json_response['title']).to eq('other v3_api test') +    end +  end + +  describe 'DELETE /projects/:id/snippets/:snippet_id' do +    before { snippet } + +    it 'deletes existing project snippet' do +      expect do +        delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) +      end.to change { Snippet.count }.by(-1) +      expect(response).to have_http_status(200) +    end + +    it 'returns 404 when deleting unknown snippet id' do +      delete v3_api("/projects/#{project.id}/snippets/1234", user) +      expect(response).to have_http_status(404) +    end +  end + +  describe 'GET /projects/:id/snippets/:snippet_id/raw' do +    it 'gets a raw project snippet' do +      get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) +      expect(response).to have_http_status(200) +    end + +    it 'returns a 404 error if raw project snippet not found' do +      get v3_api("/projects/#{project.id}/snippets/5555/raw", user) +      expect(response).to have_http_status(404) +    end +  end + +  describe :fork_admin do +    let(:project_fork_target) { create(:empty_project) } +    let(:project_fork_source) { create(:empty_project, :public) } + +    describe 'POST /projects/:id/fork/:forked_from_id' do +      let(:new_project_fork_source) { create(:empty_project, :public) } + +      it "is not available for non admin users" do +        post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) +        expect(response).to have_http_status(403) +      end + +      it 'allows project to be forked from an existing project' do +        expect(project_fork_target.forked?).not_to be_truthy +        post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) +        expect(response).to have_http_status(201) +        project_fork_target.reload +        expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) +        expect(project_fork_target.forked_project_link).not_to be_nil +        expect(project_fork_target.forked?).to be_truthy +      end + +      it 'fails if forked_from project which does not exist' do +        post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) +        expect(response).to have_http_status(404) +      end + +      it 'fails with 409 if already forked' do +        post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) +        project_fork_target.reload +        expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) +        post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) +        expect(response).to have_http_status(409) +        project_fork_target.reload +        expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) +        expect(project_fork_target.forked?).to be_truthy +      end +    end + +    describe 'DELETE /projects/:id/fork' do +      it "is not visible to users outside group" do +        delete v3_api("/projects/#{project_fork_target.id}/fork", user) +        expect(response).to have_http_status(404) +      end + +      context 'when users belong to project group' do +        let(:project_fork_target) { create(:empty_project, group: create(:group)) } + +        before do +          project_fork_target.group.add_owner user +          project_fork_target.group.add_developer user2 +        end + +        it 'is forbidden to non-owner users' do +          delete v3_api("/projects/#{project_fork_target.id}/fork", user2) +          expect(response).to have_http_status(403) +        end + +        it 'makes forked project unforked' do +          post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) +          project_fork_target.reload +          expect(project_fork_target.forked_from_project).not_to be_nil +          expect(project_fork_target.forked?).to be_truthy +          delete v3_api("/projects/#{project_fork_target.id}/fork", admin) +          expect(response).to have_http_status(200) +          project_fork_target.reload +          expect(project_fork_target.forked_from_project).to be_nil +          expect(project_fork_target.forked?).not_to be_truthy +        end + +        it 'is idempotent if not forked' do +          expect(project_fork_target.forked_from_project).to be_nil +          delete v3_api("/projects/#{project_fork_target.id}/fork", admin) +          expect(response).to have_http_status(304) +          expect(project_fork_target.reload.forked_from_project).to be_nil +        end +      end +    end +  end + +  describe "POST /projects/:id/share" do +    let(:group) { create(:group) } + +    it "shares project with group" do +      expires_at = 10.days.from_now.to_date + +      expect do +        post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at +      end.to change { ProjectGroupLink.count }.by(1) + +      expect(response).to have_http_status(201) +      expect(json_response['group_id']).to eq(group.id) +      expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) +      expect(json_response['expires_at']).to eq(expires_at.to_s) +    end + +    it "returns a 400 error when group id is not given" do +      post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER +      expect(response).to have_http_status(400) +    end + +    it "returns a 400 error when access level is not given" do +      post v3_api("/projects/#{project.id}/share", user), group_id: group.id +      expect(response).to have_http_status(400) +    end + +    it "returns a 400 error when sharing is disabled" do +      project.namespace.update(share_with_group_lock: true) +      post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER +      expect(response).to have_http_status(400) +    end + +    it 'returns a 404 error when user cannot read group' do +      private_group = create(:group, :private) + +      post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER + +      expect(response).to have_http_status(404) +    end + +    it 'returns a 404 error when group does not exist' do +      post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER + +      expect(response).to have_http_status(404) +    end + +    it "returns a 400 error when wrong params passed" do +      post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 + +      expect(response).to have_http_status(400) +      expect(json_response['error']).to eq 'group_access does not have a valid value' +    end +  end + +  describe 'DELETE /projects/:id/share/:group_id' do +    it 'returns 204 when deleting a group share' do +      group = create(:group, :public) +      create(:project_group_link, group: group, project: project) + +      delete v3_api("/projects/#{project.id}/share/#{group.id}", user) + +      expect(response).to have_http_status(204) +      expect(project.project_group_links).to be_empty +    end + +    it 'returns a 400 when group id is not an integer' do +      delete v3_api("/projects/#{project.id}/share/foo", user) + +      expect(response).to have_http_status(400) +    end + +    it 'returns a 404 error when group link does not exist' do +      delete v3_api("/projects/#{project.id}/share/1234", user) + +      expect(response).to have_http_status(404) +    end + +    it 'returns a 404 error when project does not exist' do +      delete v3_api("/projects/123/share/1234", user) + +      expect(response).to have_http_status(404) +    end +  end + +  describe 'GET /projects/search/:query' do +    let!(:query)            { 'query'} +    let!(:search)           { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } +    let!(:pre)              { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } +    let!(:post)             { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } +    let!(:pre_post)         { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } +    let!(:unfound)          { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } +    let!(:internal)         { create(:empty_project, :internal, name: "internal #{query}") } +    let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } +    let!(:public)           { create(:empty_project, :public, name: "public #{query}") } +    let!(:unfound_public)   { create(:empty_project, :public, name: 'unfound public') } +    let!(:one_dot_two)      { create(:empty_project, :public, name: "one.dot.two") } + +    shared_examples_for 'project search response' do |args = {}| +      it 'returns project search responses' do +        get v3_api("/projects/search/#{args[:query]}", current_user) + +        expect(response).to have_http_status(200) +        expect(json_response).to be_an Array +        expect(json_response.size).to eq(args[:results]) +        json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } +      end +    end + +    context 'when unauthenticated' do +      it_behaves_like 'project search response', query: 'query', results: 1 do +        let(:current_user) { nil } +      end +    end + +    context 'when authenticated' do +      it_behaves_like 'project search response', query: 'query', results: 6 do +        let(:current_user) { user } +      end +      it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do +        let(:current_user) { user } +      end +    end + +    context 'when authenticated as a different user' do +      it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do +        let(:current_user) { user2 } +      end +    end +  end + +  describe 'PUT /projects/:id' do +    before { project } +    before { user } +    before { user3 } +    before { user4 } +    before { project3 } +    before { project4 } +    before { project_member3 } +    before { project_member2 } + +    context 'when unauthenticated' do +      it 'returns authentication error' do +        project_param = { name: 'bar' } +        put v3_api("/projects/#{project.id}"), project_param +        expect(response).to have_http_status(401) +      end +    end + +    context 'when authenticated as project owner' do +      it 'updates name' do +        project_param = { name: 'bar' } +        put v3_api("/projects/#{project.id}", user), project_param +        expect(response).to have_http_status(200) +        project_param.each_pair do |k, v| +          expect(json_response[k.to_s]).to eq(v) +        end +      end + +      it 'updates visibility_level' do +        project_param = { visibility_level: 20 } +        put v3_api("/projects/#{project3.id}", user), project_param +        expect(response).to have_http_status(200) +        project_param.each_pair do |k, v| +          expect(json_response[k.to_s]).to eq(v) +        end +      end + +      it 'updates visibility_level from public to private' do +        project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) +        project_param = { public: false } +        put v3_api("/projects/#{project3.id}", user), project_param +        expect(response).to have_http_status(200) +        project_param.each_pair do |k, v| +          expect(json_response[k.to_s]).to eq(v) +        end +        expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) +      end + +      it 'does not update name to existing name' do +        project_param = { name: project3.name } +        put v3_api("/projects/#{project.id}", user), project_param +        expect(response).to have_http_status(400) +        expect(json_response['message']['name']).to eq(['has already been taken']) +      end + +      it 'updates request_access_enabled' do +        project_param = { request_access_enabled: false } + +        put v3_api("/projects/#{project.id}", user), project_param + +        expect(response).to have_http_status(200) +        expect(json_response['request_access_enabled']).to eq(false) +      end + +      it 'updates path & name to existing path & name in different namespace' do +        project_param = { path: project4.path, name: project4.name } +        put v3_api("/projects/#{project3.id}", user), project_param +        expect(response).to have_http_status(200) +        project_param.each_pair do |k, v| +          expect(json_response[k.to_s]).to eq(v) +        end +      end +    end + +    context 'when authenticated as project master' do +      it 'updates path' do +        project_param = { path: 'bar' } +        put v3_api("/projects/#{project3.id}", user4), project_param +        expect(response).to have_http_status(200) +        project_param.each_pair do |k, v| +          expect(json_response[k.to_s]).to eq(v) +        end +      end + +      it 'updates other attributes' do +        project_param = { issues_enabled: true, +                          wiki_enabled: true, +                          snippets_enabled: true, +                          merge_requests_enabled: true, +                          description: 'new description' } + +        put v3_api("/projects/#{project3.id}", user4), project_param +        expect(response).to have_http_status(200) +        project_param.each_pair do |k, v| +          expect(json_response[k.to_s]).to eq(v) +        end +      end + +      it 'does not update path to existing path' do +        project_param = { path: project.path } +        put v3_api("/projects/#{project3.id}", user4), project_param +        expect(response).to have_http_status(400) +        expect(json_response['message']['path']).to eq(['has already been taken']) +      end + +      it 'does not update name' do +        project_param = { name: 'bar' } +        put v3_api("/projects/#{project3.id}", user4), project_param +        expect(response).to have_http_status(403) +      end + +      it 'does not update visibility_level' do +        project_param = { visibility_level: 20 } +        put v3_api("/projects/#{project3.id}", user4), project_param +        expect(response).to have_http_status(403) +      end +    end + +    context 'when authenticated as project developer' do +      it 'does not update other attributes' do +        project_param = { path: 'bar', +                          issues_enabled: true, +                          wiki_enabled: true, +                          snippets_enabled: true, +                          merge_requests_enabled: true, +                          description: 'new description', +                          request_access_enabled: true } +        put v3_api("/projects/#{project.id}", user3), project_param +        expect(response).to have_http_status(403) +      end +    end +  end + +  describe 'POST /projects/:id/archive' do +    context 'on an unarchived project' do +      it 'archives the project' do +        post v3_api("/projects/#{project.id}/archive", user) + +        expect(response).to have_http_status(201) +        expect(json_response['archived']).to be_truthy +      end +    end + +    context 'on an archived project' do +      before do +        project.archive! +      end + +      it 'remains archived' do +        post v3_api("/projects/#{project.id}/archive", user) + +        expect(response).to have_http_status(201) +        expect(json_response['archived']).to be_truthy +      end +    end + +    context 'user without archiving rights to the project' do +      before do +        project.team << [user3, :developer] +      end + +      it 'rejects the action' do +        post v3_api("/projects/#{project.id}/archive", user3) + +        expect(response).to have_http_status(403) +      end +    end +  end + +  describe 'POST /projects/:id/unarchive' do +    context 'on an unarchived project' do +      it 'remains unarchived' do +        post v3_api("/projects/#{project.id}/unarchive", user) + +        expect(response).to have_http_status(201) +        expect(json_response['archived']).to be_falsey +      end +    end + +    context 'on an archived project' do +      before do +        project.archive! +      end + +      it 'unarchives the project' do +        post v3_api("/projects/#{project.id}/unarchive", user) + +        expect(response).to have_http_status(201) +        expect(json_response['archived']).to be_falsey +      end +    end + +    context 'user without archiving rights to the project' do +      before do +        project.team << [user3, :developer] +      end + +      it 'rejects the action' do +        post v3_api("/projects/#{project.id}/unarchive", user3) + +        expect(response).to have_http_status(403) +      end +    end +  end + +  describe 'POST /projects/:id/star' do +    context 'on an unstarred project' do +      it 'stars the project' do +        expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) + +        expect(response).to have_http_status(201) +        expect(json_response['star_count']).to eq(1) +      end +    end + +    context 'on a starred project' do +      before do +        user.toggle_star(project) +        project.reload +      end + +      it 'does not modify the star count' do +        expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + +        expect(response).to have_http_status(304) +      end +    end +  end + +  describe 'DELETE /projects/:id/star' do +    context 'on a starred project' do +      before do +        user.toggle_star(project) +        project.reload +      end + +      it 'unstars the project' do +        expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + +        expect(response).to have_http_status(200) +        expect(json_response['star_count']).to eq(0) +      end +    end + +    context 'on an unstarred project' do +      it 'does not modify the star count' do +        expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + +        expect(response).to have_http_status(304) +      end +    end +  end + +  describe 'DELETE /projects/:id' do +    context 'when authenticated as user' do +      it 'removes project' do +        delete v3_api("/projects/#{project.id}", user) +        expect(response).to have_http_status(200) +      end + +      it 'does not remove a project if not an owner' do +        user3 = create(:user) +        project.team << [user3, :developer] +        delete v3_api("/projects/#{project.id}", user3) +        expect(response).to have_http_status(403) +      end + +      it 'does not remove a non existing project' do +        delete v3_api('/projects/1328', user) +        expect(response).to have_http_status(404) +      end + +      it 'does not remove a project not attached to user' do +        delete v3_api("/projects/#{project.id}", user2) +        expect(response).to have_http_status(404) +      end +    end + +    context 'when authenticated as admin' do +      it 'removes any existing project' do +        delete v3_api("/projects/#{project.id}", admin) +        expect(response).to have_http_status(200) +      end + +      it 'does not remove a non existing project' do +        delete v3_api('/projects/1328', admin) +        expect(response).to have_http_status(404) +      end +    end +  end +end diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index fde807cc410..7b29b043296 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do          expect { service.execute }.to change(Board, :count).by(1)        end -      it 'creates default lists' do +      it 'creates the default lists' do          board = service.execute -        expect(board.lists.size).to eq 2 -        expect(board.lists.first).to be_backlog -        expect(board.lists.last).to be_done +        expect(board.lists.size).to eq 1 +        expect(board.lists.first).to be_done        end      end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 7c206cf3ce7..305278843f5 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do      let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }      let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } -    let!(:backlog) { create(:backlog_list, board: board) }      let!(:list1)   { create(:list, board: board, label: development, position: 0) }      let!(:list2)   { create(:list, board: board, label: testing, position: 1) }      let!(:done)    { create(:done_list, board: board) } @@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do      end      context 'sets default order to priority' do -      it 'returns opened issues when listing issues from Backlog' do -        params = { board_id: board.id, id: backlog.id } +      it 'returns opened issues when list id is missing' do +        params = { board_id: board.id }          issues = described_class.new(project, user, params).execute diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index c43b2aec490..77f75167b3d 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do      let(:development) { create(:label, project: project, name: 'Development') }      let(:testing)  { create(:label, project: project, name: 'Testing') } -    let!(:backlog) { create(:backlog_list, board: board1) }      let!(:list1)   { create(:list, board: board1, label: development, position: 0) }      let!(:list2)   { create(:list, board: board1, label: testing, position: 1) }      let!(:done)    { create(:done_list, board: board1) } @@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do        project.team << [user, :developer]      end -    context 'when moving from backlog' do -      it 'adds the label of the list it goes to' do -        issue = create(:labeled_issue, project: project, labels: [bug]) -        params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id } - -        described_class.new(project, user, params).execute(issue) - -        expect(issue.reload.labels).to contain_exactly(bug, development) -      end -    end - -    context 'when moving to backlog' do -      it 'removes all list-labels' do -        issue = create(:labeled_issue, project: project, labels: [bug, development, testing]) -        params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id } - -        described_class.new(project, user, params).execute(issue) - -        expect(issue.reload.labels).to contain_exactly(bug) -      end -    end - -    context 'when moving from backlog to done' do -      it 'closes the issue' do -        issue = create(:labeled_issue, project: project, labels: [bug]) -        params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id } - -        described_class.new(project, user, params).execute(issue) -        issue.reload - -        expect(issue.labels).to contain_exactly(bug) -        expect(issue).to be_closed -      end -    end -      context 'when moving an issue between lists' do        let(:issue)  { create(:labeled_issue, project: project, labels: [bug, development]) }        let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } } @@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do        end      end -    context 'when moving from done to backlog' do -      it 'reopens the issue' do -        issue = create(:labeled_issue, :closed, project: project, labels: [bug]) -        params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id } - -        described_class.new(project, user, params).execute(issue) -        issue.reload - -        expect(issue.labels).to contain_exactly(bug) -        expect(issue).to be_reopened -      end -    end -      context 'when moving to same list' do        let(:issue)  { create(:labeled_issue, project: project, labels: [bug, development]) }        let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } } diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index a7e9efcf93f..ebac38e68f1 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do        end      end -    context 'when board lists has backlog, and done lists' do +    context 'when board lists has the done list' do        it 'creates a new list at beginning of the list' do          list = service.execute(board) @@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do        end      end -    context 'when board lists has backlog, label and done lists' do +    context 'when board lists has label and done lists' do        it 'creates a new list at end of the label lists' do          list1 = create(:list, board: board, position: 0) diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index 628caf03476..a30860f828a 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do        end        it 'decrements position of higher lists' do -        backlog     = board.backlog_list          development = create(:list, board: board, position: 0)          review      = create(:list, board: board, position: 1)          staging     = create(:list, board: board, position: 2) @@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do          described_class.new(project, user).execute(development) -        expect(backlog.reload.position).to be_nil          expect(review.reload.position).to eq 0          expect(staging.reload.position).to eq 1          expect(done.reload.position).to be_nil        end      end -    it 'does not remove list from board when list type is backlog' do -      list = board.backlog_list -      service = described_class.new(project, user) - -      expect { service.execute(list) }.not_to change(board.lists, :count) -    end -      it 'does not remove list from board when list type is done' do        list = board.done_list        service = described_class.new(project, user) diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index 334cee3f06d..2dffc62b215 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do        service = described_class.new(project, double) -      expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list] +      expect(service.execute(board)).to eq [list, board.done_list]      end    end  end diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb index 63fa0bb8c5f..3786dc82bf0 100644 --- a/spec/services/boards/lists/move_service_spec.rb +++ b/spec/services/boards/lists/move_service_spec.rb @@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do      let(:board)   { create(:board, project: project) }      let(:user)    { create(:user) } -    let!(:backlog)     { create(:backlog_list, board: board) }      let!(:planning)    { create(:list, board: board, position: 0) }      let!(:development) { create(:list, board: board, position: 1) }      let!(:review)      { create(:list, board: board, position: 2) } @@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do        end      end -    it 'keeps position of lists when list type is backlog' do -      service = described_class.new(project, user, position: 2) - -      service.execute(backlog) - -      expect(current_list_positions).to eq [0, 1, 2, 3] -    end -      it 'keeps position of lists when list type is done' do        service = described_class.new(project, user, position: 2) diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 66fc8fc360b..0b0925983eb 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do          let(:issuable) { issue }        end      end + +    context '/target_branch command' do +      let(:non_empty_project) { create(:project) } +      let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } +      let(:service) { described_class.new(non_empty_project, developer)} + +      it 'updates target_branch if /target_branch command is executed' do +        _, updates = service.execute('/target_branch merge-test', merge_request) + +        expect(updates).to eq(target_branch: 'merge-test') +      end + +      it 'handles blanks around param' do +        _, updates = service.execute('/target_branch  merge-test     ', merge_request) + +        expect(updates).to eq(target_branch: 'merge-test') +      end + +      context 'ignores command with no argument' do +        it_behaves_like 'empty command' do +          let(:content) { '/target_branch' } +          let(:issuable) { another_merge_request } +        end +      end + +      context 'ignores non-existing target branch' do +        it_behaves_like 'empty command' do +          let(:content) { '/target_branch totally_non_existing_branch' } +          let(:issuable) { another_merge_request } +        end +      end +    end    end  end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index bd7269045e1..7913a180f9b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -752,13 +752,13 @@ describe SystemNoteService, services: true do        it 'sets the note text' do          noteable.update_attribute(:time_estimate, 277200) -        expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h" +        expect(subject.note).to eq "changed time estimate to 1w 4d 5h"        end      end      context 'without a time estimate' do        it 'sets the note text' do -        expect(subject.note).to eq "Removed time estimate on this issue" +        expect(subject.note).to eq "removed time estimate"        end      end    end @@ -782,7 +782,7 @@ describe SystemNoteService, services: true do        it 'sets the note text' do          spend_time!(277200) -        expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request" +        expect(subject.note).to eq "added 1w 4d 5h of time spent"        end      end @@ -790,7 +790,7 @@ describe SystemNoteService, services: true do        it 'sets the note text' do          spend_time!(-277200) -        expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request" +        expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"        end      end @@ -798,7 +798,7 @@ describe SystemNoteService, services: true do        it 'sets the note text' do          spend_time!(:reset) -        expect(subject.note).to eq "Removed time spent on this merge request" +        expect(subject.note).to eq "removed time spent"        end      end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 68b196d9033..ae6e708cf87 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -17,8 +17,8 @@ module ApiHelpers    #   => "/api/v2/issues?foo=bar&private_token=..."    #    # Returns the relative path to the requested API resource -  def api(path, user = nil) -    "/api/#{API::API.version}#{path}" + +  def api(path, user = nil, version: API::API.version) +    "/api/#{version}#{path}" +        # Normalize query string        (path.index('?') ? '' : '?') + @@ -31,6 +31,11 @@ module ApiHelpers        end    end +  # Temporary helper method for simplifying V3 exclusive API specs +  def v3_api(path, user = nil) +    api(path, user, version: 'v3') +  end +    def ci_api(path, user = nil)      "/ci/api/v1/#{path}" + diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb index 1b0a4583f5c..944ea30656f 100644 --- a/spec/support/import_export/export_file_helper.rb +++ b/spec/support/import_export/export_file_helper.rb @@ -35,7 +35,7 @@ module ExportFileHelper             project: project,             commit_id: ci_pipeline.sha) -    create(:event, target: milestone, project: project, action: Event::CREATED, author: user) +    create(:event, :created, target: milestone, project: project, author: user)      create(:project_member, :master, user: user, project: project)      create(:ci_variable, project: project)      create(:ci_trigger, project: project) diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 6c4c246a68b..444612cf871 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -43,7 +43,8 @@ module KubernetesHelpers          url:  container_exec_url(service.api_url, service.namespace, pod_name, container['name']),          subprotocols: ['channel.k8s.io'],          headers: { 'Authorization' => ["Bearer #{service.token}"] }, -        created_at: DateTime.parse(pod['metadata']['creationTimestamp']) +        created_at: DateTime.parse(pod['metadata']['creationTimestamp']), +        max_session_time: 0        }        terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?        terminal diff --git a/spec/support/matchers/match_file.rb b/spec/support/matchers/match_file.rb new file mode 100644 index 00000000000..d1888b3376a --- /dev/null +++ b/spec/support/matchers/match_file.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :match_file do |expected| +  match do |actual| +    expect(Digest::MD5.hexdigest(actual)).to eq(Digest::MD5.hexdigest(File.read(expected))) +  end +end diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb deleted file mode 100644 index 643b161cdf4..00000000000 --- a/spec/teaspoon_env.rb +++ /dev/null @@ -1,178 +0,0 @@ -Teaspoon.configure do |config| -  # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to -  # `http://localhost:3000/jasmine` to run your tests. -  config.mount_at = "/teaspoon" - -  # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can -  # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). -  # Note: Defaults to `Rails.root` if nil. -  config.root = nil - -  # Paths that will be appended to the Rails assets paths -  # Note: Relative to `config.root`. -  config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] - -  # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will -  # be rendered as fixtures. -  config.fixture_paths = ["spec/javascripts/fixtures"] - -  # SUITES -  # -  # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. -  # -  # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can -  # omit various directives and the ones defined in the default suite will be used. -  # -  # To run a specific suite -  # - in the browser: http://localhost/teaspoon/[suite_name] -  # - with the rake task: rake teaspoon suite=[suite_name] -  # - with the cli: teaspoon --suite=[suite_name] -  config.suite do |suite| -    # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for -    # you -- which you can override with the directives below. This should be specified first, as it can override other -    # directives. -    # Note: If no version is specified, the latest is assumed. -    # -    # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 -    suite.use_framework :jasmine, "2.2.0" - -    # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These -    # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. -    suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}" - -    # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. -    # suite.javascripts = [] - -    # You can include your own stylesheets if you want to change how Teaspoon looks. -    # Note: Spec related CSS can and should be loaded using fixtures. -    # suite.stylesheets = ["teaspoon"] - -    # This suites spec helper, which can require additional support files. This file is loaded before any of your test -    # files are loaded. -    suite.helper = "spec_helper" - -    # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating -    # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. -    # -    # Available: boot, boot_require_js -    suite.boot_partial = "boot" - -    # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. -    suite.body_partial = "body" - -    # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a -    # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. -    # suite.hook :fixtures, &proc{} - -    # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated -    # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, -    # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. -    # suite.expand_assets = true -  end - -  # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also -  # be run in the default suite -- but can be focused into a more specific suite. -  # config.suite :targeted do |suite| -  #  suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" -  # end - -  # CONSOLE RUNNER SPECIFIC -  # -  # These configuration directives are applicable only when running via the rake task or command line interface. These -  # directives can be overridden using the command line interface arguments or with ENV variables when using the rake -  # task. -  # -  # Command Line Interface: -  # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js -  # -  # Rake: -  # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite - -  # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. -  # -  # Available: :phantomjs, :selenium, :capybara_webkit -  # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS -  # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver -  # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit -  # config.driver = :phantomjs - -  # Specify additional options for the driver. -  # -  # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS -  # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver -  # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit -  # config.driver_options = nil - -  # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be -  # considered a failure. This is to avoid issues that can arise where tests stall. -  # config.driver_timeout = 180 - -  # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. -  # config.server = nil - -  # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. -  # config.server_port = nil - -  # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may -  # want to lower this if you know it shouldn't take long to start. -  # config.server_timeout = 20 - -  # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have -  # several suites, but in environments like CI this may not be desirable. -  # config.fail_fast = true - -  # Specify the formatters to use when outputting the results. -  # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. -  # -  # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity -  # config.formatters = [:dot] - -  # Specify if you want color output from the formatters. -  # config.color = true - -  # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to -  # remove them, but in verbose applications this may not be desirable. -  # config.suppress_log = false - -  # COVERAGE REPORTS / THRESHOLD ASSERTIONS -  # -  # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and -  # display coverage statistics. -  # -  # Coverage configurations are similar to suites. You can define several, and use different ones under different -  # conditions. -  # -  # To run with a specific coverage configuration -  # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] -  # - with the cli: teaspoon --coverage=[coverage_name] - -  # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage -  # on the CLI. -  # Set this to "true" or the name of your coverage config. -  config.use_coverage = true - -  # You can have multiple coverage configs by passing a name to config.coverage. -  # e.g. config.coverage :ci do |coverage| -  # The default coverage config name is :default. -  config.coverage do |coverage| -    # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. -    # -    # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity -    coverage.reports = ["text-summary", "html"] - -    # The path that the coverage should be written to - when there's an artifact to write to disk. -    # Note: Relative to `config.root`. -    coverage.output_path = "coverage-javascript" - -    # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The -    # default excludes assets from vendor, gems and support libraries. -    coverage.ignore = [%r{vendor/}, %r{spec/javascripts/(?!helpers/)}] - -    # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any -    # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. -    # coverage.statements = nil -    # coverage.functions = nil -    # coverage.branches = nil -    # coverage.lines = nil -  end -end  | 
