diff options
Diffstat (limited to 'spec/frontend/pipeline_editor/components')
6 files changed, 375 insertions, 14 deletions
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js index ae2a9e5065d..aae25a3aa6d 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js @@ -16,8 +16,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { ...props, }, - // attachToDocument is required for input/submit events - attachToDocument: mountFn === mount, + // attachTo is required for input/submit events + attachTo: mountFn === mount ? document.body : null, }); }; diff --git a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js new file mode 100644 index 00000000000..8a991d82018 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js @@ -0,0 +1,113 @@ +import { escape } from 'lodash'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { sprintf } from '~/locale'; +import ValidationSegment, { i18n } from '~/pipeline_editor/components/info/validation_segment.vue'; +import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; +import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data'; + +describe('~/pipeline_editor/components/info/validation_segment.vue', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = extendedWrapper( + shallowMount(ValidationSegment, { + provide: { + ymlHelpPagePath: mockYmlHelpPagePath, + }, + propsData: { + ciConfig: mergeUnwrappedCiConfig(), + loading: false, + ...props, + }, + }), + ); + }; + + const findIcon = () => wrapper.findComponent(GlIcon); + const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink'); + const findValidationMsg = () => wrapper.findByTestId('validationMsg'); + + it('shows the loading state', () => { + createComponent({ loading: true }); + + expect(wrapper.text()).toBe(i18n.loading); + }); + + describe('when config is valid', () => { + beforeEach(() => { + createComponent({}); + }); + + it('has check icon', () => { + expect(findIcon().props('name')).toBe('check'); + }); + + it('shows a message for valid state', () => { + expect(findValidationMsg().text()).toContain(i18n.valid); + }); + + it('shows the learn more link', () => { + expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath); + expect(findLearnMoreLink().text()).toBe(i18n.learnMore); + }); + }); + + describe('when config is not valid', () => { + beforeEach(() => { + createComponent({ + ciConfig: mergeUnwrappedCiConfig({ + status: CI_CONFIG_STATUS_INVALID, + }), + }); + }); + + it('has warning icon', () => { + expect(findIcon().props('name')).toBe('warning-solid'); + }); + + it('has message for invalid state', () => { + expect(findValidationMsg().text()).toBe(i18n.invalid); + }); + + it('shows an invalid state with an error', () => { + const firstError = 'First Error'; + const secondError = 'Second Error'; + + createComponent({ + ciConfig: mergeUnwrappedCiConfig({ + status: CI_CONFIG_STATUS_INVALID, + errors: [firstError, secondError], + }), + }); + + // 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', () => { + const evilError = '<script>evil();</script>'; + + createComponent({ + ciConfig: mergeUnwrappedCiConfig({ + status: CI_CONFIG_STATUS_INVALID, + errors: [evilError], + }), + }); + + 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'); + }); + }); +}); 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 e9c6ed60860..5e9471376bd 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 @@ -25,24 +25,51 @@ describe('CI Lint Results', () => { }; const findTable = () => wrapper.find(GlTable); - const findByTestId = selector => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`); - const findAllByTestId = selector => () => wrapper.findAll(`[data-testid="ci-lint-${selector}"]`); + const findByTestId = (selector) => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`); + const findAllByTestId = (selector) => () => + wrapper.findAll(`[data-testid="ci-lint-${selector}"]`); const findLinkToDoc = () => wrapper.find(GlLink); const findErrors = findByTestId('errors'); const findWarnings = findByTestId('warnings'); const findStatus = findByTestId('status'); const findOnlyExcept = findByTestId('only-except'); const findLintParameters = findAllByTestId('parameter'); + const findLintValues = findAllByTestId('value'); const findBeforeScripts = findAllByTestId('before-script'); const findScripts = findAllByTestId('script'); const findAfterScripts = findAllByTestId('after-script'); - const filterEmptyScripts = property => mockJobs.filter(job => job[property].length !== 0); + const filterEmptyScripts = (property) => mockJobs.filter((job) => job[property].length !== 0); afterEach(() => { wrapper.destroy(); wrapper = null; }); + describe('Empty results', () => { + it('renders with no jobs, errors or warnings defined', () => { + createComponent({ jobs: undefined, errors: undefined, warnings: undefined }, shallowMount); + expect(findTable().exists()).toBe(true); + }); + + it('renders when job has no properties defined', () => { + // job with no attributes such as `tagList` or `environment` + const job = { + stage: 'Stage Name', + name: 'test job', + }; + createComponent({ jobs: [job] }, mount); + + const param = findLintParameters().at(0); + const value = findLintValues().at(0); + + expect(param.text()).toBe(`${job.stage} Job - ${job.name}`); + + // This test should be updated once properties of each job are shown + // See https://gitlab.com/gitlab-org/gitlab/-/issues/291031 + expect(value.text()).toBe(''); + }); + }); + describe('Invalid results', () => { beforeEach(() => { createComponent({ valid: false, errors: mockErrors, warnings: mockWarnings }, mount); diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js new file mode 100644 index 00000000000..5ccf4bbdab4 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js @@ -0,0 +1,81 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { GlAlert, GlLink } from '@gitlab/ui'; +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) => { + wrapper = mountFn(CiLint, { + provide: { + lintHelpPagePath: mockLintHelpPagePath, + }, + propsData: { + ciConfig: mergeUnwrappedCiConfig(), + ...props, + }, + }); + }; + + const findAllByTestId = (selector) => wrapper.findAll(`[data-testid="${selector}"]`); + const findAlert = () => wrapper.find(GlAlert); + const findLintParameters = () => findAllByTestId('ci-lint-parameter'); + const findLintParameterAt = (i) => findLintParameters().at(i); + const findLintValueAt = (i) => findAllByTestId('ci-lint-value').at(i); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('Valid Results', () => { + beforeEach(() => { + createComponent({}, mount); + }); + + it('displays valid results', () => { + expect(findAlert().text()).toMatch('Status: Syntax is correct.'); + }); + + it('displays link to the right help page', () => { + expect(findAlert().find(GlLink).attributes('href')).toBe(mockLintHelpPagePath); + }); + + it('displays jobs', () => { + expect(findLintParameters()).toHaveLength(3); + + expect(findLintParameterAt(0).text()).toBe('Test Job - job_test_1'); + expect(findLintParameterAt(1).text()).toBe('Test Job - job_test_2'); + expect(findLintParameterAt(2).text()).toBe('Build Job - job_build'); + }); + + it('displays jobs details', () => { + expect(findLintParameters()).toHaveLength(3); + + expect(findLintValueAt(0).text()).toMatchInterpolatedText( + 'echo "test 1" Only policy: branches, tags When: on_success', + ); + expect(findLintValueAt(1).text()).toMatchInterpolatedText( + 'echo "test 2" Only policy: branches, tags When: on_success', + ); + expect(findLintValueAt(2).text()).toMatchInterpolatedText( + 'echo "build" Only policy: branches, tags When: on_success', + ); + }); + + it('displays invalid results', () => { + createComponent( + { + ciConfig: mergeUnwrappedCiConfig({ + status: CI_CONFIG_STATUS_INVALID, + }), + }, + mount, + ); + + expect(findAlert().text()).toMatch('Status: Syntax is incorrect.'); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js index 18f71ebc95c..9221d64c44b 100644 --- a/spec/frontend/pipeline_editor/components/text_editor_spec.js +++ b/spec/frontend/pipeline_editor/components/text_editor_spec.js @@ -1,30 +1,69 @@ import { shallowMount } from '@vue/test-utils'; -import EditorLite from '~/vue_shared/components/editor_lite.vue'; -import { mockCiYml } from '../mock_data'; +import { + mockCiConfigPath, + mockCiYml, + mockCommitSha, + mockProjectPath, + mockProjectNamespace, +} from '../mock_data'; import TextEditor from '~/pipeline_editor/components/text_editor.vue'; describe('~/pipeline_editor/components/text_editor.vue', () => { let wrapper; - const editorReadyListener = jest.fn(); - const createComponent = (attrs = {}, mountFn = shallowMount) => { + let editorReadyListener; + let mockUse; + let mockRegisterCiSchema; + + const MockEditorLite = { + template: '<div/>', + props: ['value', 'fileName'], + mounted() { + this.$emit('editor-ready'); + }, + methods: { + getEditor: () => ({ + use: mockUse, + registerCiSchema: mockRegisterCiSchema, + }), + }, + }; + + const createComponent = (opts = {}, mountFn = shallowMount) => { wrapper = mountFn(TextEditor, { + provide: { + projectPath: mockProjectPath, + projectNamespace: mockProjectNamespace, + }, + propsData: { + ciConfigPath: mockCiConfigPath, + commitSha: mockCommitSha, + }, attrs: { value: mockCiYml, - ...attrs, }, listeners: { 'editor-ready': editorReadyListener, }, + stubs: { + EditorLite: MockEditorLite, + }, + ...opts, }); }; - const findEditor = () => wrapper.find(EditorLite); + const findEditor = () => wrapper.find(MockEditorLite); + + beforeEach(() => { + editorReadyListener = jest.fn(); + mockUse = jest.fn(); + mockRegisterCiSchema = jest.fn(); - it('contains an editor', () => { createComponent(); + }); + it('contains an editor', () => { expect(findEditor().exists()).toBe(true); }); @@ -32,8 +71,18 @@ describe('~/pipeline_editor/components/text_editor.vue', () => { expect(findEditor().props('value')).toBe(mockCiYml); }); - it('editor is configured for .yml', () => { - expect(findEditor().props('fileName')).toBe('*.yml'); + it('editor is configured for the CI config path', () => { + expect(findEditor().props('fileName')).toBe(mockCiConfigPath); + }); + + it('editor is configured with syntax highligting', async () => { + expect(mockUse).toHaveBeenCalledTimes(1); + expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1); + expect(mockRegisterCiSchema).toHaveBeenCalledWith({ + projectNamespace: mockProjectNamespace, + projectPath: mockProjectPath, + ref: mockCommitSha, + }); }); it('bubbles up events', () => { diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js new file mode 100644 index 00000000000..d3d9bf08209 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js @@ -0,0 +1,91 @@ +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlTabs } from '@gitlab/ui'; + +import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; + +const mockContent1 = 'MOCK CONTENT 1'; +const mockContent2 = 'MOCK CONTENT 2'; + +describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { + let wrapper; + let mockChildMounted = jest.fn(); + + const MockChild = { + props: ['content'], + template: '<div>{{content}}</div>', + mounted() { + mockChildMounted(this.content); + }, + }; + + const MockTabbedContent = { + components: { + EditorTab, + GlTabs, + MockChild, + }, + template: ` + <gl-tabs> + <editor-tab :title-link-attributes="{ 'data-testid': 'tab1-btn' }" :lazy="true"> + <mock-child content="${mockContent1}"/> + </editor-tab> + <editor-tab :title-link-attributes="{ 'data-testid': 'tab2-btn' }" :lazy="true"> + <mock-child content="${mockContent2}"/> + </editor-tab> + </gl-tabs> + `, + }; + + const createWrapper = () => { + wrapper = mount(MockTabbedContent); + }; + + beforeEach(() => { + mockChildMounted = jest.fn(); + }); + + it('tabs are mounted lazily', async () => { + createWrapper(); + + expect(mockChildMounted).toHaveBeenCalledTimes(0); + }); + + it('first tab is only mounted after nextTick', async () => { + createWrapper(); + + await nextTick(); + + expect(mockChildMounted).toHaveBeenCalledTimes(1); + expect(mockChildMounted).toHaveBeenCalledWith(mockContent1); + }); + + describe('user interaction', () => { + const clickTab = async (testid) => { + wrapper.find(`[data-testid="${testid}"]`).trigger('click'); + await nextTick(); + }; + + beforeEach(() => { + createWrapper(); + }); + + it('mounts a tab once after selecting it', async () => { + await clickTab('tab2-btn'); + + expect(mockChildMounted).toHaveBeenCalledTimes(2); + expect(mockChildMounted).toHaveBeenNthCalledWith(1, mockContent1); + expect(mockChildMounted).toHaveBeenNthCalledWith(2, mockContent2); + }); + + it('mounts each tab once after selecting each', async () => { + await clickTab('tab2-btn'); + await clickTab('tab1-btn'); + await clickTab('tab2-btn'); + + expect(mockChildMounted).toHaveBeenCalledTimes(2); + expect(mockChildMounted).toHaveBeenNthCalledWith(1, mockContent1); + expect(mockChildMounted).toHaveBeenNthCalledWith(2, mockContent2); + }); + }); +}); |