Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-15 09:09:23 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-15 09:09:23 +0300
commitde671a855fff66bcb0eae10b654e806b777f0751 (patch)
treed8c3e0bab80dfc6b7cf5ea717c3ff24189a3bf78 /spec
parent29c6745feab7edf3c0485b34b5e5fffdded9c402 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/explore/catalog_spec.rb42
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_header_spec.js2
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_search_spec.js103
-rw-r--r--spec/frontend/ci/catalog/components/list/empty_state_spec.js64
-rw-r--r--spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js111
-rw-r--r--spec/frontend/fixtures/merge_requests.rb8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js60
-rw-r--r--spec/requests/api/merge_request_approvals_spec.rb2
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