summaryrefslogtreecommitdiff
path: root/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js')
-rw-r--r--spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js314
1 files changed, 314 insertions, 0 deletions
diff --git a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
new file mode 100644
index 00000000000..ae25142b455
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
@@ -0,0 +1,314 @@
+import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import CiValidate, { i18n } from '~/ci/pipeline_editor/components/validate/ci_validate.vue';
+import ValidatePipelinePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
+import {
+ mockBlobContentQueryResponse,
+ mockCiLintPath,
+ mockCiYml,
+ mockSimulatePipelineHelpPagePath,
+} from '../../mock_data';
+import { mockLintDataError, mockLintDataValid } from '../../../ci_lint/mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('Pipeline Editor Validate Tab', () => {
+ let wrapper;
+ let mockApollo;
+ let mockBlobContentData;
+ let trackingSpy;
+
+ const createComponent = ({
+ props,
+ stubs,
+ options,
+ isBlobLoading = false,
+ isSimulationLoading = false,
+ } = {}) => {
+ wrapper = shallowMountExtended(CiValidate, {
+ propsData: {
+ ciFileContent: mockCiYml,
+ ...props,
+ },
+ provide: {
+ ciConfigPath: '/path/to/ci-config',
+ ciLintPath: mockCiLintPath,
+ currentBranch: 'main',
+ projectFullPath: '/path/to/project',
+ validateTabIllustrationPath: '/path/to/img',
+ simulatePipelineHelpPagePath: mockSimulatePipelineHelpPagePath,
+ },
+ stubs,
+ mocks: {
+ $apollo: {
+ queries: {
+ initialBlobContent: {
+ loading: isBlobLoading,
+ },
+ },
+ mutations: {
+ lintCiMutation: {
+ loading: isSimulationLoading,
+ },
+ },
+ },
+ },
+ ...options,
+ });
+ };
+
+ const createComponentWithApollo = ({ props, stubs } = {}) => {
+ const handlers = [[getBlobContent, mockBlobContentData]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ props,
+ stubs,
+ options: {
+ localVue,
+ apolloProvider: mockApollo,
+ mocks: {},
+ },
+ });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findCancelBtn = () => wrapper.findByTestId('cancel-simulation');
+ const findContentChangeStatus = () => wrapper.findByTestId('content-status');
+ const findCta = () => wrapper.findByTestId('simulate-pipeline-button');
+ const findDisabledCtaTooltip = () => wrapper.findByTestId('cta-tooltip');
+ const findHelpIcon = () => wrapper.findComponent(GlIcon);
+ const findIllustration = () => wrapper.findByRole('img');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPipelineSource = () => wrapper.findComponent(GlDropdown);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findCiLintResults = () => wrapper.findComponent(CiLintResults);
+ const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button');
+
+ beforeEach(() => {
+ mockBlobContentData = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while initial CI content is loading', () => {
+ beforeEach(() => {
+ createComponent({ isBlobLoading: true });
+ });
+
+ it('renders disabled CTA with tooltip', () => {
+ expect(findCta().props('disabled')).toBe(true);
+ expect(findDisabledCtaTooltip().exists()).toBe(true);
+ });
+ });
+
+ describe('after initial CI content is loaded', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo({ stubs: { GlPopover, ValidatePipelinePopover } });
+ });
+
+ it('renders disabled pipeline source dropdown', () => {
+ expect(findPipelineSource().exists()).toBe(true);
+ expect(findPipelineSource().attributes('text')).toBe(i18n.pipelineSourceDefault);
+ expect(findPipelineSource().props('disabled')).toBe(true);
+ });
+
+ it('renders enabled CTA without tooltip', () => {
+ expect(findCta().exists()).toBe(true);
+ expect(findCta().props('disabled')).toBe(false);
+ expect(findDisabledCtaTooltip().exists()).toBe(false);
+ });
+
+ it('popover is set to render when hovering over help icon', () => {
+ expect(findPopover().props('target')).toBe(findHelpIcon().attributes('id'));
+ expect(findPopover().props('triggers')).toBe('hover focus');
+ });
+ });
+
+ describe('simulating the pipeline', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks the simulation event', () => {
+ const {
+ label,
+ actions: { simulatePipeline },
+ } = pipelineEditorTrackingOptions;
+ findCta().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, simulatePipeline, { label });
+ });
+
+ it('renders loading state while simulation is ongoing', async () => {
+ findCta().vm.$emit('click');
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCancelBtn().exists()).toBe(true);
+ expect(findCta().props('loading')).toBe(true);
+ });
+
+ it('calls mutation with the correct input', async () => {
+ await findCta().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: lintCIMutation,
+ variables: {
+ dry: true,
+ content: mockCiYml,
+ endpoint: mockCiLintPath,
+ },
+ });
+ });
+
+ describe('when results are successful', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ await findCta().vm.$emit('click');
+ });
+
+ it('renders success alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().attributes('variant')).toBe('success');
+ expect(findAlert().attributes('title')).toBe(i18n.successAlertTitle);
+ });
+
+ it('does not render content change status or CTA for results page', () => {
+ expect(findContentChangeStatus().exists()).toBe(false);
+ expect(findResultsCta().exists()).toBe(false);
+ });
+
+ it('renders CI lint results with correct props', () => {
+ expect(findCiLintResults().exists()).toBe(true);
+ expect(findCiLintResults().props()).toMatchObject({
+ dryRun: true,
+ hideAlert: true,
+ isValid: true,
+ jobs: mockLintDataValid.data.lintCI.jobs,
+ });
+ });
+ });
+
+ describe('when results have errors', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataError);
+ await findCta().vm.$emit('click');
+ });
+
+ it('renders error alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().attributes('variant')).toBe('danger');
+ expect(findAlert().attributes('title')).toBe(i18n.errorAlertTitle);
+ });
+
+ it('renders CI lint results with correct props', () => {
+ expect(findCiLintResults().exists()).toBe(true);
+ expect(findCiLintResults().props()).toMatchObject({
+ dryRun: true,
+ hideAlert: true,
+ isValid: false,
+ errors: mockLintDataError.data.lintCI.errors,
+ warnings: mockLintDataError.data.lintCI.warnings,
+ });
+ });
+ });
+ });
+
+ describe('when CI content has changed after a simulation', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ await findCta().vm.$emit('click');
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks the second simulation event', async () => {
+ const {
+ label,
+ actions: { resimulatePipeline },
+ } = pipelineEditorTrackingOptions;
+
+ await wrapper.setProps({ ciFileContent: 'new yaml content' });
+ findResultsCta().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, resimulatePipeline, { label });
+ });
+
+ it('renders content change status', async () => {
+ await wrapper.setProps({ ciFileContent: 'new yaml content' });
+
+ expect(findContentChangeStatus().exists()).toBe(true);
+ expect(findResultsCta().exists()).toBe(true);
+ });
+
+ it('calls mutation with new content', async () => {
+ await wrapper.setProps({ ciFileContent: 'new yaml content' });
+ await findResultsCta().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(2);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: lintCIMutation,
+ variables: {
+ dry: true,
+ content: 'new yaml content',
+ endpoint: mockCiLintPath,
+ },
+ });
+ });
+ });
+
+ describe('canceling a simulation', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo();
+ });
+
+ it('returns to init state', async () => {
+ // init state
+ expect(findIllustration().exists()).toBe(true);
+ expect(findCiLintResults().exists()).toBe(false);
+
+ // mutations should have successful results
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ findCta().vm.$emit('click');
+ await nextTick();
+
+ // cancel before simulation succeeds
+ expect(findCancelBtn().exists()).toBe(true);
+ await findCancelBtn().vm.$emit('click');
+
+ // should still render init state
+ expect(findIllustration().exists()).toBe(true);
+ expect(findCiLintResults().exists()).toBe(false);
+ });
+ });
+});