Commit 44f53d46 authored by Clement Ho's avatar Clement Ho

Merge branch 'winh-notes-app-jest' into 'master'

Move NoteApp tests to Jest

Closes #62399

See merge request gitlab-org/gitlab-ce!28903
parents 8a2afff0 aea013ea
import $ from 'jquery'; import $ from 'jquery';
import 'at.js';
import _ from 'underscore'; import _ from 'underscore';
import glRegexp from './lib/utils/regexp'; import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache'; import AjaxCache from './lib/utils/ajax_cache';
......
...@@ -10,7 +10,7 @@ import Flash from '../../flash'; ...@@ -10,7 +10,7 @@ import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue'; import noteActions from './note_actions.vue';
import noteBody from './note_body.vue'; import NoteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
...@@ -21,7 +21,7 @@ export default { ...@@ -21,7 +21,7 @@ export default {
userAvatarLink, userAvatarLink,
noteHeader, noteHeader,
noteActions, noteActions,
noteBody, NoteBody,
TimelineEntryItem, TimelineEntryItem,
}, },
mixins: [noteable, resolvable, draftMixin], mixins: [noteable, resolvable, draftMixin],
...@@ -209,7 +209,10 @@ export default { ...@@ -209,7 +209,10 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning // we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content // this is something we intentionally do so we need to recover the content
this.note.note = noteText; this.note.note = noteText;
this.$refs.noteBody.note.note = noteText; const { noteBody } = this.$refs;
if (noteBody) {
noteBody.note.note = noteText;
}
}, },
}, },
}; };
......
...@@ -127,6 +127,9 @@ export default { ...@@ -127,6 +127,9 @@ export default {
initUserPopovers(this.$el.querySelectorAll('.js-user-link')); initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
}); });
}, },
beforeDestroy() {
this.stopPolling();
},
methods: { methods: {
...mapActions([ ...mapActions([
'setLoadingState', 'setLoadingState',
...@@ -144,6 +147,7 @@ export default { ...@@ -144,6 +147,7 @@ export default {
'expandDiscussion', 'expandDiscussion',
'startTaskList', 'startTaskList',
'convertToDiscussion', 'convertToDiscussion',
'stopPolling',
]), ]),
fetchNotes() { fetchNotes() {
if (this.isFetching) return null; if (this.isFetching) return null;
......
import $ from 'jquery';
global.$ = $;
global.jQuery = $;
export default $;
import $ from 'jquery'; import $ from 'helpers/jquery';
import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import NotesApp from '~/notes/components/notes_app.vue'; import NotesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service'; import service from '~/notes/services/notes_service';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import * as mockData from '../mock_data'; import { setTestTimeout } from 'helpers/timeout';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491)
import * as mockData from '../../../javascripts/notes/mock_data';
const originalInterceptors = [...Vue.http.interceptors];
const emptyResponseInterceptor = (request, next) => {
next(
request.respondWith(JSON.stringify([]), {
status: 200,
}),
);
};
setTestTimeout(1000);
describe('note_app', () => { describe('note_app', () => {
let mountComponent; let mountComponent;
let wrapper; let wrapper;
let store; let store;
/**
* waits for fetchNotes() to complete
*/
const waitForDiscussionsRequest = () =>
new Promise(resolve => {
const { vm } = wrapper.find(NotesApp);
const unwatch = vm.$watch('isFetching', isFetching => {
if (isFetching) {
return;
}
unwatch();
resolve();
});
});
beforeEach(() => { beforeEach(() => {
$('body').attr('data-page', 'projects:merge_requests:show'); $('body').attr('data-page', 'projects:merge_requests:show');
...@@ -33,6 +62,7 @@ describe('note_app', () => { ...@@ -33,6 +62,7 @@ describe('note_app', () => {
template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>', template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>',
}, },
{ {
attachToDocument: true,
propsData, propsData,
store, store,
localVue, localVue,
...@@ -44,24 +74,14 @@ describe('note_app', () => { ...@@ -44,24 +74,14 @@ describe('note_app', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
Vue.http.interceptors = [...originalInterceptors];
}); });
describe('set data', () => { describe('set data', () => {
const responseInterceptor = (request, next) => {
next(
request.respondWith(JSON.stringify([]), {
status: 200,
}),
);
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(responseInterceptor); Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent(); wrapper = mountComponent();
}); return waitForDiscussionsRequest();
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor);
}); });
it('should set notes data', () => { it('should set notes data', () => {
...@@ -87,29 +107,23 @@ describe('note_app', () => { ...@@ -87,29 +107,23 @@ describe('note_app', () => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor); Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent(); wrapper = mountComponent();
return waitForDiscussionsRequest();
}); });
afterEach(() => { it('should render list of notes', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
it('should render list of notes', done => {
const note = const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json' '/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0]; ][0].notes[0];
setTimeout(() => { expect(
expect( wrapper
wrapper .find('.main-notes-list .note-header-author-name')
.find('.main-notes-list .note-header-author-name') .text()
.text() .trim(),
.trim(), ).toEqual(note.author.name);
).toEqual(note.author.name);
expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html); expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
done();
}, 0);
}); });
it('should render form', () => { it('should render form', () => {
...@@ -120,37 +134,42 @@ describe('note_app', () => { ...@@ -120,37 +134,42 @@ describe('note_app', () => {
}); });
it('should not render form when commenting is disabled', () => { it('should not render form when commenting is disabled', () => {
wrapper.destroy();
store.state.commentsDisabled = true; store.state.commentsDisabled = true;
wrapper = mountComponent(); wrapper = mountComponent();
return waitForDiscussionsRequest().then(() => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false); expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
}); });
it('should render discussion filter note `commentsDisabled` is true', () => { it('should render discussion filter note `commentsDisabled` is true', () => {
wrapper.destroy();
store.state.commentsDisabled = true; store.state.commentsDisabled = true;
wrapper = mountComponent(); wrapper = mountComponent();
return waitForDiscussionsRequest().then(() => {
expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true); expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
});
}); });
it('should render form comment button as disabled', () => { it('should render form comment button as disabled', () => {
expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled');
}); });
it('updates discussions badge', done => { it('updates discussions badge', () => {
setTimeout(() => { expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
done();
});
}); });
}); });
describe('while fetching data', () => { describe('while fetching data', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent(); wrapper = mountComponent();
}); });
afterEach(() => waitForDiscussionsRequest());
it('renders skeleton notes', () => { it('renders skeleton notes', () => {
expect(wrapper.find('.animation-container').exists()).toBe(true); expect(wrapper.find('.animation-container').exists()).toBe(true);
}); });
...@@ -165,78 +184,55 @@ describe('note_app', () => { ...@@ -165,78 +184,55 @@ describe('note_app', () => {
describe('update note', () => { describe('update note', () => {
describe('individual note', () => { describe('individual note', () => {
beforeEach(done => { beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor); Vue.http.interceptors.push(mockData.individualNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough(); jest.spyOn(service, 'updateNote');
wrapper = mountComponent(); wrapper = mountComponent();
setTimeout(() => { return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click'); wrapper.find('.js-note-edit').trigger('click');
Vue.nextTick(done); });
}, 0);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors,
mockData.individualNoteInterceptor,
);
}); });
it('renders edit form', () => { it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true); expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
}); });
it('calls the service to update the note', done => { it('calls the service to update the note', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note'; wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click'); wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled(); expect(service.updateNote).toHaveBeenCalled();
// Wait for the requests to finish before destroying
setTimeout(() => {
done();
});
}); });
}); });
describe('discussion note', () => { describe('discussion note', () => {
beforeEach(done => { beforeEach(() => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor); Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
spyOn(service, 'updateNote').and.callThrough(); jest.spyOn(service, 'updateNote');
wrapper = mountComponent(); wrapper = mountComponent();
return waitForDiscussionsRequest().then(() => {
setTimeout(() => {
wrapper.find('.js-note-edit').trigger('click'); wrapper.find('.js-note-edit').trigger('click');
Vue.nextTick(done); });
}, 0);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors,
mockData.discussionNoteInterceptor,
);
}); });
it('renders edit form', () => { it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true); expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
}); });
it('updates the note and resets the edit form', done => { it('updates the note and resets the edit form', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note'; wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click'); wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled(); expect(service.updateNote).toHaveBeenCalled();
// Wait for the requests to finish before destroying
setTimeout(() => {
done();
});
}); });
}); });
}); });
describe('new note form', () => { describe('new note form', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent(); wrapper = mountComponent();
return waitForDiscussionsRequest();
}); });
it('should render markdown docs url', () => { it('should render markdown docs url', () => {
...@@ -266,43 +262,37 @@ describe('note_app', () => { ...@@ -266,43 +262,37 @@ describe('note_app', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor); Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent(); wrapper = mountComponent();
return waitForDiscussionsRequest();
}); });
afterEach(() => { it('should render markdown docs url', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); wrapper.find('.js-note-edit').trigger('click');
}); const { markdownDocsPath } = mockData.notesDataMock;
it('should render markdown docs url', done => { return Vue.nextTick().then(() => {
setTimeout(() => { expect(
wrapper.find('.js-note-edit').trigger('click'); wrapper
const { markdownDocsPath } = mockData.notesDataMock; .find(`.edit-note a[href="${markdownDocsPath}"]`)
.text()
Vue.nextTick(() => { .trim(),
expect( ).toEqual('Markdown is supported');
wrapper });
.find(`.edit-note a[href="${markdownDocsPath}"]`)
.text()
.trim(),
).toEqual('Markdown is supported');
done();
});
}, 0);
}); });
it('should not render quick actions docs url', done => { it('should not render quick actions docs url', () => {
setTimeout(() => { wrapper.find('.js-note-edit').trigger('click');
wrapper.find('.js-note-edit').trigger('click'); const { quickActionsDocsPath } = mockData.notesDataMock;
const { quickActionsDocsPath } = mockData.notesDataMock; expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
Vue.nextTick(() => {
expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
done();
});
}, 0);
}); });
}); });
describe('emoji awards', () => { describe('emoji awards', () => {
beforeEach(() => {
Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
return waitForDiscussionsRequest();
});
it('dispatches toggleAward after toggleAward event', () => { it('dispatches toggleAward after toggleAward event', () => {
const toggleAwardEvent = new CustomEvent('toggleAward', { const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: { detail: {
...@@ -310,17 +300,18 @@ describe('note_app', () => { ...@@ -310,17 +300,18 @@ describe('note_app', () => {
noteId: 1, noteId: 1,
}, },
}); });
const toggleAwardAction = jasmine.createSpy('toggleAward'); const toggleAwardAction = jest.fn().mockName('toggleAward');
wrapper.vm.$store.hotUpdate({ wrapper.vm.$store.hotUpdate({
actions: { actions: {
toggleAward: toggleAwardAction, toggleAward: toggleAwardAction,
stopPolling() {},
}, },
}); });
wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent); wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent);
expect(toggleAwardAction).toHaveBeenCalledTimes(1); expect(toggleAwardAction).toHaveBeenCalledTimes(1);
const [, payload] = toggleAwardAction.calls.argsFor(0); const [, payload] = toggleAwardAction.mock.calls[0];
expect(payload).toEqual({ expect(payload).toEqual({
awardName: 'test', awardName: 'test',
......
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