diff options
Diffstat (limited to 'spec/frontend/pipeline_editor')
8 files changed, 256 insertions, 101 deletions
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js index fb191fccb0d..7dd8a77d055 100644 --- a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js +++ b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js @@ -8,7 +8,7 @@ import { mockLintResponse, mockCiConfigPath } from '../../mock_data'; describe('Text editor component', () => { let wrapper; - const MockEditorLite = { + const MockSourceEditor = { template: '<div/>', props: ['value', 'fileName', 'editorOptions'], mounted() { @@ -26,13 +26,13 @@ describe('Text editor component', () => { ciConfigPath: mockCiConfigPath, }, stubs: { - EditorLite: MockEditorLite, + SourceEditor: MockSourceEditor, }, }); }; const findIcon = () => wrapper.findComponent(GlIcon); - const findEditor = () => wrapper.findComponent(MockEditorLite); + const findEditor = () => wrapper.findComponent(MockSourceEditor); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js new file mode 100644 index 00000000000..3ee53d4a055 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js @@ -0,0 +1,53 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue'; +import { + pipelineEditorTrackingOptions, + TEMPLATE_REPOSITORY_URL, +} from '~/pipeline_editor/constants'; + +describe('CI Editor Header', () => { + let wrapper; + let trackingSpy = null; + + const createComponent = () => { + wrapper = shallowMount(CiEditorHeader, {}); + }; + + const findLinkBtn = () => wrapper.findComponent(GlButton); + + afterEach(() => { + wrapper.destroy(); + unmockTracking(); + }); + + describe('link button', () => { + beforeEach(() => { + createComponent(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + it('finds the browse template button', () => { + expect(findLinkBtn().exists()).toBe(true); + }); + + it('contains the link to the template repo', () => { + expect(findLinkBtn().attributes('href')).toBe(TEMPLATE_REPOSITORY_URL); + }); + + it('has the external-link icon', () => { + expect(findLinkBtn().props('icon')).toBe('external-link'); + }); + + it('tracks the click on the browse button', async () => { + const { label, actions } = pipelineEditorTrackingOptions; + + await findLinkBtn().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.browse_templates, { + label, + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js index 6f9245e39aa..c6c7f593cc5 100644 --- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js +++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { EDITOR_READY_EVENT } from '~/editor/constants'; -import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base'; +import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import { mockCiConfigPath, @@ -19,7 +19,7 @@ describe('Pipeline Editor | Text editor component', () => { let mockUse; let mockRegisterCiSchema; - const MockEditorLite = { + const MockSourceEditor = { template: '<div/>', props: ['value', 'fileName'], mounted() { @@ -55,15 +55,15 @@ describe('Pipeline Editor | Text editor component', () => { [EDITOR_READY_EVENT]: editorReadyListener, }, stubs: { - EditorLite: MockEditorLite, + SourceEditor: MockSourceEditor, }, }); }; - const findEditor = () => wrapper.findComponent(MockEditorLite); + const findEditor = () => wrapper.findComponent(MockSourceEditor); beforeEach(() => { - EditorLiteExtension.deferRerender = jest.fn(); + SourceEditorExtension.deferRerender = jest.fn(); }); afterEach(() => { diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js index e731ad8695e..85b51d08f88 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -207,7 +207,8 @@ describe('Pipeline editor branch switcher', () => { it('updates session history when selecting a different branch', async () => { const branch = findDropdownItems().at(1); - await branch.vm.$emit('click'); + branch.vm.$emit('click'); + await waitForPromises(); expect(window.history.pushState).toHaveBeenCalled(); expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`); @@ -215,7 +216,8 @@ describe('Pipeline editor branch switcher', () => { it('does not update session history when selecting current branch', async () => { const branch = findDropdownItems().at(0); - await branch.vm.$emit('click'); + branch.vm.$emit('click'); + await waitForPromises(); expect(branch.text()).toBe(mockDefaultBranch); expect(window.history.pushState).not.toHaveBeenCalled(); @@ -227,7 +229,8 @@ describe('Pipeline editor branch switcher', () => { expect(branch.text()).not.toBe(mockDefaultBranch); expect(wrapper.emitted('refetchContent')).toBeUndefined(); - await branch.vm.$emit('click'); + branch.vm.$emit('click'); + await waitForPromises(); expect(wrapper.emitted('refetchContent')).toBeDefined(); expect(wrapper.emitted('refetchContent')).toHaveLength(1); @@ -239,10 +242,20 @@ describe('Pipeline editor branch switcher', () => { expect(branch.text()).toBe(mockDefaultBranch); expect(wrapper.emitted('refetchContent')).toBeUndefined(); - await branch.vm.$emit('click'); + branch.vm.$emit('click'); + await waitForPromises(); expect(wrapper.emitted('refetchContent')).toBeUndefined(); }); + + it('emits the updateCommitSha event when selecting a different branch', async () => { + expect(wrapper.emitted('updateCommitSha')).toBeUndefined(); + + const branch = findDropdownItems().at(1); + branch.vm.$emit('click'); + + expect(wrapper.emitted('updateCommitSha')).toHaveLength(1); + }); }); describe('when searching', () => { diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js index 8def83d578b..3becf82ed6e 100644 --- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js +++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js @@ -6,7 +6,7 @@ import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; const mockContent1 = 'MOCK CONTENT 1'; const mockContent2 = 'MOCK CONTENT 2'; -const MockEditorLite = { +const MockSourceEditor = { template: '<div>EDITOR</div>', }; @@ -48,12 +48,12 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { wrapper = mount(EditorTab, { propsData: props, slots: { - default: MockEditorLite, + default: MockSourceEditor, }, }); }; - const findSlotComponent = () => wrapper.findComponent(MockEditorLite); + const findSlotComponent = () => wrapper.findComponent(MockSourceEditor); const findAlert = () => wrapper.findComponent(GlAlert); beforeEach(() => { diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js index d39c0d80296..76ae96c623a 100644 --- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js +++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js @@ -1,15 +1,8 @@ import MockAdapter from 'axios-mock-adapter'; -import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; import { resolvers } from '~/pipeline_editor/graphql/resolvers'; -import { - mockCiConfigPath, - mockCiYml, - mockDefaultBranch, - mockLintResponse, - mockProjectFullPath, -} from '../mock_data'; +import { mockLintResponse } from '../mock_data'; jest.mock('~/api', () => { return { @@ -18,36 +11,6 @@ jest.mock('~/api', () => { }); describe('~/pipeline_editor/graphql/resolvers', () => { - describe('Query', () => { - describe('blobContent', () => { - beforeEach(() => { - Api.getRawFile.mockResolvedValue({ - data: mockCiYml, - }); - }); - - afterEach(() => { - Api.getRawFile.mockReset(); - }); - - it('resolves lint data with type names', async () => { - const result = resolvers.Query.blobContent(null, { - projectPath: mockProjectFullPath, - path: mockCiConfigPath, - ref: mockDefaultBranch, - }); - - expect(Api.getRawFile).toHaveBeenCalledWith(mockProjectFullPath, mockCiConfigPath, { - ref: mockDefaultBranch, - }); - - // eslint-disable-next-line no-underscore-dangle - expect(result.__typename).toBe('BlobContent'); - await expect(result.rawData).resolves.toBe(mockCiYml); - }); - }); - }); - describe('Mutation', () => { describe('lintCI', () => { let mock; diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index cadcdf6ae2e..4d4a8c21d78 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -35,6 +35,23 @@ job_build: - echo "build" needs: ["job_test_2"] `; +export const mockBlobContentQueryResponse = { + data: { + project: { repository: { blobs: { nodes: [{ rawBlob: mockCiYml }] } } }, + }, +}; + +export const mockBlobContentQueryResponseNoCiFile = { + data: { + project: { repository: { blobs: { nodes: [] } } }, + }, +}; + +export const mockBlobContentQueryResponseEmptyCiFile = { + data: { + project: { repository: { blobs: { nodes: [{ rawBlob: '' }] } } }, + }, +}; const mockJobFields = { beforeScript: [], @@ -139,6 +156,35 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => { }; }; +export const mockNewCommitShaResults = { + data: { + project: { + pipelines: { + nodes: [ + { + id: 'gid://gitlab/Ci::Pipeline/1', + sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca', + path: `/${mockProjectFullPath}/-/pipelines/488`, + commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`, + }, + { + id: 'gid://gitlab/Ci::Pipeline/2', + sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa', + path: `/${mockProjectFullPath}/-/pipelines/487`, + commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`, + }, + { + id: 'gid://gitlab/Ci::Pipeline/3', + sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4', + path: `/${mockProjectFullPath}/-/pipelines/433`, + commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`, + }, + ], + }, + }, + }, +}; + export const mockProjectBranches = { data: { project: { diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index c88fe159c0d..b0d1a69ee56 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import httpStatusCodes from '~/lib/utils/http_status'; import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; @@ -11,21 +10,30 @@ import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tab import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue'; import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants'; +import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql'; import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql'; +import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql'; +import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql'; +import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql'; import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; import { mockCiConfigPath, mockCiConfigQueryResponse, + mockBlobContentQueryResponse, + mockBlobContentQueryResponseEmptyCiFile, + mockBlobContentQueryResponseNoCiFile, mockCiYml, + mockCommitSha, mockDefaultBranch, mockProjectFullPath, + mockNewCommitShaResults, } from './mock_data'; const localVue = createLocalVue(); localVue.use(VueApollo); -const MockEditorLite = { +const MockSourceEditor = { template: '<div/>', }; @@ -44,6 +52,10 @@ describe('Pipeline editor app component', () => { let mockApollo; let mockBlobContentData; let mockCiConfigData; + let mockGetTemplate; + let mockUpdateCommitSha; + let mockLatestCommitShaQuery; + let mockPipelineQuery; const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => { wrapper = shallowMount(PipelineEditorApp, { @@ -55,7 +67,7 @@ describe('Pipeline editor app component', () => { PipelineEditorHome, PipelineEditorTabs, PipelineEditorMessages, - EditorLite: MockEditorLite, + SourceEditor: MockSourceEditor, PipelineEditorEmptyState, }, mocks: { @@ -75,16 +87,23 @@ describe('Pipeline editor app component', () => { }; const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => { - const handlers = [[getCiConfigData, mockCiConfigData]]; + const handlers = [ + [getBlobContent, mockBlobContentData], + [getCiConfigData, mockCiConfigData], + [getTemplate, mockGetTemplate], + [getLatestCommitShaQuery, mockLatestCommitShaQuery], + [getPipelineQuery, mockPipelineQuery], + ]; + const resolvers = { Query: { - blobContent() { - return { - __typename: 'BlobContent', - rawData: mockBlobContentData(), - }; + commitSha() { + return mockCommitSha; }, }, + Mutation: { + updateCommitSha: mockUpdateCommitSha, + }, }; mockApollo = createMockApollo(handlers, resolvers); @@ -116,6 +135,10 @@ describe('Pipeline editor app component', () => { beforeEach(() => { mockBlobContentData = jest.fn(); mockCiConfigData = jest.fn(); + mockGetTemplate = jest.fn(); + mockUpdateCommitSha = jest.fn(); + mockLatestCommitShaQuery = jest.fn(); + mockPipelineQuery = jest.fn(); }); afterEach(() => { @@ -133,7 +156,7 @@ describe('Pipeline editor app component', () => { describe('when queries are called', () => { beforeEach(() => { - mockBlobContentData.mockResolvedValue(mockCiYml); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse); }); @@ -154,39 +177,19 @@ describe('Pipeline editor app component', () => { expect(mockCiConfigData).toHaveBeenCalledWith({ content: mockCiYml, projectPath: mockProjectFullPath, + sha: mockCommitSha, }); }); }); describe('when no CI config file exists', () => { - describe('in a project without a repository', () => { - it('shows an empty state and does not show editor home component', async () => { - mockBlobContentData.mockRejectedValueOnce({ - response: { - status: httpStatusCodes.BAD_REQUEST, - }, - }); - await createComponentWithApollo(); - - expect(findEmptyState().exists()).toBe(true); - expect(findAlert().exists()).toBe(false); - expect(findEditorHome().exists()).toBe(false); - }); - }); - - describe('in a project with a repository', () => { - it('shows an empty state and does not show editor home component', async () => { - mockBlobContentData.mockRejectedValueOnce({ - response: { - status: httpStatusCodes.NOT_FOUND, - }, - }); - await createComponentWithApollo(); + it('shows an empty state and does not show editor home component', async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); + await createComponentWithApollo(); - expect(findEmptyState().exists()).toBe(true); - expect(findAlert().exists()).toBe(false); - expect(findEditorHome().exists()).toBe(false); - }); + expect(findEmptyState().exists()).toBe(true); + expect(findAlert().exists()).toBe(false); + expect(findEditorHome().exists()).toBe(false); }); describe('because of a fetching error', () => { @@ -204,13 +207,28 @@ describe('Pipeline editor app component', () => { }); }); + describe('with an empty CI config file', () => { + describe('with empty state feature flag on', () => { + it('does not show the empty screen state', async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseEmptyCiFile); + + await createComponentWithApollo({ + provide: { + glFeatures: { + pipelineEditorEmptyStateAction: true, + }, + }, + }); + + expect(findEmptyState().exists()).toBe(false); + expect(findTextEditor().exists()).toBe(true); + }); + }); + }); + describe('when landing on the empty state with feature flag on', () => { it('user can click on CTA button and see an empty editor', async () => { - mockBlobContentData.mockRejectedValueOnce({ - response: { - status: httpStatusCodes.NOT_FOUND, - }, - }); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); await createComponentWithApollo({ provide: { @@ -315,21 +333,83 @@ describe('Pipeline editor app component', () => { }); it('hides start screen when refetch fetches CI file', async () => { - mockBlobContentData.mockRejectedValue({ - response: { - status: httpStatusCodes.NOT_FOUND, - }, - }); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile); await createComponentWithApollo(); expect(findEmptyState().exists()).toBe(true); expect(findEditorHome().exists()).toBe(false); - mockBlobContentData.mockResolvedValue(mockCiYml); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); await wrapper.vm.$apollo.queries.initialCiFileContent.refetch(); expect(findEmptyState().exists()).toBe(false); expect(findEditorHome().exists()).toBe(true); }); }); + + describe('when a template parameter is present in the URL', () => { + const { location } = window; + + beforeEach(() => { + delete window.location; + window.location = new URL('https://localhost?template=Android'); + }); + + afterEach(() => { + window.location = location; + }); + + it('renders the given template', async () => { + await createComponentWithApollo(); + + expect(mockGetTemplate).toHaveBeenCalledWith({ + projectPath: mockProjectFullPath, + templateName: 'Android', + }); + + expect(findEmptyState().exists()).toBe(false); + expect(findTextEditor().exists()).toBe(true); + }); + }); + + describe('when updating commit sha', () => { + const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha; + + beforeEach(async () => { + mockUpdateCommitSha.mockResolvedValue(newCommitSha); + mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults); + await createComponentWithApollo(); + }); + + it('fetches updated commit sha for the new branch', async () => { + expect(mockLatestCommitShaQuery).not.toHaveBeenCalled(); + + wrapper + .findComponent(PipelineEditorHome) + .vm.$emit('updateCommitSha', { newBranch: 'new-branch' }); + await waitForPromises(); + + expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({ + projectPath: mockProjectFullPath, + ref: 'new-branch', + }); + }); + + it('updates commit sha with the newly fetched commit sha', async () => { + expect(mockUpdateCommitSha).not.toHaveBeenCalled(); + + wrapper + .findComponent(PipelineEditorHome) + .vm.$emit('updateCommitSha', { newBranch: 'new-branch' }); + await waitForPromises(); + + expect(mockUpdateCommitSha).toHaveBeenCalled(); + expect(mockUpdateCommitSha).toHaveBeenCalledWith( + expect.any(Object), + { commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha }, + expect.any(Object), + expect.any(Object), + ); + }); + }); }); |