Commit 8d03fe1b authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'pipeline-editor-branch-switcher-query' into 'master'

Fetch and search available branches in pipeline editor

See merge request gitlab-org/gitlab!59217
parents ab76da78 6fc333ec
<script> <script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlIcon } from '@gitlab/ui'; import {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlInfiniteScroll,
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
import { historyPushState } from '~/lib/utils/common_utils'; import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility'; import { setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants'; import {
BRANCH_PAGINATION_LIMIT,
BRANCH_SEARCH_DEBOUNCE,
DEFAULT_FAILURE,
} from '~/pipeline_editor/constants';
import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql'; import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql';
import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.graphql'; import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.graphql';
export default { export default {
i18n: { i18n: {
dropdownHeader: s__('Switch Branch'),
title: s__('Branches'), title: s__('Branches'),
fetchError: s__('Unable to fetch branch list for this project.'), fetchError: s__('Unable to fetch branch list for this project.'),
}, },
inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlIcon, GlInfiniteScroll,
GlLoadingIcon,
GlSearchBoxByType,
},
inject: ['projectFullPath', 'totalBranches'],
props: {
paginationLimit: {
type: Number,
required: false,
default: BRANCH_PAGINATION_LIMIT,
},
},
data() {
return {
branches: [],
page: {
limit: this.paginationLimit,
offset: 0,
searchTerm: '',
},
};
}, },
inject: ['projectFullPath'],
apollo: { apollo: {
branches: { availableBranches: {
query: getAvailableBranches, query: getAvailableBranches,
variables() { variables() {
return { return {
limit: this.page.limit,
offset: this.page.offset,
projectFullPath: this.projectFullPath, projectFullPath: this.projectFullPath,
searchPattern: this.searchPattern,
}; };
}, },
update(data) { update(data) {
return data.project?.repository?.branches || []; return data.project?.repository?.branchNames || [];
},
result({ data }) {
const newBranches = data.project?.repository?.branchNames || [];
// check that we're not re-concatenating existing fetch results
if (!this.branches.includes(newBranches[0])) {
this.branches = this.branches.concat(newBranches);
}
}, },
error() { error() {
this.$emit('showError', { this.$emit('showError', {
...@@ -42,11 +85,37 @@ export default { ...@@ -42,11 +85,37 @@ export default {
}, },
}, },
computed: { computed: {
hasBranchList() { isBranchesLoading() {
return this.branches?.length > 0; return this.$apollo.queries.availableBranches.loading;
},
showBranchSwitcher() {
return this.branches.length > 0 || this.page.searchTerm.length > 0;
},
searchPattern() {
if (this.page.searchTerm === '') {
return '*';
}
return `*${this.page.searchTerm}*`;
}, },
}, },
methods: { methods: {
// if there is no searchPattern, paginate by {paginationLimit} branches
fetchNextBranches() {
if (
this.isBranchesLoading ||
this.page.searchTerm.length > 0 ||
this.branches.length === this.totalBranches
) {
return;
}
this.page = {
...this.page,
limit: this.paginationLimit,
offset: this.page.offset + this.paginationLimit,
};
},
async selectBranch(newBranch) { async selectBranch(newBranch) {
if (newBranch === this.currentBranch) { if (newBranch === this.currentBranch) {
return; return;
...@@ -62,24 +131,53 @@ export default { ...@@ -62,24 +131,53 @@ export default {
this.$emit('refetchContent'); this.$emit('refetchContent');
}, },
setSearchTerm(newSearchTerm) {
this.branches = [];
this.page = {
limit: newSearchTerm.trim() === '' ? this.paginationLimit : this.totalBranches,
offset: 0,
searchTerm: newSearchTerm.trim(),
};
},
}, },
}; };
</script> </script>
<template> <template>
<gl-dropdown v-if="hasBranchList" class="gl-ml-2" :text="currentBranch" icon="branch"> <gl-dropdown
v-if="showBranchSwitcher"
class="gl-ml-2"
:header-text="$options.i18n.dropdownHeader"
:text="currentBranch"
icon="branch"
>
<gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
<gl-dropdown-section-header> <gl-dropdown-section-header>
{{ this.$options.i18n.title }} {{ $options.i18n.title }}
</gl-dropdown-section-header> </gl-dropdown-section-header>
<gl-infinite-scroll
:fetched-items="branches.length"
:total-items="totalBranches"
:max-list-height="250"
@bottomReached="fetchNextBranches"
>
<template #items>
<gl-dropdown-item <gl-dropdown-item
v-for="branch in branches" v-for="branch in branches"
:key="branch.name" :key="branch"
:is-checked="currentBranch === branch.name" :is-checked="currentBranch === branch"
:is-check-item="true" :is-check-item="true"
@click="selectBranch(branch.name)" @click="selectBranch(branch)"
> >
<gl-icon name="check" class="gl-visibility-hidden" /> {{ branch }}
{{ branch.name }} </gl-dropdown-item>
</template>
<template #default>
<gl-dropdown-item v-if="isBranchesLoading" key="loading">
<gl-loading-icon size="md" />
</gl-dropdown-item> </gl-dropdown-item>
</template>
</gl-infinite-scroll>
</gl-dropdown> </gl-dropdown>
</template> </template>
...@@ -28,3 +28,6 @@ export const COMMIT_ACTION_CREATE = 'CREATE'; ...@@ -28,3 +28,6 @@ export const COMMIT_ACTION_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE'; export const COMMIT_ACTION_UPDATE = 'UPDATE';
export const DRAWER_EXPANDED_KEY = 'pipeline_editor_drawer_expanded'; export const DRAWER_EXPANDED_KEY = 'pipeline_editor_drawer_expanded';
export const BRANCH_PAGINATION_LIMIT = 20;
export const BRANCH_SEARCH_DEBOUNCE = '500';
query getAvailableBranches($projectFullPath: ID!) { query getAvailableBranches(
project(fullPath: $projectFullPath) @client { $limit: Int!
$offset: Int!
$projectFullPath: ID!
$searchPattern: String!
) {
project(fullPath: $projectFullPath) {
repository { repository {
branches { branchNames(limit: $limit, offset: $offset, searchPattern: $searchPattern)
name
}
} }
} }
} }
...@@ -11,22 +11,6 @@ export const resolvers = { ...@@ -11,22 +11,6 @@ export const resolvers = {
}), }),
}; };
}, },
/* eslint-disable @gitlab/require-i18n-strings */
project() {
return {
__typename: 'Project',
repository: {
__typename: 'Repository',
branches: [
{ __typename: 'Branch', name: 'main' },
{ __typename: 'Branch', name: 'develop' },
{ __typename: 'Branch', name: 'production' },
{ __typename: 'Branch', name: 'test' },
],
},
};
},
/* eslint-enable @gitlab/require-i18n-strings */
}, },
Mutation: { Mutation: {
lintCI: (_, { endpoint, content, dry_run }) => { lintCI: (_, { endpoint, content, dry_run }) => {
......
...@@ -43,6 +43,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -43,6 +43,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectPath, projectPath,
projectNamespace, projectNamespace,
runnerHelpPagePath, runnerHelpPagePath,
totalBranches,
ymlHelpPagePath, ymlHelpPagePath,
} = el?.dataset; } = el?.dataset;
...@@ -100,6 +101,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -100,6 +101,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectPath, projectPath,
projectNamespace, projectNamespace,
runnerHelpPagePath, runnerHelpPagePath,
totalBranches: parseInt(totalBranches, 10),
ymlHelpPagePath, ymlHelpPagePath,
}, },
render(h) { render(h) {
......
...@@ -27,6 +27,7 @@ module Ci ...@@ -27,6 +27,7 @@ module Ci
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/README'), "runner-help-page-path" => help_page_path('ci/runners/README'),
"total-branches" => project.repository.branches.length,
"yml-help-page-path" => help_page_path('ci/yaml/README') "yml-help-page-path" => help_page_path('ci/yaml/README')
} }
end end
......
...@@ -31474,6 +31474,9 @@ msgstr "" ...@@ -31474,6 +31474,9 @@ msgstr ""
msgid "Survey Response" msgid "Survey Response"
msgstr "" msgstr ""
msgid "Switch Branch"
msgstr ""
msgid "Switch branch/tag" msgid "Switch branch/tag"
msgstr "" msgstr ""
......
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; import {
import { shallowMount, createLocalVue } from '@vue/test-utils'; GlDropdown,
GlDropdownItem,
GlInfiniteScroll,
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue'; import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants'; import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
import { mockDefaultBranch, mockProjectBranches, mockProjectFullPath } from '../../mock_data'; import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql';
import {
mockBranchPaginationLimit,
mockDefaultBranch,
mockEmptySearchBranches,
mockProjectBranches,
mockProjectFullPath,
mockSearchBranches,
mockTotalBranches,
mockTotalBranchResults,
mockTotalSearchResults,
} from '../../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -15,30 +32,64 @@ describe('Pipeline editor branch switcher', () => { ...@@ -15,30 +32,64 @@ describe('Pipeline editor branch switcher', () => {
let mockApollo; let mockApollo;
let mockAvailableBranchQuery; let mockAvailableBranchQuery;
const createComponentWithApollo = () => { const createComponent = (
const resolvers = { { isQueryLoading, mountFn, options } = {
Query: { isQueryLoading: false,
project: mockAvailableBranchQuery, mountFn: shallowMount,
options: {},
}, },
) => {
wrapper = mountFn(BranchSwitcher, {
propsData: {
paginationLimit: mockBranchPaginationLimit,
},
provide: {
projectFullPath: mockProjectFullPath,
totalBranches: mockTotalBranches,
},
mocks: {
$apollo: {
queries: {
availableBranches: {
loading: isQueryLoading,
},
},
},
},
data() {
return {
branches: ['main'],
currentBranch: mockDefaultBranch,
};
},
...options,
});
}; };
mockApollo = createMockApollo([], resolvers); const createComponentWithApollo = (mountFn = shallowMount) => {
wrapper = shallowMount(BranchSwitcher, { const handlers = [[getAvailableBranches, mockAvailableBranchQuery]];
mockApollo = createMockApollo(handlers);
createComponent({
mountFn,
options: {
localVue, localVue,
apolloProvider: mockApollo, apolloProvider: mockApollo,
provide: { mocks: {},
projectFullPath: mockProjectFullPath,
},
data() { data() {
return { return {
currentBranch: mockDefaultBranch, currentBranch: mockDefaultBranch,
}; };
}, },
},
}); });
}; };
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem); const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll);
beforeEach(() => { beforeEach(() => {
mockAvailableBranchQuery = jest.fn(); mockAvailableBranchQuery = jest.fn();
...@@ -48,7 +99,7 @@ describe('Pipeline editor branch switcher', () => { ...@@ -48,7 +99,7 @@ describe('Pipeline editor branch switcher', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('while querying', () => { describe('when querying for the first time', () => {
beforeEach(() => { beforeEach(() => {
createComponentWithApollo(); createComponentWithApollo();
}); });
...@@ -61,41 +112,31 @@ describe('Pipeline editor branch switcher', () => { ...@@ -61,41 +112,31 @@ describe('Pipeline editor branch switcher', () => {
describe('after querying', () => { describe('after querying', () => {
beforeEach(async () => { beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
createComponentWithApollo(); createComponentWithApollo(mount);
await waitForPromises(); await waitForPromises();
}); });
it('query is called with correct variables', async () => { it('renders search box', () => {
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1); expect(findSearchBox().exists()).toBe(true);
expect(mockAvailableBranchQuery).toHaveBeenCalledWith(
expect.anything(),
{
fullPath: mockProjectFullPath,
},
expect.anything(),
expect.anything(),
);
}); });
it('renders list of branches', () => { it('renders list of branches', () => {
expect(findDropdown().exists()).toBe(true); expect(findDropdown().exists()).toBe(true);
expect(findDropdownItems()).toHaveLength(mockProjectBranches.repository.branches.length); expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
}); });
it('renders current branch at the top of the list with a check mark', () => { it('renders current branch with a check mark', () => {
const firstDropdownItem = findDropdownItems().at(0); const defaultBranchInDropdown = findDropdownItems().at(0);
const icon = firstDropdownItem.findComponent(GlIcon);
expect(firstDropdownItem.text()).toBe(mockDefaultBranch); expect(defaultBranchInDropdown.text()).toBe(mockDefaultBranch);
expect(icon.exists()).toBe(true); expect(defaultBranchInDropdown.props('isChecked')).toBe(true);
expect(icon.props('name')).toBe('check');
}); });
it('does not render check mark for other branches', () => { it('does not render check mark for other branches', () => {
const secondDropdownItem = findDropdownItems().at(1); const nonDefaultBranch = findDropdownItems().at(1);
const icon = secondDropdownItem.findComponent(GlIcon);
expect(icon.classes()).toContain('gl-visibility-hidden'); expect(nonDefaultBranch.text()).not.toBe(mockDefaultBranch);
expect(nonDefaultBranch.props('isChecked')).toBe(false);
}); });
}); });
...@@ -125,7 +166,7 @@ describe('Pipeline editor branch switcher', () => { ...@@ -125,7 +166,7 @@ describe('Pipeline editor branch switcher', () => {
beforeEach(async () => { beforeEach(async () => {
jest.spyOn(window.history, 'pushState').mockImplementation(() => {}); jest.spyOn(window.history, 'pushState').mockImplementation(() => {});
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
createComponentWithApollo(); createComponentWithApollo(mount);
await waitForPromises(); await waitForPromises();
}); });
...@@ -168,4 +209,138 @@ describe('Pipeline editor branch switcher', () => { ...@@ -168,4 +209,138 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined(); expect(wrapper.emitted('refetchContent')).toBeUndefined();
}); });
}); });
describe('when searching', () => {
beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
createComponentWithApollo(mount);
await waitForPromises();
mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
});
describe('with a search term', () => {
it('calls query with correct variables', async () => {
findSearchBox().vm.$emit('input', 'te');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
limit: mockTotalBranches, // fetch all branches
offset: 0,
projectFullPath: mockProjectFullPath,
searchPattern: '*te*',
});
});
it('fetches new list of branches', async () => {
expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
findSearchBox().vm.$emit('input', 'te');
await waitForPromises();
expect(findDropdownItems()).toHaveLength(mockTotalSearchResults);
});
it('does not hide dropdown when search result is empty', async () => {
mockAvailableBranchQuery.mockResolvedValue(mockEmptySearchBranches);
findSearchBox().vm.$emit('input', 'aaaaa');
await waitForPromises();
expect(findDropdown().exists()).toBe(true);
expect(findDropdownItems()).toHaveLength(0);
});
});
describe('without a search term', () => {
beforeEach(async () => {
findSearchBox().vm.$emit('input', 'te');
await waitForPromises();
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
});
it('calls query with correct variables', async () => {
findSearchBox().vm.$emit('input', '');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
limit: mockBranchPaginationLimit, // only fetch first n branches first
offset: 0,
projectFullPath: mockProjectFullPath,
searchPattern: '*',
});
});
it('fetches new list of branches', async () => {
expect(findDropdownItems()).toHaveLength(mockTotalSearchResults);
findSearchBox().vm.$emit('input', '');
await waitForPromises();
expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
});
});
});
describe('loading icon', () => {
test.each`
isQueryLoading | isRendered
${true} | ${true}
${false} | ${false}
`('checks if query is loading before rendering', ({ isQueryLoading, isRendered }) => {
createComponent({ isQueryLoading, mountFn: mount });
expect(findLoadingIcon().exists()).toBe(isRendered);
});
});
describe('when scrolling to the bottom of the list', () => {
beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
createComponentWithApollo();
await waitForPromises();
});
afterEach(() => {
mockAvailableBranchQuery.mockClear();
});
describe('when search term is empty', () => {
it('fetches more branches', async () => {
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1);
findInfiniteScroll().vm.$emit('bottomReached');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2);
});
it('calls the query with the correct variables', async () => {
findInfiniteScroll().vm.$emit('bottomReached');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
limit: mockBranchPaginationLimit,
offset: mockBranchPaginationLimit, // offset changed
projectFullPath: mockProjectFullPath,
searchPattern: '*',
});
});
});
describe('when search term exists', () => {
it('does not fetch more branches', async () => {
findSearchBox().vm.$emit('input', 'te');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2);
mockAvailableBranchQuery.mockClear();
findInfiniteScroll().vm.$emit('bottomReached');
await waitForPromises();
expect(mockAvailableBranchQuery).not.toHaveBeenCalled();
});
});
});
}); });
...@@ -9,7 +9,6 @@ import { ...@@ -9,7 +9,6 @@ import {
mockDefaultBranch, mockDefaultBranch,
mockLintResponse, mockLintResponse,
mockProjectFullPath, mockProjectFullPath,
mockProjectBranches,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/api', () => { jest.mock('~/api', () => {
...@@ -47,23 +46,6 @@ describe('~/pipeline_editor/graphql/resolvers', () => { ...@@ -47,23 +46,6 @@ describe('~/pipeline_editor/graphql/resolvers', () => {
await expect(result.rawData).resolves.toBe(mockCiYml); await expect(result.rawData).resolves.toBe(mockCiYml);
}); });
}); });
describe('project', () => {
it('resolves project data with type names', async () => {
const result = await resolvers.Query.project();
// eslint-disable-next-line no-underscore-dangle
expect(result.__typename).toBe('Project');
});
it('resolves project with available list of branches', async () => {
const result = await resolvers.Query.project();
expect(result.repository.branches).toHaveLength(
mockProjectBranches.repository.branches.length,
);
});
});
}); });
describe('Mutation', () => { describe('Mutation', () => {
......
...@@ -139,18 +139,54 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => { ...@@ -139,18 +139,54 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
}; };
export const mockProjectBranches = { export const mockProjectBranches = {
__typename: 'Project', data: {
project: {
repository: { repository: {
__typename: 'Repository', branchNames: [
branches: [ 'main',
{ __typename: 'Branch', name: 'main' }, 'develop',
{ __typename: 'Branch', name: 'develop' }, 'production',
{ __typename: 'Branch', name: 'production' }, 'test',
{ __typename: 'Branch', name: 'test' }, 'better-feature',
'feature-abc',
'update-ci',
'mock-feature',
'test-merge-request',
'staging',
], ],
}, },
},
},
};
export const mockTotalBranchResults =
mockProjectBranches.data.project.repository.branchNames.length;
export const mockSearchBranches = {
data: {
project: {
repository: {
branchNames: ['test', 'better-feature', 'update-ci', 'test-merge-request'],
},
},
},
};
export const mockTotalSearchResults = mockSearchBranches.data.project.repository.branchNames.length;
export const mockEmptySearchBranches = {
data: {
project: {
repository: {
branchNames: [],
},
},
},
}; };
export const mockBranchPaginationLimit = 10;
export const mockTotalBranches = 20; // must be greater than mockBranchPaginationLimit to test pagination
export const mockProjectPipeline = { export const mockProjectPipeline = {
pipeline: { pipeline: {
commitPath: '/-/commit/aabbccdd', commitPath: '/-/commit/aabbccdd',
......
...@@ -55,6 +55,7 @@ RSpec.describe Ci::PipelineEditorHelper do ...@@ -55,6 +55,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/README'), "runner-help-page-path" => help_page_path('ci/runners/README'),
"total-branches" => project.repository.branches.length,
"yml-help-page-path" => help_page_path('ci/yaml/README') "yml-help-page-path" => help_page_path('ci/yaml/README')
}) })
end end
...@@ -81,6 +82,7 @@ RSpec.describe Ci::PipelineEditorHelper do ...@@ -81,6 +82,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/README'), "runner-help-page-path" => help_page_path('ci/runners/README'),
"total-branches" => 0,
"yml-help-page-path" => help_page_path('ci/yaml/README') "yml-help-page-path" => help_page_path('ci/yaml/README')
}) })
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment