diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-15 09:09:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-15 09:09:23 +0300 |
commit | de671a855fff66bcb0eae10b654e806b777f0751 (patch) | |
tree | d8c3e0bab80dfc6b7cf5ea717c3ff24189a3bf78 /spec | |
parent | 29c6745feab7edf3c0485b34b5e5fffdded9c402 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
8 files changed, 317 insertions, 75 deletions
diff --git a/spec/features/explore/catalog_spec.rb b/spec/features/explore/catalog_spec.rb index 52ce52e43fe..5886ef43e5d 100644 --- a/spec/features/explore/catalog_spec.rb +++ b/spec/features/explore/catalog_spec.rb @@ -44,6 +44,46 @@ RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3) end + context 'when searching for a resource' do + let(:project_name) { ci_resource_projects[0].name } + + before do + find('input[data-testid="catalog-search-bar"]').send_keys project_name + find('input[data-testid="catalog-search-bar"]').send_keys :enter + wait_for_requests + end + + it 'renders only a subset of items' do + expect(find_all('[data-testid="catalog-resource-item"]').length).to be(1) + within_testid('catalog-resource-item', match: :first) do + expect(page).to have_content(project_name) + end + end + end + + context 'when sorting' do + context 'with the creation date option' do + it 'sorts resources from last to first by default' do + expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3) + expect(find_all('[data-testid="catalog-resource-item"]')[0]).to have_content(ci_resource_projects[2].name) + expect(find_all('[data-testid="catalog-resource-item"]')[2]).to have_content(ci_resource_projects[0].name) + end + + context 'when changing the sort direction' do + before do + find('.sorting-direction-button').click + wait_for_requests + end + + it 'sorts resources from first to last' do + expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3) + expect(find_all('[data-testid="catalog-resource-item"]')[0]).to have_content(ci_resource_projects[0].name) + expect(find_all('[data-testid="catalog-resource-item"]')[2]).to have_content(ci_resource_projects[2].name) + end + end + end + end + context 'for a single CI/CD catalog resource' do it 'renders resource details', :aggregate_failures do within_testid('catalog-resource-item', match: :first) do @@ -58,7 +98,7 @@ RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do find_by_testid('ci-resource-link', match: :first).click end - it 'navigate to the details page' do + it 'navigates to the details page' do expect(page).to have_content('Go to the project') end end diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js index 2a5c24d0515..deda0128977 100644 --- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js +++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js @@ -18,7 +18,7 @@ describe('CatalogHeader', () => { const findBanner = () => wrapper.findComponent(GlBanner); const findFeedbackButton = () => findBanner().findComponent(GlButton); const findTitle = () => wrapper.find('h1'); - const findDescription = () => wrapper.findByTestId('description'); + const findDescription = () => wrapper.findByTestId('page-description'); const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => { wrapper = shallowMountExtended(CatalogHeader, { diff --git a/spec/frontend/ci/catalog/components/list/catalog_search_spec.js b/spec/frontend/ci/catalog/components/list/catalog_search_spec.js new file mode 100644 index 00000000000..c6f8498f2fd --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/catalog_search_spec.js @@ -0,0 +1,103 @@ +import { GlSearchBoxByClick, GlSorting, GlSortingItem } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import CatalogSearch from '~/ci/catalog/components/list/catalog_search.vue'; +import { SORT_ASC, SORT_DESC, SORT_OPTION_CREATED } from '~/ci/catalog/constants'; + +describe('CatalogSearch', () => { + let wrapper; + + const findSearchBar = () => wrapper.findComponent(GlSearchBoxByClick); + const findSorting = () => wrapper.findComponent(GlSorting); + const findAllSortingItems = () => wrapper.findAllComponents(GlSortingItem); + + const createComponent = () => { + wrapper = shallowMountExtended(CatalogSearch, {}); + }; + + beforeEach(() => { + createComponent(); + }); + + describe('default UI', () => { + it('renders the search bar', () => { + expect(findSearchBar().exists()).toBe(true); + }); + + it('renders the sorting options', () => { + expect(findSorting().exists()).toBe(true); + expect(findAllSortingItems()).toHaveLength(1); + }); + + it('renders the `Created at` option as the default', () => { + expect(findAllSortingItems().at(0).text()).toBe('Created at'); + }); + }); + + describe('search', () => { + it('passes down the search value to the search component', async () => { + const newSearchTerm = 'cat'; + + expect(findSearchBar().props().value).toBe(''); + + await findSearchBar().vm.$emit('input', newSearchTerm); + + expect(findSearchBar().props().value).toBe(newSearchTerm); + }); + + it('does not submit only when typing', async () => { + expect(wrapper.emitted('update-search-term')).toBeUndefined(); + + await findSearchBar().vm.$emit('input', 'new'); + + expect(wrapper.emitted('update-search-term')).toBeUndefined(); + }); + + describe('when submitting the search', () => { + const newSearchTerm = 'dog'; + + beforeEach(async () => { + await findSearchBar().vm.$emit('input', newSearchTerm); + await findSearchBar().vm.$emit('submit'); + }); + + it('emits the event up with the new payload', () => { + expect(wrapper.emitted('update-search-term')).toEqual([[newSearchTerm]]); + }); + }); + + describe('when clearing the search', () => { + beforeEach(async () => { + await findSearchBar().vm.$emit('input', 'new'); + await findSearchBar().vm.$emit('clear'); + }); + + it('emits an update event with an empty string payload', () => { + expect(wrapper.emitted('update-search-term')).toEqual([['']]); + }); + }); + }); + + describe('sort', () => { + describe('when changing sort order', () => { + it('changes the `isAscending` prop to the sorting component', async () => { + expect(findSorting().props().isAscending).toBe(false); + + await findSorting().vm.$emit('sortDirectionChange'); + + expect(findSorting().props().isAscending).toBe(true); + }); + + it('emits an `update-sorting` event with the new direction', async () => { + expect(wrapper.emitted('update-sorting')).toBeUndefined(); + + await findSorting().vm.$emit('sortDirectionChange'); + await findSorting().vm.$emit('sortDirectionChange'); + + expect(wrapper.emitted('update-sorting')).toEqual([ + [`${SORT_OPTION_CREATED}_${SORT_ASC}`], + [`${SORT_OPTION_CREATED}_${SORT_DESC}`], + ]); + }); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/components/list/empty_state_spec.js b/spec/frontend/ci/catalog/components/list/empty_state_spec.js index f589ad96a9d..5db0c61371d 100644 --- a/spec/frontend/ci/catalog/components/list/empty_state_spec.js +++ b/spec/frontend/ci/catalog/components/list/empty_state_spec.js @@ -1,27 +1,83 @@ -import { GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; +import { COMPONENTS_DOCS_URL } from '~/ci/catalog/constants'; describe('EmptyState', () => { let wrapper; const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findComponentsDocLink = () => wrapper.findComponent(GlLink); const createComponent = ({ props = {} } = {}) => { wrapper = shallowMountExtended(EmptyState, { propsData: { ...props, }, + stubs: { + GlEmptyState, + GlSprintf, + }, }); }; - describe('when mounted', () => { + describe('default', () => { beforeEach(() => { createComponent(); }); - it('renders the empty state', () => { - expect(findEmptyState().exists()).toBe(true); + it('renders the default empty state', () => { + const emptyState = findEmptyState(); + + expect(emptyState.exists()).toBe(true); + expect(emptyState.props().title).toBe('Get started with the CI/CD Catalog'); + expect(emptyState.props().description).toBe( + 'Create a pipeline component repository and make reusing pipeline configurations faster and easier.', + ); + }); + }); + + describe('when there is a search query', () => { + beforeEach(() => { + createComponent({ + props: { searchTerm: 'a' }, + }); + }); + + it('renders the search description', () => { + expect(findEmptyState().text()).toContain( + 'Edit your search and try again. Or learn to create a component repository.', + ); + }); + + it('renders the link to the components documentation', () => { + const docsLink = findComponentsDocLink(); + expect(docsLink.exists()).toBe(true); + expect(docsLink.attributes().href).toBe(COMPONENTS_DOCS_URL); + }); + + describe('and it is less than 3 characters', () => { + beforeEach(() => { + createComponent({ + props: { searchTerm: 'a' }, + }); + }); + + it('render the too few chars empty state title', () => { + expect(findEmptyState().props().title).toBe('Search must be at least 3 characters'); + }); + }); + + describe('and it has more than 3 characters', () => { + beforeEach(() => { + createComponent({ + props: { searchTerm: 'my component' }, + }); + }); + + it('renders the search empty state title', () => { + expect(findEmptyState().props().title).toBe('No result found'); + }); }); }); }); diff --git a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js index e18b418b155..80d3d2cea26 100644 --- a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js +++ b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js @@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { createAlert } from '~/alert'; import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue'; +import CatalogSearch from '~/ci/catalog/components/list/catalog_search.vue'; import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue'; import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue'; import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; @@ -24,6 +25,8 @@ describe('CiResourcesPage', () => { let wrapper; let catalogResourcesResponse; + const defaultQueryVariables = { first: 20 }; + const createComponent = () => { const handlers = [[getCatalogResources, catalogResourcesResponse]]; const mockApollo = createMockApollo(handlers, {}, cacheConfig); @@ -36,6 +39,7 @@ describe('CiResourcesPage', () => { }; const findCatalogHeader = () => wrapper.findComponent(CatalogHeader); + const findCatalogSearch = () => wrapper.findComponent(CatalogSearch); const findCiResourcesList = () => wrapper.findComponent(CiResourcesList); const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader); const findEmptyState = () => wrapper.findComponent(EmptyState); @@ -71,8 +75,14 @@ describe('CiResourcesPage', () => { }); it('renders the empty state', () => { - expect(findLoadingState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(true); + }); + + it('renders the search', () => { + expect(findCatalogSearch().exists()).toBe(true); + }); + + it('does not render the list', () => { expect(findCiResourcesList().exists()).toBe(false); }); }); @@ -99,6 +109,10 @@ describe('CiResourcesPage', () => { totalCount: count, }); }); + + it('renders the search and sort', () => { + expect(findCatalogSearch().exists()).toBe(true); + }); }); }); @@ -121,11 +135,12 @@ describe('CiResourcesPage', () => { if (eventName === 'onNextPage') { expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, after: pageInfo.endCursor, - first: 20, }); } else { expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, before: pageInfo.startCursor, last: 20, first: null, @@ -134,6 +149,73 @@ describe('CiResourcesPage', () => { }); }); + describe('search and sort', () => { + describe('on initial load', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + }); + + it('calls the query without search or sort', () => { + expect(catalogResourcesResponse).toHaveBeenCalledTimes(1); + expect(catalogResourcesResponse.mock.calls[0][0]).toEqual({ + ...defaultQueryVariables, + }); + }); + }); + + describe('when sorting changes', () => { + const newSort = 'MOST_AWESOME_ASC'; + + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + await findCatalogSearch().vm.$emit('update-sorting', newSort); + }); + + it('passes it to the graphql query', () => { + expect(catalogResourcesResponse).toHaveBeenCalledTimes(2); + expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, + sortValue: newSort, + }); + }); + }); + + describe('when search component emits a new search term', () => { + const newSearch = 'sloths'; + + describe('and there are no results', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody); + await createComponent(); + await findCatalogSearch().vm.$emit('update-search-term', newSearch); + }); + + it('renders the empty state and passes down the search query', () => { + expect(findEmptyState().exists()).toBe(true); + expect(findEmptyState().props().searchTerm).toBe(newSearch); + }); + }); + + describe('and there are results', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + await findCatalogSearch().vm.$emit('update-search-term', newSearch); + }); + + it('passes it to the graphql query', () => { + expect(catalogResourcesResponse).toHaveBeenCalledTimes(2); + expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, + searchTerm: newSearch, + }); + }); + }); + }); + }); + describe('pages count', () => { describe('when the fetchMore call suceeds', () => { beforeEach(async () => { @@ -157,6 +239,31 @@ describe('CiResourcesPage', () => { }); }); + describe.each` + event | payload + ${'update-search-term'} | ${'cat'} + ${'update-sorting'} | ${'CREATED_ASC'} + `('when $event event is emitted', ({ event, payload }) => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + }); + + it('resets the page count', async () => { + expect(findCiResourcesList().props().currentPage).toBe(1); + + findCiResourcesList().vm.$emit('onNextPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(2); + + await findCatalogSearch().vm.$emit(event, payload); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(1); + }); + }); + describe('when the fetchMore call fails', () => { const errorMessage = 'there was an error'; diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index e8272a1f93a..a1896a6470b 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -2,13 +2,7 @@ require 'spec_helper' -RSpec - .describe( - Projects::MergeRequestsController, - '(JavaScript fixtures)', - type: :controller, - feature_category: :code_review_workflow - ) do +RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:namespace) { create(:namespace, name: 'frontend-fixtures') } diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js index c81f4328d2a..2aed037be6f 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js @@ -4,7 +4,6 @@ import { GlButton, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client'; import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; -import { visitUrl } from '~/lib/utils/url_utility'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -29,10 +28,6 @@ jest.mock('~/alert', () => ({ dismiss: mockAlertDismiss, })), })); -jest.mock('~/lib/utils/url_utility', () => ({ - ...jest.requireActual('~/lib/utils/url_utility'), - visitUrl: jest.fn(), -})); const TEST_HELP_PATH = 'help/path'; const testApprovedBy = () => [1, 7, 10].map((id) => ({ id })); @@ -118,7 +113,6 @@ describe('MRWidget approvals', () => { targetProjectFullPath: 'gitlab-org/gitlab', id: 1, iid: '1', - requireSamlAuthToApprove: false, }; jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); @@ -178,22 +172,6 @@ describe('MRWidget approvals', () => { category: 'primary', }); }); - - describe('with SAML auth requried for approval', () => { - beforeEach(async () => { - const response = createCanApproveResponse(); - mr.requireSamlAuthToApprove = true; - createComponent({}, { query: response }); - await waitForPromises(); - }); - it('approve action is rendered with correct text', () => { - expect(findActionData()).toEqual({ - variant: 'confirm', - text: 'Approve with SAML', - category: 'primary', - }); - }); - }); }); describe('and MR is approved', () => { @@ -216,25 +194,6 @@ describe('MRWidget approvals', () => { }); }); - describe('with approvers, with SAML auth requried for approval', () => { - beforeEach(async () => { - canApproveResponse.data.project.mergeRequest.approvedBy.nodes = - approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes; - canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 69; - mr.requireSamlAuthToApprove = true; - createComponent({}, { query: canApproveResponse }); - await waitForPromises(); - }); - - it('approve additionally action is rendered with correct text', () => { - expect(findActionData()).toEqual({ - variant: 'confirm', - text: 'Approve additionally with SAML', - category: 'secondary', - }); - }); - }); - describe('with approvers', () => { beforeEach(async () => { canApproveResponse.data.project.mergeRequest.approvedBy.nodes = @@ -256,25 +215,6 @@ describe('MRWidget approvals', () => { }); }); - describe('when SAML auth is required and user clicks Approve with SAML', () => { - const fakeGroupSamlPath = '/example_group_saml'; - - beforeEach(async () => { - mr.requireSamlAuthToApprove = true; - mr.samlApprovalPath = fakeGroupSamlPath; - - createComponent({}, { query: createCanApproveResponse() }); - await waitForPromises(); - }); - - it('redirects the user to the group SAML path', async () => { - const action = findAction(); - action.vm.$emit('click'); - await nextTick(); - expect(visitUrl).toHaveBeenCalledWith(fakeGroupSamlPath); - }); - }); - describe('when approve action is clicked', () => { beforeEach(async () => { createComponent({}, { query: canApproveResponse }); diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb index 2de59750273..df2b20c62c3 100644 --- a/spec/requests/api/merge_request_approvals_spec.rb +++ b/spec/requests/api/merge_request_approvals_spec.rb @@ -8,6 +8,8 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage let_it_be(:bot) { create(:user, :project_bot) } let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } let_it_be(:approver) { create :user } + let_it_be(:group) { create :group } + let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project) } describe 'GET :id/merge_requests/:merge_request_iid/approvals' do |