Skip to content
Snippets Groups Projects
Commit 925eea26 authored by Winnie Hellmann's avatar Winnie Hellmann Committed by Phil Hughes
Browse files

Make JavaScript tests fail for unhandled Promise rejections

parent 9c7bf123
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -17,7 +17,7 @@ export default {
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
if (this.title.trim() === '') return Promise.resolve();
 
this.error = false;
 
Loading
Loading
@@ -29,7 +29,10 @@ export default {
assignees: [],
});
 
this.list.newIssue(issue)
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
return this.list.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
Loading
Loading
@@ -47,9 +50,6 @@ export default {
// Show error message
this.error = true;
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
},
cancel() {
this.title = '';
Loading
Loading
Loading
Loading
@@ -112,8 +112,7 @@ class List {
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
})
.then(() => {
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
Loading
Loading
Loading
Loading
@@ -40,6 +40,10 @@ class FilteredSearchManager {
return [];
})
.then((searches) => {
if (!searches) {
return;
}
// Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones
const resultantSearches = this.recentSearchesStore.setRecentSearches(
Loading
Loading
Loading
Loading
@@ -12,6 +12,7 @@ import './mock_data';
describe('Issue boards new issue form', () => {
let vm;
let list;
let newIssueMock;
const promiseReturn = {
json() {
return {
Loading
Loading
@@ -21,7 +22,11 @@ describe('Issue boards new issue form', () => {
};
 
const submitIssue = () => {
vm.$el.querySelector('.btn-success').click();
const dummySubmitEvent = {
preventDefault() {},
};
vm.$refs.submitButton = vm.$el.querySelector('.btn-success');
return vm.submit(dummySubmitEvent);
};
 
beforeEach((done) => {
Loading
Loading
@@ -32,29 +37,35 @@ describe('Issue boards new issue form', () => {
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
 
setTimeout(() => {
list = new List(listObj);
spyOn(gl.boardService, 'newIssue').and.callFake(() => new Promise((resolve, reject) => {
if (vm.title === 'error') {
reject();
} else {
resolve(promiseReturn);
}
}));
vm = new BoardNewIssueComp({
propsData: {
list,
},
}).$mount();
done();
}, 0);
list = new List(listObj);
newIssueMock = Promise.resolve(promiseReturn);
spyOn(list, 'newIssue').and.callFake(() => newIssueMock);
vm = new BoardNewIssueComp({
propsData: {
list,
},
}).$mount();
Vue.nextTick()
.then(done)
.catch(done.fail);
});
 
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
it('calls submit if submit button is clicked', (done) => {
spyOn(vm, 'submit');
vm.title = 'Testing Title';
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.btn-success').click();
expect(vm.submit.calls.count()).toBe(1);
expect(vm.$refs['submit-button']).toBe(vm.$el.querySelector('.btn-success'));
})
.then(done)
.catch(done.fail);
});
 
it('disables submit button if title is empty', () => {
Loading
Loading
@@ -64,136 +75,122 @@ describe('Issue boards new issue form', () => {
it('enables submit button if title is not empty', (done) => {
vm.title = 'Testing Title';
 
setTimeout(() => {
expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title');
expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true);
done();
}, 0);
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title');
expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true);
})
.then(done)
.catch(done.fail);
});
 
it('clears title after clicking cancel', (done) => {
vm.$el.querySelector('.btn-default').click();
 
setTimeout(() => {
expect(vm.title).toBe('');
done();
}, 0);
Vue.nextTick()
.then(() => {
expect(vm.title).toBe('');
})
.then(done)
.catch(done.fail);
});
 
it('does not create new issue if title is empty', (done) => {
submitIssue();
setTimeout(() => {
expect(gl.boardService.newIssue).not.toHaveBeenCalled();
done();
}, 0);
submitIssue()
.then(() => {
expect(list.newIssue).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
 
describe('submit success', () => {
it('creates new issue', (done) => {
vm.title = 'submit title';
 
setTimeout(() => {
submitIssue();
expect(gl.boardService.newIssue).toHaveBeenCalled();
done();
}, 0);
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(list.newIssue).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
 
it('enables button after submit', (done) => {
vm.title = 'submit issue';
 
setTimeout(() => {
submitIssue();
expect(vm.$el.querySelector('.btn-success').disabled).toBe(false);
done();
}, 0);
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(vm.$el.querySelector('.btn-success').disabled).toBe(false);
})
.then(done)
.catch(done.fail);
});
 
it('clears title after submit', (done) => {
vm.title = 'submit issue';
 
Vue.nextTick(() => {
submitIssue();
setTimeout(() => {
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(vm.title).toBe('');
done();
}, 0);
});
});
it('adds new issue to top of list after submit request', (done) => {
vm.title = 'submit issue';
setTimeout(() => {
submitIssue();
setTimeout(() => {
expect(list.issues.length).toBe(2);
expect(list.issues[0].title).toBe('submit issue');
expect(list.issues[0].subscribed).toBe(true);
done();
}, 0);
}, 0);
})
.then(done)
.catch(done.fail);
});
 
it('sets detail issue after submit', (done) => {
expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe(undefined);
vm.title = 'submit issue';
 
setTimeout(() => {
submitIssue();
setTimeout(() => {
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue');
done();
}, 0);
}, 0);
})
.then(done)
.catch(done.fail);
});
 
it('sets detail list after submit', (done) => {
vm.title = 'submit issue';
 
setTimeout(() => {
submitIssue();
setTimeout(() => {
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id);
done();
}, 0);
}, 0);
})
.then(done)
.catch(done.fail);
});
});
 
describe('submit error', () => {
it('removes issue', (done) => {
beforeEach(() => {
newIssueMock = Promise.reject(new Error('My hovercraft is full of eels!'));
vm.title = 'error';
});
 
setTimeout(() => {
submitIssue();
setTimeout(() => {
it('removes issue', (done) => {
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(list.issues.length).toBe(1);
done();
}, 0);
}, 0);
})
.then(done)
.catch(done.fail);
});
 
it('shows error', (done) => {
vm.title = 'error';
setTimeout(() => {
submitIssue();
setTimeout(() => {
Vue.nextTick()
.then(submitIssue)
.then(() => {
expect(vm.error).toBe(true);
done();
}, 0);
}, 0);
})
.then(done)
.catch(done.fail);
});
});
});
Loading
Loading
@@ -150,4 +150,41 @@ describe('List model', () => {
expect(list.getIssues).toHaveBeenCalled();
});
});
describe('newIssue', () => {
beforeEach(() => {
spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({
json() {
return {
iid: 42,
};
},
}));
});
it('adds new issue to top of list', (done) => {
list.issues.push(new ListIssue({
title: 'Testing',
iid: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
}));
const dummyIssue = new ListIssue({
title: 'new issue',
iid: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
});
list.newIssue(dummyIssue)
.then(() => {
expect(list.issues.length).toBe(2);
expect(list.issues[0]).toBe(dummyIssue);
})
.then(done)
.catch(done.fail);
});
});
});
Loading
Loading
@@ -48,18 +48,23 @@ describe('Filtered Search Manager', () => {
</div>
`);
 
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
});
const initializeManager = () => {
/* eslint-disable jasmine/no-unsafe-spy */
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
/* eslint-enable jasmine/no-unsafe-spy */
 
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager();
manager.setup();
});
};
 
afterEach(() => {
manager.cleanup();
Loading
Loading
@@ -67,33 +72,34 @@ describe('Filtered Search Manager', () => {
 
describe('class constructor', () => {
const isLocalStorageAvailable = 'isLocalStorageAvailable';
let filteredSearchManager;
 
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
spyOn(recentSearchesStoreSrc, 'default');
spyOn(RecentSearchesRoot.prototype, 'render');
filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
return filteredSearchManager;
});
 
it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
manager = new gl.FilteredSearchManager();
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
isLocalStorageAvailable,
allowedKeys: gl.FilteredSearchTokenKeys.getKeys(),
});
});
});
describe('setup', () => {
beforeEach(() => {
manager = new gl.FilteredSearchManager();
});
 
it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() => Promise.reject(new RecentSearchesServiceError()));
spyOn(window, 'Flash');
 
filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
manager.setup();
 
expect(window.Flash).not.toHaveBeenCalled();
});
Loading
Loading
@@ -102,6 +108,7 @@ describe('Filtered Search Manager', () => {
describe('searchState', () => {
beforeEach(() => {
spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {});
initializeManager();
});
 
it('should blur button', () => {
Loading
Loading
@@ -148,6 +155,10 @@ describe('Filtered Search Manager', () => {
describe('search', () => {
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
 
beforeEach(() => {
initializeManager();
});
it('should search with a single word', (done) => {
input.value = 'searchTerm';
 
Loading
Loading
@@ -197,6 +208,10 @@ describe('Filtered Search Manager', () => {
});
 
describe('handleInputPlaceholder', () => {
beforeEach(() => {
initializeManager();
});
it('should render placeholder when there is no input', () => {
expect(input.placeholder).toEqual(placeholder);
});
Loading
Loading
@@ -223,6 +238,10 @@ describe('Filtered Search Manager', () => {
});
 
describe('checkForBackspace', () => {
beforeEach(() => {
initializeManager();
});
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
Loading
Loading
@@ -260,6 +279,10 @@ describe('Filtered Search Manager', () => {
});
 
describe('removeToken', () => {
beforeEach(() => {
initializeManager();
});
it('removes token even when it is already selected', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
Loading
Loading
@@ -291,6 +314,7 @@ describe('Filtered Search Manager', () => {
 
describe('removeSelectedTokenKeydown', () => {
beforeEach(() => {
initializeManager();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
Loading
Loading
@@ -344,27 +368,39 @@ describe('Filtered Search Manager', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
manager.removeSelectedToken();
initializeManager();
});
 
it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
manager.removeSelectedToken();
expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
});
 
it('calls handleInputPlaceholder', () => {
manager.removeSelectedToken();
expect(manager.handleInputPlaceholder).toHaveBeenCalled();
});
 
it('calls toggleClearSearchButton', () => {
manager.removeSelectedToken();
expect(manager.toggleClearSearchButton).toHaveBeenCalled();
});
 
it('calls update dropdown offset', () => {
manager.removeSelectedToken();
expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
});
});
 
describe('toggleInputContainerFocus', () => {
beforeEach(() => {
initializeManager();
});
it('toggles on focus', () => {
input.focus();
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true);
Loading
Loading
Loading
Loading
@@ -3,17 +3,9 @@ import '~/render_math';
import '~/render_gfm';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import Poll from '~/lib/utils/poll';
import issueShowData from '../mock_data';
 
const issueShowInterceptor = data => (request, next) => {
next(request.respondWith(JSON.stringify(data), {
status: 200,
headers: {
'POLL-INTERVAL': 1,
},
}));
};
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
}
Loading
Loading
@@ -24,10 +16,10 @@ describe('Issuable output', () => {
let vm;
 
beforeEach(() => {
const IssuableDescriptionComponent = Vue.extend(issuableApp);
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
spyOn(eventHub, '$emit');
spyOn(Poll.prototype, 'makeRequest');
const IssuableDescriptionComponent = Vue.extend(issuableApp);
 
vm = new IssuableDescriptionComponent({
propsData: {
Loading
Loading
@@ -54,9 +46,18 @@ describe('Issuable output', () => {
});
 
it('should render a title/description/edited and update title/description/edited on update', (done) => {
setTimeout(() => {
const editedText = vm.$el.querySelector('.edited-text');
vm.poll.options.successCallback({
json() {
return issueShowData.initialRequest;
},
});
 
let editedText;
Vue.nextTick()
.then(() => {
editedText = vm.$el.querySelector('.edited-text');
})
.then(() => {
expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
Loading
Loading
@@ -64,22 +65,27 @@ describe('Issuable output', () => {
expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/);
expect(editedText.querySelector('.author_link').href).toMatch(/\/some_user$/);
expect(editedText.querySelector('time')).toBeTruthy();
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
setTimeout(() => {
expect(document.querySelector('title').innerText).toContain('2 (#1)');
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
expect(vm.$el.querySelector('.edited-text')).toBeTruthy();
expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(/Edited[\s\S]+?by Other User/);
expect(editedText.querySelector('.author_link').href).toMatch(/\/other_user$/);
expect(editedText.querySelector('time')).toBeTruthy();
done();
})
.then(() => {
vm.poll.options.successCallback({
json() {
return issueShowData.secondRequest;
},
});
});
})
.then(Vue.nextTick)
.then(() => {
expect(document.querySelector('title').innerText).toContain('2 (#1)');
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
expect(vm.$el.querySelector('.edited-text')).toBeTruthy();
expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(/Edited[\s\S]+?by Other User/);
expect(editedText.querySelector('.author_link').href).toMatch(/\/other_user$/);
expect(editedText.querySelector('time')).toBeTruthy();
})
.then(done)
.catch(done.fail);
});
 
it('shows actions if permissions are correct', (done) => {
Loading
Loading
@@ -344,21 +350,23 @@ describe('Issuable output', () => {
 
describe('open form', () => {
it('shows locked warning if form is open & data is different', (done) => {
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
vm.poll.options.successCallback({
json() {
return issueShowData.initialRequest;
},
});
 
Vue.nextTick()
.then(() => new Promise((resolve) => {
setTimeout(resolve);
}))
.then(() => {
vm.openForm();
 
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
return new Promise((resolve) => {
setTimeout(resolve);
vm.poll.options.successCallback({
json() {
return issueShowData.secondRequest;
},
});
})
.then(Vue.nextTick)
.then(() => {
expect(
vm.formState.lockedWarningVisible,
Loading
Loading
@@ -367,9 +375,8 @@ describe('Issuable output', () => {
expect(
vm.$el.querySelector('.alert'),
).not.toBeNull();
done();
})
.then(done)
.catch(done.fail);
});
});
Loading
Loading
Loading
Loading
@@ -22,6 +22,19 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = 'http://test.host';
window.gon = window.gon || {};
 
let hasUnhandledPromiseRejections = false;
window.addEventListener('unhandledrejection', (event) => {
hasUnhandledPromiseRejections = true;
console.error('Unhandled promise rejection:');
console.error(event.reason.stack || event.reason);
});
const checkUnhandledPromiseRejections = (done) => {
expect(hasUnhandledPromiseRejections).toBe(false);
done();
};
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
// because it appears to lock up the thread that communicates to Karma's socket
// This async beforeEach gets called on every spec and releases the JS thread long
Loading
Loading
@@ -63,6 +76,10 @@ testsContext.keys().forEach(function (path) {
}
});
 
it('has no unhandled Promise rejections', (done) => {
setTimeout(checkUnhandledPromiseRejections(done), 1000);
});
// if we're generating coverage reports, make sure to include all files so
// that we can catch files with 0% coverage
// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment