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-06-01 00:09:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-01 00:09:09 +0300
commit404895390afe87ce8ab939448bf7dff7dc4b7169 (patch)
tree93c323d7df6b70c84dce7b3e4e4f3d57180394a0 /spec
parente9885f7a36065b9b45a35feb6c427c7742a906a4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js31
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js44
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js123
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js150
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js70
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js40
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js17
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js59
-rw-r--r--spec/helpers/sidebars_helper_spec.rb11
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/project_feature_spec.rb1
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/requests/api/project_attributes.yml1
15 files changed, 543 insertions, 9 deletions
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1b485e47127..6e3e119ddab 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -42,6 +42,7 @@ FactoryBot.define do
feature_flags_access_level { ProjectFeature::ENABLED }
releases_access_level { ProjectFeature::ENABLED }
infrastructure_access_level { ProjectFeature::ENABLED }
+ model_experiments_access_level { ProjectFeature::ENABLED }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index b8526e569ec..29759f828e4 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 8ca88472bf1..9d93ba332e9 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
@@ -16,7 +17,7 @@ describe('Pipeline Status', () => {
let mockApollo;
let mockPipelineQuery;
- const createComponentWithApollo = () => {
+ const createComponentWithApollo = ({ ciGraphqlPipelineMiniGraph = false } = {}) => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@@ -26,6 +27,9 @@ describe('Pipeline Status', () => {
commitSha: mockCommitSha,
},
provide: {
+ glFeatures: {
+ ciGraphqlPipelineMiniGraph,
+ },
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf },
@@ -34,6 +38,7 @@ describe('Pipeline Status', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGraphqlPipelineMiniGraph = () => wrapper.findComponent(GraphqlPipelineMiniGraph);
const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
@@ -128,4 +133,28 @@ describe('Pipeline Status', () => {
});
});
});
+
+ describe('feature flag behavior', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue({
+ data: { project: mockProjectPipeline() },
+ });
+ });
+
+ it.each`
+ state | provide | showPipelineMiniGraph | showGraphqlPipelineMiniGraph
+ ${true} | ${{ ciGraphqlPipelineMiniGraph: true }} | ${false} | ${true}
+ ${false} | ${{}} | ${true} | ${false}
+ `(
+ 'renders the correct component when the feature flag is set to $state',
+ async ({ provide, showPipelineMiniGraph, showGraphqlPipelineMiniGraph }) => {
+ createComponentWithApollo(provide);
+
+ await waitForPromises();
+
+ expect(findPipelineEditorMiniGraph().exists()).toBe(showPipelineMiniGraph);
+ expect(findGraphqlPipelineMiniGraph().exists()).toBe(showGraphqlPipelineMiniGraph);
+ },
+ );
+ });
});
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 7be68df61de..7983f8fddf5 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -7,10 +7,11 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
import * as sharedGraphQlUtils from '~/graphql_shared/utils';
import {
mockDownstreamQueryResponse,
@@ -28,6 +29,7 @@ describe('Commit box pipeline mini graph', () => {
let wrapper;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGraphqlPipelineMiniGraph = () => wrapper.findComponent(GraphqlPipelineMiniGraph);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const downstreamHandler = jest.fn().mockResolvedValue(mockDownstreamQueryResponse);
@@ -52,7 +54,7 @@ describe('Commit box pipeline mini graph', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (handler) => {
+ const createComponent = ({ handler, ciGraphqlPipelineMiniGraph = false } = {}) => {
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
@@ -63,6 +65,9 @@ describe('Commit box pipeline mini graph', () => {
iid,
dataMethod: 'graphql',
graphqlResourceEtag: '/api/graphql:pipelines/id/320',
+ glFeatures: {
+ ciGraphqlPipelineMiniGraph,
+ },
},
apolloProvider: createMockApolloProvider(handler),
}),
@@ -148,7 +153,7 @@ describe('Commit box pipeline mini graph', () => {
});
it('should pass the pipeline path prop for the counter badge', async () => {
- createComponent(downstreamHandler);
+ createComponent({ handler: downstreamHandler });
await waitForPromises();
@@ -159,7 +164,7 @@ describe('Commit box pipeline mini graph', () => {
});
it('should render an upstream pipeline only', async () => {
- createComponent(upstreamHandler);
+ createComponent({ handler: upstreamHandler });
await waitForPromises();
@@ -171,7 +176,7 @@ describe('Commit box pipeline mini graph', () => {
});
it('should render downstream and upstream pipelines', async () => {
- createComponent(upstreamDownstreamHandler);
+ createComponent({ handler: upstreamDownstreamHandler });
await waitForPromises();
@@ -255,4 +260,31 @@ describe('Commit box pipeline mini graph', () => {
);
});
});
+
+ describe('feature flag behavior', () => {
+ it.each`
+ state | provide | showPipelineMiniGraph | showGraphqlPipelineMiniGraph
+ ${true} | ${{ ciGraphqlPipelineMiniGraph: true }} | ${false} | ${true}
+ ${false} | ${{}} | ${true} | ${false}
+ `(
+ 'renders the correct component when the feature flag is set to $state',
+ async ({ provide, showPipelineMiniGraph, showGraphqlPipelineMiniGraph }) => {
+ createComponent(provide);
+
+ await waitForPromises();
+
+ expect(findPipelineMiniGraph().exists()).toBe(showPipelineMiniGraph);
+ expect(findGraphqlPipelineMiniGraph().exists()).toBe(showGraphqlPipelineMiniGraph);
+ },
+ );
+
+ it('skips queries when the feature flag is enabled', async () => {
+ createComponent({ ciGraphqlPipelineMiniGraph: true });
+
+ await waitForPromises();
+
+ expect(stagesHandler).not.toHaveBeenCalled();
+ expect(downstreamHandler).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js
new file mode 100644
index 00000000000..69b223461bd
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js
@@ -0,0 +1,123 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
+import * as sharedGraphQlUtils from '~/graphql_shared/utils';
+
+import {
+ linkedPipelinesFetchError,
+ stagesFetchError,
+ mockPipelineStagesQueryResponse,
+ mockUpstreamDownstreamQueryResponse,
+} from './mock_data';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('GraphqlPipelineMiniGraph', () => {
+ let wrapper;
+ let linkedPipelinesResponse;
+ let pipelineStagesResponse;
+
+ const fullPath = 'gitlab-org/gitlab';
+ const iid = '315';
+ const pipelineEtag = '/api/graphql:pipelines/id/315';
+
+ const createComponent = ({
+ pipelineStagesHandler = pipelineStagesResponse,
+ linkedPipelinesHandler = linkedPipelinesResponse,
+ } = {}) => {
+ const handlers = [
+ [getLinkedPipelinesQuery, linkedPipelinesHandler],
+ [getPipelineStagesQuery, pipelineStagesHandler],
+ ];
+ const mockApollo = createMockApollo(handlers);
+
+ wrapper = shallowMountExtended(GraphqlPipelineMiniGraph, {
+ propsData: {
+ fullPath,
+ iid,
+ pipelineEtag,
+ },
+ apolloProvider: mockApollo,
+ });
+
+ return waitForPromises();
+ };
+
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ beforeEach(() => {
+ linkedPipelinesResponse = jest.fn().mockResolvedValue(mockUpstreamDownstreamQueryResponse);
+ pipelineStagesResponse = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse);
+ });
+
+ describe('when initial queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a loading icon and no mini graph', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPipelineMiniGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ it('does not show a loading icon', async () => {
+ await createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders the Pipeline Mini Graph', async () => {
+ await createComponent();
+
+ expect(findPipelineMiniGraph().exists()).toBe(true);
+ });
+
+ it('fires the queries', async () => {
+ await createComponent();
+
+ expect(linkedPipelinesResponse).toHaveBeenCalledWith({ iid, fullPath });
+ expect(pipelineStagesResponse).toHaveBeenCalledWith({ iid, fullPath });
+ });
+ });
+
+ describe('polling', () => {
+ it('toggles query polling with visibility check', async () => {
+ jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility');
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('when pipeline queries are unsuccessful', () => {
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ it.each`
+ query | handlerName | errorMessage
+ ${'pipeline stages'} | ${'pipelineStagesHandler'} | ${stagesFetchError}
+ ${'linked pipelines'} | ${'linkedPipelinesHandler'} | ${linkedPipelinesFetchError}
+ `('throws an error for the $query query', async ({ errorMessage, handlerName }) => {
+ await createComponent({ [handlerName]: failedHandler });
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js b/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js
new file mode 100644
index 00000000000..1c13e9eb62b
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js
@@ -0,0 +1,150 @@
+export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: includeSourceJobRetried ? false : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-611-611',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+});
+
+const upstream = {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-610-610',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+};
+
+export const mockPipelineStagesQueryResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/320',
+ stages: {
+ nodes: [
+ {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/409',
+ name: 'build',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-409-409',
+ icon: 'status_success',
+ group: 'success',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockPipelineStatusResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/320',
+ detailedStatus: {
+ id: 'pending-320-320',
+ detailsPath: '/root/ci-project/-/pipelines/320',
+ icon: 'status_pending',
+ group: 'pending',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockUpstreamDownstreamQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ id: 'pipeline-1',
+ path: '/root/ci-project/-/pipelines/790',
+ downstream: mockDownstreamPipelinesGraphql(),
+ upstream,
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const linkedPipelinesFetchError = 'There was a problem fetching linked pipelines.';
+export const stagesFetchError = 'There was a problem fetching the pipeline stages.';
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
new file mode 100644
index 00000000000..a079188190a
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -0,0 +1,70 @@
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CommandPaletteItems from '~/super_sidebar/components/global_search/command_palette/command_palette_items.vue';
+import {
+ COMMAND_HANDLE,
+ COMMANDS_GROUP_TITLE,
+} from '~/super_sidebar/components/global_search/command_palette/constants';
+import { COMMAND_PALETTE_COMMANDS } from './mock_data';
+
+const commands = COMMAND_PALETTE_COMMANDS.map(({ text, href, keywords }) => ({
+ text,
+ href,
+ keywords: keywords.join(''),
+}));
+
+describe('CommandPaletteItems', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(CommandPaletteItems, {
+ propsData: {
+ handle: COMMAND_HANDLE,
+ searchQuery: '',
+ ...props,
+ },
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
+ provide: {
+ commandPaletteData: COMMAND_PALETTE_COMMANDS,
+ },
+ });
+ };
+
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
+
+ it('renders all commands initially', () => {
+ createComponent();
+ expect(findItems()).toHaveLength(COMMAND_PALETTE_COMMANDS.length);
+ expect(findGroup().props('group')).toEqual({
+ name: COMMANDS_GROUP_TITLE,
+ items: commands,
+ });
+ });
+
+ describe('with search query', () => {
+ it('should filter by the search query', async () => {
+ jest.spyOn(fuzzaldrinPlus, 'filter');
+ createComponent({ searchQuery: 'mr' });
+ const searchQuery = 'todo';
+ await wrapper.setProps({ searchQuery });
+ expect(fuzzaldrinPlus.filter).toHaveBeenCalledWith(
+ commands,
+ searchQuery,
+ expect.objectContaining({ key: 'keywords' }),
+ );
+ });
+
+ it('should display no results message when no command matched the search qery', async () => {
+ jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue([]);
+ createComponent({ searchQuery: 'mr' });
+ const searchQuery = 'todo';
+ await wrapper.setProps({ searchQuery });
+ expect(wrapper.text()).toBe('No results found');
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js
new file mode 100644
index 00000000000..0aeb4c89d06
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js
@@ -0,0 +1,40 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import FakeSearchInput from '~/super_sidebar/components/global_search/command_palette/fake_search_input.vue';
+import {
+ COMMAND_HANDLE,
+ SEARCH_SCOPE,
+} from '~/super_sidebar/components/global_search/command_palette/constants';
+
+describe('FakeSearchInput', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(FakeSearchInput, {
+ propsData: {
+ scope: COMMAND_HANDLE,
+ userInput: '',
+ ...props,
+ },
+ });
+ };
+
+ const findSearchScope = () => wrapper.findByTestId('search-scope');
+ const findSearchScopePlaceholder = () => wrapper.findByTestId('search-scope-placeholder');
+
+ it('should render the search scope', () => {
+ createComponent();
+ expect(findSearchScope().text()).toBe(COMMAND_HANDLE);
+ });
+
+ describe('placeholder', () => {
+ it('should render the placeholder for its search scope when there is no user input', () => {
+ createComponent();
+ expect(findSearchScopePlaceholder().text()).toBe(SEARCH_SCOPE[COMMAND_HANDLE]);
+ });
+
+ it('should NOT render the placeholder when there is user input', () => {
+ createComponent({ userInput: 'todo' });
+ expect(findSearchScopePlaceholder().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
new file mode 100644
index 00000000000..7469154e363
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -0,0 +1,17 @@
+export const COMMAND_PALETTE_COMMANDS = [
+ {
+ text: 'New project/repository',
+ href: '/projects/new',
+ keywords: ['new', 'project', 'repository'],
+ },
+ {
+ text: 'New group',
+ href: '/groups/new',
+ keywords: ['new', 'group'],
+ },
+ {
+ text: 'New snippet',
+ href: '/-/snippets/new',
+ keywords: ['new', 'snippet'],
+ },
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index f78e141afad..fd79e274ff6 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -7,6 +7,12 @@ import GlobalSearchModal from '~/super_sidebar/components/global_search/componen
import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import FakeSearchInput from '~/super_sidebar/components/global_search/command_palette/fake_search_input.vue';
+import CommandPaletteItems from '~/super_sidebar/components/global_search/command_palette/command_palette_items.vue';
+import {
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+ COMMAND_HANDLE,
+} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
@@ -17,6 +23,7 @@ import {
IS_SEARCHING,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/super_sidebar/components/global_search/constants';
+import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -53,7 +60,18 @@ describe('GlobalSearchModal', () => {
},
};
- const createComponent = (initialState, mockGetters, stubs) => {
+ const defaultMockGetters = {
+ searchQuery: () => MOCK_SEARCH_QUERY,
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ };
+
+ const createComponent = (
+ initialState = deafaultMockState,
+ mockGetters = defaultMockGetters,
+ stubs,
+ glFeatures = { commandPalette: false },
+ ) => {
const store = new Vuex.Store({
state: {
...deafaultMockState,
@@ -71,6 +89,7 @@ describe('GlobalSearchModal', () => {
wrapper = shallowMountExtended(GlobalSearchModal, {
store,
stubs,
+ provide: { glFeatures },
});
};
@@ -98,6 +117,8 @@ describe('GlobalSearchModal', () => {
wrapper.findComponent(GlobalSearchAutocompleteItems);
const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
+ const findCommandPaletteItems = () => wrapper.findComponent(CommandPaletteItems);
+ const findFakeSearchInput = () => wrapper.findComponent(FakeSearchInput);
describe('template', () => {
describe('always renders', () => {
@@ -281,6 +302,42 @@ describe('GlobalSearchModal', () => {
).toBe(iconName);
});
});
+
+ describe('Command palette', () => {
+ describe('when FF `command_palette` is disabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should not render command mode components', () => {
+ expect(findCommandPaletteItems().exists()).toBe(false);
+ expect(findFakeSearchInput().exists()).toBe(false);
+ });
+
+ it('should provide default placeholder to the search input', () => {
+ expect(findGlobalSearchInput().attributes('placeholder')).toBe(SEARCH_GITLAB);
+ });
+ });
+
+ describe('when FF `command_palette` is enabled', () => {
+ beforeEach(() => {
+ createComponent({ search: COMMAND_HANDLE }, undefined, undefined, {
+ commandPalette: true,
+ });
+ });
+
+ it('should render command mode components', () => {
+ expect(findCommandPaletteItems().exists()).toBe(true);
+ expect(findFakeSearchInput().exists()).toBe(true);
+ });
+
+ it('should provide an alternative placeholder to the search input', () => {
+ expect(findGlobalSearchInput().attributes('placeholder')).toBe(
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+ );
+ });
+ });
+ });
});
describe('events', () => {
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 6648663b634..f4df7a69b8f 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -319,6 +319,17 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
)
end
+ it 'returns command palette items', :use_clean_rails_memory_store_caching do
+ expect(subject[:command_palette_commands]).to match_array([
+ { href: "/projects/new",
+ text: "New project/repository", keywords: [_('Create a new project/repository')] },
+ { href: "/groups/new", text: "New group",
+ keywords: ['Create a new group'] },
+ { href: "/-/snippets/new", text: "New snippet",
+ keywords: ['Create a new snippet'] }
+ ])
+ end
+
describe 'current context' do
context 'when current context is a project' do
let_it_be(:project) { build(:project) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 9e5b5ccd64a..abdd8741377 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -704,6 +704,7 @@ ProjectFeature:
- releases_access_level
- monitor_access_level
- infrastructure_access_level
+- model_experiments_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 8dd1f7b1831..48c9567ebb3 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe ProjectFeature, feature_category: :groups_and_projects do
specify { expect(subject.releases_access_level).to eq(ProjectFeature::ENABLED) }
specify { expect(subject.package_registry_access_level).to eq(ProjectFeature::ENABLED) }
specify { expect(subject.container_registry_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.model_experiments_access_level).to eq(ProjectFeature::ENABLED) }
end
describe 'PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 12a34bcee78..1074a328103 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1051,6 +1051,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:environments_access_level).to(:project_feature) }
+ it { is_expected.to delegate_method(:model_experiments_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:feature_flags_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:releases_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:infrastructure_access_level).to(:project_feature) }
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index bf233ed5929..e0e9c944fe4 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -127,6 +127,7 @@ project_feature:
- project_id
- updated_at
- operations_access_level
+ - model_experiments_access_level
computed_attributes:
- issues_enabled
- jobs_enabled