Commit 78f1d2fb authored by Denys Mishunov's avatar Denys Mishunov Committed by Kushal Pandya

Initialized Snippet Edit Vue app

On the Snippet Edit form, initialize root Vue application

Support creation of new snippets
parent 93d29d34
<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