diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
commit | 9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch) | |
tree | 70467ae3692a0e35e5ea56bcb803eb512a10bedb /spec/frontend/pipeline_editor | |
parent | 4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff) | |
download | gitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'spec/frontend/pipeline_editor')
15 files changed, 513 insertions, 168 deletions
diff --git a/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js new file mode 100644 index 00000000000..d03f12bc249 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js @@ -0,0 +1,61 @@ +import { within } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import { merge } from 'lodash'; +import { TEST_HOST } from 'helpers/test_constants'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue'; +import { CODE_SNIPPET_SOURCE_API_FUZZING } from '~/pipeline_editor/components/code_snippet_alert/constants'; + +const apiFuzzingConfigurationPath = '/namespace/project/-/security/configuration/api_fuzzing'; + +describe('EE - CodeSnippetAlert', () => { + let wrapper; + + const createWrapper = (options) => { + wrapper = extendedWrapper( + mount( + CodeSnippetAlert, + merge( + { + provide: { + configurationPaths: { + [CODE_SNIPPET_SOURCE_API_FUZZING]: apiFuzzingConfigurationPath, + }, + }, + propsData: { + source: CODE_SNIPPET_SOURCE_API_FUZZING, + }, + }, + options, + ), + ), + ); + }; + + const withinComponent = () => within(wrapper.element); + const findDocsLink = () => withinComponent().getByRole('link', { name: /read documentation/i }); + const findConfigurationLink = () => + withinComponent().getByRole('link', { name: /Go back to configuration/i }); + + beforeEach(() => { + createWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it("provides a link to the feature's documentation", () => { + const docsLink = findDocsLink(); + + expect(docsLink).not.toBe(null); + expect(docsLink.href).toBe(`${TEST_HOST}/help/user/application_security/api_fuzzing/index`); + }); + + it("provides a link to the feature's configuration form", () => { + const configurationLink = findConfigurationLink(); + + expect(configurationLink).not.toBe(null); + expect(configurationLink.href).toBe(TEST_HOST + apiFuzzingConfigurationPath); + }); +}); 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 866069f337b..fb191fccb0d 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 @@ -1,10 +1,8 @@ -import { GlAlert, GlIcon } from '@gitlab/ui'; +import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { EDITOR_READY_EVENT } from '~/editor/constants'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; -import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; -import { INVALID_CI_CONFIG } from '~/pipelines/constants'; import { mockLintResponse, mockCiConfigPath } from '../../mock_data'; describe('Text editor component', () => { @@ -33,28 +31,11 @@ describe('Text editor component', () => { }); }; - const findAlert = () => wrapper.findComponent(GlAlert); const findIcon = () => wrapper.findComponent(GlIcon); const findEditor = () => wrapper.findComponent(MockEditorLite); afterEach(() => { wrapper.destroy(); - wrapper = null; - }); - - describe('when status is invalid', () => { - beforeEach(() => { - createComponent({ props: { ciConfigData: { status: CI_CONFIG_STATUS_INVALID } } }); - }); - - it('show an error message', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[INVALID_CI_CONFIG]); - }); - - it('hides the editor', () => { - expect(findEditor().exists()).toBe(false); - }); }); describe('when status is valid', () => { 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 new file mode 100644 index 00000000000..fa937100982 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -0,0 +1,123 @@ +import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; +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 BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue'; +import { DEFAULT_FAILURE } from '~/pipeline_editor/constants'; +import { mockDefaultBranch, mockProjectBranches, mockProjectFullPath } from '../../mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('Pipeline editor branch switcher', () => { + let wrapper; + let mockApollo; + let mockAvailableBranchQuery; + + const createComponentWithApollo = () => { + const resolvers = { + Query: { + project: mockAvailableBranchQuery, + }, + }; + + mockApollo = createMockApollo([], resolvers); + wrapper = shallowMount(BranchSwitcher, { + localVue, + apolloProvider: mockApollo, + provide: { + projectFullPath: mockProjectFullPath, + }, + data() { + return { + currentBranch: mockDefaultBranch, + }; + }, + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + + beforeEach(() => { + mockAvailableBranchQuery = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('while querying', () => { + beforeEach(() => { + createComponentWithApollo(); + }); + + it('does not render dropdown', () => { + expect(findDropdown().exists()).toBe(false); + }); + }); + + describe('after querying', () => { + beforeEach(async () => { + mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); + createComponentWithApollo(); + await waitForPromises(); + }); + + it('query is called with correct variables', async () => { + expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1); + expect(mockAvailableBranchQuery).toHaveBeenCalledWith( + expect.anything(), + { + fullPath: mockProjectFullPath, + }, + expect.anything(), + expect.anything(), + ); + }); + + it('renders list of branches', () => { + expect(findDropdown().exists()).toBe(true); + expect(findDropdownItems()).toHaveLength(mockProjectBranches.repository.branches.length); + }); + + it('renders current branch at the top of the list with a check mark', () => { + const firstDropdownItem = findDropdownItems().at(0); + const icon = firstDropdownItem.findComponent(GlIcon); + + expect(firstDropdownItem.text()).toBe(mockDefaultBranch); + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('check'); + }); + + it('does not render check mark for other branches', () => { + const secondDropdownItem = findDropdownItems().at(1); + const icon = secondDropdownItem.findComponent(GlIcon); + + expect(icon.classes()).toContain('gl-visibility-hidden'); + }); + }); + + describe('on fetch error', () => { + beforeEach(async () => { + mockAvailableBranchQuery.mockResolvedValue(new Error()); + createComponentWithApollo(); + await waitForPromises(); + }); + + it('does not render dropdown', () => { + expect(findDropdown().exists()).toBe(false); + }); + + it('shows an error message', () => { + expect(wrapper.emitted('showError')).toBeDefined(); + expect(wrapper.emitted('showError')[0]).toEqual([ + { + reasons: [wrapper.vm.$options.i18n.fetchError], + type: DEFAULT_FAILURE, + }, + ]); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js new file mode 100644 index 00000000000..94a0a7d14ee --- /dev/null +++ b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js @@ -0,0 +1,49 @@ +import { shallowMount } from '@vue/test-utils'; +import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue'; +import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; + +describe('Pipeline editor file nav', () => { + let wrapper; + const mockProvide = { + glFeatures: { + pipelineEditorBranchSwitcher: true, + }, + }; + + const createComponent = ({ provide = {} } = {}) => { + wrapper = shallowMount(PipelineEditorFileNav, { + provide: { + ...mockProvide, + ...provide, + }, + }); + }; + + const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the branch switcher', () => { + expect(findBranchSwitcher().exists()).toBe(true); + }); + }); + + describe('with branch switcher feature flag OFF', () => { + it('does not render the branch switcher', () => { + createComponent({ + provide: { + glFeatures: { pipelineEditorBranchSwitcher: false }, + }, + }); + + expect(findBranchSwitcher().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js index ef8ca574e59..27652bb268b 100644 --- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js @@ -13,7 +13,7 @@ describe('Pipeline editor header', () => { }, }; - const createComponent = ({ provide = {} } = {}) => { + const createComponent = ({ provide = {}, props = {} } = {}) => { wrapper = shallowMount(PipelineEditorHeader, { provide: { ...mockProvide, @@ -23,6 +23,8 @@ describe('Pipeline editor header', () => { ciConfigData: mockLintResponse, ciFileContent: mockCiYml, isCiConfigDataLoading: false, + isNewCiConfigFile: false, + ...props, }, }); }; @@ -36,15 +38,21 @@ describe('Pipeline editor header', () => { }); describe('template', () => { - beforeEach(() => { - createComponent(); + it('hides the pipeline status for new projects without a CI file', () => { + createComponent({ props: { isNewCiConfigFile: true } }); + + expect(findPipelineStatus().exists()).toBe(false); }); - it('renders the pipeline status', () => { + it('renders the pipeline status when CI file exists', () => { + createComponent({ props: { isNewCiConfigFile: false } }); + expect(findPipelineStatus().exists()).toBe(true); }); it('renders the validation segment', () => { + createComponent(); + expect(findValidationSegment().exists()).toBe(true); }); }); diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js index de6e112866b..b6d49d0d0f8 100644 --- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js @@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue'; +import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data'; @@ -19,32 +20,9 @@ describe('Pipeline Status', () => { let mockApollo; let mockPipelineQuery; - const createComponent = ({ hasPipeline = true, isQueryLoading = false }) => { - const pipeline = hasPipeline - ? { loading: isQueryLoading, ...mockProjectPipeline.pipeline } - : { loading: isQueryLoading }; - - wrapper = shallowMount(PipelineStatus, { - provide: mockProvide, - stubs: { GlLink, GlSprintf }, - data: () => (hasPipeline ? { pipeline } : {}), - mocks: { - $apollo: { - queries: { - pipeline, - }, - }, - }, - }); - }; - const createComponentWithApollo = () => { - const resolvers = { - Query: { - project: mockPipelineQuery, - }, - }; - mockApollo = createMockApollo([], resolvers); + const handlers = [[getPipelineQuery, mockPipelineQuery]]; + mockApollo = createMockApollo(handlers); wrapper = shallowMount(PipelineStatus, { localVue, @@ -78,16 +56,17 @@ describe('Pipeline Status', () => { wrapper = null; }); - describe('while querying', () => { - it('renders loading icon', () => { - createComponent({ isQueryLoading: true, hasPipeline: false }); + describe('loading icon', () => { + it('renders while query is being fetched', () => { + createComponentWithApollo(); expect(findLoadingIcon().exists()).toBe(true); expect(findPipelineLoadingMsg().text()).toBe(i18n.fetchLoading); }); - it('does not render loading icon if pipeline data is already set', () => { - createComponent({ isQueryLoading: true }); + it('does not render if query is no longer loading', async () => { + createComponentWithApollo(); + await waitForPromises(); expect(findLoadingIcon().exists()).toBe(false); }); @@ -96,7 +75,9 @@ describe('Pipeline Status', () => { describe('when querying data', () => { describe('when data is set', () => { beforeEach(async () => { - mockPipelineQuery.mockResolvedValue(mockProjectPipeline); + mockPipelineQuery.mockResolvedValue({ + data: { project: mockProjectPipeline }, + }); createComponentWithApollo(); await waitForPromises(); @@ -104,14 +85,10 @@ describe('Pipeline Status', () => { it('query is called with correct variables', async () => { expect(mockPipelineQuery).toHaveBeenCalledTimes(1); - expect(mockPipelineQuery).toHaveBeenCalledWith( - expect.anything(), - { - fullPath: mockProjectFullPath, - }, - expect.anything(), - expect.anything(), - ); + expect(mockPipelineQuery).toHaveBeenCalledWith({ + fullPath: mockProjectFullPath, + sha: mockCommitSha, + }); }); it('does not render error', () => { diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js index 274c2d1b8da..fd8a100bb2c 100644 --- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js +++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js @@ -6,13 +6,19 @@ import { sprintf } from '~/locale'; import ValidationSegment, { i18n, } from '~/pipeline_editor/components/header/validation_segment.vue'; -import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; +import { + CI_CONFIG_STATUS_INVALID, + EDITOR_APP_STATUS_EMPTY, + EDITOR_APP_STATUS_INVALID, + EDITOR_APP_STATUS_LOADING, + EDITOR_APP_STATUS_VALID, +} from '~/pipeline_editor/constants'; import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data'; describe('Validation segment component', () => { let wrapper; - const createComponent = (props = {}) => { + const createComponent = ({ props = {}, appStatus }) => { wrapper = extendedWrapper( shallowMount(ValidationSegment, { provide: { @@ -21,9 +27,14 @@ describe('Validation segment component', () => { propsData: { ciConfig: mergeUnwrappedCiConfig(), ciFileContent: mockCiYml, - loading: false, ...props, }, + // Simulate graphQL client query result + data() { + return { + appStatus, + }; + }, }), ); }; @@ -34,18 +45,17 @@ describe('Validation segment component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); it('shows the loading state', () => { - createComponent({ loading: true }); + createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); expect(wrapper.text()).toBe(i18n.loading); }); describe('when config is empty', () => { beforeEach(() => { - createComponent({ ciFileContent: '' }); + createComponent({ appStatus: EDITOR_APP_STATUS_EMPTY }); }); it('has check icon', () => { @@ -59,7 +69,7 @@ describe('Validation segment component', () => { describe('when config is valid', () => { beforeEach(() => { - createComponent({}); + createComponent({ appStatus: EDITOR_APP_STATUS_VALID }); }); it('has check icon', () => { @@ -79,12 +89,9 @@ describe('Validation segment component', () => { describe('when config is invalid', () => { beforeEach(() => { createComponent({ - ciConfig: mergeUnwrappedCiConfig({ - status: CI_CONFIG_STATUS_INVALID, - }), + appStatus: EDITOR_APP_STATUS_INVALID, }); }); - it('has warning icon', () => { expect(findIcon().props('name')).toBe('warning-solid'); }); @@ -93,43 +100,53 @@ describe('Validation segment component', () => { expect(findValidationMsg().text()).toBe(i18n.invalid); }); - it('shows an invalid state with an error', () => { + it('shows the learn more link', () => { + expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath); + expect(findLearnMoreLink().text()).toBe('Learn more'); + }); + + describe('with multiple errors', () => { const firstError = 'First Error'; const secondError = 'Second Error'; - createComponent({ - ciConfig: mergeUnwrappedCiConfig({ - status: CI_CONFIG_STATUS_INVALID, - errors: [firstError, secondError], - }), + beforeEach(() => { + createComponent({ + props: { + ciConfig: mergeUnwrappedCiConfig({ + status: CI_CONFIG_STATUS_INVALID, + errors: [firstError, secondError], + }), + }, + }); + }); + it('shows an invalid state with an error', () => { + // Test the error is shown _and_ the string matches + expect(findValidationMsg().text()).toContain(firstError); + expect(findValidationMsg().text()).toBe( + sprintf(i18n.invalidWithReason, { reason: firstError }), + ); }); - - // Test the error is shown _and_ the string matches - expect(findValidationMsg().text()).toContain(firstError); - expect(findValidationMsg().text()).toBe( - sprintf(i18n.invalidWithReason, { reason: firstError }), - ); }); - it('shows an invalid state with an error while preventing XSS', () => { + describe('with XSS inside the error', () => { const evilError = '<script>evil();</script>'; - createComponent({ - ciConfig: mergeUnwrappedCiConfig({ - status: CI_CONFIG_STATUS_INVALID, - errors: [evilError], - }), + beforeEach(() => { + createComponent({ + props: { + ciConfig: mergeUnwrappedCiConfig({ + status: CI_CONFIG_STATUS_INVALID, + errors: [evilError], + }), + }, + }); }); + it('shows an invalid state with an error while preventing XSS', () => { + const { innerHTML } = findValidationMsg().element; - const { innerHTML } = findValidationMsg().element; - - expect(innerHTML).not.toContain(evilError); - expect(innerHTML).toContain(escape(evilError)); - }); - - it('shows the learn more link', () => { - expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath); - expect(findLearnMoreLink().text()).toBe('Learn more'); + expect(innerHTML).not.toContain(evilError); + expect(innerHTML).toContain(escape(evilError)); + }); }); }); }); diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js index 6775433deb9..5fc0880b09e 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js @@ -7,7 +7,7 @@ import { mockJobs, mockErrors, mockWarnings } from '../../mock_data'; describe('CI Lint Results', () => { let wrapper; const defaultProps = { - valid: true, + isValid: true, jobs: mockJobs, errors: [], warnings: [], @@ -42,7 +42,6 @@ describe('CI Lint Results', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('Empty results', () => { @@ -72,7 +71,7 @@ describe('CI Lint Results', () => { describe('Invalid results', () => { beforeEach(() => { - createComponent({ valid: false, errors: mockErrors, warnings: mockWarnings }, mount); + createComponent({ isValid: false, errors: mockErrors, warnings: mockWarnings }, mount); }); it('does not display the table', () => { diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js index fdddca3d62b..238942a34ff 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js @@ -1,13 +1,12 @@ import { GlAlert, GlLink } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; -import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data'; describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { let wrapper; - const createComponent = (props = {}, mountFn = shallowMount) => { + const createComponent = ({ props, mountFn = shallowMount } = {}) => { wrapper = mountFn(CiLint, { provide: { lintHelpPagePath: mockLintHelpPagePath, @@ -27,12 +26,11 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('Valid Results', () => { beforeEach(() => { - createComponent({}, mount); + createComponent({ props: { isValid: true }, mountFn: mount }); }); it('displays valid results', () => { @@ -66,14 +64,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { }); it('displays invalid results', () => { - createComponent( - { - ciConfig: mergeUnwrappedCiConfig({ - status: CI_CONFIG_STATUS_INVALID, - }), - }, - mount, - ); + createComponent({ props: { isValid: false }, mountFn: mount }); expect(findAlert().text()).toMatch('Status: Syntax is incorrect.'); }); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 24af17e9ce6..eba853180cd 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -4,8 +4,15 @@ import { nextTick } from 'vue'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; +import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; +import { + EDITOR_APP_STATUS_EMPTY, + EDITOR_APP_STATUS_ERROR, + EDITOR_APP_STATUS_LOADING, + EDITOR_APP_STATUS_INVALID, + EDITOR_APP_STATUS_VALID, +} from '~/pipeline_editor/constants'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; - import { mockLintResponse, mockCiYml } from '../mock_data'; describe('Pipeline editor tabs component', () => { @@ -20,17 +27,27 @@ describe('Pipeline editor tabs component', () => { }, }; - const createComponent = ({ props = {}, provide = {}, mountFn = shallowMount } = {}) => { + const createComponent = ({ + props = {}, + provide = {}, + appStatus = EDITOR_APP_STATUS_VALID, + mountFn = shallowMount, + } = {}) => { wrapper = mountFn(PipelineEditorTabs, { propsData: { ciConfigData: mockLintResponse, ciFileContent: mockCiYml, - isCiConfigDataLoading: false, ...props, }, + data() { + return { + appStatus, + }; + }, provide: { ...mockProvide, ...provide }, stubs: { TextEditor: MockTextEditor, + EditorTab, }, }); }; @@ -49,7 +66,6 @@ describe('Pipeline editor tabs component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('editor tab', () => { @@ -69,7 +85,7 @@ describe('Pipeline editor tabs component', () => { describe('with feature flag on', () => { describe('while loading', () => { beforeEach(() => { - createComponent({ props: { isCiConfigDataLoading: true } }); + createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); }); it('displays a loading icon if the lint query is loading', () => { @@ -108,7 +124,7 @@ describe('Pipeline editor tabs component', () => { describe('lint tab', () => { describe('while loading', () => { beforeEach(() => { - createComponent({ props: { isCiConfigDataLoading: true } }); + createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); }); it('displays a loading icon if the lint query is loading', () => { @@ -135,7 +151,7 @@ describe('Pipeline editor tabs component', () => { describe('with feature flag on', () => { describe('while loading', () => { beforeEach(() => { - createComponent({ props: { isCiConfigDataLoading: true } }); + createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); }); it('displays a loading icon if the lint query is loading', () => { @@ -143,9 +159,9 @@ describe('Pipeline editor tabs component', () => { }); }); - describe('when `mergedYaml` is undefined', () => { + describe('when there is a fetch error', () => { beforeEach(() => { - createComponent({ props: { ciConfigData: {} } }); + createComponent({ appStatus: EDITOR_APP_STATUS_ERROR }); }); it('show an error message', () => { @@ -180,4 +196,24 @@ describe('Pipeline editor tabs component', () => { }); }); }); + + describe('show tab content based on status', () => { + it.each` + appStatus | editor | viz | lint | merged + ${undefined} | ${true} | ${true} | ${true} | ${true} + ${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${false} | ${false} + ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false} + ${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true} + `( + 'when status is $appStatus, we show - editor:$editor | viz:$viz | lint:$lint | merged:$merged ', + ({ appStatus, editor, viz, lint, merged }) => { + createComponent({ appStatus }); + + expect(findTextEditor().exists()).toBe(editor); + expect(findPipelineGraph().exists()).toBe(viz); + expect(findCiLint().exists()).toBe(lint); + expect(findMergedPreview().exists()).toBe(merged); + }, + ); + }); }); 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 291468c5229..8def83d578b 100644 --- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js +++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js @@ -1,12 +1,15 @@ -import { GlTabs } from '@gitlab/ui'; +import { GlAlert, GlTabs } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; - import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; const mockContent1 = 'MOCK CONTENT 1'; const mockContent2 = 'MOCK CONTENT 2'; +const MockEditorLite = { + template: '<div>EDITOR</div>', +}; + describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { let wrapper; let mockChildMounted = jest.fn(); @@ -37,22 +40,34 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { `, }; - const createWrapper = () => { + const createMockedWrapper = () => { wrapper = mount(MockTabbedContent); }; + const createWrapper = ({ props } = {}) => { + wrapper = mount(EditorTab, { + propsData: props, + slots: { + default: MockEditorLite, + }, + }); + }; + + const findSlotComponent = () => wrapper.findComponent(MockEditorLite); + const findAlert = () => wrapper.findComponent(GlAlert); + beforeEach(() => { mockChildMounted = jest.fn(); }); it('tabs are mounted lazily', async () => { - createWrapper(); + createMockedWrapper(); expect(mockChildMounted).toHaveBeenCalledTimes(0); }); it('first tab is only mounted after nextTick', async () => { - createWrapper(); + createMockedWrapper(); await nextTick(); @@ -60,6 +75,36 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { expect(mockChildMounted).toHaveBeenCalledWith(mockContent1); }); + describe('showing the tab content depending on `isEmpty` and `isInvalid`', () => { + it.each` + isEmpty | isInvalid | showSlotComponent | text + ${undefined} | ${undefined} | ${true} | ${'renders'} + ${false} | ${false} | ${true} | ${'renders'} + ${undefined} | ${true} | ${false} | ${'hides'} + ${true} | ${false} | ${false} | ${'hides'} + ${false} | ${true} | ${false} | ${'hides'} + `( + '$text the slot component when isEmpty:$isEmpty and isInvalid:$isInvalid', + ({ isEmpty, isInvalid, showSlotComponent }) => { + createWrapper({ + props: { isEmpty, isInvalid }, + }); + expect(findSlotComponent().exists()).toBe(showSlotComponent); + expect(findAlert().exists()).toBe(!showSlotComponent); + }, + ); + + it('can have a custom empty message', () => { + const text = 'my custom alert message'; + createWrapper({ props: { isEmpty: true, emptyMessage: text } }); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + expect(alert.text()).toBe(text); + }); + }); + describe('user interaction', () => { const clickTab = async (testid) => { wrapper.find(`[data-testid="${testid}"]`).trigger('click'); @@ -67,7 +112,7 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { }; beforeEach(() => { - createWrapper(); + createMockedWrapper(); }); it('mounts a tab once after selecting it', async () => { diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js index 196a4133eea..f0932fc55d3 100644 --- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js +++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js @@ -9,6 +9,7 @@ import { mockDefaultBranch, mockLintResponse, mockProjectFullPath, + mockProjectBranches, } from '../mock_data'; jest.mock('~/api', () => { @@ -47,21 +48,20 @@ describe('~/pipeline_editor/graphql/resolvers', () => { }); }); - describe('pipeline', () => { - it('resolves pipeline data with type names', async () => { - const result = await resolvers.Query.project(null); + 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 pipeline data with necessary data', async () => { - const result = await resolvers.Query.project(null); - const pipelineKeys = Object.keys(result.pipeline); - const statusKeys = Object.keys(result.pipeline.detailedStatus); + it('resolves project with available list of branches', async () => { + const result = await resolvers.Query.project(); - expect(pipelineKeys).toContain('id', 'commitPath', 'detailedStatus', 'shortSha'); - expect(statusKeys).toContain('detailsPath', 'text'); + expect(result.repository.branches).toHaveLength( + mockProjectBranches.repository.branches.length, + ); }); }); }); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index 16d5ba0e714..7f651a42231 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -138,6 +138,20 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => { }; }; +export const mockProjectBranches = { + __typename: 'Project', + repository: { + __typename: 'Repository', + branches: [ + { __typename: 'Branch', name: 'master' }, + { __typename: 'Branch', name: 'main' }, + { __typename: 'Branch', name: 'develop' }, + { __typename: 'Branch', name: 'production' }, + { __typename: 'Branch', name: 'test' }, + ], + }, +}; + export const mockProjectPipeline = { pipeline: { commitPath: '/-/commit/aabbccdd', diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index 887d296222f..d8e3436479c 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -2,8 +2,11 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import httpStatusCodes from '~/lib/utils/http_status'; +import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue'; +import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants'; import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; @@ -72,7 +75,7 @@ describe('Pipeline editor app component', () => { }); }; - const createComponentWithApollo = ({ props = {}, provide = {} } = {}) => { + const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => { const handlers = [[getCiConfigData, mockCiConfigData]]; const resolvers = { Query: { @@ -94,6 +97,8 @@ describe('Pipeline editor app component', () => { }; createComponent({ props, provide, options }); + + return waitForPromises(); }; const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); @@ -103,6 +108,7 @@ describe('Pipeline editor app component', () => { const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState); const findEmptyStateButton = () => wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton); + const findCodeSnippetAlert = () => wrapper.findComponent(CodeSnippetAlert); beforeEach(() => { mockBlobContentData = jest.fn(); @@ -116,11 +122,55 @@ describe('Pipeline editor app component', () => { wrapper.destroy(); }); - it('displays a loading icon if the blob query is loading', () => { - createComponent({ blobLoading: true }); + describe('loading state', () => { + it('displays a loading icon if the blob query is loading', () => { + createComponent({ blobLoading: true }); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findTextEditor().exists()).toBe(false); + }); + }); + + describe('code snippet alert', () => { + const setCodeSnippetUrlParam = (value) => { + global.jsdom.reconfigure({ + url: `${TEST_HOST}/?code_snippet_copied_from=${value}`, + }); + }; + + it('does not show by default', () => { + createComponent(); + + expect(findCodeSnippetAlert().exists()).toBe(false); + }); + + it.each(CODE_SNIPPET_SOURCES)('shows if URL param is %s, and cleans up URL', (source) => { + jest.spyOn(window.history, 'replaceState'); + setCodeSnippetUrlParam(source); + createComponent(); + + expect(findCodeSnippetAlert().exists()).toBe(true); + expect(window.history.replaceState).toHaveBeenCalledWith({}, document.title, `${TEST_HOST}/`); + }); + + it('does not show if URL param is invalid', () => { + setCodeSnippetUrlParam('foo_bar'); + createComponent(); + + expect(findCodeSnippetAlert().exists()).toBe(false); + }); + + it('disappears on dismiss', async () => { + setCodeSnippetUrlParam('api_fuzzing'); + createComponent(); + const alert = findCodeSnippetAlert(); + + expect(alert.exists()).toBe(true); + + await alert.vm.$emit('dismiss'); - expect(findLoadingIcon().exists()).toBe(true); - expect(findTextEditor().exists()).toBe(false); + expect(alert.exists()).toBe(false); + }); }); describe('when queries are called', () => { @@ -131,9 +181,7 @@ describe('Pipeline editor app component', () => { describe('when file exists', () => { beforeEach(async () => { - createComponentWithApollo(); - - await waitForPromises(); + await createComponentWithApollo(); }); it('shows pipeline editor home component', () => { @@ -145,10 +193,6 @@ describe('Pipeline editor app component', () => { }); it('ci config query is called with correct variables', async () => { - createComponentWithApollo(); - - await waitForPromises(); - expect(mockCiConfigData).toHaveBeenCalledWith({ content: mockCiYml, projectPath: mockProjectFullPath, @@ -164,9 +208,7 @@ describe('Pipeline editor app component', () => { status: httpStatusCodes.BAD_REQUEST, }, }); - createComponentWithApollo(); - - await waitForPromises(); + await createComponentWithApollo(); expect(findEmptyState().exists()).toBe(true); expect(findAlert().exists()).toBe(false); @@ -181,9 +223,7 @@ describe('Pipeline editor app component', () => { status: httpStatusCodes.NOT_FOUND, }, }); - createComponentWithApollo(); - - await waitForPromises(); + await createComponentWithApollo(); expect(findEmptyState().exists()).toBe(true); expect(findAlert().exists()).toBe(false); @@ -194,8 +234,7 @@ describe('Pipeline editor app component', () => { describe('because of a fetching error', () => { it('shows a unkown error message', async () => { mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); - createComponentWithApollo(); - await waitForPromises(); + await createComponentWithApollo(); expect(findEmptyState().exists()).toBe(false); expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]); @@ -212,7 +251,7 @@ describe('Pipeline editor app component', () => { }, }); - createComponentWithApollo({ + await createComponentWithApollo({ provide: { glFeatures: { pipelineEditorEmptyStateAction: true, @@ -220,8 +259,6 @@ describe('Pipeline editor app component', () => { }, }); - await waitForPromises(); - expect(findEmptyState().exists()).toBe(true); expect(findTextEditor().exists()).toBe(false); @@ -254,9 +291,9 @@ describe('Pipeline editor app component', () => { describe('and the commit mutation fails', () => { const commitFailedReasons = ['Commit failed']; - beforeEach(() => { + beforeEach(async () => { window.scrollTo = jest.fn(); - createComponent(); + await createComponentWithApollo(); findEditorHome().vm.$emit('showError', { type: COMMIT_FAILURE, @@ -278,9 +315,9 @@ describe('Pipeline editor app component', () => { describe('when an unknown error occurs', () => { const unknownReasons = ['Commit failed']; - beforeEach(() => { + beforeEach(async () => { window.scrollTo = jest.fn(); - createComponent(); + await createComponentWithApollo(); findEditorHome().vm.$emit('showError', { type: COMMIT_FAILURE, diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js index 9864f3c13f9..a1e3d24acfa 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; +import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants'; @@ -18,6 +19,7 @@ describe('Pipeline editor home wrapper', () => { ciConfigData: mockLintResponse, ciFileContent: mockCiYml, isCiConfigDataLoading: false, + isNewCiConfigFile: false, ...props, }, }); @@ -26,6 +28,7 @@ describe('Pipeline editor home wrapper', () => { const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); const findCommitSection = () => wrapper.findComponent(CommitSection); + const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav); afterEach(() => { wrapper.destroy(); @@ -37,6 +40,10 @@ describe('Pipeline editor home wrapper', () => { createComponent(); }); + it('shows the file nav', () => { + expect(findFileNav().exists()).toBe(true); + }); + it('shows the pipeline editor header', () => { expect(findPipelineEditorHeader().exists()).toBe(true); }); |