diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 12:45:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 12:45:46 +0300 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /spec/frontend/pipeline_wizard | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'spec/frontend/pipeline_wizard')
5 files changed, 644 insertions, 0 deletions
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js new file mode 100644 index 00000000000..6496850b028 --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/commit_spec.js @@ -0,0 +1,282 @@ +import { GlButton, GlFormGroup } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { __, s__, sprintf } from '~/locale'; +import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper'; +import CommitStep, { i18n } from '~/pipeline_wizard/components/commit.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import createCommitMutation from '~/pipeline_wizard/queries/create_commit.graphql'; +import getFileMetadataQuery from '~/pipeline_wizard/queries/get_file_meta.graphql'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import flushPromises from 'helpers/flush_promises'; +import { + createCommitMutationErrorResult, + createCommitMutationResult, + fileQueryErrorResult, + fileQueryResult, + fileQueryEmptyResult, +} from '../mock/query_responses'; + +Vue.use(VueApollo); + +const COMMIT_MESSAGE_ADD_FILE = s__('PipelineWizardDefaultCommitMessage|Add %{filename}'); +const COMMIT_MESSAGE_UPDATE_FILE = s__('PipelineWizardDefaultCommitMessage|Update %{filename}'); + +describe('Pipeline Wizard - Commit Page', () => { + const createCommitMutationHandler = jest.fn(); + const $toast = { + show: jest.fn(), + }; + + let wrapper; + + const getMockApollo = (scenario = {}) => { + return createMockApollo([ + [ + createCommitMutation, + createCommitMutationHandler.mockResolvedValue( + scenario.commitHasError ? createCommitMutationErrorResult : createCommitMutationResult, + ), + ], + [ + getFileMetadataQuery, + (vars) => { + if (scenario.fileResultByRef) return scenario.fileResultByRef[vars.ref]; + if (scenario.hasError) return fileQueryErrorResult; + return scenario.fileExists ? fileQueryResult : fileQueryEmptyResult; + }, + ], + ]); + }; + const createComponent = (props = {}, mockApollo = getMockApollo()) => { + wrapper = mountExtended(CommitStep, { + apolloProvider: mockApollo, + propsData: { + projectPath: 'some/path', + defaultBranch: 'main', + filename: 'newFile.yml', + ...props, + }, + mocks: { $toast }, + stubs: { + RefSelector: true, + GlFormGroup, + }, + }); + }; + + function getButtonWithLabel(label) { + return wrapper.findAllComponents(GlButton).filter((n) => n.text().match(label)); + } + + describe('ui setup', () => { + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('shows a commit message input with the correct label', () => { + expect(wrapper.findByTestId('commit_message').exists()).toBe(true); + expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel); + }); + + it('shows a branch selector with the correct label', () => { + expect(wrapper.findByTestId('branch').exists()).toBe(true); + expect(wrapper.find('label[for="branch"]').text()).toBe(i18n.branchSelectorLabel); + }); + + it('shows a commit button', () => { + expect(getButtonWithLabel(i18n.commitButtonLabel).exists()).toBe(true); + }); + + it('shows a back button', () => { + expect(getButtonWithLabel(__('Back')).exists()).toBe(true); + }); + + it('does not show a next button', () => { + expect(getButtonWithLabel(__('Next')).exists()).toBe(false); + }); + }); + + describe('loading the remote file', () => { + const projectPath = 'foo/bar'; + const filename = 'foo.yml'; + + it('does not show a load error if call is successful', async () => { + createComponent({ projectPath, filename }); + await flushPromises(); + expect(wrapper.findByTestId('load-error').exists()).not.toBe(true); + }); + + it('shows a load error if call returns an unexpected error', async () => { + const branch = 'foo'; + createComponent( + { defaultBranch: branch, projectPath, filename }, + createMockApollo([[getFileMetadataQuery, () => fileQueryErrorResult]]), + ); + await flushPromises(); + expect(wrapper.findByTestId('load-error').exists()).toBe(true); + expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError); + }); + + afterEach(() => { + wrapper.destroy(); + }); + }); + + describe('commit result handling', () => { + describe('successful commit', () => { + beforeEach(async () => { + createComponent(); + await flushPromises(); + await getButtonWithLabel(__('Commit')).trigger('click'); + await flushPromises(); + }); + + it('will not show an error', async () => { + expect(wrapper.findByTestId('commit-error').exists()).not.toBe(true); + }); + + it('will show a toast message', () => { + expect($toast.show).toHaveBeenCalledWith( + s__('PipelineWizard|The file has been committed.'), + ); + }); + + it('emits a done event', () => { + expect(wrapper.emitted().done.length).toBe(1); + }); + + afterEach(() => { + wrapper.destroy(); + jest.clearAllMocks(); + }); + }); + + describe('failed commit', () => { + beforeEach(async () => { + createComponent({}, getMockApollo({ commitHasError: true })); + await flushPromises(); + await getButtonWithLabel(__('Commit')).trigger('click'); + await flushPromises(); + }); + + it('will show an error', async () => { + expect(wrapper.findByTestId('commit-error').exists()).toBe(true); + expect(wrapper.findByTestId('commit-error').text()).toBe(i18n.errors.commitError); + }); + + it('will not show a toast message', () => { + expect($toast.show).not.toHaveBeenCalledWith(i18n.commitSuccessMessage); + }); + + it('will not emit a done event', () => { + expect(wrapper.emitted().done?.length).toBeFalsy(); + }); + + afterEach(() => { + wrapper.destroy(); + jest.clearAllMocks(); + }); + }); + }); + + describe('modelling different input combinations', () => { + const projectPath = 'some/path'; + const defaultBranch = 'foo'; + const fileContent = 'foo: bar'; + + describe.each` + filename | fileExistsOnDefaultBranch | fileExistsOnInputtedBranch | fileLoadError | commitMessageInputValue | branchInputValue | expectedCommitBranch | expectedCommitMessage | expectedAction + ${'foo.yml'} | ${false} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'CREATE'} + ${'foo.yml'} | ${true} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'UPDATE'} + ${'foo.yml'} | ${false} | ${true} | ${false} | ${'foo'} | ${'dev'} | ${'dev'} | ${'foo'} | ${'UPDATE'} + ${'foo.yml'} | ${false} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_ADD_FILE} | ${'CREATE'} + ${'foo.yml'} | ${true} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'} + ${'foo.yml'} | ${false} | ${true} | ${false} | ${null} | ${'dev'} | ${'dev'} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'} + `( + 'Test with fileExistsOnDefaultBranch=$fileExistsOnDefaultBranch, fileExistsOnInputtedBranch=$fileExistsOnInputtedBranch, commitMessageInputValue=$commitMessageInputValue, branchInputValue=$branchInputValue, commitReturnsError=$commitReturnsError', + ({ + filename, + fileExistsOnDefaultBranch, + fileExistsOnInputtedBranch, + commitMessageInputValue, + branchInputValue, + expectedCommitBranch, + expectedCommitMessage, + expectedAction, + }) => { + let consoleSpy; + + beforeAll(async () => { + createComponent( + { + filename, + defaultBranch, + projectPath, + fileContent, + }, + getMockApollo({ + fileResultByRef: { + [defaultBranch]: fileExistsOnDefaultBranch ? fileQueryResult : fileQueryEmptyResult, + [branchInputValue]: fileExistsOnInputtedBranch + ? fileQueryResult + : fileQueryEmptyResult, + }, + }), + ); + + await flushPromises(); + + consoleSpy = jest.spyOn(console, 'error'); + + await wrapper + .findByTestId('commit_message') + .get('textarea') + .setValue(commitMessageInputValue); + + if (branchInputValue) { + await wrapper.getComponent(RefSelector).vm.$emit('input', branchInputValue); + } + await Vue.nextTick(); + + await flushPromises(); + }); + + afterAll(() => { + wrapper.destroy(); + }); + + it('sets up without error', async () => { + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('does not show a load error', async () => { + expect(wrapper.findByTestId('load-error').exists()).not.toBe(true); + }); + + it('sends the expected commit mutation', async () => { + await getButtonWithLabel(__('Commit')).trigger('click'); + + expect(createCommitMutationHandler).toHaveBeenCalledWith({ + input: { + actions: [ + { + action: expectedAction, + content: fileContent, + filePath: `/${filename}`, + }, + ], + branch: expectedCommitBranch, + message: sprintf(expectedCommitMessage, { filename }), + projectPath, + }, + }); + }); + }, + ); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js new file mode 100644 index 00000000000..446412a4f02 --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/editor_spec.js @@ -0,0 +1,69 @@ +import { mount } from '@vue/test-utils'; +import { Document } from 'yaml'; +import YamlEditor from '~/pipeline_wizard/components/editor.vue'; + +describe('Pages Yaml Editor wrapper', () => { + const defaultOptions = { + propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' }, + }; + + describe('mount hook', () => { + const wrapper = mount(YamlEditor, defaultOptions); + + it('editor is mounted', () => { + expect(wrapper.vm.editor).not.toBeFalsy(); + expect(wrapper.find('.gl-source-editor').exists()).toBe(true); + }); + }); + + describe('watchers', () => { + describe('doc', () => { + const doc = new Document({ baz: ['bar'] }); + let wrapper; + + beforeEach(() => { + wrapper = mount(YamlEditor, defaultOptions); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it("causes the editor's value to be set to the stringified document", async () => { + await wrapper.setProps({ doc }); + expect(wrapper.vm.editor.getValue()).toEqual(doc.toString()); + }); + + it('emits an update:yaml event with the yaml representation of doc', async () => { + await wrapper.setProps({ doc }); + const changeEvents = wrapper.emitted('update:yaml'); + expect(changeEvents[2]).toEqual([doc.toString()]); + }); + + it('does not cause the touch event to be emitted', () => { + wrapper.setProps({ doc }); + expect(wrapper.emitted('touch')).not.toBeTruthy(); + }); + }); + + describe('highlight', () => { + const highlight = 'foo'; + const wrapper = mount(YamlEditor, defaultOptions); + + it('calls editor.highlight(path, keep=true)', async () => { + const highlightSpy = jest.spyOn(wrapper.vm.yamlEditorExtension.obj, 'highlight'); + await wrapper.setProps({ highlight }); + expect(highlightSpy).toHaveBeenCalledWith(expect.anything(), highlight, true); + }); + }); + }); + + describe('events', () => { + const wrapper = mount(YamlEditor, defaultOptions); + + it('emits touch if content is changed in editor', async () => { + await wrapper.vm.editor.setValue('foo: boo'); + expect(wrapper.emitted('touch')).toBeTruthy(); + }); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/step_nav_spec.js b/spec/frontend/pipeline_wizard/components/step_nav_spec.js new file mode 100644 index 00000000000..c6eac1386fa --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/step_nav_spec.js @@ -0,0 +1,79 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import StepNav from '~/pipeline_wizard/components/step_nav.vue'; + +describe('Pipeline Wizard - Step Navigation Component', () => { + const defaultProps = { showBackButton: true, showNextButton: true }; + + let wrapper; + let prevButton; + let nextButton; + + const createComponent = (props = {}) => { + wrapper = mountExtended(StepNav, { + propsData: { + ...defaultProps, + ...props, + }, + }); + prevButton = wrapper.findByTestId('back-button'); + nextButton = wrapper.findByTestId('next-button'); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + scenario | showBackButton | showNextButton + ${'does not show prev button'} | ${false} | ${false} + ${'has prev, but not next'} | ${true} | ${false} + ${'has next, but not prev'} | ${false} | ${true} + ${'has both next and prev'} | ${true} | ${true} + `('$scenario', async ({ showBackButton, showNextButton }) => { + createComponent({ showBackButton, showNextButton }); + + expect(prevButton.exists()).toBe(showBackButton); + expect(nextButton.exists()).toBe(showNextButton); + }); + + it('shows the expected button text', () => { + createComponent(); + + expect(prevButton.text()).toBe('Back'); + expect(nextButton.text()).toBe('Next'); + }); + + it('emits "back" events when clicking prev button', async () => { + createComponent(); + + await prevButton.trigger('click'); + expect(wrapper.emitted().back.length).toBe(1); + }); + + it('emits "next" events when clicking next button', async () => { + createComponent(); + + await nextButton.trigger('click'); + expect(wrapper.emitted().next.length).toBe(1); + }); + + it('enables the next button if nextButtonEnabled ist set to true', async () => { + createComponent({ nextButtonEnabled: true }); + + expect(nextButton.attributes('disabled')).not.toBe('disabled'); + }); + + it('disables the next button if nextButtonEnabled ist set to false', async () => { + createComponent({ nextButtonEnabled: false }); + + expect(nextButton.attributes('disabled')).toBe('disabled'); + }); + + it('does not emit "next" event when clicking next button while nextButtonEnabled ist set to false', async () => { + createComponent({ nextButtonEnabled: false }); + + await nextButton.trigger('click'); + + expect(wrapper.emitted().next).toBe(undefined); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/widgets/text_spec.js b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js new file mode 100644 index 00000000000..a11c0214d15 --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js @@ -0,0 +1,152 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import TextWidget from '~/pipeline_wizard/components/widgets/text.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +describe('Pipeline Wizard - Text Widget', () => { + const defaultProps = { + label: 'This label', + description: 'some description', + placeholder: 'some placeholder', + pattern: '^[a-z]+$', + invalidFeedback: 'some feedback', + }; + + let wrapper; + + const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); + const findGlFormGroupInvalidFeedback = () => findGlFormGroup().find('.invalid-feedback'); + const findGlFormInput = () => wrapper.findComponent(GlFormInput); + + const createComponent = (props = {}, mountFn = mountExtended) => { + wrapper = mountFn(TextWidget, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + it('creates an input element with the correct label', () => { + createComponent(); + + expect(wrapper.findByLabelText(defaultProps.label).exists()).toBe(true); + }); + + it('passes the description', () => { + createComponent({}, shallowMount); + + expect(findGlFormGroup().attributes('description')).toBe(defaultProps.description); + }); + + it('sets the "text" type on the input component', () => { + createComponent(); + + expect(findGlFormInput().attributes('type')).toBe('text'); + }); + + it('passes the placeholder', () => { + createComponent(); + + expect(findGlFormInput().attributes('placeholder')).toBe(defaultProps.placeholder); + }); + + it('emits an update event on input', async () => { + createComponent(); + + const localValue = 'somevalue'; + await findGlFormInput().setValue(localValue); + + expect(wrapper.emitted('input')).toEqual([[localValue]]); + }); + + it('passes invalid feedback message', () => { + createComponent(); + + expect(findGlFormGroupInvalidFeedback().text()).toBe(defaultProps.invalidFeedback); + }); + + it('provides invalid feedback', async () => { + createComponent({ validate: true }); + + await findGlFormInput().setValue('invalid%99'); + + expect(findGlFormGroup().classes()).toContain('is-invalid'); + expect(findGlFormInput().classes()).toContain('is-invalid'); + }); + + it('provides valid feedback', async () => { + createComponent({ validate: true }); + + await findGlFormInput().setValue('valid'); + + expect(findGlFormGroup().classes()).toContain('is-valid'); + expect(findGlFormInput().classes()).toContain('is-valid'); + }); + + it('does not show validation state when untouched', () => { + createComponent({ value: 'invalid99' }); + + expect(findGlFormGroup().classes()).not.toContain('is-valid'); + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + }); + + it('shows invalid state on blur', async () => { + createComponent(); + + await findGlFormInput().setValue('invalid%99'); + + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + + await findGlFormInput().trigger('blur'); + + expect(findGlFormInput().classes()).toContain('is-invalid'); + expect(findGlFormGroup().classes()).toContain('is-invalid'); + }); + + it('shows invalid state when toggling `validate` prop', async () => { + createComponent({ + required: true, + validate: false, + }); + + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + + await wrapper.setProps({ validate: true }); + + expect(findGlFormGroup().classes()).toContain('is-invalid'); + }); + + it('does not update validation if not required', async () => { + createComponent({ + pattern: null, + validate: true, + }); + + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + }); + + it('sets default value', () => { + const defaultValue = 'foo'; + createComponent({ + default: defaultValue, + }); + + expect(wrapper.findByLabelText(defaultProps.label).element.value).toBe(defaultValue); + }); + + it('emits default value on setup', () => { + const defaultValue = 'foo'; + createComponent({ + default: defaultValue, + }); + + expect(wrapper.emitted('input')).toEqual([[defaultValue]]); + }); +}); diff --git a/spec/frontend/pipeline_wizard/mock/query_responses.js b/spec/frontend/pipeline_wizard/mock/query_responses.js new file mode 100644 index 00000000000..95dcb881a04 --- /dev/null +++ b/spec/frontend/pipeline_wizard/mock/query_responses.js @@ -0,0 +1,62 @@ +export const createCommitMutationResult = { + data: { + commitCreate: { + commit: { + id: '82a9df1', + }, + content: 'foo: bar', + errors: null, + }, + }, +}; + +export const createCommitMutationErrorResult = { + data: { + commitCreate: { + commit: null, + content: null, + errors: ['Some Error Message'], + }, + }, +}; + +export const fileQueryResult = { + data: { + project: { + id: 'gid://gitlab/Project/1', + repository: { + blobs: { + nodes: [ + { + id: 'gid://gitlab/Blob/9ff96777b315cd37188f7194d8382c718cb2933c', + }, + ], + }, + }, + }, + }, +}; + +export const fileQueryEmptyResult = { + data: { + project: { + id: 'gid://gitlab/Project/2', + repository: { + blobs: { + nodes: [], + }, + }, + }, + }, +}; + +export const fileQueryErrorResult = { + data: { + foo: 'bar', + project: { + id: null, + repository: null, + }, + }, + errors: [{ message: 'GraphQL Error' }], +}; |