diff options
author | Alfredo Sumaran <alfredo@gitlab.com> | 2017-06-07 14:45:57 -0500 |
---|---|---|
committer | Alfredo Sumaran <alfredo@gitlab.com> | 2017-06-07 14:45:57 -0500 |
commit | 3ec37e2622f6729300a988c8f58dfb6c2aecb996 (patch) | |
tree | d060b5acf30093cbe1d3642ea6dd11e79ccbf6c5 /spec/javascripts | |
parent | a65f07a256b95ce1c38342518f9469cbf3abf609 (diff) | |
parent | fc1090d9f39231e31f929e37b9703db9738b457c (diff) | |
download | gitlab-ce-3ec37e2622f6729300a988c8f58dfb6c2aecb996.tar.gz |
Merge branch 'master' into 25426-group-dashboard-ui
Diffstat (limited to 'spec/javascripts')
30 files changed, 1209 insertions, 238 deletions
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 45d12e252c4..832877de71c 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -19,6 +19,7 @@ describe('Issue boards new issue form', () => { }; }, }; + const submitIssue = () => { vm.$el.querySelector('.btn-success').click(); }; @@ -107,7 +108,7 @@ describe('Issue boards new issue form', () => { setTimeout(() => { submitIssue(); - expect(vm.$el.querySelector('.btn-success').disbled).not.toBe(true); + expect(vm.$el.querySelector('.btn-success').disabled).toBe(false); done(); }, 0); }); @@ -115,36 +116,43 @@ describe('Issue boards new issue form', () => { it('clears title after submit', (done) => { vm.title = 'submit issue'; - setTimeout(() => { + Vue.nextTick(() => { submitIssue(); - expect(vm.title).toBe(''); - done(); - }, 0); + setTimeout(() => { + expect(vm.title).toBe(''); + done(); + }, 0); + }); }); - it('adds new issue to list after submit', (done) => { + it('adds new issue to top of list after submit request', (done) => { vm.title = 'submit issue'; setTimeout(() => { submitIssue(); - expect(list.issues.length).toBe(2); - expect(list.issues[1].title).toBe('submit issue'); - expect(list.issues[1].subscribed).toBe(true); - done(); + 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); }); it('sets detail issue after submit', (done) => { + expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe(undefined); vm.title = 'submit issue'; setTimeout(() => { submitIssue(); - expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); - done(); - }); + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); + done(); + }, 0); + }, 0); }); it('sets detail list after submit', (done) => { @@ -153,8 +161,10 @@ describe('Issue boards new issue form', () => { setTimeout(() => { submitIssue(); - expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); - done(); + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); + done(); + }, 0); }, 0); }); }); @@ -169,13 +179,12 @@ describe('Issue boards new issue form', () => { setTimeout(() => { expect(list.issues.length).toBe(1); done(); - }, 500); + }, 0); }, 0); }); it('shows error', (done) => { vm.title = 'error'; - submitIssue(); setTimeout(() => { submitIssue(); @@ -183,7 +192,7 @@ describe('Issue boards new issue form', () => { setTimeout(() => { expect(vm.error).toBe(true); done(); - }, 500); + }, 0); }, 0); }); }); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js new file mode 100644 index 00000000000..c4e8966ad6c --- /dev/null +++ b/spec/javascripts/boards/components/board_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import '~/boards/services/board_service'; +import '~/boards/components/board'; +import '~/boards/models/list'; + +describe('Board component', () => { + let vm; + let el; + + beforeEach((done) => { + loadFixtures('boards/show.html.raw'); + + el = document.createElement('div'); + document.body.appendChild(el); + + // eslint-disable-next-line no-undef + gl.boardService = new BoardService('/', '/', 1); + + vm = new gl.issueBoards.Board({ + propsData: { + boardId: '1', + disabled: false, + issueLinkBase: '/', + rootPath: '/', + // eslint-disable-next-line no-undef + list: new List({ + id: 1, + position: 0, + title: 'test', + list_type: 'backlog', + }), + }, + }).$mount(el); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + // remove the component from the DOM + document.querySelector('.board').remove(); + + localStorage.removeItem(`boards.${vm.boardId}.${vm.list.type}.expanded`); + }); + + it('board is expandable when list type is backlog', () => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(true); + }); + + it('board is expandable when list type is closed', (done) => { + vm.list.type = 'closed'; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(true); + + done(); + }); + }); + + it('board is not expandable when list type is label', (done) => { + vm.list.type = 'label'; + vm.list.isExpandable = false; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(false); + + done(); + }); + }); + + it('collapses when clicking header', (done) => { + vm.$el.querySelector('.board-header').click(); + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + done(); + }); + }); + + it('created sets isExpanded to true from localStorage', (done) => { + vm.$el.querySelector('.board-header').click(); + + return Vue.nextTick() + .then(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + // call created manually + vm.$options.created[0].call(vm); + + return Vue.nextTick(); + }) + .then(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 398c593eec2..ebfd60198b2 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => { it('should render a table with the received pipelines', (done) => { setTimeout(() => { - expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.empty-state')).toBe(null); expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null); @@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => { expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.js-empty-state')).toBe(null); - expect(this.component.$el.querySelector('table')).toBe(null); + expect(this.component.$el.querySelector('.ci-table')).toBe(null); done(); }, 0); }); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 187db7485a5..44a4386b250 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -28,6 +28,32 @@ import '~/commits'; expect(CommitsList).toBeDefined(); }); + describe('processCommits', () => { + it('should join commit headers', () => { + CommitsList.$contentList = $(` + <div> + <li class="commit-header" data-day="2016-09-20"> + <span class="day">20 Sep, 2016</span> + <span class="commits-count">1 commit</span> + </li> + <li class="commit"></li> + </div> + `); + + const data = ` + <li class="commit-header" data-day="2016-09-20"> + <span class="day">20 Sep, 2016</span> + <span class="commits-count">1 commit</span> + </li> + <li class="commit"></li> + `; + + // The last commit header should be removed + // since the previous one has the same data-day value. + expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); + }); + }); + describe('on entering input', () => { let ajaxSpy; diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 793ab8c451d..a4b98f6140d 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -39,9 +39,15 @@ describe('Deploy keys key', () => { ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows remove button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Remove'); }); @@ -71,9 +77,15 @@ describe('Deploy keys key', () => { setTimeout(done); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows enable button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Enable'); }); @@ -82,7 +94,7 @@ describe('Deploy keys key', () => { Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Disable'); done(); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index 1c54cc3054c..6639a6b5e7b 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -41,7 +41,7 @@ describe('Environment', () => { setTimeout(() => { expect( component.$el.querySelector('.js-new-environment-button').textContent, - ).toContain('New Environment'); + ).toContain('New environment'); expect( component.$el.querySelector('.js-blank-state-title').textContent, @@ -271,7 +271,7 @@ describe('Environment', () => { // wait for next async request setTimeout(() => { expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); - expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all'); + expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all'); Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor); done(); diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index effbc6c3ee1..2862971bec4 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -29,6 +29,6 @@ describe('Environment item', () => { }, }).$mount(); - expect(component.$el.tagName).toEqual('TABLE'); + expect(component.$el.getAttribute('class')).toContain('ci-table'); }); }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index bb02abdeea2..f55726379f3 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -2,8 +2,12 @@ import '~/extensions/array'; import '~/filtered_search/dropdown_utils'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Dropdown Utils', () => { + const issueListFixture = 'issues/issue_list.html.raw'; + preloadFixtures(issueListFixture); + describe('getEscapedText', () => { it('should return same word when it has no space', () => { const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); @@ -314,4 +318,29 @@ describe('Dropdown Utils', () => { }); }); }); + + describe('getSearchQuery', () => { + let authorToken; + + beforeEach(() => { + loadFixtures(issueListFixture); + + authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user'); + const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term'); + + const tokensContainer = document.querySelector('.tokens-container'); + tokensContainer.appendChild(searchTermToken); + tokensContainer.appendChild(authorToken); + }); + + it('uses original value if present', () => { + const originalValue = 'original dance'; + const valueContainer = authorToken.querySelector('.value-container'); + valueContainer.dataset.originalValue = originalValue; + + const searchQuery = gl.DropdownUtils.getSearchQuery(); + + expect(searchQuery).toBe(' search term author:original dance'); + }); + }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 6e59ee96c6b..6d00d71f145 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -97,6 +97,49 @@ describe('Filtered Search Manager', () => { }); }); + describe('searchState', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {}); + }); + + it('should blur button', () => { + const e = { + currentTarget: { + blur: () => {}, + }, + }; + spyOn(e.currentTarget, 'blur').and.callThrough(); + manager.searchState(e); + + expect(e.currentTarget.blur).toHaveBeenCalled(); + }); + + it('should not call search if there is no state', () => { + const e = { + currentTarget: { + blur: () => {}, + }, + }; + + manager.searchState(e); + expect(gl.FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); + }); + + it('should call search when there is state', () => { + const e = { + currentTarget: { + blur: () => {}, + dataset: { + state: 'opened', + }, + }, + }; + + manager.searchState(e); + expect(gl.FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); + }); + }); + describe('search', () => { const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; @@ -316,42 +359,6 @@ describe('Filtered Search Manager', () => { }); }); - describe('unselects token', () => { - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} - ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} - `); - }); - - it('unselects token when input is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - - // Click directly on input attached to document - // so that the click event will propagate properly - document.querySelector('.filtered-search').click(); - - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - expect(selectedToken.classList.contains('selected')).toEqual(false); - }); - - it('unselects token when document.body is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - - document.body.click(); - - expect(selectedToken.classList.contains('selected')).toEqual(false); - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - }); - }); - describe('toggleInputContainerFocus', () => { it('toggles on focus', () => { input.focus(); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index c5fa2b17106..fa4343ffbc8 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,10 +1,22 @@ import AjaxCache from '~/lib/utils/ajax_cache'; +import UsersCache from '~/lib/utils/users_cache'; import '~/filtered_search/filtered_search_visual_tokens'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { + const subject = gl.FilteredSearchVisualTokens; + + const findElements = (tokenElement) => { + const tokenNameElement = tokenElement.querySelector('.name'); + const tokenValueContainer = tokenElement.querySelector('.value-container'); + const tokenValueElement = tokenValueContainer.querySelector('.value'); + return { tokenNameElement, tokenValueContainer, tokenValueElement }; + }; + let tokensContainer; + let authorToken; + let bugLabelToken; beforeEach(() => { setFixtures(` @@ -13,12 +25,15 @@ describe('Filtered Search Visual Tokens', () => { </ul> `); tokensContainer = document.querySelector('.tokens-container'); + + authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user'); + bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug'); }); describe('getLastVisualTokenBeforeInput', () => { it('returns when there are no visual tokens', () => { const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); expect(lastVisualToken).toEqual(null); expect(isLastVisualTokenValid).toEqual(true); @@ -27,11 +42,11 @@ describe('Filtered Search Visual Tokens', () => { describe('input is the last item in tokensContainer', () => { it('returns when there is one visual token', () => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + bugLabelToken.outerHTML, ); const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(isLastVisualTokenValid).toEqual(true); @@ -43,7 +58,7 @@ describe('Filtered Search Visual Tokens', () => { ); const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(isLastVisualTokenValid).toEqual(false); @@ -51,13 +66,13 @@ describe('Filtered Search Visual Tokens', () => { it('returns when there are multiple visual tokens', () => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} `); const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); const items = document.querySelectorAll('.tokens-container .js-visual-token'); expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); @@ -66,13 +81,13 @@ describe('Filtered Search Visual Tokens', () => { it('returns when there are multiple visual tokens and an incomplete visual token', () => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')} `); const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); const items = document.querySelectorAll('.tokens-container .js-visual-token'); expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); @@ -83,13 +98,13 @@ describe('Filtered Search Visual Tokens', () => { describe('input is a middle item in tokensContainer', () => { it('returns last token before input', () => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} ${FilteredSearchSpecHelper.createInputHTML()} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} `); const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(isLastVisualTokenValid).toEqual(true); @@ -103,7 +118,7 @@ describe('Filtered Search Visual Tokens', () => { `); const { lastVisualToken, isLastVisualTokenValid } - = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + = subject.getLastVisualTokenBeforeInput(); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(isLastVisualTokenValid).toEqual(false); @@ -114,7 +129,7 @@ describe('Filtered Search Visual Tokens', () => { describe('unselectTokens', () => { it('does nothing when there are no tokens', () => { const beforeHTML = tokensContainer.innerHTML; - gl.FilteredSearchVisualTokens.unselectTokens(); + subject.unselectTokens(); expect(tokensContainer.innerHTML).toEqual(beforeHTML); }); @@ -128,7 +143,7 @@ describe('Filtered Search Visual Tokens', () => { const selected = tokensContainer.querySelector('.js-visual-token .selected'); expect(selected.classList.contains('selected')).toEqual(true); - gl.FilteredSearchVisualTokens.unselectTokens(); + subject.unselectTokens(); expect(selected.classList.contains('selected')).toEqual(false); }); @@ -137,7 +152,7 @@ describe('Filtered Search Visual Tokens', () => { describe('selectToken', () => { beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} `); @@ -147,7 +162,7 @@ describe('Filtered Search Visual Tokens', () => { const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); firstTokenButton.classList.add('selected'); - gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); + subject.selectToken(firstTokenButton); expect(firstTokenButton.classList.contains('selected')).toEqual(false); }); @@ -156,7 +171,7 @@ describe('Filtered Search Visual Tokens', () => { it('adds selected class', () => { const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); - gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); + subject.selectToken(firstTokenButton); expect(firstTokenButton.classList.contains('selected')).toEqual(true); }); @@ -165,7 +180,7 @@ describe('Filtered Search Visual Tokens', () => { const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable'); tokenButtons[1].classList.add('selected'); - gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]); + subject.selectToken(tokenButtons[0]); expect(tokenButtons[0].classList.contains('selected')).toEqual(true); expect(tokenButtons[1].classList.contains('selected')).toEqual(false); @@ -181,7 +196,7 @@ describe('Filtered Search Visual Tokens', () => { expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); - gl.FilteredSearchVisualTokens.removeSelectedToken(); + subject.removeSelectedToken(); expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); }); @@ -193,7 +208,7 @@ describe('Filtered Search Visual Tokens', () => { expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); - gl.FilteredSearchVisualTokens.removeSelectedToken(); + subject.removeSelectedToken(); expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null); }); @@ -205,7 +220,7 @@ describe('Filtered Search Visual Tokens', () => { beforeEach(() => { setFixtures(` <div class="test-area"> - ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()} + ${subject.createVisualTokenElementHTML()} </div> `); @@ -245,7 +260,7 @@ describe('Filtered Search Visual Tokens', () => { describe('addVisualTokenElement', () => { it('renders search visual tokens', () => { - gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true); + subject.addVisualTokenElement('search term', null, true); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-term')).toEqual(true); @@ -254,7 +269,7 @@ describe('Filtered Search Visual Tokens', () => { }); it('renders filter visual token name', () => { - gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone'); + subject.addVisualTokenElement('milestone'); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -263,7 +278,7 @@ describe('Filtered Search Visual Tokens', () => { }); it('renders filter visual token name and value', () => { - gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); + subject.addVisualTokenElement('label', 'Frontend'); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -274,7 +289,7 @@ describe('Filtered Search Visual Tokens', () => { it('inserts visual token before input', () => { tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root')); - gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); + subject.addVisualTokenElement('label', 'Frontend'); const tokens = tokensContainer.querySelectorAll('.js-visual-token'); const labelToken = tokens[0]; const assigneeToken = tokens[1]; @@ -296,7 +311,7 @@ describe('Filtered Search Visual Tokens', () => { ); const original = tokensContainer.innerHTML; - gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + subject.addValueToPreviousVisualTokenElement('value'); expect(original).toEqual(tokensContainer.innerHTML); }); @@ -308,7 +323,7 @@ describe('Filtered Search Visual Tokens', () => { `); const original = tokensContainer.innerHTML; - gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + subject.addValueToPreviousVisualTokenElement('value'); expect(original).toEqual(tokensContainer.innerHTML); }); @@ -319,7 +334,7 @@ describe('Filtered Search Visual Tokens', () => { ); const original = tokensContainer.innerHTML; - gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + subject.addValueToPreviousVisualTokenElement('value'); const updatedToken = tokensContainer.querySelector('.js-visual-token'); expect(updatedToken.querySelector('.name').innerText).toEqual('label'); @@ -330,7 +345,7 @@ describe('Filtered Search Visual Tokens', () => { describe('addFilterVisualToken', () => { it('creates visual token with just tokenName', () => { - gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); + subject.addFilterVisualToken('milestone'); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -339,8 +354,8 @@ describe('Filtered Search Visual Tokens', () => { }); it('creates visual token with just tokenValue', () => { - gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); - gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17'); + subject.addFilterVisualToken('milestone'); + subject.addFilterVisualToken('%8.17'); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -349,7 +364,7 @@ describe('Filtered Search Visual Tokens', () => { }); it('creates full visual token', () => { - gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john'); + subject.addFilterVisualToken('assignee', '@john'); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -360,7 +375,7 @@ describe('Filtered Search Visual Tokens', () => { describe('addSearchVisualToken', () => { it('creates search visual token', () => { - gl.FilteredSearchVisualTokens.addSearchVisualToken('search term'); + subject.addSearchVisualToken('search term'); const token = tokensContainer.querySelector('.js-visual-token'); expect(token.classList.contains('filtered-search-term')).toEqual(true); @@ -374,7 +389,7 @@ describe('Filtered Search Visual Tokens', () => { ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} `); - gl.FilteredSearchVisualTokens.addSearchVisualToken('append this'); + subject.addSearchVisualToken('append this'); const token = tokensContainer.querySelector('.filtered-search-term'); expect(token.querySelector('.name').innerText).toEqual('search term append this'); @@ -386,10 +401,26 @@ describe('Filtered Search Visual Tokens', () => { it('should get last token value', () => { const value = '~bug'; tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value), + bugLabelToken.outerHTML, + ); + + expect(subject.getLastTokenPartial()).toEqual(value); + }); + + it('should get last token original value if available', () => { + const originalValue = '@user'; + const valueContainer = authorToken.querySelector('.value-container'); + valueContainer.dataset.originalValue = originalValue; + const avatar = document.createElement('img'); + const valueElement = valueContainer.querySelector('.value'); + valueElement.insertAdjacentElement('afterbegin', avatar); + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + authorToken.outerHTML, ); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value); + const lastTokenValue = subject.getLastTokenPartial(); + + expect(lastTokenValue).toEqual(originalValue); }); it('should get last token name if there is no value', () => { @@ -398,11 +429,11 @@ describe('Filtered Search Visual Tokens', () => { FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name), ); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name); + expect(subject.getLastTokenPartial()).toEqual(name); }); it('should return empty when there are no tokens', () => { - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(''); + expect(subject.getLastTokenPartial()).toEqual(''); }); }); @@ -414,7 +445,7 @@ describe('Filtered Search Visual Tokens', () => { expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + subject.removeLastTokenPartial(); expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null); }); @@ -426,14 +457,14 @@ describe('Filtered Search Visual Tokens', () => { expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null); - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + subject.removeLastTokenPartial(); expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null); }); it('should not remove anything when there are no tokens', () => { const html = tokensContainer.innerHTML; - gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + subject.removeLastTokenPartial(); expect(tokensContainer.innerHTML).toEqual(html); }); @@ -442,7 +473,7 @@ describe('Filtered Search Visual Tokens', () => { describe('tokenizeInput', () => { it('does not do anything if there is no input', () => { const original = tokensContainer.innerHTML; - gl.FilteredSearchVisualTokens.tokenizeInput(); + subject.tokenizeInput(); expect(tokensContainer.innerHTML).toEqual(original); }); @@ -454,7 +485,7 @@ describe('Filtered Search Visual Tokens', () => { const input = document.querySelector('.filtered-search'); input.value = 'some value'; - gl.FilteredSearchVisualTokens.tokenizeInput(); + subject.tokenizeInput(); const newToken = tokensContainer.querySelector('.filtered-search-term'); @@ -470,7 +501,7 @@ describe('Filtered Search Visual Tokens', () => { const input = document.querySelector('.filtered-search'); input.value = '@john'; - gl.FilteredSearchVisualTokens.tokenizeInput(); + subject.tokenizeInput(); const updatedToken = tokensContainer.querySelector('.filtered-search-token'); @@ -497,29 +528,39 @@ describe('Filtered Search Visual Tokens', () => { it('tokenize\'s existing input', () => { input.value = 'some text'; - spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough(); + spyOn(subject, 'tokenizeInput').and.callThrough(); - gl.FilteredSearchVisualTokens.editToken(token); + subject.editToken(token); - expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); + expect(subject.tokenizeInput).toHaveBeenCalled(); expect(input.value).not.toEqual('some text'); }); it('moves input to the token position', () => { expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null); - gl.FilteredSearchVisualTokens.editToken(token); + subject.editToken(token); expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null); expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null); }); it('input contains the visual token value', () => { - gl.FilteredSearchVisualTokens.editToken(token); + subject.editToken(token); expect(input.value).toEqual('none'); }); + it('input contains the original value if present', () => { + const originalValue = '@user'; + const valueContainer = token.querySelector('.value-container'); + valueContainer.dataset.originalValue = originalValue; + + subject.editToken(token); + + expect(input.value).toEqual(originalValue); + }); + describe('selected token is a search term token', () => { beforeEach(() => { token = document.querySelector('.filtered-search-term'); @@ -528,7 +569,7 @@ describe('Filtered Search Visual Tokens', () => { it('token is removed', () => { expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null); - gl.FilteredSearchVisualTokens.editToken(token); + subject.editToken(token); expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null); }); @@ -536,7 +577,7 @@ describe('Filtered Search Visual Tokens', () => { it('input has the same value as removed token', () => { expect(input.value).toEqual(''); - gl.FilteredSearchVisualTokens.editToken(token); + subject.editToken(token); expect(input.value).toEqual('search'); }); @@ -549,25 +590,25 @@ describe('Filtered Search Visual Tokens', () => { FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'), ); - spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {}); - spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough(); + spyOn(subject, 'tokenizeInput').and.callFake(() => {}); + spyOn(subject, 'getLastVisualTokenBeforeInput').and.callThrough(); - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + subject.moveInputToTheRight(); - expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled(); + expect(subject.tokenizeInput).toHaveBeenCalled(); + expect(subject.getLastVisualTokenBeforeInput).not.toHaveBeenCalled(); }); it('tokenize\'s input', () => { tokensContainer.innerHTML = ` ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} ${FilteredSearchSpecHelper.createInputHTML()} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} `; document.querySelector('.filtered-search').value = 'none'; - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + subject.moveInputToTheRight(); const value = tokensContainer.querySelector('.js-visual-token .value'); expect(value.innerText).toEqual('none'); @@ -577,12 +618,12 @@ describe('Filtered Search Visual Tokens', () => { tokensContainer.innerHTML = ` ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} ${FilteredSearchSpecHelper.createInputHTML()} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} `; document.querySelector('.filtered-search').value = 'test'; - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + subject.moveInputToTheRight(); const searchValue = tokensContainer.querySelector('.filtered-search-term .name'); expect(searchValue.innerText).toEqual('test'); @@ -592,10 +633,10 @@ describe('Filtered Search Visual Tokens', () => { tokensContainer.innerHTML = ` ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} ${FilteredSearchSpecHelper.createInputHTML()} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${bugLabelToken.outerHTML} `; - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + subject.moveInputToTheRight(); expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null); }); @@ -607,7 +648,7 @@ describe('Filtered Search Visual Tokens', () => { ${FilteredSearchSpecHelper.createInputHTML('', '~bug')} `; - gl.FilteredSearchVisualTokens.moveInputToTheRight(); + subject.moveInputToTheRight(); const token = tokensContainer.children[1]; expect(token.querySelector('.value').innerText).toEqual('~bug'); @@ -615,42 +656,144 @@ describe('Filtered Search Visual Tokens', () => { }); describe('renderVisualTokenValue', () => { - let searchTokens; + const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search'); + const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken('milestone', 'upcoming'); + + let updateLabelTokenColorSpy; + let updateUserTokenAppearanceSpy; beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} - ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')} + ${authorToken.outerHTML} + ${bugLabelToken.outerHTML} + ${keywordToken.outerHTML} + ${milestoneToken.outerHTML} `); - searchTokens = document.querySelectorAll('.filtered-search-token'); + spyOn(subject, 'updateLabelTokenColor'); + updateLabelTokenColorSpy = subject.updateLabelTokenColor; + + spyOn(subject, 'updateUserTokenAppearance'); + updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance; }); - it('renders a token value element', () => { - spyOn(gl.FilteredSearchVisualTokens, 'updateLabelTokenColor'); - const updateLabelTokenColorSpy = gl.FilteredSearchVisualTokens.updateLabelTokenColor; + it('renders a author token value element', () => { + const { tokenNameElement, tokenValueContainer, tokenValueElement } = + findElements(authorToken); + const tokenName = tokenNameElement.innerText; + const tokenValue = 'new value'; - expect(searchTokens.length).toBe(2); - Array.prototype.forEach.call(searchTokens, (token) => { - updateLabelTokenColorSpy.calls.reset(); + subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); - const tokenName = token.querySelector('.name').innerText; - const tokenValue = 'new value'; - gl.FilteredSearchVisualTokens.renderVisualTokenValue(token, tokenName, tokenValue); + expect(tokenValueElement.innerText).toBe(tokenValue); + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1); + const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue]; + expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs); + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + }); - const tokenValueElement = token.querySelector('.value'); - expect(tokenValueElement.innerText).toBe(tokenValue); + it('renders a label token value element', () => { + const { tokenNameElement, tokenValueContainer, tokenValueElement } = + findElements(bugLabelToken); + const tokenName = tokenNameElement.innerText; + const tokenValue = 'new value'; - if (tokenName.toLowerCase() === 'label') { - const tokenValueContainer = token.querySelector('.value-container'); - expect(updateLabelTokenColorSpy.calls.count()).toBe(1); - const expectedArgs = [tokenValueContainer, tokenValue]; - expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs); - } else { - expect(updateLabelTokenColorSpy.calls.count()).toBe(0); - } - }); + subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue); + + expect(tokenValueElement.innerText).toBe(tokenValue); + expect(updateLabelTokenColorSpy.calls.count()).toBe(1); + const expectedArgs = [tokenValueContainer, tokenValue]; + expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs); + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('renders a milestone token value element', () => { + const { tokenNameElement, tokenValueElement } = findElements(milestoneToken); + const tokenName = tokenNameElement.innerText; + const tokenValue = 'new value'; + + subject.renderVisualTokenValue(milestoneToken, tokenName, tokenValue); + + expect(tokenValueElement.innerText).toBe(tokenValue); + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + }); + + describe('updateUserTokenAppearance', () => { + let usersCacheSpy; + + beforeEach(() => { + spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username)); + }); + + it('ignores special value "none"', (done) => { + usersCacheSpy = (username) => { + expect(username).toBe('none'); + done.fail('Should not resolve "none"!'); + }; + const { tokenValueContainer, tokenValueElement } = findElements(authorToken); + + subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, 'none') + .then(done) + .catch(done.fail); + }); + + it('ignores error if UsersCache throws', (done) => { + spyOn(window, 'Flash'); + const dummyError = new Error('Earth rotated backwards'); + const { tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = (username) => { + expect(`@${username}`).toBe(tokenValue); + return Promise.reject(dummyError); + }; + + subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(window.Flash.calls.count()).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + + it('does nothing if user cannot be found', (done) => { + const { tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = (username) => { + expect(`@${username}`).toBe(tokenValue); + return Promise.resolve(undefined); + }; + + subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(tokenValueElement.innerText).toBe(tokenValue); + }) + .then(done) + .catch(done.fail); + }); + + it('replaces author token with avatar and display name', (done) => { + const dummyUser = { + name: 'Important Person', + avatar_url: 'https://host.invalid/mypics/avatar.png', + }; + const { tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = (username) => { + expect(`@${username}`).toBe(tokenValue); + return Promise.resolve(dummyUser); + }; + + subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue); + expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); + const avatar = tokenValueElement.querySelector('img.avatar'); + expect(avatar.src).toBe(dummyUser.avatar_url); + }) + .then(done) + .catch(done.fail); }); }); @@ -659,21 +802,16 @@ describe('Filtered Search Visual Tokens', () => { const dummyEndpoint = '/dummy/endpoint'; preloadFixtures(jsonFixtureName); - const labelData = getJSONFixture(jsonFixtureName); - const findLabel = tokenValue => labelData.find( - label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`, - ); - const bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug'); + let labelData; + + beforeAll(() => { + labelData = getJSONFixture(jsonFixtureName); + }); + const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist'); const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"'); - const parseColor = (color) => { - const dummyElement = document.createElement('div'); - dummyElement.style.color = color; - return dummyElement.style.color; - }; - beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` ${bugLabelToken.outerHTML} @@ -688,28 +826,60 @@ describe('Filtered Search Visual Tokens', () => { AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData; }); - const testCase = (token, done) => { - const tokenValueContainer = token.querySelector('.value-container'); - const tokenValue = token.querySelector('.value').innerText; - const label = findLabel(tokenValue); + const parseColor = (color) => { + const dummyElement = document.createElement('div'); + dummyElement.style.color = color; + return dummyElement.style.color; + }; - gl.FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue) - .then(() => { - if (label) { - expect(tokenValueContainer.getAttribute('style')).not.toBe(null); - expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color)); - expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color)); - } else { - expect(token).toBe(missingLabelToken); - expect(tokenValueContainer.getAttribute('style')).toBe(null); - } - }) - .then(done) - .catch(fail); + const expectValueContainerStyle = (tokenValueContainer, label) => { + expect(tokenValueContainer.getAttribute('style')).not.toBe(null); + expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color)); + expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color)); }; - it('updates the color of a label token', done => testCase(bugLabelToken, done)); - it('updates the color of a label token with spaces', done => testCase(spaceLabelToken, done)); - it('does not change color of a missing label', done => testCase(missingLabelToken, done)); + const findLabel = tokenValue => labelData.find( + label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`, + ); + + it('updates the color of a label token', (done) => { + const { tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + const tokenValue = tokenValueElement.innerText; + const matchingLabel = findLabel(tokenValue); + + subject.updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + expectValueContainerStyle(tokenValueContainer, matchingLabel); + }) + .then(done) + .catch(done.fail); + }); + + it('updates the color of a label token with spaces', (done) => { + const { tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken); + const tokenValue = tokenValueElement.innerText; + const matchingLabel = findLabel(tokenValue); + + subject.updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + expectValueContainerStyle(tokenValueContainer, matchingLabel); + }) + .then(done) + .catch(done.fail); + }); + + it('does not change color of a missing label', (done) => { + const { tokenValueContainer, tokenValueElement } = findElements(missingLabelToken); + const tokenValue = tokenValueElement.innerText; + const matchingLabel = findLabel(tokenValue); + expect(matchingLabel).toBe(undefined); + + subject.updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + expect(tokenValueContainer.getAttribute('style')).toBe(null); + }) + .then(done) + .catch(done.fail); + }); }); }); diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb new file mode 100644 index 00000000000..d7c3dc0a235 --- /dev/null +++ b/spec/javascripts/fixtures/boards.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'boards-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('boards/') + end + + before(:each) do + sign_in(admin) + end + + it 'boards/show.html.raw' do |example| + get(:index, + namespace_id: project.namespace, + project_id: project) + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml index ae745b292e6..84fa5395cb8 100644 --- a/spec/javascripts/fixtures/issuable_filter.html.haml +++ b/spec/javascripts/fixtures/issuable_filter.html.haml @@ -1,6 +1,6 @@ %form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} %input{id: 'utf8', name: 'utf8', value: '✓'} - %input{id: 'check_all_issues', name: 'check_all_issues'} + %input{id: 'check-all-issues', name: 'check-all-issues'} %input{id: 'search', name: 'search'} %input{id: 'author_id', name: 'author_id'} %input{id: 'assignee_id', name: 'assignee_id'} diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 88e3f860809..1a30909977e 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -36,6 +36,17 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller render_issue(example.description, issue) end + it 'issues/issue_list.html.raw' do |example| + create(:issue, project: project) + + get :index, + namespace_id: project.namespace.to_param, + project_id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + private def render_issue(fixture_file_name, issue) diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb new file mode 100644 index 00000000000..554451d1bbf --- /dev/null +++ b/spec/javascripts/fixtures/services.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } + + + render_views + + before(:all) do + clean_frontend_fixtures('services/') + end + + before(:each) do + sign_in(admin) + end + + it 'services/edit_service.html.raw' do |example| + get :edit, + namespace_id: namespace, + project_id: project, + id: service.to_param + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index b2b46640e5b..a09e0072fa8 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -192,6 +192,9 @@ describe('gl_emoji', () => { }); describe('isFlagEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isFlagEmoji('')).toBeFalsy(); + }); it('should detect flag_ac', () => { expect(isFlagEmoji('🇦🇨')).toBeTruthy(); }); @@ -216,6 +219,9 @@ describe('gl_emoji', () => { }); describe('isKeycapEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isKeycapEmoji('')).toBeFalsy(); + }); it('should detect one(keycap)', () => { expect(isKeycapEmoji('1️⃣')).toBeTruthy(); }); @@ -231,6 +237,9 @@ describe('gl_emoji', () => { }); describe('isSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect hand_splayed_tone5', () => { expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); }); @@ -255,6 +264,9 @@ describe('gl_emoji', () => { }); describe('isHorceRacingSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect horse_racing_tone2', () => { expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); }); @@ -264,6 +276,9 @@ describe('gl_emoji', () => { }); describe('isPersonZwjEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isPersonZwjEmoji('')).toBeFalsy(); + }); it('should detect couple_mm', () => { expect(isPersonZwjEmoji('👨❤️👨')).toBeTruthy(); }); @@ -300,6 +315,22 @@ describe('gl_emoji', () => { }); describe('isEmojiUnicodeSupported', () => { + it('should gracefully handle empty string with unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + { '1.0': true }, + '', + '1.0', + ); + expect(isSupported).toBeTruthy(); + }); + it('should gracefully handle empty string without unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + {}, + '', + '1.0', + ); + expect(isSupported).toBeFalsy(); + }); it('bomb(6.0) with 6.0 support', () => { const emojiKey = 'bomb'; const unicodeSupportMap = Object.assign({}, emptySupportMap, { diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js index 0d7092a2357..8933dd5def4 100644 --- a/spec/javascripts/helpers/filtered_search_spec_helper.js +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js @@ -30,12 +30,15 @@ export default class FilteredSearchSpecHelper { `; } + static createSearchVisualToken(name) { + const li = document.createElement('li'); + li.classList.add('js-visual-token', 'filtered-search-term'); + li.innerHTML = `<div class="name">${name}</div>`; + return li; + } + static createSearchVisualTokenHTML(name) { - return ` - <li class="js-visual-token filtered-search-term"> - <div class="name">${name}</div> - </li> - `; + return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML; } static createInputHTML(placeholder = '', value = '') { diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js new file mode 100644 index 00000000000..45909d4e70e --- /dev/null +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -0,0 +1,199 @@ +import IntegrationSettingsForm from '~/integrations/integration_settings_form'; + +describe('IntegrationSettingsForm', () => { + const FIXTURE = 'services/edit_service.html.raw'; + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + }); + + describe('contructor', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + spyOn(integrationSettingsForm, 'init'); + }); + + it('should initialize form element refs on class object', () => { + // Form Reference + expect(integrationSettingsForm.$form).toBeDefined(); + expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM'); + + // Form Child Elements + expect(integrationSettingsForm.$serviceToggle).toBeDefined(); + expect(integrationSettingsForm.$submitBtn).toBeDefined(); + expect(integrationSettingsForm.$submitBtnLoader).toBeDefined(); + expect(integrationSettingsForm.$submitBtnLabel).toBeDefined(); + }); + + it('should initialize form metadata on class object', () => { + expect(integrationSettingsForm.testEndPoint).toBeDefined(); + expect(integrationSettingsForm.canTestService).toBeDefined(); + }); + }); + + describe('toggleServiceState', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + }); + + it('should remove `novalidate` attribute to form when called with `true`', () => { + integrationSettingsForm.toggleServiceState(true); + + expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined(); + }); + + it('should set `novalidate` attribute to form when called with `false`', () => { + integrationSettingsForm.toggleServiceState(false); + + expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined(); + }); + }); + + describe('toggleSubmitBtnLabel', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + }); + + it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => { + integrationSettingsForm.canTestService = true; + + integrationSettingsForm.toggleSubmitBtnLabel(true); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Test settings and save changes'); + }); + + it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => { + integrationSettingsForm.canTestService = false; + + integrationSettingsForm.toggleSubmitBtnLabel(false); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); + + integrationSettingsForm.toggleSubmitBtnLabel(true); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); + + integrationSettingsForm.canTestService = true; + + integrationSettingsForm.toggleSubmitBtnLabel(false); + expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes'); + }); + }); + + describe('toggleSubmitBtnState', () => { + let integrationSettingsForm; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + }); + + it('should disable Save button and show loader animation when called with `true`', () => { + integrationSettingsForm.toggleSubmitBtnState(true); + + expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy(); + expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy(); + }); + + it('should enable Save button and hide loader animation when called with `false`', () => { + integrationSettingsForm.toggleSubmitBtnState(false); + + expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy(); + expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy(); + }); + }); + + describe('testSettings', () => { + let integrationSettingsForm; + let formData; + + beforeEach(() => { + integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + formData = integrationSettingsForm.$form.serialize(); + }); + + it('should make an ajax request with provided `formData`', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + expect($.ajax).toHaveBeenCalledWith({ + type: 'PUT', + url: integrationSettingsForm.testEndPoint, + data: formData, + }); + }); + + it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => { + const errorMessage = 'Test failed.'; + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + deferred.resolve({ error: true, message: errorMessage }); + + const $flashContainer = $('.flash-container'); + expect($flashContainer.find('.flash-text').text()).toEqual(errorMessage); + expect($flashContainer.find('.flash-action')).toBeDefined(); + expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway'); + }); + + it('should submit form if ajax request responds without any error in test', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + spyOn(integrationSettingsForm.$form, 'submit'); + deferred.resolve({ error: false }); + + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + }); + + it('should submit form when clicked on `Save anyway` action of error Flash', () => { + const errorMessage = 'Test failed.'; + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + deferred.resolve({ error: true, message: errorMessage }); + + const $flashAction = $('.flash-container .flash-action'); + expect($flashAction).toBeDefined(); + + spyOn(integrationSettingsForm.$form, 'submit'); + $flashAction.trigger('click'); + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + }); + + it('should show error Flash if ajax request failed', () => { + const errorMessage = 'Something went wrong on our end.'; + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + deferred.reject(); + + expect($('.flash-container .flash-text').text()).toEqual(errorMessage); + }); + + it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + integrationSettingsForm.testSettings(formData); + + spyOn(integrationSettingsForm, 'toggleSubmitBtnState'); + deferred.reject(); + + expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 49fa2cb8367..45f55395d3a 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -1,7 +1,7 @@ -/* global Issuable */ +/* global IssuableIndex */ import '~/lib/utils/url_utility'; -import '~/issuable'; +import '~/issuable_index'; (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; @@ -24,11 +24,11 @@ import '~/issuable'; beforeEach(() => { loadFixtures('static/issuable_filter.html.raw'); - Issuable.init(); + IssuableIndex.init(); }); it('should be defined', () => { - expect(window.Issuable).toBeDefined(); + expect(window.IssuableIndex).toBeDefined(); }); describe('filtering', () => { @@ -43,7 +43,7 @@ import '~/issuable'; it('should contain only the default parameters', () => { spyOn(gl.utils, 'visitUrl'); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); }); @@ -52,7 +52,7 @@ import '~/issuable'; spyOn(gl.utils, 'visitUrl'); updateForm({ search: 'broken' }, $filtersForm); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); const params = `${DEFAULT_PARAMS}&search=broken`; expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); @@ -64,14 +64,14 @@ import '~/issuable'; // initial filter updateForm({ milestone_title: 'v1.0' }, $filtersForm); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); // update filter updateForm({ label_name: 'Frontend' }, $filtersForm); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 0030a953119..59c006aa0af 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -14,6 +14,10 @@ const issueShowInterceptor = data => (request, next) => { })); }; +function formatText(text) { + return text.trim().replace(/\s\s+/g, ' '); +} + describe('Issuable output', () => { document.body.innerHTML = '<span id="task_status"></span>'; @@ -50,12 +54,17 @@ describe('Issuable output', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); }); - it('should render a title/description and update title/description on update', (done) => { + it('should render a title/description/edited and update title/description/edited on update', (done) => { setTimeout(() => { + const editedText = vm.$el.querySelector('.edited-text'); + 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>'); expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description'); + 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)); @@ -64,6 +73,10 @@ describe('Issuable output', () => { 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(); }); diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/javascripts/issue_show/components/edited_spec.js new file mode 100644 index 00000000000..a0d0750ae34 --- /dev/null +++ b/spec/javascripts/issue_show/components/edited_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import edited from '~/issue_show/components/edited.vue'; + +function formatText(text) { + return text.trim().replace(/\s\s+/g, ' '); +} + +describe('edited', () => { + const EditedComponent = Vue.extend(edited); + + it('should render an edited at+by string', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + updatedByName: 'Some User', + updatedByPath: '/some_user', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited[\s\S]+?by Some User/); + expect(editedComponent.$el.querySelector('.author_link').href).toMatch(/\/some_user$/); + expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + }); + + it('if no updatedAt is provided, no time element will be rendered', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedByName: 'Some User', + updatedByPath: '/some_user', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited by Some User/); + expect(editedComponent.$el.querySelector('.author_link').href).toMatch(/\/some_user$/); + expect(editedComponent.$el.querySelector('time')).toBeFalsy(); + }); + + it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + }, + }).$mount(); + + expect(formatText(editedComponent.$el.innerText)).not.toMatch(/by Some User/); + expect(editedComponent.$el.querySelector('.author_link')).toBeFalsy(); + expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + }); +}); diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js index 6683d581bc5..eb3111412a7 100644 --- a/spec/javascripts/issue_show/mock_data.js +++ b/spec/javascripts/issue_show/mock_data.js @@ -5,7 +5,9 @@ export default { description: '<p>this is a description!</p>', description_text: 'this is a description', task_status: '2 of 4 completed', - updated_at: new Date().toString(), + updated_at: '2015-05-15T12:31:04.428Z', + updated_by_name: 'Some User', + updated_by_path: '/some_user', }, secondRequest: { title: '<p>2</p>', @@ -13,7 +15,9 @@ export default { description: '<p>42</p>', description_text: '42', task_status: '0 of 0 completed', - updated_at: new Date().toString(), + updated_at: '2016-05-15T12:31:04.428Z', + updated_by_name: 'Other User', + updated_by_path: '/other_user', }, issueSpecRequest: { title: '<p>this is a title</p>', @@ -21,6 +25,8 @@ export default { description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>', description_text: '- [ ] Task List Item', task_status: '0 of 1 completed', - updated_at: new Date().toString(), + updated_at: '2017-05-15T12:31:04.428Z', + updated_by_name: 'Last User', + updated_by_path: '/last_user', }, }; diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js index e1747a82329..2c946802dcd 100644 --- a/spec/javascripts/lib/utils/ajax_cache_spec.js +++ b/spec/javascripts/lib/utils/ajax_cache_spec.js @@ -154,5 +154,36 @@ describe('AjaxCache', () => { .then(done) .catch(fail); }); + + it('makes Ajax call even if matching data exists when forceRequest parameter is provided', (done) => { + const oldDummyResponse = { + important: 'old dummy data', + }; + + AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse; + + ajaxSpy = (url) => { + expect(url).toBe(dummyEndpoint); + const deferred = $.Deferred(); + deferred.resolve(dummyResponse); + return deferred.promise(); + }; + + // Call without forceRetrieve param + AjaxCache.retrieve(dummyEndpoint) + .then((data) => { + expect(data).toBe(oldDummyResponse); + }) + .then(done) + .catch(fail); + + // Call with forceRetrieve param + AjaxCache.retrieve(dummyEndpoint, true) + .then((data) => { + expect(data).toBe(dummyResponse); + }) + .then(done) + .catch(fail); + }); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 17aa70ff3f1..24335614e09 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -13,6 +13,23 @@ import '~/notes'; window.gl = window.gl || {}; gl.utils = gl.utils || {}; + const htmlEscape = (comment) => { + const escapedString = comment.replace(/["&'<>]/g, (a) => { + const escapedToken = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }[a]; + + return escapedToken; + }); + + return escapedString; + }; + describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; var commentsTemplate = 'issues/issue_with_comment.html.raw'; @@ -445,11 +462,17 @@ import '~/notes'; }); describe('getFormData', () => { - it('should return form metadata object from form reference', () => { + let $form; + let sampleComment; + + beforeEach(() => { this.notes = new Notes('', []); - const $form = $('form'); - const sampleComment = 'foobar'; + $form = $('form'); + sampleComment = 'foobar'; + }); + + it('should return form metadata object from form reference', () => { $form.find('textarea.js-note-text').val(sampleComment); const { formData, formContent, formAction } = this.notes.getFormData($form); @@ -457,6 +480,18 @@ import '~/notes'; expect(formContent).toEqual(sampleComment); expect(formAction).toEqual($form.attr('action')); }); + + it('should return form metadata with sanitized formContent from form reference', () => { + spyOn(_, 'escape').and.callFake(htmlEscape); + + sampleComment = '<script>alert("Boom!");</script>'; + $form.find('textarea.js-note-text').val(sampleComment); + + const { formContent } = this.notes.getFormData($form); + + expect(_.escape).toHaveBeenCalledWith(sampleComment); + expect(formContent).toEqual('<script>alert("Boom!");</script>'); + }); }); describe('hasSlashCommands', () => { @@ -512,30 +547,42 @@ import '~/notes'; }); }); + describe('getSlashCommandDescription', () => { + const availableSlashCommands = [ + { name: 'close', description: 'Close this issue', params: [] }, + { name: 'title', description: 'Change title', params: [{}] }, + { name: 'estimate', description: 'Set time estimate', params: [{}] } + ]; + + beforeEach(() => { + this.notes = new Notes(); + }); + + it('should return executing slash command description when note has single slash command', () => { + const sampleComment = '/close'; + expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying command to close this issue'); + }); + + it('should return generic multiple slash command description when note has multiple slash commands', () => { + const sampleComment = '/close\n/title [Duplicate] Issue foobar'; + expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying multiple commands'); + }); + + it('should return generic slash command description when available slash commands list is not populated', () => { + const sampleComment = '/close\n/title [Duplicate] Issue foobar'; + expect(this.notes.getSlashCommandDescription(sampleComment)).toBe('Applying command'); + }); + }); + describe('createPlaceholderNote', () => { const sampleComment = 'foobar'; const uniqueId = 'b1234-a4567'; const currentUsername = 'root'; const currentUserFullname = 'Administrator'; + const currentUserAvatar = 'avatar_url'; beforeEach(() => { this.notes = new Notes('', []); - spyOn(_, 'escape').and.callFake((comment) => { - const escapedString = comment.replace(/["&'<>]/g, (a) => { - const escapedToken = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '`': '`' - }[a]; - - return escapedToken; - }); - - return escapedString; - }); }); it('should return constructed placeholder element for regular note based on form contents', () => { @@ -544,46 +591,59 @@ import '~/notes'; uniqueId, isDiscussionNote: false, currentUsername, - currentUserFullname + currentUserFullname, + currentUserAvatar, }); const $tempNoteHeader = $tempNote.find('.note-header'); expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.attr('id')).toEqual(uniqueId); + expect($tempNote.hasClass('being-posted')).toBeTruthy(); + expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() { expect($(this).attr('href')).toEqual(`/${currentUsername}`); }); + expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname); expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`); expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment); }); - it('should escape HTML characters from note based on form contents', () => { - const commentWithHtml = '<script>alert("Boom!");</script>'; + it('should return constructed placeholder element for discussion note based on form contents', () => { const $tempNote = this.notes.createPlaceholderNote({ - formContent: commentWithHtml, + formContent: sampleComment, uniqueId, - isDiscussionNote: false, + isDiscussionNote: true, currentUsername, currentUserFullname }); - expect(_.escape).toHaveBeenCalledWith(commentWithHtml); - expect($tempNote.find('.note-body .note-text p').html()).toEqual('<script>alert("Boom!");</script>'); + expect($tempNote.prop('nodeName')).toEqual('LI'); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); + }); - it('should return constructed placeholder element for discussion note based on form contents', () => { - const $tempNote = this.notes.createPlaceholderNote({ - formContent: sampleComment, + describe('createPlaceholderSystemNote', () => { + const sampleCommandDescription = 'Applying command to close this issue'; + const uniqueId = 'b1234-a4567'; + + beforeEach(() => { + this.notes = new Notes('', []); + spyOn(_, 'escape').and.callFake(htmlEscape); + }); + + it('should return constructed placeholder element for system note based on form contents', () => { + const $tempNote = this.notes.createPlaceholderSystemNote({ + formContent: sampleCommandDescription, uniqueId, - isDiscussionNote: true, - currentUsername, - currentUserFullname }); expect($tempNote.prop('nodeName')).toEqual('LI'); - expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); + expect($tempNote.attr('id')).toEqual(uniqueId); + expect($tempNote.hasClass('being-posted')).toBeTruthy(); + expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); + expect($tempNote.find('.timeline-content i').text().trim()).toEqual(sampleCommandDescription); }); }); @@ -595,7 +655,7 @@ import '~/notes'; it('shows a flash message', () => { this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline); - expect(document.querySelectorAll('.flash-alert').length).toBe(1); + expect($('.flash-alert').is(':visible')).toBeTruthy(); }); }); @@ -605,13 +665,12 @@ import '~/notes'; this.notes = new Notes(); }); - it('removes all the associated flash messages', () => { + it('hides visible flash message', () => { this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline); - this.notes.addFlash('Error message 2', FLASH_TYPE_ALERT, this.notes.parentTimeline); this.notes.clearFlash(); - expect(document.querySelectorAll('.flash-alert').length).toBe(0); + expect($('.flash-alert').is(':visible')).toBeFalsy(); }); }); }); diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js new file mode 100644 index 00000000000..cecc7ceb53d --- /dev/null +++ b/spec/javascripts/pipelines/header_component_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import headerComponent from '~/pipelines/components/header_component.vue'; +import eventHub from '~/pipelines/event_hub'; + +describe('Pipeline details header', () => { + let HeaderComponent; + let vm; + let props; + + beforeEach(() => { + HeaderComponent = Vue.extend(headerComponent); + + const threeWeeksAgo = new Date(); + threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + + props = { + pipeline: { + details: { + status: { + group: 'failed', + icon: 'ci-status-failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + }, + id: 123, + created_at: threeWeeksAgo.toISOString(), + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + retry_path: 'path', + }, + isLoading: false, + }; + + vm = new HeaderComponent({ propsData: props }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render provided pipeline info', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo'); + }); + + describe('action buttons', () => { + it('should call postAction when button action is clicked', () => { + eventHub.$on('headerPostAction', (action) => { + expect(action.path).toEqual('path'); + }); + + vm.$el.querySelector('button').click(); + }); + }); +}); diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js new file mode 100644 index 00000000000..9fec2f61f78 --- /dev/null +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; + +describe('PipelineMdediator', () => { + let mediator; + beforeEach(() => { + mediator = new PipelineMediator({ endpoint: 'foo' }); + }); + + it('should set defaults', () => { + expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.state.isLoading).toEqual(false); + expect(mediator.store).toBeDefined(); + expect(mediator.service).toBeDefined(); + }); + + describe('request and store data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ foo: 'bar' }), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + }); + + it('should store received data', (done) => { + mediator.fetchPipeline(); + + setTimeout(() => { + expect(mediator.store.state.pipeline).toEqual({ foo: 'bar' }); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/javascripts/pipelines/pipeline_store_spec.js new file mode 100644 index 00000000000..85d13445b01 --- /dev/null +++ b/spec/javascripts/pipelines/pipeline_store_spec.js @@ -0,0 +1,27 @@ +import PipelineStore from '~/pipelines/stores/pipeline_store'; + +describe('Pipeline Store', () => { + let store; + + beforeEach(() => { + store = new PipelineStore(); + }); + + it('should set defaults', () => { + expect(store.state).toEqual({ pipeline: {} }); + expect(store.state.pipeline).toEqual({}); + }); + + describe('storePipeline', () => { + it('should store empty object if none is provided', () => { + store.storePipeline(); + + expect(store.state.pipeline).toEqual({}); + }); + + it('should store received object', () => { + store.storePipeline({ foo: 'bar' }); + expect(store.state.pipeline).toEqual({ foo: 'bar' }); + }); + }); +}); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index d74b1281668..594a9856d2c 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -47,6 +47,7 @@ describe('Pipeline Url Component', () => { web_url: '/', name: 'foo', avatar_url: '/', + path: '/', }, }, }; diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 0638483e7aa..050170a54e9 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -24,6 +24,7 @@ describe('Commit component', () => { author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', + path: '/jschatz1', username: 'jschatz1', }, }, @@ -46,6 +47,7 @@ describe('Commit component', () => { author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', + path: '/jschatz1', username: 'jschatz1', }, commitIconSvg: '<svg></svg>', @@ -81,7 +83,7 @@ describe('Commit component', () => { it('should render a link to the author profile', () => { expect( component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), - ).toEqual(props.author.web_url); + ).toEqual(props.author.path); }); it('Should render the author avatar with title and alt attributes', () => { diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 1bf8916b3d0..2b51c89f311 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -33,12 +33,14 @@ describe('Header CI Component', () => { path: 'path', type: 'button', cssClass: 'btn', + isLoading: false, }, { label: 'Go', path: 'path', type: 'link', cssClass: 'link', + isLoading: false, }, ], }; @@ -79,4 +81,13 @@ describe('Header CI Component', () => { expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); }); + + it('should show loading icon', (done) => { + vm.actions[0].isLoading = true; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toEqual(''); + done(); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 286118917e8..67419cfcbea 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -76,7 +76,7 @@ describe('Pipelines Table Row', () => { it('should render user information', () => { expect( component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), - ).toEqual(pipeline.user.web_url); + ).toEqual(pipeline.user.path); expect( component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), @@ -120,7 +120,7 @@ describe('Pipelines Table Row', () => { component = buildComponent(pipeline); const { commitAuthorLink, commitAuthorName } = findElements(); - expect(commitAuthorLink).toEqual(pipeline.commit.author.web_url); + expect(commitAuthorLink).toEqual(pipeline.commit.author.path); expect(commitAuthorName).toEqual(pipeline.commit.author.username); }); |