Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 12:45:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 12:45:46 +0300
commita7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch)
tree7452bd5c3545c2fa67a28aa013835fb4fa071baf /spec/frontend/pipeline_wizard
parentee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff)
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'spec/frontend/pipeline_wizard')
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js282
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js69
-rw-r--r--spec/frontend/pipeline_wizard/components/step_nav_spec.js79
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/text_spec.js152
-rw-r--r--spec/frontend/pipeline_wizard/mock/query_responses.js62
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' }],
+};