diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-01 00:09:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-01 00:09:09 +0300 |
commit | 404895390afe87ce8ab939448bf7dff7dc4b7169 (patch) | |
tree | 93c323d7df6b70c84dce7b3e4e4f3d57180394a0 /spec | |
parent | e9885f7a36065b9b45a35feb6c427c7742a906a4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |