Commit 20f7e1a0 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '207463-edit-form-connected' into 'master'

Snippet edit form

See merge request gitlab-org/gitlab!28600
parents a792e9ed 78f1d2fb
<script> <script>
import { initEditorLite } from '~/blob/utils'; import { initEditorLite } from '~/blob/utils';
import { debounce } from 'lodash';
export default { export default {
props: { props: {
...@@ -32,16 +33,14 @@ export default { ...@@ -32,16 +33,14 @@ export default {
}); });
}, },
methods: { methods: {
triggerFileChange() { triggerFileChange: debounce(function debouncedFileChange() {
this.$emit('input', this.editor.getValue()); this.$emit('input', this.editor.getValue());
}, }, 250),
}, },
}; };
</script> </script>
<template> <template>
<div class="file-content code"> <div class="file-content code">
<pre id="editor" ref="editor" data-editor-loading @focusout="triggerFileChange">{{ <pre id="editor" ref="editor" data-editor-loading @keyup="triggerFileChange">{{ value }}</pre>
value
}}</pre>
</div> </div>
</template> </template>
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import initSnippet from '~/snippet/snippet_bundle'; import initSnippet from '~/snippet/snippet_bundle';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import GLForm from '~/gl_form'; import GLForm from '~/gl_form';
import { SnippetEditInit } from '~/snippets';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('.snippet-form'); const form = document.querySelector('.snippet-form');
...@@ -17,9 +18,15 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -17,9 +18,15 @@ document.addEventListener('DOMContentLoaded', () => {
const projectSnippetOptions = {}; const projectSnippetOptions = {};
const options = const options =
form.dataset.snippetType === 'project' ? projectSnippetOptions : personalSnippetOptions; form.dataset.snippetType === 'project' || form.dataset.projectPath
? projectSnippetOptions
: personalSnippetOptions;
initSnippet(); if (gon?.features?.snippetsEditVue) {
SnippetEditInit();
} else {
initSnippet();
new GLForm($(form), options); // eslint-disable-line no-new
}
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($(form), options); // eslint-disable-line no-new
}); });
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import Flash from '~/flash';
import { __, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import TitleField from '~/vue_shared/components/form/title.vue';
import { getBaseURL, joinPaths, redirectTo } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import { SNIPPET_VISIBILITY_PRIVATE } from '../constants';
import SnippetBlobEdit from './snippet_blob_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
export default {
components: {
SnippetDescriptionEdit,
SnippetVisibilityEdit,
SnippetBlobEdit,
TitleField,
FormFooterActions,
GlButton,
GlLoadingIcon,
},
mixins: [getSnippetMixin],
props: {
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
visibilityHelpLink: {
type: String,
default: '',
required: false,
},
projectPath: {
type: String,
default: '',
required: false,
},
},
data() {
return {
blob: {},
fileName: '',
content: '',
isContentLoading: true,
isUpdating: false,
newSnippet: false,
};
},
computed: {
updatePrevented() {
return this.snippet.title === '' || this.content === '' || this.isUpdating;
},
isProjectSnippet() {
return Boolean(this.projectPath);
},
apiData() {
return {
id: this.snippet.id,
title: this.snippet.title,
description: this.snippet.description,
visibilityLevel: this.snippet.visibilityLevel,
fileName: this.fileName,
content: this.content,
};
},
saveButtonLabel() {
if (this.newSnippet) {
return __('Create snippet');
}
return this.isUpdating ? __('Saving') : __('Save changes');
},
cancelButtonHref() {
return this.projectPath ? `/${this.projectPath}/snippets` : `/snippets`;
},
titleFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_title`;
},
descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
},
methods: {
updateFileName(newName) {
this.fileName = newName;
},
flashAPIFailure(err) {
Flash(sprintf(__("Can't update snippet: %{err}"), { err }));
},
onNewSnippetFetched() {
this.newSnippet = true;
this.snippet = this.$options.newSnippetSchema;
this.blob = this.snippet.blob;
this.isContentLoading = false;
},
onExistingSnippetFetched() {
this.newSnippet = false;
const { blob } = this.snippet;
this.blob = blob;
this.fileName = blob.name;
const baseUrl = getBaseURL();
const url = joinPaths(baseUrl, blob.rawPath);
axios
.get(url)
.then(res => {
this.content = res.data;
this.isContentLoading = false;
})
.catch(e => this.flashAPIFailure(e));
},
onSnippetFetch(snippetRes) {
if (snippetRes.data.snippets.edges.length === 0) {
this.onNewSnippetFetched();
} else {
this.onExistingSnippetFetched();
}
},
handleFormSubmit() {
this.isUpdating = true;
this.$apollo
.mutate({
mutation: this.newSnippet ? CreateSnippetMutation : UpdateSnippetMutation,
variables: {
input: {
...this.apiData,
projectPath: this.newSnippet ? this.projectPath : undefined,
},
},
})
.then(({ data }) => {
const baseObj = this.newSnippet ? data?.createSnippet : data?.updateSnippet;
const errors = baseObj?.errors;
if (errors.length) {
this.flashAPIFailure(errors[0]);
}
redirectTo(baseObj.snippet.webUrl);
})
.catch(e => {
this.isUpdating = false;
this.flashAPIFailure(e);
});
},
},
newSnippetSchema: {
title: '',
description: '',
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
blob: {},
},
};
</script>
<template>
<form
class="snippet-form js-requires-input js-quick-submit common-note-form"
:data-snippet-type="isProjectSnippet ? 'project' : 'personal'"
>
<gl-loading-icon
v-if="isLoading"
:label="__('Loading snippet')"
size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<template v-else>
<title-field :id="titleFieldId" v-model="snippet.title" required :autofocus="true" />
<snippet-description-edit
:id="descriptionFieldId"
v-model="snippet.description"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
/>
<snippet-blob-edit
v-model="content"
:file-name="fileName"
:is-loading="isContentLoading"
@name-change="updateFileName"
/>
<snippet-visibility-edit
v-model="snippet.visibilityLevel"
:help-link="visibilityHelpLink"
:is-project-snippet="isProjectSnippet"
/>
<form-footer-actions>
<template #prepend>
<gl-button
type="submit"
category="primary"
variant="success"
:disabled="updatePrevented"
@click="handleFormSubmit"
>{{ saveButtonLabel }}</gl-button
>
</template>
<template #append>
<gl-button :href="cancelButtonHref">{{ __('Cancel') }}</gl-button>
</template>
</form-footer-actions>
</template>
</form>
</template>
...@@ -50,7 +50,6 @@ export default { ...@@ -50,7 +50,6 @@ export default {
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
> >
<textarea <textarea
id="snippet-description"
slot="textarea" slot="textarea"
class="note-textarea js-gfm-input js-autosize markdown-area class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea" qa-description-textarea"
...@@ -59,6 +58,7 @@ export default { ...@@ -59,6 +58,7 @@ export default {
:value="value" :value="value"
:aria-label="__('Description')" :aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')" :placeholder="__('Write a comment or drag your files here…')"
v-bind="$attrs"
@input="$emit('input', $event.target.value)" @input="$emit('input', $event.target.value)"
> >
</textarea> </textarea>
......
...@@ -3,7 +3,8 @@ import Translate from '~/vue_shared/translate'; ...@@ -3,7 +3,8 @@ import Translate from '~/vue_shared/translate';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import SnippetsApp from './components/show.vue'; import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue';
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(Translate); Vue.use(Translate);
...@@ -31,7 +32,11 @@ function appFactory(el, Component) { ...@@ -31,7 +32,11 @@ function appFactory(el, Component) {
} }
export const SnippetShowInit = () => { export const SnippetShowInit = () => {
appFactory(document.getElementById('js-snippet-view'), SnippetsApp); appFactory(document.getElementById('js-snippet-view'), SnippetsShow);
};
export const SnippetEditInit = () => {
appFactory(document.getElementById('js-snippet-edit'), SnippetsEdit);
}; };
export default () => {}; export default () => {};
mutation CreateSnippet($input: CreateSnippetInput!) {
createSnippet(input: $input) {
errors
snippet {
webUrl
}
}
}
mutation UpdateSnippet($input: UpdateSnippetInput!) {
updateSnippet(input: $input) {
errors
snippet {
webUrl
}
}
}
...@@ -10,6 +10,6 @@ export default { ...@@ -10,6 +10,6 @@ export default {
</script> </script>
<template> <template>
<gl-form-group :label="__('Title')" label-for="title-field-edit"> <gl-form-group :label="__('Title')" label-for="title-field-edit">
<gl-form-input id="title-field-edit" v-bind="$attrs" v-on="$listeners" /> <gl-form-input v-bind="$attrs" v-on="$listeners" />
</gl-form-group> </gl-form-group>
</template> </template>
- content_for :page_specific_javascripts do - if Feature.disabled?(:monaco_snippets)
= page_specific_javascript_tag('lib/ace.js') - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
.snippet-form-holder
= form_for @snippet, url: url, - if Feature.enabled?(:snippets_edit_vue)
html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" }, #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
data: { "snippet-type": @snippet.project_id ? 'project' : 'personal'} do |f| - else
= form_errors(@snippet) .snippet-form-holder
= form_for @snippet, url: url,
.form-group html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
= f.label :title, class: 'label-bold' data: { "snippet-type": @snippet.project_id ? 'project' : 'personal'} do |f|
= f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true = form_errors(@snippet)
.form-group.js-description-input .form-group
- description_placeholder = s_('Snippets|Optionally add a description about what your snippet does or how to use it...') = f.label :title, class: 'label-bold'
- is_expanded = @snippet.description && !@snippet.description.empty? = f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true
= f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold'
.js-collapsible-input .form-group.js-description-input
.js-collapsed{ class: ('d-none' if is_expanded) } - description_placeholder = s_('Snippets|Optionally add a description about what your snippet does or how to use it...')
= text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder, data: { qa_selector: 'description_placeholder' } - is_expanded = @snippet.description && !@snippet.description.empty?
.js-expanded{ class: ('d-none' if !is_expanded) } = f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold'
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do .js-collapsible-input
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder, qa_selector: 'description_field' .js-collapsed{ class: ('d-none' if is_expanded) }
= render 'shared/notes/hints' = text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder, data: { qa_selector: 'description_placeholder' }
.js-expanded{ class: ('d-none' if !is_expanded) }
.form-group.file-editor = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= f.label :file_name, s_('Snippets|File') = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder, qa_selector: 'description_field'
.file-holder.snippet = render 'shared/notes/hints'
.js-file-title.file-title-flex-parent
= f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control js-snippet-file-name qa-snippet-file-name' .form-group.file-editor
.file-content.code = f.label :file_name, s_('Snippets|File')
%pre#editor{ data: { 'editor-loading': true } }= @snippet.content .file-holder.snippet
= f.hidden_field :content, class: 'snippet-file-content' .js-file-title.file-title-flex-parent
= f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control js-snippet-file-name qa-snippet-file-name'
.form-group .file-content.code
.font-weight-bold %pre#editor{ data: { 'editor-loading': true } }= @snippet.content
= _('Visibility level') = f.hidden_field :content, class: 'snippet-file-content'
= link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank'
= render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false .form-group
.font-weight-bold
- if params[:files] = _('Visibility level')
- params[:files].each_with_index do |file, index| = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank'
= hidden_field_tag "files[]", file, id: "files_#{index}" = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false
.form-actions - if params[:files]
- if @snippet.new_record? - params[:files].each_with_index do |file, index|
= f.submit 'Create snippet', class: "btn-success btn qa-create-snippet-button" = hidden_field_tag "files[]", file, id: "files_#{index}"
- else
= f.submit 'Save changes', class: "btn-success btn" .form-actions
- if @snippet.new_record?
- if @snippet.project_id = f.submit 'Create snippet', class: "btn-success btn qa-create-snippet-button"
= link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel" - else
- else = f.submit 'Save changes', class: "btn-success btn"
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
- if @snippet.project_id
= link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel"
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
---
title: Refactored Snippet edit form to Vue
merge_request: 28600
author:
type: added
...@@ -3408,6 +3408,9 @@ msgstr "" ...@@ -3408,6 +3408,9 @@ msgstr ""
msgid "Can't scan the code?" msgid "Can't scan the code?"
msgstr "" msgstr ""
msgid "Can't update snippet: %{err}"
msgstr ""
msgid "Canary" msgid "Canary"
msgstr "" msgstr ""
...@@ -6070,6 +6073,9 @@ msgstr "" ...@@ -6070,6 +6073,9 @@ msgstr ""
msgid "Create requirement" msgid "Create requirement"
msgstr "" msgstr ""
msgid "Create snippet"
msgstr ""
msgid "Create wildcard: %{searchTerm}" msgid "Create wildcard: %{searchTerm}"
msgstr "" msgstr ""
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
shared_examples_for 'snippet editor' do shared_examples_for 'snippet editor' do
before do before do
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(monaco_snippets: flag) stub_feature_flags(monaco_snippets: flag)
end end
......
...@@ -11,6 +11,7 @@ describe 'Projects > Snippets > User updates a snippet', :js do ...@@ -11,6 +11,7 @@ describe 'Projects > Snippets > User updates a snippet', :js do
before do before do
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(version_snippets: version_snippet_enabled) stub_feature_flags(version_snippets: version_snippet_enabled)
project.add_maintainer(user) project.add_maintainer(user)
......
...@@ -10,6 +10,7 @@ shared_examples_for 'snippet editor' do ...@@ -10,6 +10,7 @@ shared_examples_for 'snippet editor' do
before do before do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(monaco_snippets: flag) stub_feature_flags(monaco_snippets: flag)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
......
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
shared_examples_for 'snippet editor' do shared_examples_for 'snippet editor' do
before do before do
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(monaco_snippets: flag) stub_feature_flags(monaco_snippets: flag)
sign_in(user) sign_in(user)
visit new_snippet_path visit new_snippet_path
......
...@@ -14,6 +14,7 @@ describe 'User edits snippet', :js do ...@@ -14,6 +14,7 @@ describe 'User edits snippet', :js do
before do before do
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(version_snippets: version_snippet_enabled) stub_feature_flags(version_snippets: version_snippet_enabled)
sign_in(user) sign_in(user)
......
...@@ -80,7 +80,7 @@ describe('Blob Header Editing', () => { ...@@ -80,7 +80,7 @@ describe('Blob Header Editing', () => {
getValue: jest.fn().mockReturnValue(value), getValue: jest.fn().mockReturnValue(value),
}; };
editorEl.trigger('focusout'); editorEl.trigger('keyup');
return nextTick().then(() => { return nextTick().then(() => {
expect(wrapper.emitted().input[0]).toEqual([value]); expect(wrapper.emitted().input[0]).toEqual([value]);
......
export const triggerDOMEvent = type => {
window.document.dispatchEvent(
new Event(type, {
bubbles: true,
cancelable: true,
}),
);
};
export default () => {};
import '~/snippet/snippet_edit';
import { SnippetEditInit } from '~/snippets';
import initSnippet from '~/snippet/snippet_bundle';
import { triggerDOMEvent } from 'jest/helpers/dom_events_helper';
jest.mock('~/snippet/snippet_bundle');
jest.mock('~/snippets');
describe('Snippet edit form initialization', () => {
const setFF = flag => {
gon.features = { snippetsEditVue: flag };
};
let features;
beforeEach(() => {
features = gon.features;
setFixtures('<div class="snippet-form"></div>');
});
afterEach(() => {
gon.features = features;
});
it.each`
name | flag | isVue
${'Regular'} | ${false} | ${false}
${'Vue'} | ${true} | ${true}
`('correctly initializes $name Snippet Edit form', ({ flag, isVue }) => {
initSnippet.mockClear();
SnippetEditInit.mockClear();
setFF(flag);
triggerDOMEvent('DOMContentLoaded');
if (isVue) {
expect(initSnippet).not.toHaveBeenCalled();
expect(SnippetEditInit).toHaveBeenCalled();
} else {
expect(initSnippet).toHaveBeenCalled();
expect(SnippetEditInit).not.toHaveBeenCalled();
}
});
});
...@@ -39,7 +39,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = ...@@ -39,7 +39,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
qa-description-textarea" qa-description-textarea"
data-supports-quick-actions="false" data-supports-quick-actions="false"
dir="auto" dir="auto"
id="snippet-description"
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
/> />
</markdown-field-stub> </markdown-field-stub>
......
import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { joinPaths, redirectTo } from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import TitleField from '~/vue_shared/components/form/title.vue';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { ApolloMutation } from 'vue-apollo';
jest.mock('~/lib/utils/url_utility', () => ({
getBaseURL: jest.fn().mockReturnValue('foo/'),
redirectTo: jest.fn().mockName('redirectTo'),
joinPaths: jest
.fn()
.mockName('joinPaths')
.mockReturnValue('contentApiURL'),
}));
let flashSpy;
const contentMock = 'Foo Bar';
const rawPathMock = '/foo/bar';
const rawProjectPathMock = '/project/path';
const newlyEditedSnippetUrl = 'http://foo.bar';
const apiError = { message: 'Ufff' };
const defaultProps = {
snippetGid: 'gid://gitlab/PersonalSnippet/42',
markdownPreviewPath: 'http://preview.foo.bar',
markdownDocsPath: 'http://docs.foo.bar',
};
describe('Snippet Edit app', () => {
let wrapper;
let axiosMock;
const resolveMutate = jest.fn().mockResolvedValue({
data: {
updateSnippet: {
errors: [],
snippet: {
webUrl: newlyEditedSnippetUrl,
},
},
},
});
const rejectMutation = jest.fn().mockRejectedValue(apiError);
const mutationTypes = {
RESOLVE: resolveMutate,
REJECT: rejectMutation,
};
function createComponent({
props = defaultProps,
data = {},
loading = false,
mutationRes = mutationTypes.RESOLVE,
} = {}) {
const $apollo = {
queries: {
snippet: {
loading,
},
},
mutate: mutationRes,
};
wrapper = shallowMount(SnippetEditApp, {
mocks: { $apollo },
stubs: {
FormFooterActions,
ApolloMutation,
},
propsData: {
...props,
},
data() {
return data;
},
});
flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
}
afterEach(() => {
wrapper.destroy();
});
const findSubmitButton = () => wrapper.find('[type=submit]');
describe('rendering', () => {
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders all required components', () => {
createComponent();
expect(wrapper.contains(TitleField)).toBe(true);
expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
expect(wrapper.contains(SnippetBlobEdit)).toBe(true);
expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
expect(wrapper.contains(FormFooterActions)).toBe(true);
});
it('does not fail if there is no snippet yet (new snippet creation)', () => {
const snippetGid = '';
createComponent({
props: {
...defaultProps,
snippetGid,
},
});
expect(wrapper.props('snippetGid')).toBe(snippetGid);
});
it.each`
title | content | expectation
${''} | ${''} | ${true}
${'foo'} | ${''} | ${true}
${''} | ${'foo'} | ${true}
${'foo'} | ${'bar'} | ${false}
`(
'disables submit button unless both title and content are present',
({ title, content, expectation }) => {
createComponent({
data: {
snippet: { title },
content,
},
});
const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled'));
expect(isBtnDisabled).toBe(expectation);
},
);
});
describe('functionality', () => {
describe('handling of the data from GraphQL response', () => {
const snippet = {
blob: {
rawPath: rawPathMock,
},
};
const getResSchema = newSnippet => {
return {
data: {
snippets: {
edges: newSnippet ? [] : [snippet],
},
},
};
};
const bootstrapForExistingSnippet = resp => {
createComponent({
data: {
snippet,
},
});
if (resp === 500) {
axiosMock.onGet('contentApiURL').reply(500);
} else {
axiosMock.onGet('contentApiURL').reply(200, contentMock);
}
wrapper.vm.onSnippetFetch(getResSchema());
};
const bootstrapForNewSnippet = () => {
createComponent();
wrapper.vm.onSnippetFetch(getResSchema(true));
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
axiosMock.restore();
});
it('fetches blob content with the additional query', () => {
bootstrapForExistingSnippet();
return waitForPromises().then(() => {
expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock);
expect(wrapper.vm.newSnippet).toBe(false);
expect(wrapper.vm.content).toBe(contentMock);
});
});
it('flashes the error message if fetching content fails', () => {
bootstrapForExistingSnippet(500);
return waitForPromises().then(() => {
expect(flashSpy).toHaveBeenCalled();
expect(wrapper.vm.content).toBe('');
});
});
it('does not fetch content for new snippet', () => {
bootstrapForNewSnippet();
return waitForPromises().then(() => {
// we keep using waitForPromises to make sure we do not run failed test
expect(wrapper.vm.newSnippet).toBe(true);
expect(wrapper.vm.content).toBe('');
expect(joinPaths).not.toHaveBeenCalled();
expect(wrapper.vm.snippet).toEqual(wrapper.vm.$options.newSnippetSchema);
});
});
});
describe('form submission handling', () => {
it.each`
newSnippet | projectPath | mutation | mutationName
${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'}
${true} | ${''} | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'}
${false} | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'}
${false} | ${''} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'}
`('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => {
createComponent({
data: {
newSnippet,
},
props: {
...defaultProps,
projectPath,
},
});
const mutationPayload = {
mutation,
variables: {
input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object),
},
};
wrapper.vm.handleFormSubmit();
expect(resolveMutate).toHaveBeenCalledWith(mutationPayload);
});
it('redirects to snippet view on successful mutation', () => {
createComponent();
wrapper.vm.handleFormSubmit();
return waitForPromises().then(() => {
expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl);
});
});
it('flashes an error if mutation failed', () => {
createComponent({
mutationRes: mutationTypes.REJECT,
});
wrapper.vm.handleFormSubmit();
return waitForPromises().then(() => {
expect(redirectTo).not.toHaveBeenCalled();
expect(flashSpy).toHaveBeenCalledWith(apiError);
});
});
});
});
});
...@@ -5,8 +5,6 @@ exports[`Title edit field matches the snapshot 1`] = ` ...@@ -5,8 +5,6 @@ exports[`Title edit field matches the snapshot 1`] = `
label="Title" label="Title"
label-for="title-field-edit" label-for="title-field-edit"
> >
<gl-form-input-stub <gl-form-input-stub />
id="title-field-edit"
/>
</gl-form-group-stub> </gl-form-group-stub>
`; `;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment