diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-11 21:08:31 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-11 21:08:31 +0300 |
commit | 1a2f754734eb189e371e25e685413808f69a7f2c (patch) | |
tree | 2c97884971f36d9026600897b74364d2e212a109 /spec | |
parent | f1ce71c88c407709987dd4a7b40bdb7596b6baa2 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
63 files changed, 1421 insertions, 474 deletions
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb index 0074b4b1eb0..dc280133a20 100644 --- a/spec/features/ide/user_opens_merge_request_spec.rb +++ b/spec/features/ide/user_opens_merge_request_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do + include CookieHelper + let(:merge_request) { create(:merge_request, :simple, source_project: project) } let(:project) { create(:project, :public, :repository) } let(:user) { project.first_owner } @@ -12,6 +14,8 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') + visit(merge_request_path(merge_request)) end diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb index 709919d0196..a166ff46177 100644 --- a/spec/features/incidents/incident_details_spec.rb +++ b/spec/features/incidents/incident_details_spec.rb @@ -94,6 +94,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d end it 'routes the user to the incident details page when the `issue_type` is set to incident' do + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue) wait_for_requests @@ -113,6 +114,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d end it 'routes the user to the issue details page when the `issue_type` is set to issue' do + set_cookie('new-actions-popover-viewed', 'true') visit incident_project_issues_path(project, incident) wait_for_requests diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb index 47865d2b6ba..fb9addff1a2 100644 --- a/spec/features/issues/discussion_lock_spec.rb +++ b/spec/features/issues/discussion_lock_spec.rb @@ -9,6 +9,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do before do sign_in(user) + stub_feature_flags(moved_mr_sidebar: false) end context 'when a user is a team member' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 2bd5373b715..665c7307231 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do + include CookieHelper + let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') } @@ -45,6 +47,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do before do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue_to_edit) wait_for_requests diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index d5f90bb9260..29a61d584ee 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -98,6 +98,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do project.add_developer(user_to_be_deleted) sign_in(user_to_be_deleted) + stub_feature_flags(moved_mr_sidebar: false) visit project_issue_path(project, issue) wait_for_requests @@ -129,7 +130,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do describe 'when an issue `issue_type` is edited' do before do sign_in(user) - + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue) wait_for_requests end @@ -163,7 +164,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do describe 'when an incident `issue_type` is edited' do before do sign_in(user) - + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, incident) wait_for_requests end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 2ae347d4f9e..ee71181fba2 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe 'Issue Sidebar', feature_category: :team_planning do include MobileHelpers include Features::InviteMembersModalHelpers + include CookieHelper let_it_be(:group) { create(:group, :nested) } let_it_be(:project) { create(:project, :public, namespace: group) } @@ -20,6 +21,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do context 'when signed in' do before do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end context 'when concerning the assignee', :js do @@ -205,6 +207,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do context 'as an allowed user' do before do + stub_feature_flags(moved_mr_sidebar: false) project.add_developer(user) visit_issue(project, issue) end @@ -293,6 +296,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do context 'as a guest' do before do + stub_feature_flags(moved_mr_sidebar: false) project.add_guest(user) visit_issue(project, issue) end diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index 3a927e76fd1..4730406c2b2 100644 --- a/spec/features/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do + include CookieHelper + let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) } let_it_be(:user) { create(:user) } @@ -18,6 +20,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin project.add_developer(user) project_with_milestones.add_developer(user) sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end context "from edit page" do diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index 904fafdf56a..00b04c10d33 100644 --- a/spec/features/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb @@ -10,6 +10,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin context 'user is not logged in' do before do + stub_feature_flags(moved_mr_sidebar: false) visit(project_issue_path(project, issue)) end @@ -20,9 +21,9 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin context 'user is logged in' do before do + stub_feature_flags(moved_mr_sidebar: false) project.add_developer(user) sign_in(user) - visit(project_issue_path(project, issue)) end @@ -52,6 +53,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin context 'user is logged in without edit permission' do before do + stub_feature_flags(moved_mr_sidebar: false) sign_in(user2) visit(project_issue_path(project, issue)) diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb index d4ccc4a93b5..3bcc8255ab7 100644 --- a/spec/features/merge_request/user_manages_subscription_spec.rb +++ b/spec/features/merge_request/user_manages_subscription_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'User manages subscription', :js, feature_category: :code_review_workflow do + include CookieHelper + let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:user) { create(:user) } @@ -10,7 +12,7 @@ RSpec.describe 'User manages subscription', :js, feature_category: :code_review_ before do stub_feature_flags(moved_mr_sidebar: moved_mr_sidebar_enabled) - + set_cookie('new-actions-popover-viewed', 'true') project.add_maintainer(user) sign_in(user) diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb index 7cb1c95f6dc..601310cbacf 100644 --- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb +++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do include ProjectForksHelper + include CookieHelper let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } @@ -11,6 +12,7 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_ before do project.add_maintainer(user) sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end describe 'for fork' do diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb index ad2ceeb23e2..21c62b0d0d8 100644 --- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb +++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do + include CookieHelper + let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } @@ -10,6 +12,7 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_ before do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') visit project_merge_request_path(project, merge_request) wait_for_requests diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index 0de59ea21c5..dae28cbb05c 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do include ListboxHelpers + include CookieHelper let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } @@ -17,6 +18,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ before do project.add_maintainer(user) sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end it 'selects the source branch sha when a tag with the same name exists' do diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index adf410ce6e8..77f88994bfb 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'issuable templates', :js, feature_category: :projects do include ProjectForksHelper + include CookieHelper let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } @@ -12,6 +13,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :projects do before do project.add_maintainer(user) sign_in user + set_cookie('new-actions-popover-viewed', 'true') end context 'user creates an issue using templates' do diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb index 55e7f5897bc..a18cdf27294 100644 --- a/spec/features/reportable_note/issue_spec.rb +++ b/spec/features/reportable_note/issue_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning do + include CookieHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } @@ -11,7 +13,7 @@ RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning before do project.add_maintainer(user) sign_in(user) - + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue) end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 1df0aa411d6..e93c0c790c2 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -128,6 +128,51 @@ RSpec.describe NotesFinder do end end + context 'for notes from users who have been banned', :enable_admin_mode, feature_category: :instance_resiliency do + subject(:finder) { described_class.new(user, project: project).execute } + + let_it_be(:banned_user) { create(:banned_user).user } + let!(:banned_note) { create(:note_on_issue, project: project, author: banned_user) } + + context 'when :hidden_notes feature is not enabled' do + before do + stub_feature_flags(hidden_notes: false) + end + + context 'when user is not an admin' do + it { is_expected.to include(banned_note) } + end + + context 'when @current_user is nil' do + let(:user) { nil } + + it { is_expected.to be_empty } + end + end + + context 'when :hidden_notes feature is enabled' do + before do + stub_feature_flags(hidden_notes: true) + end + + context 'when user is an admin' do + let(:user) { create(:admin) } + + it { is_expected.to include(banned_note) } + end + + context 'when user is not an admin' do + it { is_expected.not_to include(banned_note) } + end + + context 'when @current_user is nil' do + let(:user) { nil } + + it { is_expected.to be_empty } + end + end + end + context 'for target type' do let(:project) { create(:project, :repository) } let!(:note1) { create :note_on_issue, project: project } diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js index 148e696b57b..77ba6cdc9c0 100644 --- a/spec/frontend/boards/components/board_app_spec.js +++ b/spec/frontend/boards/components/board_app_spec.js @@ -1,14 +1,20 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; +import createMockApollo from 'helpers/mock_apollo_helper'; import BoardApp from '~/boards/components/board_app.vue'; +import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql'; +import { rawIssue } from '../mock_data'; describe('BoardApp', () => { let wrapper; let store; + const mockApollo = createMockApollo(); Vue.use(Vuex); + Vue.use(VueApollo); const createStore = ({ mockGetters = {} } = {}) => { store = new Vuex.Store({ @@ -23,12 +29,22 @@ describe('BoardApp', () => { }); }; - const createComponent = () => { + const createComponent = ({ isApolloBoard = false, issue = rawIssue } = {}) => { + mockApollo.clients.defaultClient.cache.writeQuery({ + query: activeBoardItemQuery, + data: { + activeBoardItem: issue, + }, + }); + wrapper = shallowMount(BoardApp, { + apolloProvider: mockApollo, store, provide: { initialBoardId: 'gid://gitlab/Board/1', initialFilterParams: {}, + isIssueBoard: true, + isApolloBoard, }, }); }; @@ -50,4 +66,22 @@ describe('BoardApp', () => { expect(wrapper.classes()).not.toContain('is-compact'); }); + + describe('Apollo boards', () => { + beforeEach(async () => { + createComponent({ isApolloBoard: true }); + await nextTick(); + }); + + it('should have is-compact class when a card is selected', () => { + expect(wrapper.classes()).toContain('is-compact'); + }); + + it('should not have is-compact class when no card is selected', async () => { + createComponent({ isApolloBoard: true, issue: {} }); + await nextTick(); + + expect(wrapper.classes()).not.toContain('is-compact'); + }); + }); }); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 46116bed4cf..91e9b6f8cfa 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,8 +1,10 @@ import { GlLabel } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardCard from '~/boards/components/board_card.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; import { inactiveId } from '~/boards/constants'; @@ -14,6 +16,14 @@ describe('Board card', () => { let mockActions; Vue.use(Vuex); + Vue.use(VueApollo); + + const mockSetActiveBoardItemResolver = jest.fn(); + const mockApollo = createMockApollo([], { + Mutation: { + setActiveBoardItem: mockSetActiveBoardItemResolver, + }, + }); const createStore = ({ initialState = {} } = {}) => { mockActions = { @@ -36,11 +46,11 @@ describe('Board card', () => { const mountComponent = ({ propsData = {}, provide = {}, - mountFn = shallowMount, stubs = { BoardCardInner }, item = mockIssue, } = {}) => { - wrapper = mountFn(BoardCard, { + wrapper = shallowMountExtended(BoardCard, { + apolloProvider: mockApollo, stubs: { ...stubs, BoardCardInner, @@ -56,9 +66,9 @@ describe('Board card', () => { groupId: null, rootPath: '/', scopedLabelsAvailable: false, + isIssueBoard: true, isEpicBoard: false, issuableType: 'issue', - isProjectBoard: false, isGroupBoard: true, disabled: false, isApolloBoard: false, @@ -218,4 +228,25 @@ describe('Board card', () => { expect(wrapper.attributes('style')).toBeUndefined(); }); }); + + describe('Apollo boards', () => { + beforeEach(async () => { + createStore(); + mountComponent({ provide: { isApolloBoard: true } }); + await nextTick(); + }); + + it('set active board item on client when clicking on card', async () => { + await selectCard(); + + expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith( + {}, + { + boardItem: mockIssue, + }, + expect.anything(), + expect.anything(), + ); + }); + }); }); diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index 90376a4a553..558a0a3b933 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -1,10 +1,15 @@ import { GlDrawer } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import { MountingPortal } from 'portal-vue'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { stubComponent } from 'helpers/stub_component'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { ISSUABLE } from '~/boards/constants'; @@ -14,13 +19,21 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue'; -import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; +import { mockActiveIssue, mockIssue, rawIssue } from '../mock_data'; Vue.use(Vuex); +Vue.use(VueApollo); describe('BoardContentSidebar', () => { let wrapper; let store; + const mockSetActiveBoardItemResolver = jest.fn(); + const mockApollo = createMockApollo([], { + Mutation: { + setActiveBoardItem: mockSetActiveBoardItemResolver, + }, + }); + const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => { store = new Vuex.Store({ state: { @@ -32,30 +45,29 @@ describe('BoardContentSidebar', () => { activeBoardItem: () => { return { ...mockActiveIssue, epic: null }; }, - groupPathForActiveIssue: () => mockIssueGroupPath, - projectPathForActiveIssue: () => mockIssueProjectPath, - isSidebarOpen: () => true, ...mockGetters, }, actions: mockActions, }); }; - const createComponent = () => { - /* - Dynamically imported components (in our case ee imports) - aren't stubbed automatically in VTU v1: - https://github.com/vuejs/vue-test-utils/issues/1279. + const createComponent = ({ isApolloBoard = false } = {}) => { + mockApollo.clients.defaultClient.cache.writeQuery({ + query: activeBoardItemQuery, + data: { + activeBoardItem: rawIssue, + }, + }); - This requires us to additionally mock apollo or vuex stores. - */ - wrapper = shallowMount(BoardContentSidebar, { + wrapper = shallowMountExtended(BoardContentSidebar, { + apolloProvider: mockApollo, provide: { canUpdate: true, rootPath: '/', groupId: 1, issuableType: TYPE_ISSUE, isGroupBoard: false, + isApolloBoard, }, store, stubs: { @@ -63,24 +75,6 @@ describe('BoardContentSidebar', () => { template: '<div><slot name="header"></slot><slot></slot></div>', }), }, - mocks: { - $apollo: { - queries: { - participants: { - loading: false, - }, - currentIteration: { - loading: false, - }, - iterations: { - loading: false, - }, - attributesList: { - loading: false, - }, - }, - }, - }, }); }; @@ -101,10 +95,12 @@ describe('BoardContentSidebar', () => { }); }); - it('does not render GlDrawer when isSidebarOpen is false', () => { - createStore({ mockGetters: { isSidebarOpen: () => false } }); + it('does not render GlDrawer when no active item is set', async () => { + createStore({ mockGetters: { activeBoardItem: () => ({ id: '', iid: '' }) } }); createComponent(); + await nextTick(); + expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false); }); @@ -189,4 +185,27 @@ describe('BoardContentSidebar', () => { expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(true); }); }); + + describe('Apollo boards', () => { + beforeEach(async () => { + createStore(); + createComponent({ isApolloBoard: true }); + await nextTick(); + }); + + it('calls setActiveBoardItemMutation on close', async () => { + wrapper.findComponent(GlDrawer).vm.$emit('close'); + + await waitForPromises(); + + expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith( + {}, + { + boardItem: null, + }, + expect.anything(), + expect.anything(), + ); + }); + }); }); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 33351bf8efd..ab51f477966 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -6,6 +6,7 @@ import Draggable from 'vuedraggable'; import Vuex from 'vuex'; import eventHub from '~/boards/eventhub'; +import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; @@ -78,6 +79,11 @@ describe('BoardContent', () => { isApolloBoard, }, store, + stubs: { + BoardContentSidebar: stubComponent(BoardContentSidebar, { + template: '<div></div>', + }), + }, }); }; diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js index a20884baf3b..296f25d96c4 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js @@ -1,6 +1,7 @@ import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import waitForPromises from 'helpers/wait_for_promises'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { createStore } from '~/boards/stores'; @@ -21,7 +22,7 @@ const TEST_ISSUE_B = { webUrl: 'webUrl', }; -describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { +describe('BoardSidebarTitle', () => { let wrapper; let store; @@ -39,6 +40,10 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { store, provide: { canUpdate: true, + isApolloBoard: false, + }, + propsData: { + activeItem: item, }, stubs: { 'board-editable-item': BoardEditableItem, @@ -86,7 +91,8 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { await nextTick(); }); - it('collapses sidebar and renders new title', () => { + it('collapses sidebar and renders new title', async () => { + await waitForPromises(); expect(findCollapsed().isVisible()).toBe(true); expect(findTitle().text()).toContain(TEST_TITLE); }); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index d5c6871d9c4..cc0945da9f4 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -277,6 +277,9 @@ export const labels = [ }, ]; +export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test'; +export const mockEpicFullPath = 'gitlab-org/test-subgroup'; + export const rawIssue = { title: 'Issue 1', id: 'gid://gitlab/Issue/436', @@ -302,12 +305,24 @@ export const rawIssue = { epic: { id: 'gid://gitlab/Epic/41', }, + totalTimeSpent: 0, + humanTimeEstimate: null, + humanTotalTimeSpent: null, + emailsDisabled: false, + hidden: false, + webUrl: `${mockIssueFullPath}/-/issue/27`, + relativePosition: null, + severity: null, + milestone: null, + weight: null, + blocked: false, + blockedByCount: 0, + iteration: null, + healthStatus: null, type: 'ISSUE', + __typename: 'Issue', }; -export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test'; -export const mockEpicFullPath = 'gitlab-org/test-subgroup'; - export const mockIssue = { id: 'gid://gitlab/Issue/436', iid: '27', @@ -329,7 +344,22 @@ export const mockIssue = { epic: { id: 'gid://gitlab/Epic/41', }, + totalTimeSpent: 0, + humanTimeEstimate: null, + humanTotalTimeSpent: null, + emailsDisabled: false, + hidden: false, + webUrl: `${mockIssueFullPath}/-/issue/27`, + relativePosition: null, + severity: null, + milestone: null, + weight: null, + blocked: false, + blockedByCount: 0, + iteration: null, + healthStatus: null, type: 'ISSUE', + __typename: 'Issue', }; export const mockEpic = { diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/ci/artifacts/components/app_spec.js index 931c4703e95..435b03e82ab 100644 --- a/spec/frontend/artifacts/components/app_spec.js +++ b/spec/frontend/ci/artifacts/components/app_spec.js @@ -2,13 +2,13 @@ import { GlSkeletonLoader } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import ArtifactsApp from '~/artifacts/components/app.vue'; -import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue'; -import getBuildArtifactsSizeQuery from '~/artifacts/graphql/queries/get_build_artifacts_size.query.graphql'; +import ArtifactsApp from '~/ci/artifacts/components/app.vue'; +import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue'; +import getBuildArtifactsSizeQuery from '~/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/artifacts/constants'; +import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/ci/artifacts/constants'; const TEST_BUILD_ARTIFACTS_SIZE = 1024; const TEST_PROJECT_PATH = 'project/path'; diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/ci/artifacts/components/artifact_row_spec.js index 268772ed4c0..86875cd8566 100644 --- a/spec/frontend/artifacts/components/artifact_row_spec.js +++ b/spec/frontend/ci/artifacts/components/artifact_row_spec.js @@ -1,10 +1,10 @@ import { GlBadge, GlButton, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui'; -import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import ArtifactRow from '~/artifacts/components/artifact_row.vue'; -import { BULK_DELETE_FEATURE_FLAG } from '~/artifacts/constants'; +import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue'; +import { BULK_DELETE_FEATURE_FLAG } from '~/ci/artifacts/constants'; describe('ArtifactRow component', () => { let wrapper; diff --git a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js index 876906b2c3c..dd077dcac9f 100644 --- a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js +++ b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js @@ -1,12 +1,12 @@ import { GlSprintf, GlModal } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue'; -import bulkDestroyArtifactsMutation from '~/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql'; +import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue'; +import bulkDestroyArtifactsMutation from '~/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql'; Vue.use(VueApollo); diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js index 6bf3498f9b0..ebdb7e25c45 100644 --- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js +++ b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js @@ -1,15 +1,15 @@ import { GlModal } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import waitForPromises from 'helpers/wait_for_promises'; -import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue'; -import ArtifactRow from '~/artifacts/components/artifact_row.vue'; -import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue'; +import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue'; +import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue'; +import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql'; -import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants'; +import destroyArtifactMutation from '~/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql'; +import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/ci/artifacts/constants'; import { createAlert } from '~/alert'; jest.mock('~/alert'); diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js index af9599daefa..53e0fdac6f6 100644 --- a/spec/frontend/artifacts/components/feedback_banner_spec.js +++ b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js @@ -1,12 +1,12 @@ import { GlBanner } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import FeedbackBanner from '~/artifacts/components/feedback_banner.vue'; +import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue'; import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import { I18N_FEEDBACK_BANNER_TITLE, I18N_FEEDBACK_BANNER_BUTTON, FEEDBACK_URL, -} from '~/artifacts/constants'; +} from '~/ci/artifacts/constants'; const mockBannerImagePath = 'banner/image/path'; diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js index 40f3c9633ab..2855b0ecb37 100644 --- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js @@ -9,17 +9,17 @@ import { } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import waitForPromises from 'helpers/wait_for_promises'; -import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue'; -import FeedbackBanner from '~/artifacts/components/feedback_banner.vue'; -import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue'; -import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue'; -import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue'; +import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue'; +import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue'; +import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue'; +import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue'; +import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql'; +import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { ARCHIVE_FILE_TYPE, @@ -27,8 +27,8 @@ import { I18N_FETCH_ERROR, INITIAL_CURRENT_PAGE, BULK_DELETE_FEATURE_FLAG, -} from '~/artifacts/constants'; -import { totalArtifactsSizeForJob } from '~/artifacts/utils'; +} from '~/ci/artifacts/constants'; +import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils'; import { createAlert } from '~/alert'; jest.mock('~/alert'); diff --git a/spec/frontend/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js index 95cc548b8c8..ae70bb4b17b 100644 --- a/spec/frontend/artifacts/components/job_checkbox_spec.js +++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js @@ -1,7 +1,7 @@ import { GlFormCheckbox } from '@gitlab/ui'; -import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import JobCheckbox from '~/artifacts/components/job_checkbox.vue'; +import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue'; describe('JobCheckbox component', () => { let wrapper; diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js index 4d610328298..3c415534c7c 100644 --- a/spec/frontend/artifacts/graphql/cache_update_spec.js +++ b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js @@ -1,5 +1,5 @@ -import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql'; -import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update'; +import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql'; +import { removeArtifactFromStore } from '~/ci/artifacts/graphql/cache_update'; describe('Artifact table cache updates', () => { let store; diff --git a/spec/frontend/fixtures/job_artifacts.rb b/spec/frontend/fixtures/job_artifacts.rb index e53cdbbaaa5..6dadd6750f1 100644 --- a/spec/frontend/fixtures/job_artifacts.rb +++ b/spec/frontend/fixtures/job_artifacts.rb @@ -12,7 +12,7 @@ RSpec.describe 'Job Artifacts (GraphQL fixtures)' do let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:user) { create(:user) } - job_artifacts_query_path = 'artifacts/graphql/queries/get_job_artifacts.query.graphql' + job_artifacts_query_path = 'ci/artifacts/graphql/queries/get_job_artifacts.query.graphql' it "graphql/#{job_artifacts_query_path}.json" do create(:ci_build, :failed, :artifacts, :trace_artifact, pipeline: pipeline) diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index 0c0998c037a..ba30073dff2 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -21,6 +21,7 @@ import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants'; describe('IDE commit form', () => { let wrapper; let store; + const showModalSpy = jest.fn(); const createComponent = () => { wrapper = shallowMount(CommitForm, { @@ -29,7 +30,11 @@ describe('IDE commit form', () => { GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { - GlModal: stubComponent(GlModal), + GlModal: stubComponent(GlModal, { + methods: { + show: showModalSpy, + }, + }), }, }); }; @@ -57,6 +62,7 @@ describe('IDE commit form', () => { tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title, }); const findForm = () => wrapper.find('form'); + const findModal = () => wrapper.findComponent(GlModal); const submitForm = () => findForm().trigger('submit'); const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField); const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val); @@ -298,22 +304,19 @@ describe('IDE commit form', () => { ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }} ${createUnexpectedCommitError} | ${{ actionPrimary: null }} `('opens error modal if commitError with $error', async ({ createError, props }) => { - const modal = wrapper.findComponent(GlModal); - modal.vm.show = jest.fn(); - const error = createError(); store.state.commit.commitError = error; await nextTick(); - expect(modal.vm.show).toHaveBeenCalled(); - expect(modal.props()).toMatchObject({ + expect(showModalSpy).toHaveBeenCalled(); + expect(findModal().props()).toMatchObject({ actionCancel: { text: 'Cancel' }, ...props, }); // Because of the legacy 'mountComponent' approach here, the only way to // test the text of the modal is by viewing the content of the modal added to the document. - expect(modal.html()).toContain(error.messageHTML); + expect(findModal().html()).toContain(error.messageHTML); }); }); @@ -339,7 +342,7 @@ describe('IDE commit form', () => { await nextTick(); - wrapper.findComponent(GlModal).vm.$emit('ok'); + findModal().vm.$emit('ok'); await waitForPromises(); diff --git a/spec/frontend/invite_members/components/invite_group_notification_spec.js b/spec/frontend/invite_members/components/invite_group_notification_spec.js index 3e6ba6da9f4..1da2e7b705d 100644 --- a/spec/frontend/invite_members/components/invite_group_notification_spec.js +++ b/spec/frontend/invite_members/components/invite_group_notification_spec.js @@ -2,7 +2,7 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { sprintf } from '~/locale'; import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue'; -import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants'; +import { GROUP_MODAL_TO_GROUP_ALERT_BODY } from '~/invite_members/constants'; describe('InviteGroupNotification', () => { let wrapper; @@ -13,7 +13,11 @@ describe('InviteGroupNotification', () => { const createComponent = () => { wrapper = shallowMountExtended(InviteGroupNotification, { provide: { freeUsersLimit: 5 }, - propsData: { name: 'name' }, + propsData: { + name: 'name', + notificationLink: '_notification_link_', + notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY, + }, stubs: { GlSprintf }, }); }; @@ -28,15 +32,13 @@ describe('InviteGroupNotification', () => { }); it('shows the correct message', () => { - const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 }); + const message = sprintf(GROUP_MODAL_TO_GROUP_ALERT_BODY, { count: 5 }); expect(findAlert().text()).toMatchInterpolatedText(message); }); it('has a help link', () => { - expect(findLink().attributes('href')).toEqual( - 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group', - ); + expect(findLink().attributes('href')).toEqual('_notification_link_'); }); }); }); diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js index 82b4717fbf1..4f082145562 100644 --- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js @@ -12,6 +12,12 @@ import { displaySuccessfulInvitationAlert, reloadOnInvitationSuccess, } from '~/invite_members/utils/trigger_successful_invite_alert'; +import { + GROUP_MODAL_TO_GROUP_ALERT_BODY, + GROUP_MODAL_TO_GROUP_ALERT_LINK, + GROUP_MODAL_TO_PROJECT_ALERT_BODY, + GROUP_MODAL_TO_PROJECT_ALERT_LINK, +} from '~/invite_members/constants'; import { propsData, sharedGroup } from '../mock_data/group_modal'; jest.mock('~/invite_members/utils/trigger_successful_invite_alert'); @@ -91,6 +97,26 @@ describe('InviteGroupsModal', () => { expect(findInviteGroupAlert().exists()).toBe(false); }); + + it('shows the user limit notification alert with correct link and text for group', () => { + createComponent({ freeUserCapEnabled: true }); + + expect(findInviteGroupAlert().props()).toMatchObject({ + name: propsData.name, + notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY, + notificationLink: GROUP_MODAL_TO_GROUP_ALERT_LINK, + }); + }); + + it('shows the user limit notification alert with correct link and text for project', () => { + createComponent({ freeUserCapEnabled: true, isProject: true }); + + expect(findInviteGroupAlert().props()).toMatchObject({ + name: propsData.name, + notificationText: GROUP_MODAL_TO_PROJECT_ALERT_BODY, + notificationLink: GROUP_MODAL_TO_PROJECT_ALERT_LINK, + }); + }); }); describe('submitting the invite form', () => { diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js index 58ec7387851..bd8e79a90ec 100644 --- a/spec/frontend/issues/show/components/header_actions_spec.js +++ b/spec/frontend/issues/show/components/header_actions_spec.js @@ -2,6 +2,8 @@ import Vue, { nextTick } from 'vue'; import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; +import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; import { mockTracking } from 'helpers/tracking_helper'; import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants'; @@ -14,17 +16,22 @@ import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutatio import * as urlUtility from '~/lib/utils/url_utility'; import eventHub from '~/notes/event_hub'; import createStore from '~/notes/stores'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; +import updateIssueMutation from '~/issues/show/queries/update_issue.mutation.graphql'; +import toast from '~/vue_shared/plugins/global_toast'; jest.mock('~/alert'); jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() })); +jest.mock('~/vue_shared/plugins/global_toast'); describe('HeaderActions component', () => { let dispatchEventSpy; - let mutateMock; let wrapper; let visitUrlSpy; Vue.use(Vuex); + Vue.use(VueApollo); const store = createStore(); @@ -45,15 +52,28 @@ describe('HeaderActions component', () => { reportedUserId: 1, reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32', submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam', + issuableEmailAddress: null, + fullPath: 'full-path', }; - const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } }; + const updateIssueMutationResponse = { + data: { + updateIssue: { + errors: [], + issuable: { + id: 'gid://gitlab/Issue/511', + state: STATUS_OPEN, + }, + }, + }, + }; const promoteToEpicMutationResponse = { data: { promoteToEpic: { errors: [], epic: { + id: 'gid://gitlab/Epic/1', webPath: '/groups/gitlab-org/-/epics/1', }, }, @@ -69,6 +89,20 @@ describe('HeaderActions component', () => { }, }; + const mockIssueReferenceData = { + data: { + workspace: { + id: 'gid://gitlab/Project/7', + issuable: { + id: 'gid://gitlab/Issue/511', + reference: 'flightjs/Flight#33', + __typename: 'Issue', + }, + __typename: 'Project', + }, + }, + }; + const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`); const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`); @@ -77,33 +111,54 @@ describe('HeaderActions component', () => { const findDesktopDropdown = () => findDropdownBy('desktop-dropdown'); const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem); const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem); + const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); + const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`); + const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`); + const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`); + const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`); + const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`); const findModal = () => wrapper.findComponent(GlModal); const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index); + const issueReferenceSuccessHandler = jest.fn().mockResolvedValue(mockIssueReferenceData); + const updateIssueMutationResponseHandler = jest + .fn() + .mockResolvedValue(updateIssueMutationResponse); + const promoteToEpicMutationSuccessResponseHandler = jest + .fn() + .mockResolvedValue(promoteToEpicMutationResponse); + const promoteToEpicMutationErrorHandler = jest + .fn() + .mockResolvedValue(promoteToEpicMutationErrorResponse); + const mountComponent = ({ props = {}, issueState = STATUS_OPEN, blockedByIssues = [], - mutateResponse = {}, + movedMrSidebarEnabled = false, + promoteToEpicHandler = promoteToEpicMutationSuccessResponseHandler, } = {}) => { - mutateMock = jest.fn().mockResolvedValue(mutateResponse); - store.dispatch('setNoteableData', { blocked_by_issues: blockedByIssues, state: issueState, }); + const handlers = [ + [issueReferenceQuery, issueReferenceSuccessHandler], + [updateIssueMutation, updateIssueMutationResponseHandler], + [promoteToEpicMutation, promoteToEpicHandler], + ]; + return shallowMount(HeaderActions, { + apolloProvider: createMockApollo(handlers), store, provide: { ...defaultProps, ...props, - }, - mocks: { - $apollo: { - mutate: mutateMock, + glFeatures: { + movedMrSidebar: movedMrSidebarEnabled, }, }, stubs: { @@ -138,7 +193,6 @@ describe('HeaderActions component', () => { wrapper = mountComponent({ props: { issueType }, issueState, - mutateResponse: updateIssueMutationResponse, }); }); @@ -149,23 +203,19 @@ describe('HeaderActions component', () => { it('calls apollo mutation', () => { findToggleIssueStateButton().vm.$emit('click'); - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - iid: defaultProps.iid, - projectPath: defaultProps.projectPath, - stateEvent: newIssueState, - }, - }, - }), - ); + expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + stateEvent: newIssueState, + }, + }); }); it('dispatches a custom event to update the issue page', async () => { findToggleIssueStateButton().vm.$emit('click'); - await nextTick(); + await waitForPromises(); expect(dispatchEventSpy).toHaveBeenCalledTimes(1); }); @@ -290,28 +340,25 @@ describe('HeaderActions component', () => { describe('when "Promote to epic" button is clicked', () => { describe('when response is successful', () => { - beforeEach(() => { + beforeEach(async () => { visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); wrapper = mountComponent({ - mutateResponse: promoteToEpicMutationResponse, + promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler, }); wrapper.find('[data-testid="promote-button"]').vm.$emit('click'); + + await waitForPromises(); }); it('invokes GraphQL mutation when clicked', () => { - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - mutation: promoteToEpicMutation, - variables: { - input: { - iid: defaultProps.iid, - projectPath: defaultProps.projectPath, - }, - }, - }), - ); + expect(promoteToEpicMutationSuccessResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + }, + }); }); it('shows a success message and tells the user they are being redirected', () => { @@ -329,14 +376,16 @@ describe('HeaderActions component', () => { }); describe('when response contains errors', () => { - beforeEach(() => { + beforeEach(async () => { visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); wrapper = mountComponent({ - mutateResponse: promoteToEpicMutationErrorResponse, + promoteToEpicHandler: promoteToEpicMutationErrorHandler, }); wrapper.find('[data-testid="promote-button"]').vm.$emit('click'); + + await waitForPromises(); }); it('shows an error message', () => { @@ -349,21 +398,17 @@ describe('HeaderActions component', () => { describe('when `toggle.issuable.state` event is emitted', () => { it('invokes a method to toggle the issue state', () => { - wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse }); + wrapper = mountComponent(); eventHub.$emit('toggle.issuable.state'); - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - iid: defaultProps.iid, - projectPath: defaultProps.projectPath, - stateEvent: ISSUE_STATE_EVENT_CLOSE, - }, - }, - }), - ); + expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + stateEvent: ISSUE_STATE_EVENT_CLOSE, + }, + }); }); }); @@ -392,17 +437,13 @@ describe('HeaderActions component', () => { it('calls apollo mutation when primary button is clicked', () => { findModal().vm.$emit('primary'); - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - iid: defaultProps.iid.toString(), - projectPath: defaultProps.projectPath, - stateEvent: ISSUE_STATE_EVENT_CLOSE, - }, - }, - }), - ); + expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid.toString(), + projectPath: defaultProps.projectPath, + stateEvent: ISSUE_STATE_EVENT_CLOSE, + }, + }); }); describe.each` @@ -434,8 +475,6 @@ describe('HeaderActions component', () => { }); describe('abuse category selector', () => { - const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); - beforeEach(() => { wrapper = mountComponent({ props: { isIssueAuthor: false } }); }); @@ -445,7 +484,7 @@ describe('HeaderActions component', () => { }); it('opens the drawer', async () => { - findDesktopDropdownItems().at(2).vm.$emit('click'); + findReportAbuseSelectorItem().vm.$emit('click'); await nextTick(); @@ -453,10 +492,160 @@ describe('HeaderActions component', () => { }); it('closes the drawer', async () => { - await findDesktopDropdownItems().at(2).vm.$emit('click'); + await findReportAbuseSelectorItem().vm.$emit('click'); await findAbuseCategorySelector().vm.$emit('close-drawer'); expect(findAbuseCategorySelector().exists()).toEqual(false); }); }); + + describe('notification toggle', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | visible + ${true} | ${TYPE_ISSUE} | ${true} + ${true} | ${TYPE_INCIDENT} | ${true} + ${false} | ${TYPE_ISSUE} | ${false} + ${false} | ${TYPE_INCIDENT} | ${false} + `( + `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`, + ({ movedMrSidebarEnabled, issueType, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Notification toggle`, () => { + expect(findNotificationWidget().exists()).toBe(visible); + }); + }, + ); + }); + }); + + describe('lock issue option', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | visible + ${true} | ${TYPE_ISSUE} | ${true} + ${true} | ${TYPE_INCIDENT} | ${false} + ${false} | ${TYPE_ISSUE} | ${false} + ${false} | ${TYPE_INCIDENT} | ${false} + `( + `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`, + ({ movedMrSidebarEnabled, issueType, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Lock issue option`, () => { + expect(findLockIssueWidget().exists()).toBe(visible); + }); + }, + ); + }); + }); + + describe('copy reference option', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | visible + ${true} | ${TYPE_ISSUE} | ${true} + ${true} | ${TYPE_INCIDENT} | ${true} + ${false} | ${TYPE_ISSUE} | ${false} + ${false} | ${TYPE_INCIDENT} | ${false} + `( + 'when movedMrSidebarFlagEnabled is "$movedMrSidebarEnabled" with issue type "$issueType"', + ({ movedMrSidebarEnabled, issueType, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Copy reference option`, () => { + expect(findCopyRefenceDropdownItem().exists()).toBe(visible); + }); + }, + ); + }); + + describe('clicking when visible', () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType: TYPE_ISSUE, + }, + movedMrSidebarEnabled: true, + }); + }); + + it('shows toast message', () => { + findCopyRefenceDropdownItem().vm.$emit('click'); + + expect(toast).toHaveBeenCalledWith('Reference copied'); + }); + }); + }); + + describe('copy email option', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | issuableEmailAddress | visible + ${true} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${true} + ${true} | ${TYPE_ISSUE} | ${''} | ${false} + ${true} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${true} + ${true} | ${TYPE_INCIDENT} | ${''} | ${false} + ${false} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${false} + ${false} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${false} + `( + 'when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" issue type is "$issueType" and issuableEmailAddress="$issuableEmailAddress"', + ({ movedMrSidebarEnabled, issueType, issuableEmailAddress, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + issuableEmailAddress, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Copy email option`, () => { + expect(findCopyEmailItem().exists()).toBe(visible); + }); + }, + ); + }); + + describe('clicking when visible', () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType: TYPE_ISSUE, + issuableEmailAddress: 'mock-email-address', + }, + movedMrSidebarEnabled: true, + }); + }); + + it('shows toast message', () => { + findCopyEmailItem().vm.$emit('click'); + + expect(toast).toHaveBeenCalledWith('Email address copied'); + }); + }); + }); }); diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js new file mode 100644 index 00000000000..71b7a3da1c3 --- /dev/null +++ b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js @@ -0,0 +1,67 @@ +import { GlPopover } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue'; +import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants'; +import { TYPE_ISSUE } from '~/issues/constants'; +import * as utils from '~/lib/utils/common_utils'; + +describe('NewHeaderActionsPopover', () => { + let wrapper; + + const createComponent = ({ issueType = TYPE_ISSUE }) => { + wrapper = shallowMountExtended(NewHeaderActionsPopover, { + propsData: { + issueType, + }, + stubs: { + GlPopover, + }, + }); + }; + + const findPopover = () => wrapper.findComponent(GlPopover); + const findConfirmButton = () => wrapper.findByTestId('confirm-button'); + + describe('without the popover cookie', () => { + beforeEach(() => { + utils.setCookie = jest.fn(); + + createComponent({}); + }); + + it('renders the popover with correct text', () => { + expect(findPopover().exists()).toBe(true); + expect(findPopover().text()).toContain('issue actions'); + }); + + it('does not call setCookie', () => { + expect(utils.setCookie).not.toHaveBeenCalled(); + }); + + describe('when the confirm button is clicked', () => { + beforeEach(() => { + findConfirmButton().vm.$emit('click'); + }); + + it('sets the popover cookie', () => { + expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true); + }); + + it('hides the popover', () => { + expect(findPopover().exists()).toBe(false); + }); + }); + }); + + describe('with the popover cookie', () => { + beforeEach(() => { + jest.spyOn(utils, 'getCookie').mockReturnValue('true'); + + createComponent({}); + }); + + it('does not render the popover', () => { + expect(findPopover().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 00f75b14e6b..e385c478dd6 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -1,4 +1,4 @@ -import { GlLink } from '@gitlab/ui'; +import { GlLink, GlFormCheckbox } from '@gitlab/ui'; import { nextTick } from 'vue'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; import NoteForm from '~/notes/components/note_form.vue'; @@ -234,7 +234,7 @@ describe('issue_note_form component', () => { }); it('shows resolve checkbox', () => { - expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(true); + expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true); }); it('hides resolve checkbox', async () => { @@ -253,7 +253,7 @@ describe('issue_note_form component', () => { }, }); - expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false); + expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(false); }); it('hides actions for commits', async () => { diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap index 213a732d59b..e9ee6ebdb5c 100644 --- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap +++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap @@ -5,7 +5,6 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = ` aria-label="Breadcrumb" class="gl-breadcrumbs" > - <ol class="breadcrumb gl-breadcrumb-list" > @@ -16,7 +15,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = ` class="" target="_self" > - + <!----> + <span> + + </span> </a> </li> @@ -30,7 +32,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = ` href="#" target="_self" > - + <!----> + <span> + + </span> </a> </li> @@ -44,7 +49,6 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = ` aria-label="Breadcrumb" class="gl-breadcrumbs" > - <ol class="breadcrumb gl-breadcrumb-list" > @@ -56,7 +60,10 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = ` class="" target="_self" > - + <!----> + <span> + + </span> </a> </li> diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js index d26ef7298ce..5e766e9a41c 100644 --- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js +++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js @@ -29,6 +29,7 @@ describe('IssuableLockForm', () => { const findEditForm = () => wrapper.findComponent(EditForm); const findSidebarLockStatusTooltip = () => getBinding(findSidebarCollapseIcon().element, 'gl-tooltip'); + const findIssuableLockClickable = () => wrapper.find('[data-testid="issuable-lock"]'); const initStore = (isLocked) => { if (issuableType === ISSUABLE_TYPE_ISSUE) { @@ -48,7 +49,7 @@ describe('IssuableLockForm', () => { store.getters.getNoteableData.discussion_locked = isLocked; }; - const createComponent = ({ props = {} }, movedMrSidebar = false) => { + const createComponent = ({ props = {}, movedMrSidebar = false }) => { wrapper = shallowMount(IssuableLockForm, { store, provide: { @@ -169,11 +170,27 @@ describe('IssuableLockForm', () => { `('displays $message when merge request is $locked', async ({ locked, message }) => { initStore(locked); - createComponent({}, true); + createComponent({ movedMrSidebar: true }); await wrapper.find('.dropdown-item').trigger('click'); expect(toast).toHaveBeenCalledWith(message); }); }); + + describe('moved_mr_sidebar flag', () => { + describe('when the flag is off', () => { + it('does not show the non editable lock status', () => { + createComponent({ movedMrSidebar: false }); + expect(findIssuableLockClickable().exists()).toBe(false); + }); + }); + + describe('when the flag is on', () => { + it('does not show the non editable lock status', () => { + createComponent({ movedMrSidebar: true }); + expect(findIssuableLockClickable().exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js index 128ef69e5f1..037b56d9904 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -1,4 +1,3 @@ -import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue'; @@ -23,8 +22,9 @@ describe('UncollapsedReviewerList component', () => { let wrapper; const findAllRerequestButtons = () => wrapper.findAll('[data-testid="re-request-button"]'); - const findAllRerequestSuccessIcons = () => wrapper.findAll('[data-testid="re-request-success"]'); - const findAllReviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]'); + const findAllReviewerApprovalIcons = () => wrapper.findAll('[data-testid="approved"]'); + const findAllReviewedNotApprovedIcons = () => + wrapper.findAll('[data-testid="reviewed-not-approved"]'); const findAllReviewerAvatarLinks = () => wrapper.findAllComponents(ReviewerAvatarLink); function createComponent(props = {}, glFeatures = {}) { @@ -42,13 +42,6 @@ describe('UncollapsedReviewerList component', () => { }); } - const callRerequestCallback = async () => { - const payload = wrapper.emitted('request-review')[0][0]; - // Call payload which is normally called by a parent component - payload.callback(payload.userId, true); - await nextTick(); - }; - describe('single reviewer', () => { const user = userDataMock(); @@ -71,13 +64,6 @@ describe('UncollapsedReviewerList component', () => { expect(findAllRerequestButtons().at(0).props('loading')).toBe(true); }); - - it('renders re-request success icon', async () => { - await findAllRerequestButtons().at(0).vm.$emit('click'); - await callRerequestCallback(); - - expect(findAllRerequestSuccessIcons().at(0).exists()).toBe(true); - }); }); describe('multiple reviewers', () => { @@ -92,20 +78,32 @@ describe('UncollapsedReviewerList component', () => { approved: true, }, }; + const user3 = { + ...user, + id: 3, + name: 'lizabeth-wilderman', + username: 'lizabeth-wilderman', + mergeRequestInteraction: { + ...user.mergeRequestInteraction, + approved: false, + reviewed: true, + }, + }; beforeEach(() => { createComponent({ - users: [user, user2], + users: [user, user2, user3], }); }); - it('has both users', () => { - expect(findAllReviewerAvatarLinks()).toHaveLength(2); + it('has three users', () => { + expect(findAllReviewerAvatarLinks()).toHaveLength(3); }); - it('shows both users with avatar, and author name', () => { + it('shows all users with avatar, and author name', () => { expect(wrapper.text()).toContain(user.name); expect(wrapper.text()).toContain(user2.name); + expect(wrapper.text()).toContain(user3.name); }); it('renders approval icon', () => { @@ -118,21 +116,19 @@ describe('UncollapsedReviewerList component', () => { expect(icon.attributes('title')).toBe('Approved by @hello-world'); }); + it('shows that lizabeth-wilderman reviewed but did not approve', () => { + const icon = findAllReviewedNotApprovedIcons().at(1); + + expect(icon.attributes('title')).toBe('Reviewed by @lizabeth-wilderman but not yet approved'); + }); + it('renders re-request loading icon', async () => { await findAllRerequestButtons().at(1).vm.$emit('click'); const allRerequestButtons = findAllRerequestButtons(); - expect(allRerequestButtons).toHaveLength(2); + expect(allRerequestButtons).toHaveLength(3); expect(allRerequestButtons.at(1).props('loading')).toBe(true); }); - - it('renders re-request success icon', async () => { - await findAllRerequestButtons().at(1).vm.$emit('click'); - await callRerequestCallback(); - - expect(findAllRerequestButtons()).toHaveLength(1); - expect(findAllRerequestSuccessIcons()).toHaveLength(1); - }); }); }); diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb index 045c1620815..d955ec5023e 100644 --- a/spec/graphql/types/work_items/widget_interface_spec.rb +++ b/spec/graphql/types/work_items/widget_interface_spec.rb @@ -15,13 +15,14 @@ RSpec.describe Types::WorkItems::WidgetInterface do using RSpec::Parameterized::TableSyntax where(:widget_class, :widget_type_name) do - WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType - WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType - WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType - WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType - WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType - WorkItems::Widgets::Notifications | Types::WorkItems::Widgets::NotificationsType + WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType + WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType + WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType + WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType + WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType + WorkItems::Widgets::Notifications | Types::WorkItems::Widgets::NotificationsType WorkItems::Widgets::CurrentUserTodos | Types::WorkItems::Widgets::CurrentUserTodosType + WorkItems::Widgets::AwardEmoji | Types::WorkItems::Widgets::AwardEmojiType end with_them do diff --git a/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb b/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb new file mode 100644 index 00000000000..493e628ac83 --- /dev/null +++ b/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::Widgets::AwardEmojiType, feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[award_emoji downvotes upvotes type] + + expect(described_class.graphql_name).to eq('WorkItemWidgetAwardEmoji') + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index d940c696fb3..38cbb5a1d66 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe IssuesHelper do + include Features::MergeRequestHelpers + let_it_be(:project) { create(:project) } let_it_be_with_reload(:issue) { create(:issue, project: project) } @@ -235,10 +237,13 @@ RSpec.describe IssuesHelper do describe '#issue_header_actions_data' do let(:current_user) { create(:user) } + let(:merge_request) { create(:merge_request, :opened, source_project: project, author: current_user) } + let(:issuable_sidebar_issue) { serialize_issuable_sidebar(current_user, project, merge_request) } before do allow(helper).to receive(:current_user).and_return(current_user) allow(helper).to receive(:can?).and_return(true) + allow(helper).to receive(:issuable_sidebar).and_return(issuable_sidebar_issue) end it 'returns expected result' do @@ -257,10 +262,11 @@ RSpec.describe IssuesHelper do report_abuse_path: add_category_abuse_reports_path, reported_user_id: issue.author.id, reported_from_url: issue_url(issue), - submit_as_spam_path: mark_as_spam_project_issue_path(project, issue) + submit_as_spam_path: mark_as_spam_project_issue_path(project, issue), + issuable_email_address: issuable_sidebar_issue[:create_note_email] } - expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected) + expect(helper.issue_header_actions_data(project, issue, current_user, issuable_sidebar_issue)).to include(expected) end end diff --git a/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb b/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb index 065b6d00ddb..7161ca35edd 100644 --- a/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb +++ b/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb @@ -4,24 +4,5 @@ require 'spec_helper' require_migration! RSpec.describe AddNotificationsWorkItemWidget, :migration, feature_category: :team_planning do - let(:migration) { described_class.new } - let(:work_item_definitions) { table(:work_item_widget_definitions) } - - describe '#up' do - it 'creates notifications widget definition in all types' do - work_item_definitions.where(name: 'Notifications').delete_all - - expect { migrate! }.to change { work_item_definitions.count }.by(7) - expect(work_item_definitions.all.pluck(:name)).to include('Notifications') - end - end - - describe '#down' do - it 'removes definitions for notifications widget' do - migrate! - - expect { migration.down }.to change { work_item_definitions.count }.by(-7) - expect(work_item_definitions.all.pluck(:name)).not_to include('Notifications') - end - end + it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Notifications' end diff --git a/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb b/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb index f044b15fa6b..1df80a519f2 100644 --- a/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb +++ b/spec/migrations/20230317162059_add_current_user_todos_work_item_widget_spec.rb @@ -4,24 +4,5 @@ require 'spec_helper' require_migration! RSpec.describe AddCurrentUserTodosWorkItemWidget, :migration, feature_category: :team_planning do - let(:migration) { described_class.new } - let(:work_item_definitions) { table(:work_item_widget_definitions) } - - describe '#up' do - it 'creates notifications widget definition in all types' do - work_item_definitions.where(name: 'Current user todos').delete_all - - expect { migrate! }.to change { work_item_definitions.count }.by(7) - expect(work_item_definitions.all.pluck(:name)).to include('Current user todos') - end - end - - describe '#down' do - it 'removes definitions for notifications widget' do - migrate! - - expect { migration.down }.to change { work_item_definitions.count }.by(-7) - expect(work_item_definitions.all.pluck(:name)).not_to include('Current user todos') - end - end + it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Current user todos' end diff --git a/spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb b/spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb new file mode 100644 index 00000000000..16a205c5da5 --- /dev/null +++ b/spec/migrations/20230323101138_add_award_emoji_work_item_widget_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddAwardEmojiWorkItemWidget, :migration, feature_category: :team_planning do + it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Award emoji' +end diff --git a/spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb b/spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb new file mode 100644 index 00000000000..efbbe22fd1b --- /dev/null +++ b/spec/migrations/20230328100534_truncate_error_tracking_tables_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe TruncateErrorTrackingTables, :migration, feature_category: :redis do + let(:migration) { described_class.new } + + context 'when on GitLab.com' do + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + + context 'when using Main db' do + it 'truncates the table' do + expect(described_class.connection).to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE') + + migration.up + end + end + + context 'when uses CI db', migration: :gitlab_ci do + before do + skip_if_multiple_databases_not_setup(:ci) + end + + it 'does not truncate the table' do + expect(described_class.connection).not_to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE') + + migration.up + end + end + end + + context 'when on self-managed' do + before do + allow(Gitlab).to receive(:com?).and_return(false) + end + + context 'when using Main db' do + it 'does not truncate the table' do + expect(described_class.connection).not_to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE') + + migration.up + end + end + + context 'when uses CI db', migration: :gitlab_ci do + it 'does not truncate the table' do + expect(described_class.connection).not_to receive(:execute).with('TRUNCATE table error_tracking_errors CASCADE') + + migration.up + end + end + end +end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index c1de8125c0d..ed2bf26e129 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1666,6 +1666,32 @@ RSpec.describe Note do end end end + + describe '.without_hidden' do + subject { described_class.without_hidden } + + context 'when a note with a banned author exists' do + let_it_be(:banned_user) { create(:banned_user).user } + let_it_be(:banned_note) { create(:note, author: banned_user) } + + context 'when the :hidden_notes feature is disabled' do + before do + stub_feature_flags(hidden_notes: false) + end + + it { is_expected.to include(banned_note, note1) } + end + + context 'when the :hidden_notes feature is enabled' do + before do + stub_feature_flags(hidden_notes: true) + end + + it { is_expected.not_to include(banned_note) } + it { is_expected.to include(note1) } + end + end + end end describe 'banzai_render_context' do diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb index f4d132bec52..a33e08a1bf2 100644 --- a/spec/models/work_items/widget_definition_spec.rb +++ b/spec/models/work_items/widget_definition_spec.rb @@ -13,7 +13,8 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do ::WorkItems::Widgets::Milestone, ::WorkItems::Widgets::Notes, ::WorkItems::Widgets::Notifications, - ::WorkItems::Widgets::CurrentUserTodos + ::WorkItems::Widgets::CurrentUserTodos, + ::WorkItems::Widgets::AwardEmoji ] if Gitlab.ee? diff --git a/spec/models/work_items/widgets/award_emoji_spec.rb b/spec/models/work_items/widgets/award_emoji_spec.rb new file mode 100644 index 00000000000..bb61aa41669 --- /dev/null +++ b/spec/models/work_items/widgets/award_emoji_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::AwardEmoji, feature_category: :team_planning do + let_it_be(:work_item) { create(:work_item) } + let_it_be(:emoji1) { create(:award_emoji, name: 'star', awardable: work_item) } + let_it_be(:emoji2) { create(:award_emoji, :upvote, awardable: work_item) } + let_it_be(:emoji3) { create(:award_emoji, :downvote, awardable: work_item) } + + describe '.type' do + it { expect(described_class.type).to eq(:award_emoji) } + end + + describe '#type' do + it { expect(described_class.new(work_item).type).to eq(:award_emoji) } + end + + describe '#downvotes' do + it { expect(described_class.new(work_item).downvotes).to eq(1) } + end + + describe '#upvotes' do + it { expect(described_class.new(work_item).upvotes).to eq(1) } + end + + describe '#award_emoji' do + it { expect(described_class.new(work_item).award_emoji).to match_array([emoji1, emoji2, emoji3]) } + end +end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index d5dd12de63e..b792505374e 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -341,6 +341,51 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end end + context 'when fetching work item award emoji widget' do + let(:fields) do + <<~GRAPHQL + nodes { + widgets { + type + ... on WorkItemWidgetAwardEmoji { + awardEmoji { + nodes { + name + emoji + user { id } + } + } + upvotes + downvotes + } + } + } + GRAPHQL + end + + before do + create(:award_emoji, name: 'star', user: current_user, awardable: item1) + create(:award_emoji, :upvote, awardable: item1) + create(:award_emoji, :downvote, awardable: item1) + end + + it 'executes limited number of N+1 queries', :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create_list(:work_item, 2, project: project) do |item| + create(:award_emoji, name: 'rocket', awardable: item) + create_list(:award_emoji, 2, :upvote, awardable: item) + create_list(:award_emoji, 2, :downvote, awardable: item) + end + + expect { post_graphql(query, current_user: current_user) } + .not_to exceed_all_query_limit(control) + expect_graphql_errors_to_be_empty + end + end + def item_ids graphql_dig_at(items_data, :node, :id) end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index fe6f75548a5..a89c2268208 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -486,6 +486,48 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end end end + + describe 'award emoji widget' do + let_it_be(:emoji) { create(:award_emoji, name: 'star', awardable: work_item) } + let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item) } + let_it_be(:downvote) { create(:award_emoji, :downvote, awardable: work_item) } + + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetAwardEmoji { + upvotes + downvotes + awardEmoji { + nodes { + name + } + } + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'AWARD_EMOJI', + 'upvotes' => work_item.upvotes, + 'downvotes' => work_item.downvotes, + 'awardEmoji' => { + 'nodes' => match_array( + [emoji, upvote, downvote].map { |e| { 'name' => e.name } } + ) + } + ) + ) + ) + end + end end context 'when an Issue Global ID is provided' do diff --git a/spec/scripts/generate_rspec_pipeline_spec.rb b/spec/scripts/generate_rspec_pipeline_spec.rb index b3eaf9e9127..91b5739cf63 100644 --- a/spec/scripts/generate_rspec_pipeline_spec.rb +++ b/spec/scripts/generate_rspec_pipeline_spec.rb @@ -13,42 +13,49 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin "spec/lib/gitlab/background_migration/a_spec.rb spec/lib/gitlab/background_migration/b_spec.rb " \ "spec/models/a_spec.rb spec/models/b_spec.rb " \ "spec/controllers/a_spec.rb spec/controllers/b_spec.rb " \ - "spec/features/a_spec.rb spec/features/b_spec.rb" + "spec/features/a_spec.rb spec/features/b_spec.rb " \ + "ee/spec/features/a_spec.rb" end let(:pipeline_template) { Tempfile.new(['pipeline_template', '.yml.erb']) } let(:pipeline_template_content) do <<~YAML - <% if rspec_files_per_test_level[:migration][:files].size > 0 %> + <% if test_suite_prefix.nil? && rspec_files_per_test_level[:migration][:files].size > 0 %> rspec migration: <% if rspec_files_per_test_level[:migration][:parallelization] > 1 %> parallel: <%= rspec_files_per_test_level[:migration][:parallelization] %> <% end %> <% end %> - <% if rspec_files_per_test_level[:background_migration][:files].size > 0 %> + <% if test_suite_prefix.nil? && rspec_files_per_test_level[:background_migration][:files].size > 0 %> rspec background_migration: <% if rspec_files_per_test_level[:background_migration][:parallelization] > 1 %> parallel: <%= rspec_files_per_test_level[:background_migration][:parallelization] %> <% end %> <% end %> - <% if rspec_files_per_test_level[:unit][:files].size > 0 %> + <% if test_suite_prefix.nil? && rspec_files_per_test_level[:unit][:files].size > 0 %> rspec unit: <% if rspec_files_per_test_level[:unit][:parallelization] > 1 %> parallel: <%= rspec_files_per_test_level[:unit][:parallelization] %> <% end %> <% end %> - <% if rspec_files_per_test_level[:integration][:files].size > 0 %> + <% if test_suite_prefix.nil? && rspec_files_per_test_level[:integration][:files].size > 0 %> rspec integration: <% if rspec_files_per_test_level[:integration][:parallelization] > 1 %> parallel: <%= rspec_files_per_test_level[:integration][:parallelization] %> <% end %> <% end %> - <% if rspec_files_per_test_level[:system][:files].size > 0 %> + <% if test_suite_prefix.nil? && rspec_files_per_test_level[:system][:files].size > 0 %> rspec system: <% if rspec_files_per_test_level[:system][:parallelization] > 1 %> parallel: <%= rspec_files_per_test_level[:system][:parallelization] %> <% end %> <% end %> + <% if test_suite_prefix == 'ee/' && rspec_files_per_test_level[:system][:files].size > 0 %> + rspec-ee system: + <% if rspec_files_per_test_level[:system][:parallelization] > 1 %> + parallel: <%= rspec_files_per_test_level[:system][:parallelization] %> + <% end %> + <% end %> YAML end @@ -65,7 +72,8 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin "spec/controllers/a_spec.rb": 60.2, "spec/controllers/ab_spec.rb": 180.4, "spec/features/a_spec.rb": 360.1, - "spec/features/b_spec.rb": 180.5 + "spec/features/b_spec.rb": 180.5, + "ee/spec/features/a_spec.rb": 180.5 } JSON end @@ -177,6 +185,53 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin end end + context 'when test_suite_prefix is given' do + subject do + described_class.new( + rspec_files_path: rspec_files.path, + pipeline_template_path: pipeline_template.path, + knapsack_report_path: knapsack_report.path, + test_suite_prefix: 'ee/' + ) + end + + it 'generates the pipeline config based on the test_suite_prefix' do + subject.generate! + + expect(File.read("#{pipeline_template.path}.yml")) + .to eq("rspec-ee system:") + end + end + + context 'when generated_pipeline_path is given' do + let(:custom_pipeline_filename) { Tempfile.new(['custom_pipeline_filename', '.yml']) } + + around do |example| + example.run + ensure + custom_pipeline_filename.close + custom_pipeline_filename.unlink + end + + subject do + described_class.new( + rspec_files_path: rspec_files.path, + pipeline_template_path: pipeline_template.path, + generated_pipeline_path: custom_pipeline_filename.path + ) + end + + it 'writes the pipeline config in the given generated_pipeline_path' do + subject.generate! + + expect(File.read(custom_pipeline_filename.path)) + .to eq( + "rspec migration:\nrspec background_migration:\nrspec unit:\n" \ + "rspec integration:\nrspec system:" + ) + end + end + context 'when rspec_files does not exist' do subject { described_class.new(rspec_files_path: nil, pipeline_template_path: pipeline_template.path) } diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb new file mode 100644 index 00000000000..28eac52256f --- /dev/null +++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'migration that adds widget to work items definitions' do |widget_name:| + let(:migration) { described_class.new } + let(:work_item_definitions) { table(:work_item_widget_definitions) } + + describe '#up' do + it "creates widget definition in all types" do + work_item_definitions.where(name: widget_name).delete_all + + expect { migrate! }.to change { work_item_definitions.count }.by(7) + expect(work_item_definitions.all.pluck(:name)).to include(widget_name) + end + + it 'logs a warning if the type is missing' do + allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id).and_call_original + allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id) + .with('Issue', nil).and_return(nil) + + expect(Gitlab::AppLogger).to receive(:warn).with('type Issue is missing, not adding widget') + migrate! + end + end + + describe '#down' do + it "removes definitions for widget" do + migrate! + + expect { migration.down }.to change { work_item_definitions.count }.by(-7) + expect(work_item_definitions.all.pluck(:name)).not_to include(widget_name) + end + end +end diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb index 3616732e328..5932eb5e919 100644 --- a/spec/tooling/lib/tooling/find_changes_spec.rb +++ b/spec/tooling/lib/tooling/find_changes_spec.rb @@ -3,57 +3,66 @@ require_relative '../../../../tooling/lib/tooling/find_changes' require_relative '../../../support/helpers/stub_env' require 'json' +require 'tempfile' RSpec.describe Tooling::FindChanges, feature_category: :tooling do include StubENV + attr_accessor :changed_files_file, :predictive_tests_file, :frontend_fixtures_mapping_file + let(:instance) do - described_class.new( - output_file: output_file, - matched_tests_file: matched_tests_file, - frontend_fixtures_mapping_path: frontend_fixtures_mapping_path - ) + described_class.new(changed_files_pathname, predictive_tests_pathname, frontend_fixtures_mapping_pathname) end - let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles - let(:output_file) { 'output.txt' } - let(:output_file_content) { 'first_file second_file' } - let(:matched_tests_file) { 'matched_tests.txt' } - let(:frontend_fixtures_mapping_path) { 'frontend_fixtures_mapping.json' } - let(:file_changes) { ['file1.rb', 'file2.rb'] } + let(:changed_files_pathname) { changed_files_file.path } + let(:predictive_tests_pathname) { predictive_tests_file.path } + let(:frontend_fixtures_mapping_pathname) { frontend_fixtures_mapping_file.path } + let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles + + around do |example| + self.changed_files_file = Tempfile.new('changed_files_file') + self.predictive_tests_file = Tempfile.new('predictive_tests_file') + self.frontend_fixtures_mapping_file = Tempfile.new('frontend_fixtures_mapping_file') + + # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ + # Tempfile.html#class-Tempfile-label-Explicit+close + begin + example.run + ensure + frontend_fixtures_mapping_file.close + frontend_fixtures_mapping_file.unlink + predictive_tests_file.close + predictive_tests_file.unlink + changed_files_file.close + changed_files_file.unlink + end + end before do stub_env( 'CI_API_V4_URL' => 'gitlab_api_url', 'CI_MERGE_REQUEST_IID' => '1234', 'CI_MERGE_REQUEST_PROJECT_PATH' => 'dummy-project', - 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'dummy-token', - 'RSPEC_TESTS_MAPPING_PATH' => '/tmp/does-not-exist.out' + 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'dummy-token' ) allow(instance).to receive(:gitlab).and_return(gitlab_client) - allow(File).to receive(:exist?).and_call_original - allow(File).to receive(:read).and_call_original - allow(File).to receive(:write) end describe '#execute' do subject { instance.execute } - context 'when there is no output file' do - let(:output_file) { nil } + context 'when there is no changed files file' do + let(:changed_files_pathname) { nil } it 'raises an ArgumentError' do - expect { subject }.to raise_error(ArgumentError, "An path to an output file must be given as first argument.") + expect { subject }.to raise_error( + ArgumentError, "A path to the changed files file must be given as first argument." + ) end end - context 'when an output file is provided' do - before do - allow(File).to receive(:exist?).with(output_file).and_return(true) - allow(File).to receive(:read).with(output_file).and_return(output_file_content) - end - + context 'when an changed files file is provided' do it 'does not call GitLab API to retrieve the MR diff' do expect(gitlab_client).not_to receive(:merge_request_changes) @@ -61,64 +70,57 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do end context 'when there are no file changes' do - let(:output_file_content) { '' } - - it 'writes an empty string to output file' do - expect(File).to receive(:write).with(output_file, '') - - subject + it 'writes an empty string to changed files file' do + expect { subject }.not_to change { File.read(changed_files_pathname) } end end context 'when there are file changes' do - let(:output_file_content) { 'first_file_changed second_file_changed' } + before do + File.write(changed_files_pathname, changed_files_file_content) + end - it 'writes file changes to output file' do - expect(File).to receive(:write).with(output_file, output_file_content) + let(:changed_files_file_content) { 'first_file_changed second_file_changed' } - subject + # This is because we don't have frontend fixture mappings: we will just write the same data that we read. + it 'does not change the changed files file' do + expect { subject }.not_to change { File.read(changed_files_pathname) } end end context 'when there is no matched tests file' do - let(:matched_tests_file) { '' } - - it 'does not add frontend fixtures mapping to the output file' do - expect(File).to receive(:write).with(output_file, output_file_content) + let(:predictive_tests_pathname) { nil } - subject + it 'does not add frontend fixtures mapping to the changed files file' do + expect { subject }.not_to change { File.read(changed_files_pathname) } end end context 'when there is no frontend fixture files' do - let(:frontend_fixtures_mapping_path) { '' } + let(:frontend_fixtures_mapping_pathname) { nil } - it 'does not add frontend fixtures mapping to the output file' do - expect(File).to receive(:write).with(output_file, output_file_content) - - subject + it 'does not add frontend fixtures mapping to the changed files file' do + expect { subject }.not_to change { File.read(changed_files_pathname) } end end context 'when the matched tests file and frontend fixture files are provided' do before do - allow(File).to receive(:exist?).with(matched_tests_file).and_return(true) - allow(File).to receive(:exist?).with(frontend_fixtures_mapping_path).and_return(true) - - allow(File).to receive(:read).with(matched_tests_file).and_return(matched_tests) - allow(File).to receive(:read).with(frontend_fixtures_mapping_path).and_return(frontend_fixtures_mapping_json) + File.write(predictive_tests_pathname, matched_tests) + File.write(frontend_fixtures_mapping_pathname, frontend_fixtures_mapping_json) + File.write(changed_files_pathname, changed_files_file_content) end + let(:changed_files_file_content) { '' } + context 'when there are no mappings for the matched tests' do let(:matched_tests) { 'match_spec1 match_spec_2' } let(:frontend_fixtures_mapping_json) do { other_spec: ['other_mapping'] }.to_json end - it 'does not add frontend fixtures mapping to the output file' do - expect(File).to receive(:write).with(output_file, output_file_content) - - subject + it 'does not change the changed files file' do + expect { subject }.not_to change { File.read(changed_files_pathname) } end end @@ -129,10 +131,20 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do { match_spec1: spec_mappings }.to_json end - it 'adds the frontend fixtures mappings to the output file' do - expect(File).to receive(:write).with(output_file, "#{output_file_content} #{spec_mappings.join(' ')}") + context 'when the changed files file is initially empty' do + it 'adds the frontend fixtures mappings to the changed files file' do + expect { subject }.to change { File.read(changed_files_pathname) }.from('').to(spec_mappings.join(' ')) + end + end + + context 'when the changed files file is initially not empty' do + let(:changed_files_file_content) { 'initial_content1 initial_content2' } - subject + it 'adds the frontend fixtures mappings to the changed files file' do + expect { subject }.to change { File.read(changed_files_pathname) } + .from(changed_files_file_content) + .to("#{changed_files_file_content} #{spec_mappings.join(' ')}") + end end end end @@ -155,15 +167,9 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do end context 'when a file is passed as an argument' do - let(:output_file) { 'output_file.out' } - - it 'does not read the output file' do - expect(File).not_to receive(:read).with(output_file) - - subject - end + let(:changed_files_pathname) { 'does-not-exist.out' } - it 'calls GitLab API anyways' do + it 'calls GitLab API' do expect(gitlab_client).to receive(:merge_request_changes) .with('dummy-project', '1234') diff --git a/spec/tooling/lib/tooling/find_tests_spec.rb b/spec/tooling/lib/tooling/find_tests_spec.rb index e531fb9548e..905f81c4bbd 100644 --- a/spec/tooling/lib/tooling/find_tests_spec.rb +++ b/spec/tooling/lib/tooling/find_tests_spec.rb @@ -7,27 +7,29 @@ require_relative '../../../support/helpers/stub_env' RSpec.describe Tooling::FindTests, feature_category: :tooling do include StubENV - attr_accessor :changes_file, :matching_tests_paths + attr_accessor :changed_files_file, :predictive_tests_file - let(:instance) { described_class.new(changes_file, matching_tests_paths) } + let(:instance) { described_class.new(changed_files_pathname, predictive_tests_pathname) } let(:mock_test_file_finder) { instance_double(TestFileFinder::FileFinder) } let(:new_matching_tests) { ["new_matching_spec.rb"] } - let(:changes_file_content) { "changed_file1 changed_file2" } - let(:matching_tests_paths_content) { "previously_matching_spec.rb" } + let(:changed_files_pathname) { changed_files_file.path } + let(:predictive_tests_pathname) { predictive_tests_file.path } + let(:changed_files_content) { "changed_file1 changed_file2" } + let(:predictive_tests_content) { "previously_matching_spec.rb" } around do |example| - self.changes_file = Tempfile.new('changes') - self.matching_tests_paths = Tempfile.new('matching_tests') + self.changed_files_file = Tempfile.new('changed_files_file') + self.predictive_tests_file = Tempfile.new('predictive_tests_file') # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ # Tempfile.html#class-Tempfile-label-Explicit+close begin example.run ensure - changes_file.close - matching_tests_paths.close - changes_file.unlink - matching_tests_paths.unlink + changed_files_file.close + predictive_tests_file.close + changed_files_file.unlink + predictive_tests_file.unlink end end @@ -42,44 +44,44 @@ RSpec.describe Tooling::FindTests, feature_category: :tooling do ) # We write into the temp files initially, to later check how the code modified those files - File.write(changes_file, changes_file_content) - File.write(matching_tests_paths, matching_tests_paths_content) + File.write(changed_files_pathname, changed_files_content) + File.write(predictive_tests_pathname, predictive_tests_content) end describe '#execute' do subject { instance.execute } - context 'when the matching_tests_paths file does not exist' do - let(:instance) { described_class.new(non_existing_output_file, matching_tests_paths) } - let(:non_existing_output_file) { 'tmp/another_file.out' } + context 'when the predictive_tests_pathname file does not exist' do + let(:instance) { described_class.new(non_existing_output_pathname, predictive_tests_pathname) } + let(:non_existing_output_pathname) { 'tmp/another_file.out' } around do |example| example.run ensure - FileUtils.rm_rf(non_existing_output_file) + FileUtils.rm_rf(non_existing_output_pathname) end it 'creates the file' do - expect { subject }.to change { File.exist?(non_existing_output_file) }.from(false).to(true) + expect { subject }.to change { File.exist?(non_existing_output_pathname) }.from(false).to(true) end end - context 'when the matching_tests_paths file already exists' do + context 'when the predictive_tests_pathname file already exists' do it 'does not create an empty file' do - expect(File).not_to receive(:write).with(matching_tests_paths, '') + expect(File).not_to receive(:write).with(predictive_tests_pathname, '') subject end end it 'does not modify the content of the input file' do - expect { subject }.not_to change { File.read(changes_file) } + expect { subject }.not_to change { File.read(changed_files_pathname) } end it 'does not overwrite the output file' do - expect { subject }.to change { File.read(matching_tests_paths) } - .from(matching_tests_paths_content) - .to("#{matching_tests_paths_content} #{new_matching_tests.uniq.join(' ')}") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_content) + .to("#{predictive_tests_content} #{new_matching_tests.uniq.join(' ')}") end it 'loads the tests.yml file with a pattern matching mapping' do @@ -148,8 +150,8 @@ RSpec.describe Tooling::FindTests, feature_category: :tooling do it 'writes uniquely matching specs to the output' do subject - expect(File.read(matching_tests_paths).split(' ')).to match_array( - matching_tests_paths_content.split(' ') + new_matching_tests.uniq + expect(File.read(predictive_tests_pathname).split(' ')).to match_array( + predictive_tests_content.split(' ') + new_matching_tests.uniq ) end end diff --git a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb index 7c8310c4bd9..d6f68baeb90 100644 --- a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb +++ b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb @@ -42,18 +42,18 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do subject { instance.read_array_from_file(input_file_path) } context 'when the input file does not exist' do - let(:non_existing_input_file) { 'tmp/another_file.out' } + let(:non_existing_input_pathname) { 'tmp/another_file.out' } - subject { instance.read_array_from_file(non_existing_input_file) } + subject { instance.read_array_from_file(non_existing_input_pathname) } around do |example| example.run ensure - FileUtils.rm_rf(non_existing_input_file) + FileUtils.rm_rf(non_existing_input_pathname) end it 'creates the file' do - expect { subject }.to change { File.exist?(non_existing_input_file) }.from(false).to(true) + expect { subject }.to change { File.exist?(non_existing_input_pathname) }.from(false).to(true) end end @@ -67,9 +67,10 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do end describe '#write_array_to_file' do - let(:content_array) { %w[new_entry] } + let(:content_array) { %w[new_entry] } + let(:overwrite_flag) { false } - subject { instance.write_array_to_file(output_file_path, content_array) } + subject { instance.write_array_to_file(output_file_path, content_array, overwrite: overwrite_flag) } context 'when the output file does not exist' do let(:non_existing_output_file) { 'tmp/another_file.out' } @@ -93,6 +94,14 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do it 'writes the correct content to the file' do expect { subject }.to change { File.read(output_file_path) }.from('').to(content_array.join(' ')) end + + context 'when the content array is not sorted' do + let(:content_array) { %w[new_entry a_new_entry] } + + it 'sorts the array before writing it to file' do + expect { subject }.to change { File.read(output_file_path) }.from('').to(content_array.sort.join(' ')) + end + end end context 'when the output file is not empty' do @@ -100,8 +109,18 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do it 'appends the correct content to the file' do expect { subject }.to change { File.read(output_file_path) } - .from(initial_content) - .to((initial_content.split(' ') + content_array).join(' ')) + .from(initial_content) + .to((initial_content.split(' ') + content_array).join(' ')) + end + + context 'when the overwrite flag is set to true' do + let(:overwrite_flag) { true } + + it 'overwrites the previous content' do + expect { subject }.to change { File.read(output_file_path) } + .from(initial_content) + .to(content_array.join(' ')) + end end end end diff --git a/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb index 521958573fd..b6459428214 100644 --- a/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb +++ b/spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb @@ -6,11 +6,17 @@ require_relative '../../../../../tooling/lib/tooling/mappings/graphql_base_type_ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :tooling do # We set temporary folders, and those readers give access to those folder paths attr_accessor :foss_folder, :ee_folder, :jh_folder - attr_accessor :changes_file, :matching_tests_paths + attr_accessor :changed_files_file, :predictive_tests_file + + let(:changed_files_pathname) { changed_files_file.path } + let(:predictive_tests_pathname) { predictive_tests_file.path } + let(:instance) { described_class.new(changed_files_pathname, predictive_tests_pathname) } + let(:changed_files_content) { "changed_file1 changed_file2" } + let(:predictive_tests_initial_content) { "previously_matching_spec.rb" } around do |example| - self.changes_file = Tempfile.new('changes') - self.matching_tests_paths = Tempfile.new('matching_tests') + self.changed_files_file = Tempfile.new('changed_files_file') + self.predictive_tests_file = Tempfile.new('predictive_tests_file') Dir.mktmpdir('FOSS') do |foss_folder| Dir.mktmpdir('EE') do |ee_folder| @@ -24,20 +30,16 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to begin example.run ensure - changes_file.close - matching_tests_paths.close - changes_file.unlink - matching_tests_paths.unlink + changed_files_file.close + predictive_tests_file.close + changed_files_file.unlink + predictive_tests_file.unlink end end end end end - let(:instance) { described_class.new(changes_file, matching_tests_paths) } - let(:changes_file_content) { "changed_file1 changed_file2" } - let(:matching_tests_paths_initial_content) { "previously_matching_spec.rb" } - before do stub_const("Tooling::Mappings::GraphqlBaseTypeMappings::GRAPHQL_TYPES_FOLDERS", { nil => [foss_folder], @@ -46,23 +48,23 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to }) # We write into the temp files initially, to later check how the code modified those files - File.write(changes_file, changes_file_content) - File.write(matching_tests_paths, matching_tests_paths_initial_content) + File.write(changed_files_pathname, changed_files_content) + File.write(predictive_tests_pathname, predictive_tests_initial_content) end describe '#execute' do subject { instance.execute } context 'when no GraphQL files were changed' do - let(:changes_file_content) { '' } + let(:changed_files_content) { '' } it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end context 'when some GraphQL files were changed' do - let(:changes_file_content) do + let(:changed_files_content) do [ "#{foss_folder}/my_graphql_file.rb", "#{foss_folder}/my_other_graphql_file.rb" @@ -76,7 +78,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to end it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -92,9 +94,9 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to end it 'writes the correct specs in the output' do - expect { subject }.to change { File.read(matching_tests_paths) } - .from(matching_tests_paths_initial_content) - .to("#{matching_tests_paths_initial_content} spec/my_graphql_file_spec.rb") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_initial_content) + .to("#{predictive_tests_initial_content} spec/my_graphql_file_spec.rb") end end end @@ -110,7 +112,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to end context 'when no files were changed' do - let(:changes_file_content) { '' } + let(:changed_files_content) { '' } it 'returns an empty array' do expect(subject).to match_array([]) @@ -118,7 +120,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to end context 'when GraphQL files were changed' do - let(:changes_file_content) do + let(:changed_files_content) do [ "#{foss_folder}/my_graphql_file.rb", "#{foss_folder}/my_other_graphql_file.rb", @@ -135,7 +137,7 @@ RSpec.describe Tooling::Mappings::GraphqlBaseTypeMappings, feature_category: :to end context 'when files are deleted' do - let(:changes_file_content) { "#{foss_folder}/deleted.rb" } + let(:changed_files_content) { "#{foss_folder}/deleted.rb" } it 'returns an empty array' do expect(subject).to match_array([]) diff --git a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb index c36d14cd267..e1f35bedebb 100644 --- a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb +++ b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb @@ -6,23 +6,25 @@ require_relative '../../../../../tooling/lib/tooling/mappings/js_to_system_specs RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :tooling do # We set temporary folders, and those readers give access to those folder paths attr_accessor :js_base_folder, :system_specs_base_folder - attr_accessor :changes_file, :matching_tests_paths + attr_accessor :changed_files_file, :predictive_tests_file + + let(:changed_files_pathname) { changed_files_file.path } + let(:predictive_tests_pathname) { predictive_tests_file.path } + let(:changed_files_content) { "changed_file1 changed_file2" } + let(:predictive_tests_content) { "previously_matching_spec.rb" } let(:instance) do described_class.new( - changes_file, - matching_tests_paths, + changed_files_pathname, + predictive_tests_pathname, system_specs_base_folder: system_specs_base_folder, js_base_folder: js_base_folder ) end - let(:changes_file_content) { "changed_file1 changed_file2" } - let(:matching_tests_paths_content) { "previously_matching_spec.rb" } - around do |example| - self.changes_file = Tempfile.new('changes') - self.matching_tests_paths = Tempfile.new('matching_tests') + self.changed_files_file = Tempfile.new('changed_files_file') + self.predictive_tests_file = Tempfile.new('predictive_tests_file') Dir.mktmpdir do |tmp_js_base_folder| Dir.mktmpdir do |tmp_system_specs_base_folder| @@ -34,10 +36,10 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to begin example.run ensure - changes_file.close - matching_tests_paths.close - changes_file.unlink - matching_tests_paths.unlink + changed_files_file.close + predictive_tests_file.close + changed_files_file.unlink + predictive_tests_file.unlink end end end @@ -45,22 +47,22 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to before do # We write into the temp files initially, to later check how the code modified those files - File.write(changes_file, changes_file_content) - File.write(matching_tests_paths, matching_tests_paths_content) + File.write(changed_files_pathname, changed_files_content) + File.write(predictive_tests_pathname, predictive_tests_content) end describe '#execute' do subject { instance.execute } before do - File.write(changes_file, changed_files.join(' ')) + File.write(changed_files_pathname, changed_files.join(' ')) end context 'when no JS files were changed' do let(:changed_files) { [] } it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -69,7 +71,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to context 'when the JS files are not present on disk' do it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -81,7 +83,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to context 'when no system specs match the JS keyword' do it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -92,9 +94,9 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to end it 'adds the new specs to the output file' do - expect { subject }.to change { File.read(matching_tests_paths) } - .from(matching_tests_paths_content) - .to("#{matching_tests_paths_content} #{system_specs_base_folder}/confidential_issues/issues_spec.rb") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_content) + .to("#{predictive_tests_content} #{system_specs_base_folder}/confidential_issues/issues_spec.rb") end end end @@ -108,7 +110,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to File.write("#{js_base_folder}/index.js", "index.js") File.write("#{js_base_folder}/index-with-ee-in-it.js", "index-with-ee-in-it.js") File.write("#{js_base_folder}/index-with-jh-in-it.js", "index-with-jh-in-it.js") - File.write(changes_file, changed_files.join(' ')) + File.write(changed_files_pathname, changed_files.join(' ')) end context 'when no files were changed' do @@ -148,7 +150,7 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to end describe '#construct_js_keywords' do - subject { described_class.new(changes_file, matching_tests_paths).construct_js_keywords(js_files) } + subject { described_class.new(changed_files_file, predictive_tests_file).construct_js_keywords(js_files) } let(:js_files) do %w[ diff --git a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb index 30965ed985d..75ddee18985 100644 --- a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb +++ b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb @@ -5,15 +5,20 @@ require 'fileutils' require_relative '../../../../../tooling/lib/tooling/mappings/partial_to_views_mappings' RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :tooling do - attr_accessor :view_base_folder, :changes_file, :output_file + attr_accessor :view_base_folder, :changed_files_file, :views_with_partials_file - let(:instance) { described_class.new(changes_file, output_file, view_base_folder: view_base_folder) } - let(:changes_file_content) { "changed_file1 changed_file2" } - let(:output_file_content) { "previously_added_view.html.haml" } + let(:instance) do + described_class.new(changed_files_pathname, views_with_partials_pathname, view_base_folder: view_base_folder) + end + + let(:changed_files_pathname) { changed_files_file.path } + let(:views_with_partials_pathname) { views_with_partials_file.path } + let(:changed_files_content) { "changed_file1 changed_file2" } + let(:views_with_partials_content) { "previously_added_view.html.haml" } around do |example| - self.changes_file = Tempfile.new('changes') - self.output_file = Tempfile.new('output_file') + self.changed_files_file = Tempfile.new('changed_files_file') + self.views_with_partials_file = Tempfile.new('views_with_partials_file') # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ # Tempfile.html#class-Tempfile-label-Explicit+close @@ -23,24 +28,24 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too example.run end ensure - changes_file.close - output_file.close - changes_file.unlink - output_file.unlink + changed_files_file.close + views_with_partials_file.close + changed_files_file.unlink + views_with_partials_file.unlink end end before do # We write into the temp files initially, to check how the code modified those files - File.write(changes_file, changes_file_content) - File.write(output_file, output_file_content) + File.write(changed_files_pathname, changed_files_content) + File.write(views_with_partials_pathname, views_with_partials_content) end describe '#execute' do subject { instance.execute } - let(:changed_files) { ["#{view_base_folder}/my_view.html.haml"] } - let(:changes_file_content) { changed_files.join(" ") } + let(:changed_files) { ["#{view_base_folder}/my_view.html.haml"] } + let(:changed_files_content) { changed_files.join(" ") } before do # We create all of the changed_files, so that they are part of the filtered files @@ -48,12 +53,12 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too end it 'does not modify the content of the input file' do - expect { subject }.not_to change { File.read(changes_file) } + expect { subject }.not_to change { File.read(changed_files_pathname) } end context 'when no partials were modified' do it 'does not change the output file' do - expect { subject }.not_to change { File.read(output_file) } + expect { subject }.not_to change { File.read(views_with_partials_pathname) } end end @@ -77,7 +82,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too end it 'does not change the output file' do - expect { subject }.not_to change { File.read(output_file) } + expect { subject }.not_to change { File.read(views_with_partials_pathname) } end end @@ -87,9 +92,9 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too end it 'writes the view including the partial to the output' do - expect { subject }.to change { File.read(output_file) } - .from(output_file_content) - .to(output_file_content + " #{view_base_folder}/my_view.html.haml") + expect { subject }.to change { File.read(views_with_partials_pathname) } + .from(views_with_partials_content) + .to(views_with_partials_content + " #{view_base_folder}/my_view.html.haml") end end end @@ -98,7 +103,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too describe '#filter_files' do subject { instance.filter_files } - let(:changes_file_content) { file_path } + let(:changed_files_content) { file_path } context 'when the file does not exist on disk' do let(:file_path) { "#{view_base_folder}/_index.html.erb" } @@ -164,11 +169,11 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too before do FileUtils.mkdir_p("#{view_base_folder}/components/subfolder") - File.write(changes_file_content, "I am a partial!") + File.write(changed_files_content, "I am a partial!") end context 'when the partial is not part of the changed files' do - let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_not_the_partial.html.haml" } + let(:changed_files_content) { "#{view_base_folder}/components/subfolder/_not_the_partial.html.haml" } it 'returns false' do expect(subject).to be_falsey @@ -176,7 +181,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too end context 'when the partial is part of the changed files' do - let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_relative_partial.html.haml" } + let(:changed_files_content) { "#{view_base_folder}/components/subfolder/_relative_partial.html.haml" } it 'returns true' do expect(subject).to be_truthy @@ -191,11 +196,11 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too before do FileUtils.mkdir_p("#{view_base_folder}/components") FileUtils.mkdir_p("#{view_base_folder}/shared") - File.write(changes_file_content, "I am a partial!") + File.write(changed_files_content, "I am a partial!") end context 'when the partial is not part of the changed files' do - let(:changes_file_content) { "#{view_base_folder}/shared/not_the_partial" } + let(:changed_files_content) { "#{view_base_folder}/shared/not_the_partial" } it 'returns false' do expect(subject).to be_falsey @@ -203,7 +208,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too end context 'when the partial is part of the changed files' do - let(:changes_file_content) { "#{view_base_folder}/shared/_absolute_partial.html.haml" } + let(:changed_files_content) { "#{view_base_folder}/shared/_absolute_partial.html.haml" } it 'returns true' do expect(subject).to be_truthy diff --git a/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb index bf07ad1951b..6d007843716 100644 --- a/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb +++ b/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb @@ -6,23 +6,25 @@ require_relative '../../../../../tooling/lib/tooling/mappings/view_to_js_mapping RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling do # We set temporary folders, and those readers give access to those folder paths attr_accessor :view_base_folder, :js_base_folder - attr_accessor :changes_file, :matching_tests_paths + attr_accessor :changed_files_file, :predictive_tests_file + + let(:changed_files_pathname) { changed_files_file.path } + let(:predictive_tests_pathname) { predictive_tests_file.path } + let(:changed_files_content) { "changed_file1 changed_file2" } + let(:predictive_tests_content) { "previously_matching_spec.rb" } let(:instance) do described_class.new( - changes_file, - matching_tests_paths, + changed_files_pathname, + predictive_tests_pathname, view_base_folder: view_base_folder, js_base_folder: js_base_folder ) end - let(:changes_file_content) { "changed_file1 changed_file2" } - let(:matching_tests_paths_content) { "previously_matching_spec.rb" } - around do |example| - self.changes_file = Tempfile.new('changes') - self.matching_tests_paths = Tempfile.new('matching_tests') + self.changed_files_file = Tempfile.new('changed_files_file') + self.predictive_tests_file = Tempfile.new('matching_tests') Dir.mktmpdir do |tmp_js_base_folder| Dir.mktmpdir do |tmp_views_base_folder| @@ -34,10 +36,10 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d begin example.run ensure - changes_file.close - matching_tests_paths.close - changes_file.unlink - matching_tests_paths.unlink + changed_files_file.close + predictive_tests_file.close + changed_files_file.unlink + predictive_tests_file.unlink end end end @@ -45,8 +47,8 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d before do # We write into the temp files initially, to later check how the code modified those files - File.write(changes_file, changes_file_content) - File.write(matching_tests_paths, matching_tests_paths_content) + File.write(changed_files_pathname, changed_files_content) + File.write(predictive_tests_pathname, predictive_tests_content) end describe '#execute' do @@ -55,7 +57,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d subject { instance.execute } before do - File.write(changes_file, changed_files.join(' ')) + File.write(changed_files_pathname, changed_files.join(' ')) end context 'when no view files have been changed' do @@ -64,7 +66,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d end it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -82,7 +84,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d end it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -99,7 +101,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d context 'when no matching JS files are found' do it 'does not change the output file' do - expect { subject }.not_to change { File.read(matching_tests_paths) } + expect { subject }.not_to change { File.read(predictive_tests_pathname) } end end @@ -119,9 +121,9 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d end it 'adds the matching JS files to the output' do - expect { subject }.to change { File.read(matching_tests_paths) } - .from(matching_tests_paths_content) - .to("#{matching_tests_paths_content} #{js_base_folder}/index.js") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_content) + .to("#{predictive_tests_content} #{js_base_folder}/index.js") end end end @@ -165,9 +167,9 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d end it 'scans those partials for the HTML attribute value' do - expect { subject }.to change { File.read(matching_tests_paths) } - .from(matching_tests_paths_content) - .to("#{matching_tests_paths_content} #{js_base_folder}/index.js") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_content) + .to("#{predictive_tests_content} #{js_base_folder}/index.js") end end end @@ -178,7 +180,7 @@ RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling d before do File.write("#{js_base_folder}/index.js", "index.js") File.write("#{view_base_folder}/index.html", "index.html") - File.write(changes_file, changed_files.join(' ')) + File.write(changed_files_pathname, changed_files.join(' ')) end context 'when no files were changed' do diff --git a/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb index d4f4bbd7f06..b8a13c50c9b 100644 --- a/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb +++ b/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb @@ -5,15 +5,20 @@ require 'fileutils' require_relative '../../../../../tooling/lib/tooling/mappings/view_to_system_specs_mappings' RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :tooling do - attr_accessor :view_base_folder, :changes_file, :output_file + attr_accessor :view_base_folder, :changed_files_file, :predictive_tests_file - let(:instance) { described_class.new(changes_file, output_file, view_base_folder: view_base_folder) } - let(:changes_file_content) { "changed_file1 changed_file2" } - let(:output_file_initial_content) { "previously_added_spec.rb" } + let(:instance) do + described_class.new(changed_files_pathname, predictive_tests_pathname, view_base_folder: view_base_folder) + end + + let(:changed_files_pathname) { changed_files_file.path } + let(:predictive_tests_pathname) { predictive_tests_file.path } + let(:changed_files_content) { "changed_file1 changed_file2" } + let(:predictive_tests_initial_content) { "previously_added_spec.rb" } around do |example| - self.changes_file = Tempfile.new('changes') - self.output_file = Tempfile.new('output_file') + self.changed_files_file = Tempfile.new('changed_files_file') + self.predictive_tests_file = Tempfile.new('predictive_tests_file') # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ # Tempfile.html#class-Tempfile-label-Explicit+close @@ -23,10 +28,10 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: : example.run end ensure - changes_file.close - output_file.close - changes_file.unlink - output_file.unlink + changed_files_file.close + predictive_tests_file.close + changed_files_file.unlink + predictive_tests_file.unlink end end @@ -34,13 +39,13 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: : FileUtils.mkdir_p("#{view_base_folder}/app/views/dashboard") # We write into the temp files initially, to check how the code modified those files - File.write(changes_file, changes_file_content) - File.write(output_file, output_file_initial_content) + File.write(changed_files_pathname, changed_files_content) + File.write(predictive_tests_pathname, predictive_tests_initial_content) end shared_examples 'writes nothing to the output file' do it 'writes nothing to the output file' do - expect { subject }.not_to change { File.read(changes_file) } + expect { subject }.not_to change { File.read(changed_files_pathname) } end end @@ -48,7 +53,7 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: : subject { instance.execute } let(:changed_files) { ["#{view_base_folder}/app/views/dashboard/my_view.html.haml"] } - let(:changes_file_content) { changed_files.join(" ") } + let(:changed_files_content) { changed_files.join(" ") } before do # We create all of the changed_files, so that they are part of the filtered files @@ -88,9 +93,9 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: : end it 'writes that feature spec to the output file' do - expect { subject }.to change { File.read(output_file) } - .from(output_file_initial_content) - .to("#{output_file_initial_content} #{expected_feature_spec}") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_initial_content) + .to("#{predictive_tests_initial_content} #{expected_feature_spec}") end end @@ -111,9 +116,9 @@ RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: : end it 'writes all of the feature specs for the parent folder to the output file' do - expect { subject }.to change { File.read(output_file) } - .from(output_file_initial_content) - .to("#{output_file_initial_content} #{expected_feature_specs.join(' ')}") + expect { subject }.to change { File.read(predictive_tests_pathname) } + .from(predictive_tests_initial_content) + .to("#{predictive_tests_initial_content} #{expected_feature_specs.join(' ')}") end end end diff --git a/spec/tooling/lib/tooling/predictive_tests_spec.rb b/spec/tooling/lib/tooling/predictive_tests_spec.rb new file mode 100644 index 00000000000..79554037c48 --- /dev/null +++ b/spec/tooling/lib/tooling/predictive_tests_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'tempfile' +require_relative '../../../../tooling/lib/tooling/predictive_tests' +require_relative '../../../support/helpers/stub_env' + +RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do + include StubENV + + let(:instance) { described_class.new } + let(:matching_tests_initial_content) { 'initial_matching_spec' } + + attr_accessor :changed_files, :fixtures_mapping, :matching_js_files, :matching_tests, :views_with_partials + + around do |example| + self.changed_files = Tempfile.new('test-folder/changed_files.txt') + self.fixtures_mapping = Tempfile.new('test-folder/fixtures_mapping.txt') + self.matching_js_files = Tempfile.new('test-folder/matching_js_files.txt') + self.matching_tests = Tempfile.new('test-folder/matching_tests.txt') + self.views_with_partials = Tempfile.new('test-folder/views_with_partials.txt') + + # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ + # Tempfile.html#class-Tempfile-label-Explicit+close + begin + example.run + ensure + changed_files.close + changed_files.unlink + fixtures_mapping.close + fixtures_mapping.unlink + matching_js_files.close + matching_js_files.unlink + matching_tests.close + matching_tests.unlink + views_with_partials.close + views_with_partials.unlink + end + end + + before do + stub_env( + 'RSPEC_CHANGED_FILES_PATH' => changed_files.path, + 'RSPEC_MATCHING_TESTS_PATH' => matching_tests.path, + 'RSPEC_VIEWS_INCLUDING_PARTIALS_PATH' => views_with_partials.path, + 'FRONTEND_FIXTURES_MAPPING_PATH' => fixtures_mapping.path, + 'RSPEC_MATCHING_JS_FILES_PATH' => matching_js_files.path, + 'RSPEC_TESTS_MAPPING_ENABLED' => "false", + 'RSPEC_TESTS_MAPPING_PATH' => '/tmp/does-not-exist.out' + ) + + # We write some data to later on verify that we only append to this file. + File.write(matching_tests.path, matching_tests_initial_content) + File.write(fixtures_mapping.path, '{}') # We write valid JSON, so that the file can be processed + end + + describe '#execute' do + subject { instance.execute } + + context 'when ENV variables are missing' do + before do + stub_env( + 'RSPEC_CHANGED_FILES_PATH' => '', + 'FRONTEND_FIXTURES_MAPPING_PATH' => '' + ) + end + + it 'raises an error' do + expect { subject }.to raise_error( + '[predictive tests] Missing ENV variable(s): RSPEC_CHANGED_FILES_PATH,FRONTEND_FIXTURES_MAPPING_PATH.' + ) + end + end + + context 'when all ENV variables are provided' do + before do + File.write(changed_files, changed_files_content) + end + + context 'when no files were changed' do + let(:changed_files_content) { '' } + + it 'does not change any files' do + expect { subject }.not_to change { File.read(changed_files.path) } + expect { subject }.not_to change { File.read(matching_tests.path) } + expect { subject }.not_to change { File.read(views_with_partials.path) } + expect { subject }.not_to change { File.read(fixtures_mapping.path) } + expect { subject }.not_to change { File.read(matching_js_files.path) } + end + end + + context 'when some files were changed' do + let(:changed_files_content) { 'tooling/lib/tooling/predictive_tests.rb' } + + it 'appends the spec file to RSPEC_MATCHING_TESTS_PATH' do + expect { subject }.to change { File.read(matching_tests.path) } + .from(matching_tests_initial_content) + .to("#{matching_tests_initial_content} spec/tooling/lib/tooling/predictive_tests_spec.rb") + end + + it 'does not change files other than RSPEC_MATCHING_TESTS_PATH' do + expect { subject }.not_to change { File.read(changed_files.path) } + expect { subject }.not_to change { File.read(views_with_partials.path) } + expect { subject }.not_to change { File.read(fixtures_mapping.path) } + expect { subject }.not_to change { File.read(matching_js_files.path) } + end + end + end + end +end diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb index aac7d19c079..a7e4e42206a 100644 --- a/spec/tooling/quality/test_level_spec.rb +++ b/spec/tooling/quality/test_level_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do context 'when level is unit' do it 'returns a pattern' do expect(subject.pattern(:unit)) - .to eq("spec/{bin,channels,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling,components}{,/**/}*_spec.rb") + .to eq("spec/{bin,channels,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") end end @@ -121,7 +121,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do context 'when level is unit' do it 'returns a regexp' do expect(subject.regexp(:unit)) - .to eq(%r{spec/(bin|channels|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling|components)/}) + .to eq(%r{spec/(bin|channels|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/}) end end @@ -167,6 +167,13 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do end end + context 'when start_with == true' do + it 'returns a regexp' do + expect(described_class.new(['ee/']).regexp(:system, true)) + .to eq(%r{^(ee/)spec/(features)/}) + end + end + describe 'performance' do it 'memoizes the regexp for a given level' do expect(subject.regexp(:system).object_id).to eq(subject.regexp(:system).object_id) |