summaryrefslogtreecommitdiff
path: root/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js')
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js589
1 files changed, 589 insertions, 0 deletions
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
new file mode 100644
index 00000000000..2246d0bbf7e
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -0,0 +1,589 @@
+import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
+import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
+import ValidationSegment, {
+ i18n as validationSegmenti18n,
+} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
+import {
+ COMMIT_SUCCESS,
+ COMMIT_SUCCESS_WITH_REDIRECT,
+ COMMIT_FAILURE,
+ EDITOR_APP_STATUS_LOADING,
+} from '~/ci/pipeline_editor/constants';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import getCiConfigData from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import getTemplate from '~/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from '~/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
+import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+
+import PipelineEditorApp from '~/ci/pipeline_editor/pipeline_editor_app.vue';
+import PipelineEditorHome from '~/ci/pipeline_editor/pipeline_editor_home.vue';
+
+import {
+ mockCiConfigPath,
+ mockCiConfigQueryResponse,
+ mockBlobContentQueryResponse,
+ mockBlobContentQueryResponseNoCiFile,
+ mockCiYml,
+ mockCiTemplateQueryResponse,
+ mockCommitSha,
+ mockCommitShaResults,
+ mockDefaultBranch,
+ mockEmptyCommitShaResults,
+ mockNewCommitShaResults,
+ mockNewMergeRequestPath,
+ mockProjectFullPath,
+} from './mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const defaultProvide = {
+ ciConfigPath: mockCiConfigPath,
+ defaultBranch: mockDefaultBranch,
+ newMergeRequestPath: mockNewMergeRequestPath,
+ projectFullPath: mockProjectFullPath,
+ usesExternalConfig: false,
+};
+
+describe('Pipeline editor app component', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockBlobContentData;
+ let mockCiConfigData;
+ let mockGetTemplate;
+ let mockLatestCommitShaQuery;
+ let mockPipelineQuery;
+
+ const createComponent = ({
+ blobLoading = false,
+ options = {},
+ provide = {},
+ stubs = {},
+ } = {}) => {
+ wrapper = shallowMount(PipelineEditorApp, {
+ provide: { ...defaultProvide, ...provide },
+ stubs,
+ mocks: {
+ $apollo: {
+ queries: {
+ initialCiFileContent: {
+ loading: blobLoading,
+ },
+ },
+ },
+ },
+ ...options,
+ });
+ };
+
+ const createComponentWithApollo = async ({
+ provide = {},
+ stubs = {},
+ withUndefinedBranch = false,
+ } = {}) => {
+ const handlers = [
+ [getBlobContent, mockBlobContentData],
+ [getCiConfigData, mockCiConfigData],
+ [getTemplate, mockGetTemplate],
+ [getLatestCommitShaQuery, mockLatestCommitShaQuery],
+ [getPipelineQuery, mockPipelineQuery],
+ ];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ if (!withUndefinedBranch) {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getCurrentBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ current: {
+ __typename: 'WorkBranch',
+ name: mockDefaultBranch,
+ },
+ },
+ },
+ });
+ }
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'AppData',
+ status: EDITOR_APP_STATUS_LOADING,
+ },
+ },
+ });
+
+ const options = {
+ localVue,
+ mocks: {},
+ apolloProvider: mockApollo,
+ };
+
+ createComponent({ provide, stubs, options });
+
+ return waitForPromises();
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
+ const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
+ const findEmptyStateButton = () => findEmptyState().findComponent(GlButton);
+ const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
+
+ beforeEach(() => {
+ mockBlobContentData = jest.fn();
+ mockCiConfigData = jest.fn();
+ mockGetTemplate = jest.fn();
+ mockLatestCommitShaQuery = jest.fn();
+ mockPipelineQuery = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading state', () => {
+ it('displays a loading icon if the blob query is loading', () => {
+ createComponent({ blobLoading: true });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(false);
+ });
+ });
+
+ describe('skipping queries', () => {
+ describe('when branchName is undefined', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({ withUndefinedBranch: true });
+ });
+
+ it('does not calls getBlobContent', () => {
+ expect(mockBlobContentData).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when branchName is defined', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo();
+ });
+
+ it('calls getBlobContent', () => {
+ expect(mockBlobContentData).toHaveBeenCalled();
+ });
+ });
+
+ describe('when commit sha is undefined', () => {
+ beforeEach(async () => {
+ mockLatestCommitShaQuery.mockResolvedValue(undefined);
+ await createComponentWithApollo();
+ });
+
+ it('calls getBlobContent', () => {
+ expect(mockBlobContentData).toHaveBeenCalled();
+ });
+
+ it('does not call ciConfigData', () => {
+ expect(mockCiConfigData).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when commit sha is defined', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ await createComponentWithApollo();
+ });
+
+ it('calls ciConfigData', () => {
+ expect(mockCiConfigData).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when queries are called', () => {
+ beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ });
+
+ describe('when project uses an external CI config file', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ provide: {
+ usesExternalConfig: true,
+ },
+ });
+ });
+
+ it('shows an empty state and does not show editor home component', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
+ });
+ });
+
+ describe('when file exists', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo();
+
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+ });
+
+ it('shows pipeline editor home component', () => {
+ expect(findEditorHome().exists()).toBe(true);
+ });
+
+ it('no error is shown when data is set', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('ci config query is called with correct variables', async () => {
+ expect(mockCiConfigData).toHaveBeenCalledWith({
+ content: mockCiYml,
+ projectPath: mockProjectFullPath,
+ sha: mockCommitSha,
+ });
+ });
+
+ it('does not poll for the commit sha', () => {
+ expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('when no CI config file exists', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorEmptyState,
+ },
+ });
+
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+ });
+
+ it('shows an empty state and does not show editor home component', async () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
+ });
+
+ it('does not poll for the commit sha', () => {
+ expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ });
+
+ describe('because of a fetching error', () => {
+ it('shows a unkown error message', async () => {
+ const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
+
+ mockBlobContentData.mockRejectedValueOnce();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorMessages,
+ },
+ });
+
+ expect(findEmptyState().exists()).toBe(false);
+
+ expect(findAlert().text()).toBe(loadUnknownFailureText);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('with no CI config setup', () => {
+ it('user can click on CTA button to get started', async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
+
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorHome,
+ PipelineEditorEmptyState,
+ },
+ });
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(false);
+
+ await findEmptyStateButton().vm.$emit('click');
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+
+ describe('when the lint query returns a 500 error', () => {
+ beforeEach(async () => {
+ mockCiConfigData.mockRejectedValueOnce(new Error(500));
+ await createComponentWithApollo({
+ stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
+ });
+ });
+
+ it('shows that the lint service is down', () => {
+ expect(findValidationSegment().text()).toContain(
+ validationSegmenti18n.unavailableValidation,
+ );
+ });
+
+ it('does not report an error or scroll to the top', () => {
+ expect(findAlert().exists()).toBe(false);
+ expect(window.scrollTo).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when the user commits', () => {
+ const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
+ const updateSuccessMessage = 'Your changes have been successfully committed.';
+
+ describe('and the commit mutation succeeds', () => {
+ beforeEach(async () => {
+ window.scrollTo = jest.fn();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
+
+ findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
+ });
+
+ it('shows a confirmation message', () => {
+ expect(findAlert().text()).toBe(updateSuccessMessage);
+ });
+
+ it('scrolls to the top of the page to bring attention to the confirmation message', () => {
+ expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
+ });
+
+ it('polls for commit sha while pipeline data is not yet available for current branch', async () => {
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+
+ // simulate a commit to the current branch
+ findEditorHome().vm.$emit('updateCommitSha');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
+ });
+
+ it('stops polling for commit sha when pipeline data is available for newly committed branch', async () => {
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
+ .mockImplementation(jest.fn());
+
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ await wrapper.vm.$apollo.queries.commitSha.refetch();
+
+ expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ });
+
+ it('stops polling for commit sha when pipeline data is available for current branch', async () => {
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
+ .mockImplementation(jest.fn());
+
+ mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
+ findEditorHome().vm.$emit('updateCommitSha');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when the commit succeeds with a redirect', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
+
+ findEditorHome().vm.$emit('commit', {
+ type: COMMIT_SUCCESS_WITH_REDIRECT,
+ params: { sourceBranch: newBranch, targetBranch: mockDefaultBranch },
+ });
+ });
+
+ it('redirects to the merge request page with source and target branches', () => {
+ const branchesQuery = objectToQuery({
+ 'merge_request[source_branch]': newBranch,
+ 'merge_request[target_branch]': mockDefaultBranch,
+ });
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ });
+ });
+
+ describe('and the commit mutation fails', () => {
+ const commitFailedReasons = ['Commit failed'];
+
+ beforeEach(async () => {
+ window.scrollTo = jest.fn();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
+
+ findEditorHome().vm.$emit('showError', {
+ type: COMMIT_FAILURE,
+ reasons: commitFailedReasons,
+ });
+ });
+
+ it('shows an error message', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(
+ `${updateFailureMessage} ${commitFailedReasons[0]}`,
+ );
+ });
+
+ it('scrolls to the top of the page to bring attention to the error message', () => {
+ expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
+ });
+ });
+
+ describe('when an unknown error occurs', () => {
+ const unknownReasons = ['Commit failed'];
+
+ beforeEach(async () => {
+ window.scrollTo = jest.fn();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
+
+ findEditorHome().vm.$emit('showError', {
+ type: COMMIT_FAILURE,
+ reasons: unknownReasons,
+ });
+ });
+
+ it('shows an error message', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(
+ `${updateFailureMessage} ${unknownReasons[0]}`,
+ );
+ });
+
+ it('scrolls to the top of the page to bring attention to the error message', () => {
+ expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
+ });
+ });
+ });
+ });
+
+ describe('when refetching content', () => {
+ beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ });
+
+ it('refetches blob content', async () => {
+ await createComponentWithApollo();
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.initialCiFileContent, 'refetch')
+ .mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(0);
+
+ await wrapper.vm.refetchContent();
+
+ expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('hides start screen when refetch fetches CI file', async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ await createComponentWithApollo();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(false);
+
+ 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 originalLocation = window.location.href;
+
+ beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
+ setWindowLocation('?template=Android');
+ });
+
+ afterEach(() => {
+ setWindowLocation(originalLocation);
+ });
+
+ it('renders the given template', async () => {
+ await createComponentWithApollo({
+ stubs: { PipelineEditorHome, PipelineEditorTabs },
+ });
+
+ expect(mockGetTemplate).toHaveBeenCalledWith({
+ projectPath: mockProjectFullPath,
+ templateName: 'Android',
+ });
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+
+ describe('when add_new_config_file query param is present', () => {
+ const originalLocation = window.location.href;
+
+ beforeEach(() => {
+ setWindowLocation('?add_new_config_file=true');
+
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ });
+
+ afterEach(() => {
+ setWindowLocation(originalLocation);
+ });
+
+ describe('when CI config file does not exist', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
+ mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
+
+ await createComponentWithApollo();
+
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+ });
+
+ it('skips empty state and shows editor home component', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+ });
+});