diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-22 21:07:57 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-22 21:07:57 +0300 |
commit | 277c0c75bf32b40d882c35feafaae90f69c40dd9 (patch) | |
tree | 35f3969f59c1886fcfa71812cb2b942c46d9dffe /spec | |
parent | 5e44c2ba46e780552317dec29e3b51282dfd5696 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
19 files changed, 412 insertions, 169 deletions
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 332426de07e..99f1b1ab1ad 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -449,8 +449,11 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do page.within '.diff-content' do find('button[data-testid="resolve-discussion-button"]').click + wait_for_requests + find_field('Reply…').click + find('.js-unresolve-checkbox') find('.js-note-text').set 'testing' click_button 'Add comment now' diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index 6f17e4193a3..db94b392694 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -30,7 +30,7 @@ describe('BoardFilteredSearch', () => { { icon: 'labels', title: TOKEN_TITLE_LABEL, - type: 'label', + type: TOKEN_TYPE_LABEL, operators: [ { value: '=', description: 'is' }, { value: '!=', description: 'is not' }, @@ -43,7 +43,7 @@ describe('BoardFilteredSearch', () => { { icon: 'pencil', title: TOKEN_TITLE_AUTHOR, - type: 'author', + type: TOKEN_TYPE_AUTHOR, operators: [ { value: '=', description: 'is' }, { value: '!=', description: 'is not' }, @@ -109,7 +109,7 @@ describe('BoardFilteredSearch', () => { createComponent({ props: { eeFilters: { labelName: ['label'] } } }); expect(findFilteredSearch().props('initialFilterValue')).toEqual([ - { type: 'label', value: { data: 'label', operator: '=' } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } }, ]); }); }); @@ -158,7 +158,9 @@ describe('BoardFilteredSearch', () => { ['None', url('None')], ['Any', url('Any')], ])('sets the url param %s', (assigneeParam, expected) => { - const mockFilters = [{ type: 'assignee', value: { data: assigneeParam, operator: '=' } }]; + const mockFilters = [ + { type: TOKEN_TYPE_ASSIGNEE, value: { data: assigneeParam, operator: '=' } }, + ]; jest.spyOn(urlUtility, 'updateHistory'); findFilteredSearch().vm.$emit('onFilter', mockFilters); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 0cdab747a8d..30b2320097c 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -2,18 +2,22 @@ import { GlFilteredSearchToken } from '@gitlab/ui'; import { keyBy } from 'lodash'; import { ListType } from '~/boards/constants'; import { - OPERATORS_IS_NOT, OPERATORS_IS, + OPERATORS_IS_NOT, TOKEN_TITLE_ASSIGNEE, TOKEN_TITLE_AUTHOR, + TOKEN_TITLE_CONFIDENTIAL, TOKEN_TITLE_LABEL, TOKEN_TITLE_MILESTONE, + TOKEN_TITLE_MY_REACTION, TOKEN_TITLE_RELEASE, TOKEN_TITLE_TYPE, TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_CONFIDENTIAL, TOKEN_TYPE_LABEL, TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, TOKEN_TYPE_RELEASE, TOKEN_TYPE_TYPE, } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -733,18 +737,18 @@ export const mockMoveData = { }; export const mockEmojiToken = { - type: 'my-reaction', + type: TOKEN_TYPE_MY_REACTION, icon: 'thumb-up', - title: 'My-Reaction', + title: TOKEN_TITLE_MY_REACTION, unique: true, token: EmojiToken, fetchEmojis: expect.any(Function), }; export const mockConfidentialToken = { - type: 'confidential', + type: TOKEN_TYPE_CONFIDENTIAL, icon: 'eye-slash', - title: 'Confidential', + title: TOKEN_TITLE_CONFIDENTIAL, unique: true, token: GlFilteredSearchToken, operators: OPERATORS_IS, diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js index b5dce4fc924..6232bbee8af 100644 --- a/spec/frontend/diffs/components/diff_code_quality_spec.js +++ b/spec/frontend/diffs/components/diff_code_quality_spec.js @@ -34,7 +34,7 @@ describe('DiffCodeQuality', () => { wrapper = createWrapper(multipleFindingsArr); const listItems = wrapper.findAll('li'); - expect(wrapper.findAll('li').length).toBe(3); + expect(wrapper.findAll('li').length).toBe(5); listItems.wrappers.map((e, i) => { return expect(e.text()).toEqual(multipleFindingsArr[i].description); diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js index befab3b676b..7558592f6a4 100644 --- a/spec/frontend/diffs/mock_data/diff_code_quality.js +++ b/spec/frontend/diffs/mock_data/diff_code_quality.js @@ -1,25 +1,39 @@ export const multipleFindingsArr = [ { severity: 'minor', - description: 'Unexpected Debugger Statement.', + description: 'mocked minor Issue', line: 2, }, { severity: 'major', - description: - 'Function `aVeryLongFunction` has 52 lines of code (exceeds 25 allowed). Consider refactoring.', + description: 'mocked major Issue', line: 3, }, { - severity: 'minor', - description: 'Arrow function has too many statements (52). Maximum allowed is 30.', + severity: 'info', + description: 'mocked info Issue', + line: 3, + }, + { + severity: 'critical', + description: 'mocked critical Issue', + line: 3, + }, + { + severity: 'blocker', + description: 'mocked blocker Issue', line: 3, }, ]; -export const multipleFindings = { +export const fiveFindings = { + filePath: 'index.js', + codequality: multipleFindingsArr.slice(0, 5), +}; + +export const threeFindings = { filePath: 'index.js', - codequality: multipleFindingsArr, + codequality: multipleFindingsArr.slice(0, 3), }; export const singularFinding = { diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js index 91457f10bf8..ebed477fa2f 100644 --- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue'; import eventHub from '~/filtered_search/event_hub'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import { TOKEN_TYPE_AUTHOR } from '~/vue_shared/components/filtered_search_bar/constants'; describe('Recent Searches Dropdown Content', () => { let wrapper; @@ -60,7 +61,7 @@ describe('Recent Searches Dropdown Content', () => { items: [ 'foo', 'author:@root label:~foo bar', - [{ type: 'author_username', value: { data: 'toby', operator: '=' } }], + [{ type: TOKEN_TYPE_AUTHOR, value: { data: 'toby', operator: '=' } }], ], isLocalStorageAvailable: true, }); diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js index 4029e47c390..686641800b3 100644 --- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -86,7 +86,7 @@ describe('WorkItemDetailModal component', () => { isModal: true, workItemId: defaultPropsData.workItemId, workItemParentId: defaultPropsData.issueGid, - iid: null, + workItemIid: null, }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 26777b57797..d6e4b0ece36 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -11,6 +11,7 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import setWindowLocation from 'helpers/set_window_location_helper'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; @@ -94,7 +95,6 @@ describe('WorkItemDetail component', () => { error = undefined, workItemsMvc2Enabled = false, fetchByIid = false, - iidPathQueryParam = undefined, } = {}) => { const handlers = [ [workItemQuery, handler], @@ -108,7 +108,7 @@ describe('WorkItemDetail component', () => { wrapper = shallowMount(WorkItemDetail, { apolloProvider: createMockApollo(handlers), - propsData: { isModal, workItemId, iid: '1' }, + propsData: { isModal, workItemId, workItemIid: '1' }, data() { return { updateInProgress, @@ -129,18 +129,12 @@ describe('WorkItemDetail component', () => { WorkItemWeight: true, WorkItemIteration: true, }, - mocks: { - $route: { - query: { - iid_path: iidPathQueryParam, - }, - }, - }, }); }; afterEach(() => { wrapper.destroy(); + setWindowLocation(''); }); describe('when there is no `workItemId` prop', () => { @@ -633,6 +627,8 @@ describe('WorkItemDetail component', () => { }); it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => { + setWindowLocation(`?iid_path=true`); + createComponent({ fetchByIid: true, iidPathQueryParam: 'true' }); await waitForPromises(); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index 1d5472a0473..e345e5fc7cd 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -76,16 +76,21 @@ describe('WorkItemLinkChild', () => { it.each` action | event | emittedEvent - ${'clicking'} | ${'click'} | ${'click'} ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'} ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'} `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => { + titleEl.vm.$emit(event); + + expect(wrapper.emitted(emittedEvent)).toEqual([[]]); + }); + + it('emits click event with correct parameters on clicking title', () => { const eventObj = { preventDefault: jest.fn(), }; - titleEl.vm.$emit(event, eventObj); + titleEl.vm.$emit('click', eventObj); - expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]); + expect(wrapper.emitted('click')).toEqual([[eventObj]]); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index 66ce2c1becf..fe95a985177 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -4,20 +4,25 @@ import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { stubComponent } from 'helpers/stub_component'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; +import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import { FORM_TYPES } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { workItemHierarchyResponse, workItemHierarchyEmptyResponse, workItemHierarchyNoUpdatePermissionResponse, changeWorkItemParentMutationResponse, workItemQueryResponse, + projectWorkItemResponse, } from '../../mock_data'; Vue.use(VueApollo); @@ -55,6 +60,7 @@ const issueDetailsResponse = (confidential = false) => ({ }, }, }); +const showModal = jest.fn(); describe('WorkItemLinks', () => { let wrapper; @@ -71,6 +77,7 @@ describe('WorkItemLinks', () => { .mockResolvedValue(changeWorkItemParentMutationResponse); const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const childWorkItemByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); const createComponent = async ({ data = {}, @@ -78,6 +85,7 @@ describe('WorkItemLinks', () => { mutationHandler = mutationChangeParentHandler, issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()), hasIterationsFeature = false, + fetchByIid = false, } = {}) => { mockApollo = createMockApollo( [ @@ -85,6 +93,7 @@ describe('WorkItemLinks', () => { [changeWorkItemParentMutation, mutationHandler], [workItemQuery, childWorkItemQueryHandler], [issueDetailsQuery, issueDetailsQueryHandler], + [workItemByIidQuery, childWorkItemByIidHandler], ], {}, { addTypename: true }, @@ -100,12 +109,22 @@ describe('WorkItemLinks', () => { projectPath: 'project/path', iid: '1', hasIterationsFeature, + glFeatures: { + useIidInWorkItemsPath: fetchByIid, + }, }, propsData: { issuableId: 1 }, apolloProvider: mockApollo, mocks: { $toast, }, + stubs: { + WorkItemDetailModal: stubComponent(WorkItemDetailModal, { + methods: { + show: showModal, + }, + }), + }, }); await waitForPromises(); @@ -130,6 +149,7 @@ describe('WorkItemLinks', () => { afterEach(() => { wrapper.destroy(); mockApollo = null; + setWindowLocation(''); }); it('is expanded by default', () => { @@ -271,49 +291,157 @@ describe('WorkItemLinks', () => { }); }); - describe('prefetching child items', () => { - let firstChild; - - beforeEach(async () => { - await createComponent(); + describe('when parent item is confidential', () => { + it('passes correct confidentiality status to form', async () => { + await createComponent({ + issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)), + }); + findToggleFormDropdown().vm.$emit('click'); + findToggleAddFormButton().vm.$emit('click'); + await nextTick(); - firstChild = findFirstWorkItemLinkChild(); + expect(findAddLinksForm().props('parentConfidential')).toBe(true); }); + }); - it('does not fetch the child work item before hovering work item links', () => { - expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + describe('when work item is fetched by id', () => { + describe('prefetching child items', () => { + let firstChild; + + beforeEach(async () => { + await createComponent(); + + firstChild = findFirstWorkItemLinkChild(); + }); + + it('does not fetch the child work item by id before hovering work item links', () => { + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); + + it('fetches the child work item by id if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemQueryHandler).toHaveBeenCalledWith({ + id: 'gid://gitlab/WorkItem/2', + }); + }); + + it('does not fetch the child work item by id if link is hovered for less than 250 ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(200); + firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); + await waitForPromises(); + + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); + + it('does not fetch work item by iid if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemByIidHandler).not.toHaveBeenCalled(); + }); }); - it('fetches the child work item if link is hovered for 250+ ms', async () => { - firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); - jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); - await waitForPromises(); + it('starts prefetching work item by id if URL contains work item id', async () => { + setWindowLocation('?work_item_id=5'); + await createComponent(); expect(childWorkItemQueryHandler).toHaveBeenCalledWith({ - id: 'gid://gitlab/WorkItem/2', + id: 'gid://gitlab/WorkItem/5', }); }); - it('does not fetch the child work item if link is hovered for less than 250 ms', async () => { - firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); - jest.advanceTimersByTime(200); - firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); - await waitForPromises(); + it('does not open the modal if work item id URL parameter is not found in child items', async () => { + setWindowLocation('?work_item_id=555'); + await createComponent(); + + expect(showModal).not.toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(null); + }); + + it('opens the modal if work item id URL parameter is found in child items', async () => { + setWindowLocation('?work_item_id=2'); + await createComponent(); - expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + expect(showModal).toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe( + 'gid://gitlab/WorkItem/2', + ); }); }); - describe('when parent item is confidential', () => { - it('passes correct confidentiality status to form', async () => { - await createComponent({ - issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)), + describe('when work item is fetched by iid', () => { + describe('prefetching child items', () => { + let firstChild; + + beforeEach(async () => { + setWindowLocation('?iid_path=true'); + await createComponent({ fetchByIid: true }); + + firstChild = findFirstWorkItemLinkChild(); }); - findToggleFormDropdown().vm.$emit('click'); - findToggleAddFormButton().vm.$emit('click'); - await nextTick(); - expect(findAddLinksForm().props('parentConfidential')).toBe(true); + it('does not fetch the child work item by iid before hovering work item links', () => { + expect(childWorkItemByIidHandler).not.toHaveBeenCalled(); + }); + + it('fetches the child work item by iid if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemByIidHandler).toHaveBeenCalledWith({ + fullPath: 'project/path', + iid: '2', + }); + }); + + it('does not fetch the child work item by iid if link is hovered for less than 250 ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(200); + firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); + await waitForPromises(); + + expect(childWorkItemByIidHandler).not.toHaveBeenCalled(); + }); + + it('does not fetch work item by id if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); }); + + it('starts prefetching work item by iid if URL contains work item id', async () => { + setWindowLocation('?work_item_iid=5&iid_path=true'); + await createComponent({ fetchByIid: true }); + + expect(childWorkItemByIidHandler).toHaveBeenCalledWith({ + iid: '5', + fullPath: 'project/path', + }); + }); + }); + + it('does not open the modal if work item iid URL parameter is not found in child items', async () => { + setWindowLocation('?work_item_iid=555&iid_path=true'); + await createComponent({ fetchByIid: true }); + + expect(showModal).not.toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null); + }); + + it('opens the modal if work item iid URL parameter is found in child items', async () => { + setWindowLocation('?work_item_iid=2&iid_path=true'); + await createComponent({ fetchByIid: true }); + + expect(showModal).toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2'); }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 67fda49919a..6385f12ccea 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -723,6 +723,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { nodes: [ { id: 'gid://gitlab/WorkItem/2', + iid: '2', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', __typename: 'WorkItemType', @@ -747,6 +748,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { export const workItemTask = { id: 'gid://gitlab/WorkItem/4', + iid: '4', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', __typename: 'WorkItemType', @@ -761,6 +763,7 @@ export const workItemTask = { export const confidentialWorkItemTask = { id: 'gid://gitlab/WorkItem/2', + iid: '2', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', __typename: 'WorkItemType', @@ -775,6 +778,7 @@ export const confidentialWorkItemTask = { export const closedWorkItemTask = { id: 'gid://gitlab/WorkItem/3', + iid: '3', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', __typename: 'WorkItemType', @@ -791,6 +795,7 @@ export const workItemHierarchyResponse = { data: { workItem: { id: 'gid://gitlab/WorkItem/1', + iid: '1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', __typename: 'WorkItemType', @@ -821,6 +826,7 @@ export const workItemHierarchyResponse = { workItemTask, { id: 'gid://gitlab/WorkItem/5', + iid: '5', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', __typename: 'WorkItemType', diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js index 880c4271024..a766962771a 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -55,7 +55,7 @@ describe('Work items root component', () => { isModal: false, workItemId: 'gid://gitlab/WorkItem/1', workItemParentId: null, - iid: '1', + workItemIid: '1', }); }); diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 7f838167bd2..84b076ec1d2 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -419,7 +419,7 @@ RSpec.describe ApplicationHelper do end it 'includes all possible body data elements and associates the project elements with project' do - expect(helper).to receive(:can?).with(nil, :download_code, project) + expect(helper).to receive(:can?).with(nil, :read_code, project) expect(helper.body_data).to eq( { page: 'application', @@ -437,7 +437,7 @@ RSpec.describe ApplicationHelper do let_it_be(:project) { create(:project, :repository, group: create(:group)) } it 'includes all possible body data elements and associates the project elements with project' do - expect(helper).to receive(:can?).with(nil, :download_code, project) + expect(helper).to receive(:can?).with(nil, :read_code, project) expect(helper.body_data).to eq( { page: 'application', @@ -463,7 +463,7 @@ RSpec.describe ApplicationHelper do stub_controller_method(:action_name, 'show') stub_controller_method(:params, { id: issue.id }) - expect(helper).to receive(:can?).with(nil, :download_code, project).and_return(false) + expect(helper).to receive(:can?).with(nil, :read_code, project).and_return(false) expect(helper.body_data).to eq( { page: 'projects:issues:show', @@ -479,12 +479,34 @@ RSpec.describe ApplicationHelper do end end - context 'when current_user has download_code permission' do - it 'returns find_file with the default branch' do + describe 'find_file attribute' do + subject { helper.body_data[:find_file] } + + before do allow(helper).to receive(:current_user).and_return(user) + end + + context 'when the project has no repository' do + before do + allow(project).to receive(:empty_repo?).and_return(true) + end + + it { is_expected.to be_nil } + end + + context 'when user cannot read_code for the project' do + before do + allow(helper).to receive(:can?).with(user, :read_code, project).and_return(false) + end - expect(helper).to receive(:can?).with(user, :download_code, project).and_return(true) - expect(helper.body_data[:find_file]).to end_with(project.default_branch) + it { is_expected.to be_nil } + end + + context 'when current_user has read_code permission' do + it 'returns find_file with the default branch' do + expect(helper).to receive(:can?).with(user, :read_code, project).and_return(true) + expect(subject).to end_with(project.default_branch) + end end end end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index 9aba6ac293c..59bfe2042fa 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) } let(:subscriber) { described_class.new } - let(:event) { double(:event, duration: 15.2) } + let(:event) { double(:event, duration: 15.2, payload: { key: %w[a b c] }) } describe '#cache_read' do it 'increments the cache_read duration' do @@ -64,6 +64,40 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do end end + describe '#cache_read_multi' do + subject { subscriber.cache_read_multi(event) } + + context 'with a transaction' do + before do + allow(subscriber).to receive(:current_transaction) + .and_return(transaction) + end + + it 'observes multi-key count' do + expect(transaction).to receive(:observe) + .with(:gitlab_cache_read_multikey_count, event.payload[:key].size) + + subject + end + end + + context 'with no transaction' do + it 'does not observes multi-key count' do + expect(transaction).not_to receive(:observe) + .with(:gitlab_cache_read_multikey_count, event.payload[:key].size) + + subject + end + end + + it 'observes read_multi duration' do + expect(subscriber).to receive(:observe) + .with(:read_multi, event.duration) + + subject + end + end + describe '#cache_write' do it 'observes write duration' do expect(subscriber).to receive(:observe) diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 0bea06f602f..e4cb1e74d32 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -273,6 +273,22 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) end + context 'when Rails environment is production' do + before do + allow(Rails.env).to receive(:development?).and_return(false) + allow(Rails.env).to receive(:test?).and_return(false) + end + + it 'reports only UnknownEvent exception' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + .with(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) + .once + .and_call_original + + expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.not_to raise_error + end + end + it 'reports an error if Feature.enabled raise an error' do expect(Feature).to receive(:enabled?).and_raise(StandardError.new) expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index c32cc87afbb..378a7ef8dd6 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -76,21 +76,21 @@ RSpec.describe ProjectPresenter do let_it_be(:project) { create(:project, :public, :repository) } it 'returns files and readme if user has repository access' do - allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(true) + allow(presenter).to receive(:can?).with(nil, :read_code, project).and_return(true) expect(presenter.default_view).to eq('files') end it 'returns wiki if user does not have repository access and can read wiki, which exists' do allow(project).to receive(:wiki_repository_exists?).and_return(true) - allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(nil, :read_code, project).and_return(false) allow(presenter).to receive(:can?).with(nil, :read_wiki, project).and_return(true) expect(presenter.default_view).to eq('wiki') end it 'returns activity if user does not have repository or wiki access' do - allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(nil, :read_code, project).and_return(false) allow(presenter).to receive(:can?).with(nil, :read_issue, project).and_return(false) allow(presenter).to receive(:can?).with(nil, :read_wiki, project).and_return(false) @@ -117,7 +117,7 @@ RSpec.describe ProjectPresenter do context 'when the user is allowed to see the code' do it 'returns the project view' do - allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(true) + allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(true) expect(presenter.default_view).to eq('readme') end @@ -126,7 +126,7 @@ RSpec.describe ProjectPresenter do context 'with wikis enabled and the right policy for the user' do before do project.project_feature.update_attribute(:issues_access_level, 0) - allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(false) end it 'returns wiki if the user has the right policy and the wiki exists' do @@ -146,7 +146,7 @@ RSpec.describe ProjectPresenter do context 'with issues as a feature available' do it 'return issues' do - allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(false) allow(presenter).to receive(:can?).with(user, :read_issue, project).and_return(true) allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false) @@ -157,7 +157,7 @@ RSpec.describe ProjectPresenter do context 'with no activity, no wikies and no issues' do it 'returns activity as default' do project.project_feature.update_attribute(:issues_access_level, 0) - allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(false) allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false) allow(presenter).to receive(:can?).with(user, :read_issue, project).and_return(false) diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb index ccf1a8fd26a..ed5650e7310 100644 --- a/spec/scripts/lib/glfm/update_specification_spec.rb +++ b/spec/scripts/lib/glfm/update_specification_spec.rb @@ -53,9 +53,9 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do let(:ghfm_spec_txt_examples) do <<~MARKDOWN - # Section with Examples + # Section with examples - ## Emphasis and Strong + ## Emphasis and strong ```````````````````````````````` example _EMPHASIS LINE 1_ @@ -101,20 +101,24 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do <<~MARKDOWN # Official Specification Section with Examples - Some examples. + ```````````````````````````````` example + official example + . + <p>official example</p> + ```````````````````````````````` + MARKDOWN end let(:glfm_official_specification_md_contents) do <<~MARKDOWN - # GLFM Introduction - - GLFM intro text. + GLFM official text before examples - <!-- BEGIN TESTS --> + #{described_class::BEGIN_TESTS_COMMENT_LINE_TEXT} #{glfm_official_specification_md_examples} - <!-- END TESTS --> - # Non-example official content + #{described_class::END_TESTS_COMMENT_LINE_TEXT} + + GLFM official text after examples MARKDOWN end @@ -122,17 +126,19 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do <<~MARKDOWN # Internal Extension Section with Examples - Some examples. + ```````````````````````````````` example + internal example + . + <p>internal extension example</p> + ```````````````````````````````` MARKDOWN end let(:glfm_internal_extensions_md_contents) do <<~MARKDOWN - # Non-example internal content - <!-- BEGIN TESTS --> + #{described_class::BEGIN_TESTS_COMMENT_LINE_TEXT} #{glfm_internal_extensions_md_examples} - <!-- END TESTS --> - # More non-example internal content + #{described_class::END_TESTS_COMMENT_LINE_TEXT} MARKDOWN end @@ -258,29 +264,46 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do describe 'writing output_example_snapshots/snapshot_spec.md' do let(:es_snapshot_spec_md_contents) { reread_io(es_snapshot_spec_md_io) } - before do - subject.process(skip_spec_html_generation: true) - end + context 'with valid glfm_internal_extensions.md' do + before do + subject.process(skip_spec_html_generation: true) + end - it 'replaces the header text with the GitLab version' do - expect(es_snapshot_spec_md_contents).not_to match(/GitHub Flavored Markdown Spec/m) - expect(es_snapshot_spec_md_contents).not_to match(/^version: \d\.\d/m) - expect(es_snapshot_spec_md_contents).not_to match(/^date: /m) + it 'replaces the header text with the GitLab version' do + expect(es_snapshot_spec_md_contents).not_to match(/GitHub Flavored Markdown Spec/m) + expect(es_snapshot_spec_md_contents).not_to match(/^version: \d\.\d/m) + expect(es_snapshot_spec_md_contents).not_to match(/^date: /m) - expect(es_snapshot_spec_md_contents).to match(/#{Regexp.escape(described_class::GLFM_SPEC_TXT_HEADER)}/mo) + expect(es_snapshot_spec_md_contents).to match(/#{Regexp.escape(described_class::ES_SNAPSHOT_SPEC_MD_HEADER)}/mo) + end + + it 'includes header and all examples', :unlimited_max_formatted_output_length do + # rubocop:disable Style/StringConcatenation (string contatenation is more readable) + expected = described_class::ES_SNAPSHOT_SPEC_MD_HEADER + + ghfm_spec_txt_examples + + "\n" + + glfm_official_specification_md_examples + + "\n\n" + # NOTE: We want a blank line between the official and internal examples + glfm_internal_extensions_md_examples + + "\n" + # rubocop:enable Style/StringConcatenation + expect(es_snapshot_spec_md_contents).to eq(expected) + end end - it 'includes header and all examples', :unlimited_max_formatted_output_length do - # rubocop:disable Style/StringConcatenation (string contatenation is more readable) - expected = described_class::GLFM_SPEC_TXT_HEADER + - ghfm_spec_txt_examples + - "\n" + - glfm_official_specification_md_examples + - "\n\n" + # NOTE: We want a blank line between the official and internal examples - glfm_internal_extensions_md_examples + - "\n" - # rubocop:enable Style/StringConcatenation - expect(es_snapshot_spec_md_contents).to eq(expected) + context 'with invalid non-example content in glfm_internal_extensions.md' do + let(:glfm_internal_extensions_md_contents) do + <<~MARKDOWN + THIS TEXT IS NOT ALLOWED IN THIS FILE, ONLY EXAMPLES IN BEGIN/END TEST BLOCK ARE ALLOWED + #{described_class::BEGIN_TESTS_COMMENT_LINE_TEXT} + #{glfm_internal_extensions_md_examples} + #{described_class::END_TESTS_COMMENT_LINE_TEXT} + MARKDOWN + end + + it 'raises an error' do + expect { subject.process(skip_spec_html_generation: true) }.to raise_error /no content is allowed outside/i + end end end @@ -294,66 +317,56 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do end it 'renders expected HTML', :unlimited_max_formatted_output_length do - # NOTE: We do assertions for both output HTML files in this same `it` example block, + # NOTE: We do all assertions for both output HTML files in this same `it` example block, # because calling a full `subject.process` without `skip_spec_html_generation: true` - # is very slow, and want to avoid doing it twice. - - expected_spec_html = <<~RENDERED_HTML - <div class="gl-relative markdown-code-block js-markdown-code"> - <pre data-sourcepos="1:1-4:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span> - <span id="LC2" class="line" lang="yaml"><span class="na">version</span><span class="pi">:</span> <span class="s">alpha</span></span></code></pre> - <copy-code></copy-code> - </div> - <h1 data-sourcepos="5:1-5:19" dir="auto"> - <a id="user-content-glfm-introduction" class="anchor" href="#glfm-introduction" aria-hidden="true"></a>GLFM Introduction</h1> - <p data-sourcepos="7:1-7:16" dir="auto">GLFM intro text.</p> - - <h1 data-sourcepos="10:1-10:46" dir="auto"> - <a id="user-content-official-specification-section-with-examples" class="anchor" href="#official-specification-section-with-examples" aria-hidden="true"></a>Official Specification Section with Examples</h1> - <p data-sourcepos="12:1-12:14" dir="auto">Some examples.</p> - - <h1 data-sourcepos="15:1-15:30" dir="auto"> - <a id="user-content-non-example-official-content" class="anchor" href="#non-example-official-content" aria-hidden="true"></a>Non-example official content</h1> - RENDERED_HTML - expect(spec_html_contents).to be == expected_spec_html - - expected_snapshot_spec_html = <<~RENDERED_HTML - <div class="gl-relative markdown-code-block js-markdown-code"> - <pre data-sourcepos="1:1-4:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span> - <span id="LC2" class="line" lang="yaml"><span class="na">version</span><span class="pi">:</span> <span class="s">alpha</span></span></code></pre> - <copy-code></copy-code> - </div> - <h1 data-sourcepos="5:1-5:23" dir="auto"> - <a id="user-content-section-with-examples" class="anchor" href="#section-with-examples" aria-hidden="true"></a>Section with Examples</h1> - <h2 data-sourcepos="7:1-7:22" dir="auto"> - <a id="user-content-emphasis-and-strong" class="anchor" href="#emphasis-and-strong" aria-hidden="true"></a>Emphasis and Strong</h2> - <div class="gl-relative markdown-code-block js-markdown-code"> - <pre data-sourcepos="9:1-12:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">_EMPHASIS LINE 1_</span> - <span id="LC2" class="line" lang="plaintext">_EMPHASIS LINE 2_</span></code></pre> - <copy-code></copy-code> - </div> - <div class="gl-relative markdown-code-block js-markdown-code"> - <pre data-sourcepos="14:1-17:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p><em>EMPHASIS LINE 1</em></span> - <span id="LC2" class="line" lang="plaintext"><em>EMPHASIS LINE 2</em></p></span></code></pre> - <copy-code></copy-code> - </div> - <div class="gl-relative markdown-code-block js-markdown-code"> - <pre data-sourcepos="19:1-21:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">__STRONG!__</span></code></pre> - <copy-code></copy-code> - </div> - <div class="gl-relative markdown-code-block js-markdown-code"> - <pre data-sourcepos="23:1-25:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p><strong>STRONG!</strong></p></span></code></pre> - <copy-code></copy-code> - </div> - <p data-sourcepos="27:1-27:36" dir="auto">End of last GitHub examples section.</p> - <h1 data-sourcepos="29:1-29:46" dir="auto"> - <a id="user-content-official-specification-section-with-examples" class="anchor" href="#official-specification-section-with-examples" aria-hidden="true"></a>Official Specification Section with Examples</h1> - <p data-sourcepos="31:1-31:14" dir="auto">Some examples.</p> - <h1 data-sourcepos="34:1-34:42" dir="auto"> - <a id="user-content-internal-extension-section-with-examples" class="anchor" href="#internal-extension-section-with-examples" aria-hidden="true"></a>Internal Extension Section with Examples</h1> - <p data-sourcepos="36:1-36:14" dir="auto">Some examples.</p> - RENDERED_HTML - expect(snapshot_spec_html_contents).to be == expected_snapshot_spec_html + # is very slow, and want to avoid doing it multiple times + # + # We also do fairly loose and minimal assertions around the basic structure of the files. + # Otherwise, if we asserted the complete exact structure of the HTML, this would be a + # brittle test which would breaks every time that something minor changed around the + # GLFM rendering. E.g. classes, ids, attribute ordering, etc. All of this behavior + # should be thoroughly covered elsewhere by other testing. If there are regressions + # in the update specification logic in the future which are not caught by this example, + # additional test coverage can be added as necessary. + + # -------------------- + # spec.html assertions + # -------------------- + + # correct title should in a header + expect(spec_html_contents).to match(%r{<h1.*#{described_class::GLFM_SPEC_TXT_TITLE}</h1>}o) + + # correct text should be included with correct ordering + expect(spec_html_contents) + .to match(%r{official text before.*official example.*official text after}m) + + # ----------------------------- + # snapshot_spec.html assertions + # ----------------------------- + + # correct title should in a header + expect(snapshot_spec_html_contents).to match(%r{<h1.*#{described_class::ES_SNAPSHOT_SPEC_TITLE}</h1>}o) + + # correct example text should be included + expect(snapshot_spec_html_contents) + .to match(%r{internal example}m) + + # ----------------------------- + # general formatting assertions + # ----------------------------- + + md = '_EMPHASIS LINE 1_' + html = '<em>EMPHASIS LINE 1</em>' + + # examples should have markdown and HTML in separate pre+code blocks + expected_example_1_regex = "<pre.*<code.*#{md}.*</code></pre>.*<pre.*<code.*#{html}.*</code></pre>" + expect(snapshot_spec_html_contents).to match(%r{#{expected_example_1_regex}}m) + + # examples should have proper divs prepended for numbering, links, and styling + empty_div_for_example_class = '<div>' + examplenum_div = '<div><a href="#example-1">Example 1</a></div>' + expect(snapshot_spec_html_contents) + .to match(%r{#{empty_div_for_example_class}\n#{examplenum_div}.*#{expected_example_1_regex}.*}m) end end # rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8e73073e68b..e083bb06241 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -211,7 +211,6 @@ RSpec.configure do |config| end config.before(:suite) do - Timecop.safe_mode = true TestEnv.init # Reload all feature flags definitions diff --git a/spec/views/projects/_flash_messages.html.haml_spec.rb b/spec/views/projects/_flash_messages.html.haml_spec.rb index e1858229208..231aa12d920 100644 --- a/spec/views/projects/_flash_messages.html.haml_spec.rb +++ b/spec/views/projects/_flash_messages.html.haml_spec.rb @@ -12,10 +12,10 @@ RSpec.describe 'projects/_flash_messages' do before do allow(view).to receive(:current_user).and_return(user) - allow(view).to receive(:can?).with(user, :download_code, project).and_return(true) + allow(view).to receive(:can?).with(user, :read_code, project).and_return(true) end - context 'when current_user has download_code permission' do + context 'when current_user has read_code permission' do context 'when user has a terraform state' do let_it_be(:project) { create(:project) } let_it_be(:terraform_state) { create(:terraform_state, :locked, :with_version, project: project) } |