diff options
Diffstat (limited to 'spec/javascripts')
129 files changed, 2748 insertions, 1421 deletions
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 2aa4fb1f6c6..cf3a76d0d2e 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Api from '~/api'; describe('Api', () => { @@ -7,20 +9,17 @@ describe('Api', () => { api_version: dummyApiVersion, relative_url_root: dummyUrlRoot, }; - const dummyResponse = 'hello from outer space!'; - const sendDummyResponse = () => { - const deferred = $.Deferred(); - deferred.resolve(dummyResponse); - return deferred.promise(); - }; let originalGon; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); originalGon = window.gon; window.gon = Object.assign({}, dummyGon); }); afterEach(() => { + mock.restore(); window.gon = originalGon; }); @@ -38,15 +37,13 @@ describe('Api', () => { describe('group', () => { it('fetches a group', (done) => { const groupId = '123456'; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}.json`; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - return sendDummyResponse(); + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`; + mock.onGet(expectedUrl).reply(200, { + name: 'test', }); Api.group(groupId, (response) => { - expect(response).toBe(dummyResponse); + expect(response.name).toBe('test'); done(); }); }); @@ -57,19 +54,13 @@ describe('Api', () => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`; - const expectedData = Object.assign({ - search: query, - per_page: 20, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.groups(query, options, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -79,19 +70,13 @@ describe('Api', () => { it('fetches namespaces', (done) => { const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`; - const expectedData = { - search: query, - per_page: 20, - }; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.namespaces(query, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -103,21 +88,13 @@ describe('Api', () => { const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; window.gon.current_user_id = 1; - const expectedData = Object.assign({ - search: query, - per_page: 20, - membership: true, - simple: true, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.projects(query, options, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -126,20 +103,13 @@ describe('Api', () => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; - const expectedData = Object.assign({ - search: query, - per_page: 20, - simple: true, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.projects(query, options, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -154,16 +124,16 @@ describe('Api', () => { const expectedData = { label: labelData, }; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.type).toEqual('POST'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); + mock.onPost(expectedUrl).reply((config) => { + expect(config.data).toBe(JSON.stringify(expectedData)); + + return [200, { + name: 'test', + }]; }); Api.newLabel(namespace, project, labelData, (response) => { - expect(response).toBe(dummyResponse); + expect(response.name).toBe('test'); done(); }); }); @@ -174,19 +144,13 @@ describe('Api', () => { const groupId = '123456'; const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; - const expectedData = { - search: query, - per_page: 20, - }; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.groupProjects(groupId, query, (response) => { - expect(response).toBe(dummyResponse); + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); done(); }); }); @@ -197,14 +161,10 @@ describe('Api', () => { const licenseKey = "driver's license"; const data = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.data).toEqual(data); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.licenseText(licenseKey, data, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -214,13 +174,10 @@ describe('Api', () => { it('fetches a gitignore text', (done) => { const gitignoreKey = 'ignore git'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; - spyOn(jQuery, 'get').and.callFake((url, callback) => { - expect(url).toEqual(expectedUrl); - callback(dummyResponse); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.gitignoreText(gitignoreKey, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -230,13 +187,10 @@ describe('Api', () => { it('fetches a .gitlab-ci.yml', (done) => { const gitlabCiYmlKey = 'Y CI ML'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; - spyOn(jQuery, 'get').and.callFake((url, callback) => { - expect(url).toEqual(expectedUrl); - callback(dummyResponse); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.gitlabCiYml(gitlabCiYmlKey, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -246,13 +200,10 @@ describe('Api', () => { it('fetches a Dockerfile', (done) => { const dockerfileYmlKey = 'a giant whale'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; - spyOn(jQuery, 'get').and.callFake((url, callback) => { - expect(url).toEqual(expectedUrl); - callback(dummyResponse); - }); + mock.onGet(expectedUrl).reply(200, 'test'); Api.dockerfileYml(dockerfileYmlKey, (response) => { - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -262,17 +213,13 @@ describe('Api', () => { it('fetches an issue template', (done) => { const namespace = 'some namespace'; const project = 'some project'; - const templateKey = 'template key'; + const templateKey = ' template #%?.key '; const templateType = 'template type'; - const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${templateKey}`; - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - return sendDummyResponse(); - }); + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`; + mock.onGet(expectedUrl).reply(200, 'test'); Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { - expect(error).toBe(null); - expect(response).toBe(dummyResponse); + expect(response).toBe('test'); done(); }); }); @@ -283,20 +230,14 @@ describe('Api', () => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`; - const expectedData = Object.assign({ - search: query, - per_page: 20, - }, options); - spyOn(jQuery, 'ajax').and.callFake((request) => { - expect(request.url).toEqual(expectedUrl); - expect(request.dataType).toEqual('json'); - expect(request.data).toEqual(expectedData); - return sendDummyResponse(); - }); + mock.onGet(expectedUrl).reply(200, [{ + name: 'test', + }]); Api.users(query, options) - .then((response) => { - expect(response).toBe(dummyResponse); + .then(({ data }) => { + expect(data.length).toBe(1); + expect(data[0].name).toBe('test'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js index 9eeae474e7d..38d9bba6868 100644 --- a/spec/javascripts/behaviors/secret_values_spec.js +++ b/spec/javascripts/behaviors/secret_values_spec.js @@ -1,16 +1,24 @@ import SecretValues from '~/behaviors/secret_values'; -function generateFixtureMarkup(secrets, isRevealed) { +function generateValueMarkup( + secret, + valueClass = 'js-secret-value', + placeholderClass = 'js-secret-value-placeholder', +) { + return ` + <div class="${placeholderClass}"> + *** + </div> + <div class="hide ${valueClass}"> + ${secret} + </div> + `; +} + +function generateFixtureMarkup(secrets, isRevealed, valueClass, placeholderClass) { return ` <div class="js-secret-container"> - ${secrets.map(secret => ` - <div class="js-secret-value-placeholder"> - *** - </div> - <div class="hide js-secret-value"> - ${secret} - </div> - `).join('')} + ${secrets.map(secret => generateValueMarkup(secret, valueClass, placeholderClass)).join('')} <button class="js-secret-value-reveal-button" data-secret-reveal-status="${isRevealed}" @@ -21,11 +29,25 @@ function generateFixtureMarkup(secrets, isRevealed) { `; } -function setupSecretFixture(secrets, isRevealed) { +function setupSecretFixture( + secrets, + isRevealed, + valueClass = 'js-secret-value', + placeholderClass = 'js-secret-value-placeholder', +) { const wrapper = document.createElement('div'); - wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed); - - const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container')); + wrapper.innerHTML = generateFixtureMarkup( + secrets, + isRevealed, + valueClass, + placeholderClass, + ); + + const secretValues = new SecretValues({ + container: wrapper.querySelector('.js-secret-container'), + valueSelector: `.${valueClass}`, + placeholderSelector: `.${placeholderClass}`, + }); secretValues.init(); return wrapper; @@ -49,7 +71,7 @@ describe('setupSecretValues', () => { expect(revealButton.textContent).toEqual('Hide value'); }); - it('should value hidden initially', () => { + it('should have value hidden initially', () => { const wrapper = setupSecretFixture(secrets, false); const values = wrapper.querySelectorAll('.js-secret-value'); const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); @@ -143,4 +165,64 @@ describe('setupSecretValues', () => { }); }); }); + + describe('with dynamic secrets', () => { + const secrets = ['mysecret123', 'happygoat456', 'tanuki789']; + + it('should toggle values and placeholders', () => { + const wrapper = setupSecretFixture(secrets, false); + // Insert the new dynamic row + wrapper.querySelector('.js-secret-container').insertAdjacentHTML('afterbegin', generateValueMarkup('foobarbazdynamic')); + + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + const values = wrapper.querySelectorAll('.js-secret-value'); + const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); + + revealButton.click(); + + expect(values.length).toEqual(4); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(false); + }); + expect(placeholders.length).toEqual(4); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(true); + }); + + revealButton.click(); + + expect(values.length).toEqual(4); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(true); + }); + expect(placeholders.length).toEqual(4); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(false); + }); + }); + }); + + describe('selector options', () => { + const secrets = ['mysecret123']; + + it('should respect `valueSelector` and `placeholderSelector` options', () => { + const valueClass = 'js-some-custom-placeholder-selector'; + const placeholderClass = 'js-some-custom-value-selector'; + + const wrapper = setupSecretFixture(secrets, false, valueClass, placeholderClass); + const values = wrapper.querySelectorAll(`.${valueClass}`); + const placeholders = wrapper.querySelectorAll(`.${placeholderClass}`); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + + expect(values.length).toEqual(1); + expect(placeholders.length).toEqual(1); + + revealButton.click(); + + expect(values.length).toEqual(1); + expect(values[0].classList.contains('hide')).toEqual(false); + expect(placeholders.length).toEqual(1); + expect(placeholders[0].classList.contains('hide')).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index df1b2c9960b..a143fc827d5 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -45,7 +45,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('does not show loading icon', () => { @@ -96,7 +96,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('does not show loading icon', () => { @@ -127,7 +127,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('does not show loading icon', () => { diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js index cfa6650d85f..892411a6a40 100644 --- a/spec/javascripts/blob/viewer/index_spec.js +++ b/spec/javascripts/blob/viewer/index_spec.js @@ -1,28 +1,35 @@ /* eslint-disable no-new */ +import MockAdapter from 'axios-mock-adapter'; import BlobViewer from '~/blob/viewer/index'; +import axios from '~/lib/utils/axios_utils'; describe('Blob viewer', () => { let blob; + let mock; + preloadFixtures('snippets/show.html.raw'); beforeEach(() => { + mock = new MockAdapter(axios); + loadFixtures('snippets/show.html.raw'); $('#modal-upload-blob').remove(); blob = new BlobViewer(); - spyOn($, 'ajax').and.callFake(() => { - const d = $.Deferred(); - - d.resolve({ - html: '<div>testing</div>', - }); + mock.onGet('http://test.host/snippets/1.json?viewer=rich').reply(200, { + html: '<div>testing</div>', + }); - return d.promise(); + mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, { + html: '<div>testing</div>', }); + + spyOn(axios, 'get').and.callThrough(); }); afterEach(() => { + mock.restore(); location.hash = ''; }); @@ -30,7 +37,6 @@ describe('Blob viewer', () => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); expect( document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]') .classList.contains('hidden'), @@ -46,7 +52,6 @@ describe('Blob viewer', () => { new BlobViewer(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); expect( document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]') .classList.contains('hidden'), @@ -64,12 +69,8 @@ describe('Blob viewer', () => { }); asyncClick() + .then(() => asyncClick()) .then(() => { - expect($.ajax).toHaveBeenCalled(); - return asyncClick(); - }) - .then(() => { - expect($.ajax.calls.count()).toBe(1); expect( document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'), ).toBe('true'); @@ -122,7 +123,6 @@ describe('Blob viewer', () => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); expect( copyButton.classList.contains('disabled'), ).toBeFalsy(); @@ -135,8 +135,6 @@ describe('Blob viewer', () => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { - expect($.ajax).toHaveBeenCalled(); - expect( copyButton.getAttribute('data-original-title'), ).toBe('Copy source to clipboard'); @@ -171,14 +169,14 @@ describe('Blob viewer', () => { it('sends AJAX request when switching to simple view', () => { blob.switchToViewer('simple'); - expect($.ajax).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalled(); }); it('does not send AJAX request when switching to rich view', () => { blob.switchToViewer('simple'); blob.switchToViewer('rich'); - expect($.ajax.calls.count()).toBe(1); + expect(axios.get.calls.count()).toBe(1); }); }); }); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 4e73fa1fe87..80a598e63bd 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -55,7 +55,7 @@ describe('Board card', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('returns false when detailIssue is empty', () => { diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 7c5888b6d82..a5fcb10b9dd 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -60,7 +60,7 @@ describe('Board list component', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('renders component', () => { @@ -154,6 +154,18 @@ describe('Board list component', () => { }); }); + it('sets data attribute with invalid id', (done) => { + component.showCount = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), + ).toBe('-1'); + + done(); + }); + }); + it('shows how many more issues to load', (done) => { component.showCount = true; component.list.issuesSize = 20; @@ -172,9 +184,9 @@ describe('Board list component', () => { component.$refs.list.style.height = '100px'; component.$refs.list.style.overflow = 'scroll'; - for (let i = 0; i < 19; i += 1) { - const issue = component.list.issues[0]; - issue.id += 1; + for (let i = 1; i < 20; i += 1) { + const issue = Object.assign({}, component.list.issues[0]); + issue.id += i; component.list.issues.push(issue); } diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index c62c537841c..e204985f039 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -58,7 +58,7 @@ describe('Issue boards new issue form', () => { afterEach(() => { vm.$destroy(); - mock.reset(); + mock.restore(); }); it('calls submit if submit button is clicked', (done) => { diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 49fb20f4c84..8411f4dd8a6 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -35,7 +35,7 @@ describe('Store', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('starts with a blank state', () => { diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 8ef221257be..278155c585e 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -45,6 +45,9 @@ describe('Issue card component', () => { component = new Vue({ el: document.querySelector('.test-container'), + components: { + 'issue-card': gl.issueBoards.IssueCardInner, + }, data() { return { list, @@ -53,9 +56,6 @@ describe('Issue card component', () => { rootPath: '/', }; }, - components: { - 'issue-card': gl.issueBoards.IssueCardInner, - }, template: ` <issue-card :issue="issue" diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 645ce831b53..34964b20b05 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; - +import _ from 'underscore'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; @@ -30,7 +30,7 @@ describe('List model', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('gets issues when created', (done) => { diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 9ae2d535398..0671facb285 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,5 +1,6 @@ /* global BoardService */ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ +import _ from 'underscore'; export const listObj = { id: 300, diff --git a/spec/javascripts/boards/utils/query_data_spec.js b/spec/javascripts/boards/utils/query_data_spec.js new file mode 100644 index 00000000000..922215ffc1d --- /dev/null +++ b/spec/javascripts/boards/utils/query_data_spec.js @@ -0,0 +1,27 @@ +import queryData from '~/boards/utils/query_data'; + +describe('queryData', () => { + it('parses path for label with trailing +', () => { + expect( + queryData('label_name[]=label%2B', {}), + ).toEqual({ + label_name: ['label+'], + }); + }); + + it('parses path for milestone with trailing +', () => { + expect( + queryData('milestone_title=A%2B', {}), + ).toEqual({ + milestone_title: 'A+', + }); + }); + + it('parses path for search terms with spaces', () => { + expect( + queryData('search=two+words', {}), + ).toEqual({ + search: 'two words', + }); + }); +}); diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index f5be9ea0fb2..7b38f6b7855 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -23,16 +23,24 @@ describe('Clusters', () => { }); describe('toggle', () => { - it('should update the button and the input field on click', () => { - cluster.toggleButton.click(); + it('should update the button and the input field on click', (done) => { + const toggleButton = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle'); + const toggleInput = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle-input'); - expect( - cluster.toggleButton.classList, - ).not.toContain('is-checked'); + toggleButton.click(); - expect( - cluster.toggleInput.getAttribute('value'), - ).toEqual('false'); + getSetTimeoutPromise() + .then(() => { + expect( + toggleButton.classList, + ).not.toContain('is-checked'); + + expect( + toggleInput.getAttribute('value'), + ).toEqual('false'); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js deleted file mode 100644 index 0a8b63ed5b4..00000000000 --- a/spec/javascripts/clusters/clusters_index_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import setClusterTableToggles from '~/clusters/clusters_index'; -import { setTimeout } from 'core-js/library/web/timers'; - -describe('Clusters table', () => { - preloadFixtures('clusters/index_cluster.html.raw'); - let mock; - - beforeEach(() => { - loadFixtures('clusters/index_cluster.html.raw'); - mock = new MockAdapter(axios); - setClusterTableToggles(); - }); - - describe('update cluster', () => { - it('renders loading state while request is made', () => { - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - - expect(button.classList).toContain('is-loading'); - expect(button.getAttribute('disabled')).toEqual('true'); - }); - - afterEach(() => { - mock.restore(); - }); - - it('shows updated state after sucessfull request', (done) => { - mock.onPut().reply(200, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - button.click(); - - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).not.toContain('is-checked'); - done(); - }, 0); - }); - - it('shows inital state after failed request', (done) => { - mock.onPut().reply(500, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).toContain('is-checked'); - done(); - }, 0); - }); - }); -}); diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js new file mode 100644 index 00000000000..90f290e845e --- /dev/null +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -0,0 +1,104 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Commit pipeline status component', () => { + let vm; + let Component; + let mock; + const mockCiStatus = { + details_path: '/root/hello-world/pipelines/1', + favicon: 'canceled.ico', + group: 'canceled', + has_details: true, + icon: 'status_canceled', + label: 'canceled', + text: 'canceled', + }; + + beforeEach(() => { + Component = Vue.extend(commitPipelineStatus); + }); + + describe('While polling pipeline data succesfully', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.resolve([200, { + pipelines: [ + { + details: { + status: mockCiStatus, + }, + }, + ], + }]); + return res; + }); + vm = mountComponent(Component, { + endpoint: '/dummy/endpoint', + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('shows the loading icon when polling is starting', (done) => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + setTimeout(() => { + expect(vm.$el.querySelector('.loading-container')).toBe(null); + done(); + }); + }); + + it('contains a ciStatus when the polling is succesful ', (done) => { + setTimeout(() => { + expect(vm.ciStatus).toEqual(mockCiStatus); + done(); + }); + }); + + it('contains a ci-status icon when polling is succesful', (done) => { + setTimeout(() => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); + expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(`ci-status-icon-${mockCiStatus.group}`); + done(); + }); + }); + }); + + describe('When polling data was not succesful', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.reject([502, { }]); + return res; + }); + vm = new Component({ + props: { + endpoint: '/dummy/endpoint', + }, + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('calls an errorCallback', (done) => { + spyOn(vm, 'errorCallback').and.callThrough(); + vm.$mount(); + setTimeout(() => { + expect(vm.errorCallback.calls.count()).toEqual(1); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 9fc047b1f5e..0afe09d87bc 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; @@ -9,9 +10,10 @@ describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTable = Vue.extend(pipelinesTable); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('successful request', () => { diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/javascripts/commit_merge_requests_spec.js new file mode 100644 index 00000000000..3466ef51ea8 --- /dev/null +++ b/spec/javascripts/commit_merge_requests_spec.js @@ -0,0 +1,60 @@ +import * as CommitMergeRequests from '~/commit_merge_requests'; + +describe('CommitMergeRequests', () => { + describe('createContent', () => { + it('should return created content', () => { + const content1 = CommitMergeRequests.createContent([{ iid: 1, path: '/path1', title: 'foo' }, { iid: 2, path: '/path2', title: 'baz' }])[0]; + expect(content1.tagName).toEqual('SPAN'); + expect(content1.childElementCount).toEqual(4); + + const content2 = CommitMergeRequests.createContent([])[0]; + expect(content2.tagName).toEqual('SPAN'); + expect(content2.childElementCount).toEqual(0); + expect(content2.innerText).toEqual('No related merge requests found'); + }); + }); + + describe('getHeaderText', () => { + it('should return header text', () => { + expect(CommitMergeRequests.getHeaderText(0, 1)).toEqual('1 merge request'); + expect(CommitMergeRequests.getHeaderText(0, 2)).toEqual('2 merge requests'); + expect(CommitMergeRequests.getHeaderText(1, 1)).toEqual(','); + expect(CommitMergeRequests.getHeaderText(1, 2)).toEqual(','); + }); + }); + + describe('createHeader', () => { + it('should return created header', () => { + const header = CommitMergeRequests.createHeader(0, 1)[0]; + expect(header.tagName).toEqual('SPAN'); + expect(header.innerText).toEqual('1 merge request'); + }); + }); + + describe('createItem', () => { + it('should return created item', () => { + const item = CommitMergeRequests.createItem({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(item.tagName).toEqual('SPAN'); + expect(item.childElementCount).toEqual(2); + expect(item.children[0].tagName).toEqual('A'); + expect(item.children[1].tagName).toEqual('SPAN'); + }); + }); + + describe('createLink', () => { + it('should return created link', () => { + const link = CommitMergeRequests.createLink({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(link.tagName).toEqual('A'); + expect(link.href).toMatch(/\/path$/); + expect(link.innerText).toEqual('!1'); + }); + }); + + describe('createTitle', () => { + it('should return created title', () => { + const title = CommitMergeRequests.createTitle({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(title.tagName).toEqual('SPAN'); + expect(title.innerText).toEqual('foo'); + }); + }); +}); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index d0176520440..44ec9e4eabf 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -1,4 +1,6 @@ import 'vendor/jquery.endless-scroll'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import CommitsList from '~/commits'; describe('Commits List', () => { @@ -43,30 +45,47 @@ describe('Commits List', () => { describe('on entering input', () => { let ajaxSpy; + let mock; beforeEach(() => { CommitsList.init(25); CommitsList.searchField.val(''); spyOn(history, 'replaceState').and.stub(); - ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { - req.success({ - data: '<li>Result</li>', - }); + mock = new MockAdapter(axios); + + mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, { + html: '<li>Result</li>', }); + + ajaxSpy = spyOn(axios, 'get').and.callThrough(); + }); + + afterEach(() => { + mock.restore(); }); - it('should save the last search string', () => { + it('should save the last search string', (done) => { CommitsList.searchField.val('GitLab'); - CommitsList.filterResults(); - expect(ajaxSpy).toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual('GitLab'); + CommitsList.filterResults() + .then(() => { + expect(ajaxSpy).toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual('GitLab'); + + done(); + }) + .catch(done.fail); }); - it('should not make ajax call if the input does not change', () => { - CommitsList.filterResults(); - expect(ajaxSpy).not.toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual(''); + it('should not make ajax call if the input does not change', (done) => { + CommitsList.filterResults() + .then(() => { + expect(ajaxSpy).not.toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual(''); + + done(); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js new file mode 100644 index 00000000000..143137c23ec --- /dev/null +++ b/spec/javascripts/create_item_dropdown_spec.js @@ -0,0 +1,183 @@ +import CreateItemDropdown from '~/create_item_dropdown'; + +const DROPDOWN_ITEM_DATA = [{ + title: 'one', + id: 'one', + text: 'one', +}, { + title: 'two', + id: 'two', + text: 'two', +}, { + title: 'three', + id: 'three', + text: 'three', +}]; + +describe('CreateItemDropdown', () => { + preloadFixtures('static/create_item_dropdown.html.raw'); + + let $wrapperEl; + let createItemDropdown; + + function createItemAndClearInput(text) { + // Filter for the new item + $wrapperEl.find('.dropdown-input-field') + .val(text) + .trigger('input'); + + // Create the new item + const $createButton = $wrapperEl.find('.js-dropdown-create-new-item'); + $createButton.click(); + + // Clear out the filter + $wrapperEl.find('.dropdown-input-field') + .val('') + .trigger('input'); + } + + beforeEach(() => { + loadFixtures('static/create_item_dropdown.html.raw'); + $wrapperEl = $('.js-create-item-dropdown-fixture-root'); + }); + + afterEach(() => { + $wrapperEl.remove(); + }); + + describe('items', () => { + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + }); + + it('should have a dropdown item for each piece of data', () => { + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + }); + }); + + describe('created items', () => { + const NEW_ITEM_TEXT = 'foobarbaz'; + + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + + // Open the dropdown + $('.js-dropdown-menu-toggle').click(); + + // Filter for the new item + $wrapperEl.find('.dropdown-input-field') + .val(NEW_ITEM_TEXT) + .trigger('input'); + }); + + it('create new item button should include the filter text', () => { + expect($wrapperEl.find('.js-dropdown-create-new-item code').text()).toEqual(NEW_ITEM_TEXT); + }); + + it('should update the dropdown with the newly created item', () => { + // Create the new item + const $createButton = $wrapperEl.find('.js-dropdown-create-new-item'); + $createButton.click(); + + expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual(NEW_ITEM_TEXT); + expect($wrapperEl.find('input[name="variable[environment]"]').val()).toEqual(NEW_ITEM_TEXT); + }); + + it('should include newly created item in dropdown list', () => { + createItemAndClearInput(NEW_ITEM_TEXT); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length); + expect($($itemEls.get(DROPDOWN_ITEM_DATA.length)).text()).toEqual(NEW_ITEM_TEXT); + }); + + it('should not duplicate an item when trying to create an existing item', () => { + createItemAndClearInput(DROPDOWN_ITEM_DATA[0].text); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + }); + }); + + describe('clearDropdown()', () => { + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + }); + + it('should clear all data and filter input', () => { + const filterInput = $wrapperEl.find('.dropdown-input-field'); + + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + // Filter for an item + filterInput + .val('one') + .trigger('input'); + + const $itemElsAfterFilter = $wrapperEl.find('.js-dropdown-content a'); + expect($itemElsAfterFilter.length).toEqual(1); + + createItemDropdown.clearDropdown(); + + const $itemElsAfterClear = $wrapperEl.find('.js-dropdown-content a'); + expect($itemElsAfterClear.length).toEqual(0); + expect(filterInput.val()).toEqual(''); + }); + }); + + describe('createNewItemFromValue option', () => { + beforeEach(() => { + createItemDropdown = new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + createNewItemFromValue: newValue => ({ + title: `${newValue}-title`, + id: `${newValue}-id`, + text: `${newValue}-text`, + }), + }); + }); + + it('all items go through createNewItemFromValue', () => { + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + createItemAndClearInput('new-item'); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length); + expect($($itemEls[3]).text()).toEqual('new-item-text'); + expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title'); + }); + }); +}); diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js index fb6b7fee168..64a76a6ee5f 100644 --- a/spec/javascripts/cycle_analytics/banner_spec.js +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -20,8 +20,9 @@ describe('Cycle analytics banner', () => { expect( vm.$el.querySelector('h4').textContent.trim(), ).toEqual('Introducing Cycle Analytics'); + expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.'); expect( vm.$el.querySelector('a').textContent.trim(), diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js index 31b65fd1cde..ad0fc38a856 100644 --- a/spec/javascripts/cycle_analytics/total_time_component_spec.js +++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js @@ -23,7 +23,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('3 days 4 hrs'); }); it('should render information for hours and minutes', () => { @@ -34,7 +34,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('4 hrs 35 mins'); }); it('should render information for seconds', () => { @@ -44,7 +44,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('45 s'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('45 s'); }); }); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 0ca9290d3d2..b870f87eab9 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import eventHub from '~/deploy_keys/eventhub'; import deployKeysApp from '~/deploy_keys/components/app.vue'; diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 2f28c5bbf01..b7aadf604a4 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -53,18 +53,24 @@ describe('Deploy keys key', () => { ).toBe('Remove'); }); - it('shows write access text when key has write access', (done) => { - vm.deployKey.can_push = true; + it('shows write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = true; Vue.nextTick(() => { expect( - vm.$el.querySelector('.write-access-allowed'), - ).not.toBeNull(); - - expect( - vm.$el.querySelector('.write-access-allowed').textContent.trim(), + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), ).toBe('Write access allowed'); + done(); + }); + }); + + it('does not show write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = false; + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), + ).toBe('Read access only'); done(); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 0e141adb628..7a34126eef7 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -68,7 +68,7 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit: { @@ -84,7 +84,7 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index d02adb25b4e..a41a4e5a3f7 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsComponent from '~/environments/components/environments_app.vue'; import { environment, folder } from './mock_data'; diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 4ea4d9d7499..a085074d312 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import { environmentsList } from '../mock_data'; 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 2ecb64d84b5..0684c3498a2 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index d26ea3febe8..8e74c4f859c 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle expect(response).to be_success store_frontend_fixture(response, example.description) end - - context 'rendering non-empty state' do - before do - cluster - end - - it 'clusters/index_cluster.html.raw' do |example| - get :index, - namespace_id: namespace, - project_id: project - - expect(response).to be_success - store_frontend_fixture(response, example.description) - end - end end diff --git a/spec/javascripts/fixtures/create_item_dropdown.html.haml b/spec/javascripts/fixtures/create_item_dropdown.html.haml new file mode 100644 index 00000000000..d4d91b93caf --- /dev/null +++ b/spec/javascripts/fixtures/create_item_dropdown.html.haml @@ -0,0 +1,13 @@ +.js-create-item-dropdown-fixture-root + %input{ name: 'variable[environment]', type: 'hidden' } + = dropdown_tag('some label', + options: { toggle_class: 'js-dropdown-menu-toggle', + content_class: 'js-dropdown-content', + filter: true, + dropdown_class: "dropdown-menu-selectable", + footer_content: true }) do + %ul.dropdown-footer-list + %li + %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item" } + Create wildcard + %code diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 1339ee00870..68a150f602a 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -14,7 +14,7 @@ "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", "web_url": "http://localhost:3000/u/root" }, "name": "test", diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb new file mode 100644 index 00000000000..703cd3d49fa --- /dev/null +++ b/spec/javascripts/fixtures/search.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe SearchController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + render_views + + before(:all) do + clean_frontend_fixtures('search/') + end + + it 'search/show.html.raw' do |example| + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index 97e3ab682c5..7198dbd4cf2 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -183,11 +183,15 @@ describe('Flash', () => { }); it('adds flash element into container', () => { - flash('test'); + flash('test', 'alert', document, null, false, true); expect( document.querySelector('.flash-alert'), ).not.toBeNull(); + + expect( + document.body.className, + ).toContain('flash-shown'); }); it('adds flash into specified parent', () => { @@ -220,13 +224,17 @@ describe('Flash', () => { }); it('removes element after clicking', () => { - flash('test', 'alert', document, null, false); + flash('test', 'alert', document, null, false, true); document.querySelector('.flash-alert').click(); expect( document.querySelector('.flash-alert'), ).toBeNull(); + + expect( + document.body.className, + ).not.toContain('flash-shown'); }); describe('with actionConfig', () => { diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index a3fa07d5bc2..eb9330f5e5b 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -167,30 +167,26 @@ describe('Fly out sidebar navigation', () => { describe('mouseEnterTopItems', () => { beforeEach(() => { - jasmine.clock().install(); - el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>'; }); - afterEach(() => { - jasmine.clock().uninstall(); - }); - - it('shows sub-items after 0ms if no menu is open', () => { + it('shows sub-items after 0ms if no menu is open', (done) => { mouseEnterTopItems(el); expect( getHideSubItemsInterval(), ).toBe(0); - jasmine.clock().tick(0); + setTimeout(() => { + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); - expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + done(); + }); }); - it('shows sub-items after 300ms if a menu is currently open', () => { + it('shows sub-items after 300ms if a menu is currently open', (done) => { documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, @@ -203,17 +199,19 @@ describe('Fly out sidebar navigation', () => { clientY: el.getBoundingClientRect().top + 10, }); - mouseEnterTopItems(el); + mouseEnterTopItems(el, 0); expect( getHideSubItemsInterval(), ).toBe(300); - jasmine.clock().tick(300); + setTimeout(() => { + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); - expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + done(); + }); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 97e39f6411b..8338efe915b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -256,6 +256,36 @@ describe('AppComponent', () => { }); }); + describe('showLeaveGroupModal', () => { + it('caches candidate group (as props) which is to be left', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.targetGroup).toBe(null); + expect(vm.targetParentGroup).toBe(null); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.targetGroup).not.toBe(null); + expect(vm.targetParentGroup).not.toBe(null); + }); + + it('updates props which show modal confirmation dialog', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.showModal).toBeFalsy(); + expect(vm.groupLeaveConfirmationMessage).toBe(''); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); + }); + }); + + describe('hideLeaveGroupModal', () => { + it('hides modal confirmation which is shown before leaving the group', () => { + const group = Object.assign({}, mockParentGroupItem); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + vm.hideLeaveGroupModal(); + expect(vm.showModal).toBeFalsy(); + }); + }); + describe('leaveGroup', () => { let groupItem; let childGroupItem; @@ -265,21 +295,24 @@ describe('AppComponent', () => { groupItem.children = mockChildren; childGroupItem = groupItem.children[0]; groupItem.isChildrenLoading = false; + vm.targetGroup = childGroupItem; + vm.targetParentGroup = groupItem; }); - it('should leave group and remove group item from tree', (done) => { + it('hides modal confirmation leave group and remove group item from tree', (done) => { const notice = `You left the "${childGroupItem.fullName}" group.`; spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); spyOn($, 'scrollTo'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); - expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + vm.leaveGroup(); + expect(vm.showModal).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { expect($.scrollTo).toHaveBeenCalledWith(0); - expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem); + expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup); expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); done(); }, 0); @@ -291,13 +324,13 @@ describe('AppComponent', () => { spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + vm.leaveGroup(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -309,12 +342,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -364,7 +397,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); newVm.$destroy(); @@ -404,7 +437,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); done(); @@ -439,5 +472,14 @@ describe('AppComponent', () => { done(); }); }); + + it('renders modal confirmation dialog', () => { + vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; + vm.showModal = true; + const modalDialogEl = vm.$el.querySelector('.modal'); + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + }); }); }); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 6d6fb410859..acccbe639c4 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -26,32 +26,12 @@ describe('ItemActionsComponent', () => { vm.$destroy(); }); - describe('computed', () => { - describe('leaveConfirmationMessage', () => { - it('should return appropriate string for leave group confirmation', () => { - expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?'); - }); - }); - }); - describe('methods', () => { describe('onLeaveGroup', () => { - it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => { - expect(vm.modalStatus).toBeFalsy(); - vm.onLeaveGroup(); - expect(vm.modalStatus).toBeTruthy(); - }); - }); - - describe('leaveGroup', () => { - it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { + it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => { spyOn(eventHub, '$emit'); - vm.modalStatus = true; - - vm.leaveGroup(); - - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); + vm.onLeaveGroup(); + expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', vm.group, vm.parentGroup); }); }); }); @@ -72,7 +52,8 @@ describe('ItemActionsComponent', () => { expect(editBtn.getAttribute('href')).toBe(group.editPath); expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); expect(editBtn.dataset.originalTitle).toBe('Edit group'); - expect(editBtn.querySelector('i.fa.fa-cogs')).toBeDefined(); + expect(editBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings'); newVm.$destroy(); }); @@ -88,17 +69,10 @@ describe('ItemActionsComponent', () => { expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); - expect(leaveBtn.querySelector('i.fa.fa-sign-out')).toBeDefined(); + expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave'); newVm.$destroy(); }); - - it('should show modal dialog when `modalStatus` is set to `true`', () => { - vm.modalStatus = true; - const modalDialogEl = vm.$el.querySelector('.modal'); - expect(modalDialogEl).toBeDefined(); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); - }); }); }); diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js index 686b8eaed31..1415ffb7eb3 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js +++ b/spec/javascripts/helpers/class_spec_helper_spec.js @@ -3,7 +3,7 @@ import './class_spec_helper'; describe('ClassSpecHelper', () => { - describe('itShouldBeAStaticMethod', function () { + describe('itShouldBeAStaticMethod', () => { beforeEach(() => { class TestClass { instanceMethod() { this.prop = 'val'; } @@ -14,23 +14,5 @@ describe('ClassSpecHelper', () => { }); ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod'); - - it('should have a defined spec', () => { - expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method'); - }); - - it('should pass for a static method', () => { - const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod'); - expect(spec.status()).toBe('passed'); - }); - - it('should fail for an instance method', (done) => { - const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod'); - spec.resultCallback = (result) => { - expect(result.status).toBe('failed'); - done(); - }; - spec.execute(); - }); }); }); diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js index a9783ea065c..323fee3767e 100644 --- a/spec/javascripts/helpers/user_mock_data_helper.js +++ b/spec/javascripts/helpers/user_mock_data_helper.js @@ -4,7 +4,7 @@ export default { for (let i = 0; i < numberUsers; i = i += 1) { users.push( { - avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', id: (i + 1), name: `GitLab User ${i}`, username: `gitlab${i}`, diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js index 9033eb9ce02..d0fba908e34 100644 --- a/spec/javascripts/integrations/integration_settings_form_spec.js +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -1,3 +1,5 @@ +import MockAdaptor from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import IntegrationSettingsForm from '~/integrations/integration_settings_form'; describe('IntegrationSettingsForm', () => { @@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => { describe('testSettings', () => { let integrationSettingsForm; let formData; + let mock; beforeEach(() => { + mock = new MockAdaptor(axios); + + spyOn(axios, 'put').and.callThrough(); + 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()); + afterEach(() => { + mock.restore(); + }); - integrationSettingsForm.testSettings(formData); + it('should make an ajax request with provided `formData`', (done) => { + integrationSettingsForm.testSettings(formData) + .then(() => { + expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData); - expect($.ajax).toHaveBeenCalledWith({ - type: 'PUT', - url: integrationSettingsForm.testEndPoint, - data: formData, - }); + done(); + }) + .catch(done.fail); }); - it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => { + it('should show error Flash with `Save anyway` action if ajax request responds with error in test', (done) => { const errorMessage = 'Test failed.'; - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - - integrationSettingsForm.testSettings(formData); + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: true, + message: errorMessage, + service_response: 'some error', + }); - deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' }); + integrationSettingsForm.testSettings(formData) + .then(() => { + const $flashContainer = $('.flash-container'); + expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); + expect($flashContainer.find('.flash-action')).toBeDefined(); + expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway'); - const $flashContainer = $('.flash-container'); - expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); - expect($flashContainer.find('.flash-action')).toBeDefined(); - expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway'); + done(); + }) + .catch(done.fail); }); - it('should submit form if ajax request responds without any error in test', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should submit form if ajax request responds without any error in test', (done) => { + spyOn(integrationSettingsForm.$form, 'submit'); - integrationSettingsForm.testSettings(formData); + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: false, + }); - spyOn(integrationSettingsForm.$form, 'submit'); - deferred.resolve({ error: false }); + integrationSettingsForm.testSettings(formData) + .then(() => { + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); - expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + done(); + }) + .catch(done.fail); }); - 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()); + it('should submit form when clicked on `Save anyway` action of error Flash', (done) => { + spyOn(integrationSettingsForm.$form, 'submit'); - integrationSettingsForm.testSettings(formData); + const errorMessage = 'Test failed.'; + mock.onPut(integrationSettingsForm.testEndPoint).reply(200, { + error: true, + message: errorMessage, + }); - deferred.resolve({ error: true, message: errorMessage }); + integrationSettingsForm.testSettings(formData) + .then(() => { + const $flashAction = $('.flash-container .flash-action'); + expect($flashAction).toBeDefined(); - const $flashAction = $('.flash-container .flash-action'); - expect($flashAction).toBeDefined(); + $flashAction.get(0).click(); + }) + .then(() => { + expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); - spyOn(integrationSettingsForm.$form, 'submit'); - $flashAction.get(0).click(); - expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); + done(); + }) + .catch(done.fail); }); - it('should show error Flash if ajax request failed', () => { + it('should show error Flash if ajax request failed', (done) => { const errorMessage = 'Something went wrong on our end.'; - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - integrationSettingsForm.testSettings(formData); + mock.onPut(integrationSettingsForm.testEndPoint).networkError(); - deferred.reject(); + integrationSettingsForm.testSettings(formData) + .then(() => { + expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); - expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); + done(); + }) + .catch(done.fail); }); - it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - - integrationSettingsForm.testSettings(formData); + it('should always call `toggleSubmitBtnState` with `false` once request is completed', (done) => { + mock.onPut(integrationSettingsForm.testEndPoint).networkError(); spyOn(integrationSettingsForm, 'toggleSubmitBtnState'); - deferred.reject(); - expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); + integrationSettingsForm.testSettings(formData) + .then(() => { + expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); + + done(); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 5a9112716f4..d53ffecbd35 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -1,3 +1,5 @@ +import MockAdaptor from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import IssuableIndex from '~/issuable_index'; describe('Issuable', () => { @@ -19,6 +21,8 @@ describe('Issuable', () => { }); describe('resetIncomingEmailToken', () => { + let mock; + beforeEach(() => { const element = document.createElement('a'); element.classList.add('incoming-email-token-reset'); @@ -30,14 +34,28 @@ describe('Issuable', () => { document.body.appendChild(input); Issuable = new IssuableIndex('issue_'); + + mock = new MockAdaptor(axios); + + mock.onPut('foo').reply(200, { + new_address: 'testing123', + }); }); - it('should send request to reset email token', () => { - spyOn(jQuery, 'ajax').and.callThrough(); + afterEach(() => { + mock.restore(); + }); + + it('should send request to reset email token', (done) => { + spyOn(axios, 'put').and.callThrough(); document.querySelector('.incoming-email-token-reset').click(); - expect(jQuery.ajax).toHaveBeenCalled(); - expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo'); + setTimeout(() => { + expect(axios.put).toHaveBeenCalledWith('foo'); + expect($('#issuable_email').val()).toBe('testing123'); + + done(); + }); }); }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 1454ca52018..1c9f48028f2 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -59,7 +59,7 @@ describe('Issuable output', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); realtimeRequestCount = 0; vm.poll.stop(); @@ -218,6 +218,39 @@ describe('Issuable output', () => { }); }); + describe('shows dialog when issue has unsaved changed', () => { + it('confirms on title change', (done) => { + vm.showForm = true; + vm.state.titleText = 'title has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).not.toBeNull(); + done(); + }); + }); + + it('confirms on description change', (done) => { + vm.showForm = true; + vm.state.descriptionText = 'description has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).not.toBeNull(); + done(); + }); + }); + + it('does nothing when nothing has changed', (done) => { + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).toBeNull(); + done(); + }); + }); + }); + describe('error when updating', () => { beforeEach(() => { spyOn(window, 'Flash').and.callThrough(); diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js index 2b7ee65094b..30441faf844 100644 --- a/spec/javascripts/issue_show/components/fields/description_template_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Issue description template component', () => { let vm; diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 000b53af016..50ce019c32a 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import formComponent from '~/issue_show/components/form.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Inline edit form component', () => { let vm; diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index b740c9ed893..0452934ea9e 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -52,19 +52,13 @@ describe('Job', () => { expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); }); - - it('displays the remove date correctly', () => { - const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year remaining'); - }); }); describe('running build', () => { it('updates the build trace on an interval', function () { const deferred1 = $.Deferred(); const deferred2 = $.Deferred(); - const deferred3 = $.Deferred(); - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); + spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise()); spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ @@ -75,9 +69,7 @@ describe('Job', () => { complete: false, }); - deferred2.resolve(); - - deferred3.resolve({ + deferred2.resolve({ html: '<span>More</span>', status: 'running', state: 'finalstate', @@ -99,9 +91,8 @@ describe('Job', () => { it('replaces the entire build trace', () => { const deferred1 = $.Deferred(); const deferred2 = $.Deferred(); - const deferred3 = $.Deferred(); - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); + spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise()); spyOn(urlUtils, 'visitUrl'); @@ -112,9 +103,7 @@ describe('Job', () => { complete: false, }); - deferred2.resolve(); - - deferred3.resolve({ + deferred2.resolve({ html: '<span>Different</span>', status: 'running', append: false, @@ -175,9 +164,8 @@ describe('Job', () => { it('shows incremented size', () => { const deferred1 = $.Deferred(); const deferred2 = $.Deferred(); - const deferred3 = $.Deferred(); - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); + spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise()); spyOn(urlUtils, 'visitUrl'); @@ -189,8 +177,6 @@ describe('Job', () => { total: 100, }); - deferred2.resolve(); - this.job = new Job(); expect( @@ -199,7 +185,7 @@ describe('Job', () => { jasmine.clock().tick(4001); - deferred3.resolve({ + deferred2.resolve({ html: '<span>Update</span>', status: 'success', append: true, diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index 83395ea451e..a9df0418d5d 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -31,6 +31,7 @@ describe('Job details header', () => { email: 'foo@bar.com', avatar_url: 'link', }, + started: '2018-01-08T09:48:27.319Z', new_issue_path: 'path', }, isLoading: false, @@ -43,15 +44,32 @@ describe('Job details header', () => { vm.$destroy(); }); - it('should render provided job information', () => { - expect( - vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), - ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + describe('triggered job', () => { + beforeEach(() => { + vm = mountComponent(HeaderComponent, props); + }); + + it('should render provided job information', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + }); + + it('should render new issue link', () => { + expect( + vm.$el.querySelector('.js-new-issue').getAttribute('href'), + ).toEqual(props.job.new_issue_path); + }); }); - it('should render new issue link', () => { - expect( - vm.$el.querySelector('.js-new-issue').getAttribute('href'), - ).toEqual(props.job.new_issue_path); + describe('created job', () => { + it('should render created key', () => { + props.job.started = false; + vm = mountComponent(HeaderComponent, props); + + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 created 3 weeks ago by Foo'); + }); }); }); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js index 3069a0cd60e..ca5c9cf87e4 100644 --- a/spec/javascripts/jobs/job_details_mediator_spec.js +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -12,6 +12,10 @@ describe('JobMediator', () => { mock = new MockAdapter(axios); }); + afterEach(() => { + mock.restore(); + }); + it('should set defaults', () => { expect(mediator.store).toBeDefined(); expect(mediator.service).toBeDefined(); @@ -24,10 +28,6 @@ describe('JobMediator', () => { mock.onGet().reply(200, job, {}); }); - afterEach(() => { - mock.restore(); - }); - it('should store received data', (done) => { mediator.fetchJob(); setTimeout(() => { diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 43532275121..43589d54be4 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -37,7 +37,7 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, erase_path: '/root/ci-mock/-/jobs/4757/erase', @@ -54,7 +54,7 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, active: false, @@ -107,10 +107,10 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, - author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + author_gravatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', }, diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 0a9d815f469..1052b4e7c20 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,6 +1,8 @@ /* eslint-disable promise/catch-or-return */ import * as commonUtils from '~/lib/utils/common_utils'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; describe('common_utils', () => { describe('parseUrl', () => { @@ -413,37 +415,48 @@ describe('common_utils', () => { describe('setCiStatusFavicon', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; + let mock; beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); document.body.appendChild(favicon); + mock = new MockAdapter(axios); }); afterEach(() => { + mock.restore(); document.body.removeChild(document.getElementById('favicon')); }); - it('should reset favicon in case of error', () => { - const favicon = document.getElementById('favicon'); - spyOn($, 'ajax').and.callFake(function (options) { - options.error(); - expect(favicon.getAttribute('href')).toEqual('null'); - }); + it('should reset favicon in case of error', (done) => { + mock.onGet(BUILD_URL).networkError(); - commonUtils.setCiStatusFavicon(BUILD_URL); + commonUtils.setCiStatusFavicon(BUILD_URL) + .then(() => { + const favicon = document.getElementById('favicon'); + expect(favicon.getAttribute('href')).toEqual('null'); + done(); + }) + // Error is already caught in catch() block of setCiStatusFavicon, + // It won't throw another error for us to catch + .catch(done.fail); }); - it('should set page favicon to CI status favicon based on provided status', () => { + it('should set page favicon to CI status favicon based on provided status', (done) => { const FAVICON_PATH = '//icon_status_success'; - const favicon = document.getElementById('favicon'); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ favicon: FAVICON_PATH }); - expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + mock.onGet(BUILD_URL).reply(200, { + favicon: FAVICON_PATH, }); - commonUtils.setCiStatusFavicon(BUILD_URL); + commonUtils.setCiStatusFavicon(BUILD_URL) + .then(() => { + const favicon = document.getElementById('favicon'); + expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + done(); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 6f8dad6b835..69a23d7b2f3 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -63,13 +63,13 @@ describe('text_utility', () => { }); }); - describe('stripeHtml', () => { + describe('stripHtml', () => { it('replaces html tag with the default replacement', () => { - expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); + expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); }); it('replaces html tags with the provided replacement', () => { - expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); + expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); }); }); }); diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js index ec6ea35952b..50371c8c5f6 100644 --- a/spec/javascripts/lib/utils/users_cache_spec.js +++ b/spec/javascripts/lib/utils/users_cache_spec.js @@ -92,7 +92,9 @@ describe('UsersCache', () => { apiSpy = (query, options) => { expect(query).toBe(''); expect(options).toEqual({ username: dummyUsername }); - return Promise.resolve([dummyUser]); + return Promise.resolve({ + data: [dummyUser], + }); }; UsersCache.retrieve(dummyUsername) diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index e983e4de3fc..5d0ee91d977 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 2f02c11482f..bae3219b043 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -19,17 +19,24 @@ import IssuablesHelper from '~/helpers/issuables_helper'; $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { + + it('submits an ajax request on tasklist:changed', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { expect(req.type).toBe('PATCH'); expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); - return expect(req.data.merge_request.description).not.toBe(null); + expect(req.data.merge_request.description).not.toBe(null); + done(); }); - return $('.js-task-list-field').trigger('tasklist:changed'); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); describe('class constructor', () => { + beforeEach(() => { + spyOn(jQuery, 'ajax').and.stub(); + }); + it('calls .initCloseReopenReport', () => { spyOn(IssuablesHelper, 'initCloseReopenReport'); @@ -63,8 +70,8 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + new MergeRequest(); // eslint-disable-line no-new + MergeRequest.hideCloseButton(); }); it('hides the dropdown close item and selects the next item', () => { @@ -83,8 +90,7 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + MergeRequest.hideCloseButton(); }); it('hides the close button', () => { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 481b46c3ac6..6fa6f44f953 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,7 +1,9 @@ /* eslint-disable no-new */ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; -import '~/flash'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Mini Pipeline Graph Dropdown', () => { preloadFixtures('static/mini_dropdown_graph.html.raw'); @@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => { }); describe('When dropdown is clicked', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + it('should call getBuildsList', () => { const getBuildsListSpy = spyOn( MiniPipelineGraph.prototype, @@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => { }); it('should make a request to the endpoint provided in the html', () => { - const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + const ajaxSpy = spyOn(axios, 'get').and.callThrough(); + + mock.onGet('foobar').reply(200, { + html: '', + }); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); - expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + expect(ajaxSpy.calls.allArgs()[0][0]).toEqual('foobar'); }); - it('should not close when user uses cmd/ctrl + click', () => { - spyOn($, 'ajax').and.callFake(function (params) { - params.success({ - html: `<li> - <a class="mini-pipeline-graph-dropdown-item" href="#"> - <span class="ci-status-icon ci-status-icon-failed"></span> - <span class="ci-build-text">build</span> - </a> - <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> - </li>`, - }); + it('should not close when user uses cmd/ctrl + click', (done) => { + mock.onGet('foobar').reply(200, { + html: `<li> + <a class="mini-pipeline-graph-dropdown-item" href="#"> + <span class="ci-status-icon ci-status-icon-failed"></span> + <span class="ci-build-text">build</span> + </a> + <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> + </li>`, }); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); - document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); - - expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + timeoutPromise() + .then(() => { + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + }) + .then(timeoutPromise) + .then(() => { + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + }) + .then(done) + .catch(done.fail); }); - }); - it('should close the dropdown when request returns an error', (done) => { - spyOn($, 'ajax').and.callFake(options => options.error()); + it('should close the dropdown when request returns an error', (done) => { + mock.onGet('foobar').networkError(); - new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); - document.querySelector('.js-builds-dropdown-button').click(); + document.querySelector('.js-builds-dropdown-button').click(); - setTimeout(() => { - expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); - done(); - }, 0); + setTimeout(() => { + expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); + done(); + }); + }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 9885b8a790f..eb8f6bbe50d 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -38,7 +38,7 @@ describe('Dashboard', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('shows up a loading state', (done) => { diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index a88e9ed3d99..02304bf5d7d 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -42,6 +42,18 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); + it('sanitizes output', (done) => { + Object.assign(cell, { + source: ['[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n'], + }); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('a')).toBeNull(); + + done(); + }); + }); + describe('katex', () => { beforeEach(() => { json = getJSONFixture('blob/notebook/math.json'); diff --git a/spec/javascripts/notebook/cells/output/html_sanitize_tests.js b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js new file mode 100644 index 00000000000..d587573fc9e --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js @@ -0,0 +1,66 @@ +export default { + 'protocol-based JS injection: simple, no spaces': { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before': { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces after': { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before and after': { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: preceding colon': { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: null char': { + input: '<a href=java\0script:alert("XSS")>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: invalid URL char': { + input: '<img src=java\script:alert("XSS")>', // eslint-disable-line no-useless-escape + output: '<img>', + }, + 'protocol-based JS injection: Unicode': { + input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: spaces and entities': { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'img on error': { + input: '<img src="x" onerror="alert(document.domain)" />', + output: '<img src="x">', + }, +}; diff --git a/spec/javascripts/notebook/cells/output/html_spec.js b/spec/javascripts/notebook/cells/output/html_spec.js new file mode 100644 index 00000000000..9c5385f2922 --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import htmlOutput from '~/notebook/cells/output/html.vue'; +import sanitizeTests from './html_sanitize_tests'; + +describe('html output cell', () => { + function createComponent(rawCode) { + const Component = Vue.extend(htmlOutput); + + return new Component({ + propsData: { + rawCode, + }, + }).$mount(); + } + + describe('sanitizes output', () => { + Object.keys(sanitizeTests).forEach((key) => { + it(key, () => { + const test = sanitizeTests[key]; + const vm = createComponent(test.input); + const outputEl = [...vm.$el.querySelectorAll('div')].pop(); + + expect(outputEl.innerHTML).toEqual(test.output); + + vm.$destroy(); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 20e352dd8bd..104d03377b6 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -139,13 +139,21 @@ describe('issue_comment_form component', () => { }); describe('event enter', () => { - it('should save note when cmd/ctrl+enter is pressed', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleSave).toHaveBeenCalled(); }); + + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleSave').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleSave).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 7c8d6685ee1..36c56cd3862 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 86e9e2a32a9..f841a408d09 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -69,13 +69,20 @@ describe('issue_note_form component', () => { }); describe('enter', () => { - it('should submit note', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleUpdate').and.callThrough(); vm.$el.querySelector('textarea').value = 'Foo'; vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleUpdate).toHaveBeenCalled(); }); + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleUpdate).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index c8a6cb7e612..cb63b64724d 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,4 +1,4 @@ - +import _ from 'underscore'; import Vue from 'vue'; import store from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 6b608adff15..f0c800c759d 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -29,7 +29,6 @@ export const noteableDataMock = { can_create_note: true, can_update: true, }, - deleted_at: null, description: '', due_date: null, human_time_estimate: null, @@ -108,7 +107,7 @@ export const note = { "name": "Administrator", "username": "root", "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "path": "/root" }, "created_at": "2017-08-10T15:24:03.087Z", @@ -283,7 +282,6 @@ export const loggedOutnoteableData = { "updated_by_id": 1, "created_at": "2017-02-07T10:11:18.395Z", "updated_at": "2017-08-08T10:22:51.564Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 167f074fb9b..a40821a5693 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,5 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ +import _ from 'underscore'; import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js index f90e0093d25..b24563f738b 100644 --- a/spec/javascripts/oauth_remember_me_spec.js +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -1,4 +1,4 @@ -import OAuthRememberMe from '~/oauth_remember_me'; +import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; describe('OAuthRememberMe', () => { preloadFixtures('static/oauth_remember_me.html.raw'); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 2fd87754238..b09494f0b77 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,5 +1,6 @@ /* global fixture */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as utils from '~/lib/utils/url_utility'; import Pager from '~/pager'; @@ -9,7 +10,6 @@ describe('pager', () => { beforeEach(() => { setFixtures('<div class="content_list"></div><div class="loading"></div>'); - spyOn($, 'ajax'); }); afterEach(() => { @@ -47,39 +47,90 @@ describe('pager', () => { }); describe('getOld', () => { + const urlRegex = /(.*)some_list(.*)$/; + let mock; + + function mockSuccess() { + mock.onGet(urlRegex).reply(200, { + count: 0, + html: '', + }); + } + + function mockError() { + mock.onGet(urlRegex).networkError(); + } + beforeEach(() => { setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); + spyOn(axios, 'get').and.callThrough(); + + mock = new MockAdapter(axios); + Pager.init(); }); - it('shows loader while loading next page', () => { + afterEach(() => { + mock.restore(); + }); + + it('shows loader while loading next page', (done) => { + mockSuccess(); + spyOn(Pager.loading, 'show'); Pager.getOld(); - expect(Pager.loading.show).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.show).toHaveBeenCalled(); + + done(); + }); }); - it('hides loader on success', () => { - spyOn($, 'ajax').and.callFake(options => options.success({})); + it('hides loader on success', (done) => { + mockSuccess(); + spyOn(Pager.loading, 'hide'); Pager.getOld(); - expect(Pager.loading.hide).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.hide).toHaveBeenCalled(); + + done(); + }); }); - it('hides loader on error', () => { - spyOn($, 'ajax').and.callFake(options => options.error()); + it('hides loader on error', (done) => { + mockError(); + spyOn(Pager.loading, 'hide'); Pager.getOld(); - expect(Pager.loading.hide).toHaveBeenCalled(); + + setTimeout(() => { + expect(Pager.loading.hide).toHaveBeenCalled(); + + done(); + }); }); - it('sends request to url with offset and limit params', () => { - spyOn($, 'ajax'); + it('sends request to url with offset and limit params', (done) => { Pager.offset = 100; Pager.limit = 20; Pager.getOld(); - const [{ data, url }] = $.ajax.calls.argsFor(0); - expect(data).toBe('limit=20&offset=100'); - expect(url).toBe('/some_list'); + + setTimeout(() => { + const [url, params] = axios.get.calls.argsFor(0); + + expect(params).toEqual({ + params: { + limit: 20, + offset: 100, + }, + }); + expect(url).toBe('/some_list'); + + done(); + }); }); }); }); diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js index 7f6b5873011..d2386077aa6 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js @@ -1,5 +1,5 @@ import '~/lib/utils/text_utility'; -import AbuseReports from '~/abuse_reports'; +import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports'; describe('Abuse Reports', () => { const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js new file mode 100644 index 00000000000..440a6585d57 --- /dev/null +++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../../helpers/vue_mount_component_helper'; + +describe('stop_jobs_modal.vue', () => { + const props = { + url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + beforeEach(() => { + const Component = Vue.extend(stopJobsModal); + vm = mountComponent(Component, props); + }); + + describe('onSubmit', () => { + it('stops jobs and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`; + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if stopping jobs failed', (done) => { + const dummyError = new Error('stopping jobs failed'); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.reject(dummyError); + }); + + vm.onSubmit() + .then(done.fail) + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js new file mode 100644 index 00000000000..3cd33a3e900 --- /dev/null +++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue'; +import eventHub from '~/pages/milestones/shared/event_hub'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('delete_milestone_modal.vue', () => { + const Component = Vue.extend(deleteMilestoneModal); + const props = { + issueCount: 1, + mergeRequestCount: 2, + milestoneId: 3, + milestoneTitle: 'my milestone title', + milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('onSubmit', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + spyOn(eventHub, '$emit'); + }); + + it('deletes milestone and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`; + spyOn(axios, 'delete').and.callFake((url) => { + expect(url).toBe(props.milestoneUrl); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl); + eventHub.$emit.calls.reset(); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: true }); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if deleting milestone failed', (done) => { + const dummyError = new Error('deleting milestone failed'); + dummyError.response = { status: 418 }; + spyOn(axios, 'delete').and.callFake((url) => { + expect(url).toBe(props.milestoneUrl); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl); + eventHub.$emit.calls.reset(); + return Promise.reject(dummyError); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: false }); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('text', () => { + it('contains the issue and milestone count', () => { + vm = mountComponent(Component, props); + const value = vm.text; + + expect(value).toContain('remove it from 1 issue and 2 merge requests'); + }); + + it('contains neither issue nor milestone count', () => { + vm = mountComponent(Component, { ...props, + issueCount: 0, + mergeRequestCount: 0, + }); + + const value = vm.text; + + expect(value).toContain('is not currently used'); + }); + }); +}); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 48620898357..d010d897642 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -13,7 +13,7 @@ describe('Pipelines Async Button', () => { propsData: { endpoint: '/foo', title: 'Foo', - icon: 'fa fa-foo', + icon: 'repeat', cssClass: 'bar', }, }).$mount(); @@ -23,8 +23,8 @@ describe('Pipelines Async Button', () => { expect(component.$el.tagName).toEqual('BUTTON'); }); - it('should render the provided icon', () => { - expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo'); + it('should render svg icon', () => { + expect(component.$el.querySelector('svg')).not.toBeNull(); }); it('should render the provided title', () => { diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 6611b74594f..97f04844b3a 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,11 +24,11 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continous Integration can help catch bugs by running your tests automatically'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continuous Deployment can help you deliver code to your product environment'); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 35e36e9c353..c3dc7b53d0f 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -61,14 +61,14 @@ describe('pipeline graph job component', () => { it('it should render status and name', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4257, name: 'test', status: { icon: 'icon_status_success', text: 'passed', label: 'passed', group: 'success', - details_path: '/root/ci-mock/builds/4256', + details_path: '/root/ci-mock/builds/4257', has_details: false, }, }, @@ -118,7 +118,7 @@ describe('pipeline graph job component', () => { it('should not render status label when it is not provided', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4258, name: 'test', status: { icon: 'icon_status_success', @@ -132,7 +132,7 @@ describe('pipeline graph job component', () => { it('should not render status label when it is provided', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4259, name: 'test', status: { icon: 'icon_status_success', diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index 063ab53681b..f744f1af5e6 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -4,18 +4,18 @@ import stageColumnComponent from '~/pipelines/components/graph/stage_column_comp describe('stage column component', () => { let component; const mockJob = { - id: 4256, + id: 4250, name: 'test', status: { icon: 'icon_status_success', text: 'passed', label: 'passed', group: 'success', - details_path: '/root/ci-mock/builds/4256', + details_path: '/root/ci-mock/builds/4250', action: { icon: 'retry', title: 'Retry', - path: '/root/ci-mock/builds/4256/retry', + path: '/root/ci-mock/builds/4250/retry', method: 'post', }, }, @@ -24,10 +24,17 @@ describe('stage column component', () => { beforeEach(() => { const StageColumnComponent = Vue.extend(stageColumnComponent); + const mockJobs = []; + for (let i = 0; i < 3; i += 1) { + const mockedJob = Object.assign({}, mockJob); + mockedJob.id += i; + mockJobs.push(mockedJob); + } + component = new StageColumnComponent({ propsData: { title: 'foo', - jobs: [mockJob, mockJob, mockJob], + jobs: mockJobs, }, }).$mount(); }); diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js index 9fec2f61f78..bc6413a159f 100644 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 367b42cefb0..a99ebc4e51a 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index a9126d2f4e9..de744739e42 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -24,9 +24,10 @@ describe('Pipelines Table Row', () => { beforeEach(() => { const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); - pipelineWithoutAuthor = pipelines.find(p => p.id === 2); - pipelineWithoutCommit = pipelines.find(p => p.id === 3); + + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); + pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null); + pipelineWithoutCommit = pipelines.find(p => p.user === null && p.commit === null); }); afterEach(() => { diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index ca2f9163313..4fc3c08145e 100644 --- a/spec/javascripts/pipelines/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -11,9 +11,10 @@ describe('Pipelines Table', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTableComponent = Vue.extend(pipelinesTableComp); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTableComponent = Vue.extend(pipelinesTableComp); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('table', () => { diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 1b96b2e3d51..61c2f783acc 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import stage from '~/pipelines/components/stage.vue'; diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 43e7d9e1224..6a8a85e3dfb 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; @@ -89,7 +90,7 @@ describe('Registry List', () => { it('should render empty message', (done) => { setTimeout(() => { expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toEqual('No container images stored for this project. Add one by following the instructions above.'); done(); }, 0); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js index c4d3866c922..debde1bb357 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js @@ -12,7 +12,7 @@ describe('Multi-file editor commit sidebar list collapsed', () => { vm = createComponentWithStore(Component, store); - vm.$store.state.openFiles.push(file(), file()); + vm.$store.state.openFiles.push(file('file1'), file('file2')); vm.$store.state.openFiles[0].tempFile = true; vm.$store.state.openFiles.forEach((f) => { Object.assign(f, { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js index fc7c9ae9dd7..4b20fdf70d6 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js @@ -10,7 +10,7 @@ describe('Multi-file editor commit sidebar list item', () => { beforeEach(() => { const Component = Vue.extend(listItem); - f = file(); + f = file('test-file'); vm = mountComponent(Component, { file: f, diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js index 233cca06ed0..8bbc3100357 100644 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js @@ -18,8 +18,10 @@ describe('new file modal component', () => { })); spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { - id: '123branch', + data: { + commit: { + id: '123branch', + }, }, })); diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js index 788c08e5279..667112ab21a 100644 --- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js @@ -17,8 +17,10 @@ describe('new dropdown upload', () => { })); spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { - id: '123branch', + data: { + commit: { + id: '123branch', + }, }, })); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index cd93fb3ccbf..93e94b4f24c 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -29,7 +29,7 @@ describe('RepoCommitSection', () => { comp.$store.state.rightPanelCollapsed = false; comp.$store.state.currentBranch = 'master'; - comp.$store.state.openFiles = [file(), file()]; + comp.$store.state.openFiles = [file('file1'), file('file2')]; comp.$store.state.openFiles.forEach(f => Object.assign(f, { changed: true, content: 'testing', @@ -87,8 +87,10 @@ describe('RepoCommitSection', () => { changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles)); spyOn(service, 'commit').and.returnValue(Promise.resolve({ - short_id: '1', - stats: {}, + data: { + short_id: '1', + stats: {}, + }, })); }); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 0810da87e80..27b55ed1f87 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -25,7 +25,7 @@ describe('RepoFile', () => { vm = new RepoFile({ store, propsData: { - file: file(), + file: file('t4'), }, }); spyOn(vm, 'timeFormated').and.returnValue(updated); @@ -39,7 +39,7 @@ describe('RepoFile', () => { it('does render if hasFiles is true and is loading tree', () => { vm = createComponent({ - file: file(), + file: file('t1'), }); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); @@ -47,7 +47,7 @@ describe('RepoFile', () => { it('does not render commit message and datetime if mini', (done) => { vm = createComponent({ - file: file(), + file: file('t2'), }); vm.$store.state.openFiles.push(vm.file); @@ -61,7 +61,7 @@ describe('RepoFile', () => { it('fires clickFile when the link is clicked', () => { vm = createComponent({ - file: file(), + file: file('t3'), }); spyOn(vm, 'clickFile'); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index 507bca983df..933e8d3a06a 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -56,7 +56,7 @@ describe('RepoTab', () => { }); it('renders an fa-circle icon if tab is changed', () => { - const tab = file(); + const tab = file('changedFile'); tab.changed = true; vm = createComponent({ tab, @@ -68,7 +68,7 @@ describe('RepoTab', () => { describe('methods', () => { describe('closeTab', () => { it('does not close tab if is changed', (done) => { - const tab = file(); + const tab = file('closeFile'); tab.changed = true; tab.opened = true; vm = createComponent({ diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index 0beaf643793..2c363364d70 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -4,7 +4,7 @@ import repoTabs from '~/ide/components/repo_tabs.vue'; import { file, resetStore } from '../helpers'; describe('RepoTabs', () => { - const openedFiles = [file(), file()]; + const openedFiles = [file('open1'), file('open2')]; let vm; function createComponent() { diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js index 8ce01d3bf12..e2d8f002e27 100644 --- a/spec/javascripts/repo/stores/actions/file_spec.js +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -18,7 +18,7 @@ describe('Multi-file store file actions', () => { oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - localFile = file(); + localFile = file('testFile'); localFile.active = true; localFile.opened = true; localFile.parentTreeUrl = 'parentTreeUrl'; @@ -81,7 +81,7 @@ describe('Multi-file store file actions', () => { }); it('sets next file as active', (done) => { - const f = file(); + const f = file('otherfile'); store.state.openFiles.push(f); expect(f.active).toBeFalsy(); @@ -119,7 +119,7 @@ describe('Multi-file store file actions', () => { }); it('calls scrollToTab', (done) => { - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('setThisActive')) .then(() => { expect(scrollToTabSpy).toHaveBeenCalled(); @@ -128,7 +128,7 @@ describe('Multi-file store file actions', () => { }); it('sets the file active', (done) => { - const localFile = file(); + const localFile = file('activeFile'); store.dispatch('setFileActive', localFile) .then(() => { @@ -139,7 +139,7 @@ describe('Multi-file store file actions', () => { }); it('returns early if file is already active', (done) => { - const localFile = file(); + const localFile = file('earlyActive'); localFile.active = true; store.dispatch('setFileActive', localFile) @@ -151,11 +151,11 @@ describe('Multi-file store file actions', () => { }); it('sets current active file to not active', (done) => { - const localFile = file(); + const localFile = file('currentActive'); localFile.active = true; store.state.openFiles.push(localFile); - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('newActive')) .then(() => { expect(localFile.active).toBeFalsy(); @@ -166,7 +166,7 @@ describe('Multi-file store file actions', () => { it('resets location.hash for line highlighting', (done) => { location.hash = 'test'; - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('otherActive')) .then(() => { expect(location.hash).not.toBe('test'); @@ -176,7 +176,7 @@ describe('Multi-file store file actions', () => { }); describe('getFileData', () => { - let localFile = file(); + let localFile; beforeEach(() => { spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ @@ -194,10 +194,17 @@ describe('Multi-file store file actions', () => { }), })); - localFile = file(); + localFile = file('newCreate'); localFile.url = 'getFileDataURL'; }); + afterEach(() => { + store.dispatch('closeFile', { + file: localFile, + force: true, + }); + }); + it('calls the service', (done) => { store.dispatch('getFileData', localFile) .then(() => { @@ -268,7 +275,7 @@ describe('Multi-file store file actions', () => { beforeEach(() => { spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); - tmpFile = file(); + tmpFile = file('tmpFile'); }); it('calls getRawFileData service method', (done) => { @@ -294,7 +301,7 @@ describe('Multi-file store file actions', () => { let tmpFile; beforeEach(() => { - tmpFile = file(); + tmpFile = file('tmpFile'); }); it('updates file content', (done) => { diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js index 0b0d34f072a..f678967b092 100644 --- a/spec/javascripts/repo/stores/actions_spec.js +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -48,14 +48,14 @@ describe('Multi-file store actions', () => { describe('discardAllChanges', () => { beforeEach(() => { - store.state.openFiles.push(file()); + store.state.openFiles.push(file('discardAll')); store.state.openFiles[0].changed = true; }); }); describe('closeAllFiles', () => { beforeEach(() => { - store.state.openFiles.push(file()); + store.state.openFiles.push(file('closeAll')); store.state.openFiles[0].opened = true; }); @@ -97,7 +97,7 @@ describe('Multi-file store actions', () => { it('opens discard popup if there are changed files', (done) => { store.state.editMode = true; - store.state.openFiles.push(file()); + store.state.openFiles.push(file('discardChanges')); store.state.openFiles[0].changed = true; store.dispatch('toggleEditMode') @@ -111,7 +111,7 @@ describe('Multi-file store actions', () => { it('can force closed if there are changed files', (done) => { store.state.editMode = true; - store.state.openFiles.push(file()); + store.state.openFiles.push(file('forceClose')); store.state.openFiles[0].changed = true; store.dispatch('toggleEditMode', true) @@ -124,7 +124,7 @@ describe('Multi-file store actions', () => { }); it('discards file changes', (done) => { - const f = file(); + const f = file('discard'); store.state.editMode = true; store.state.openFiles.push(f); f.changed = true; @@ -178,7 +178,9 @@ describe('Multi-file store actions', () => { it('calls service', (done) => { spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { id: '123' }, + data: { + commit: { id: '123' }, + }, })); store.dispatch('checkCommitStatus') @@ -192,7 +194,9 @@ describe('Multi-file store actions', () => { it('returns true if current ref does not equal returned ID', (done) => { spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { id: '123' }, + data: { + commit: { id: '123' }, + }, })); store.dispatch('checkCommitStatus') @@ -206,7 +210,9 @@ describe('Multi-file store actions', () => { it('returns false if current ref equals returned ID', (done) => { spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - commit: { id: '1' }, + data: { + commit: { id: '1' }, + }, })); store.dispatch('checkCommitStatus') @@ -250,13 +256,15 @@ describe('Multi-file store actions', () => { describe('success', () => { beforeEach(() => { spyOn(service, 'commit').and.returnValue(Promise.resolve({ - id: '123456', - short_id: '123', - message: 'test message', - committed_date: 'date', - stats: { - additions: '1', - deletions: '2', + data: { + id: '123456', + short_id: '123', + message: 'test message', + committed_date: 'date', + stats: { + additions: '1', + deletions: '2', + }, }, })); }); @@ -285,8 +293,8 @@ describe('Multi-file store actions', () => { }); it('adds commit data to changed files', (done) => { - const changedFile = file(); - const f = file(); + const changedFile = file('changed'); + const f = file('newfile'); changedFile.changed = true; store.state.openFiles.push(changedFile, f); @@ -300,19 +308,6 @@ describe('Multi-file store actions', () => { }).catch(done.fail); }); - it('closes all files', (done) => { - store.state.openFiles.push(file()); - store.state.openFiles[0].opened = true; - - store.dispatch('commitChanges', { payload, newMr: false }) - .then(Vue.nextTick) - .then(() => { - expect(store.state.openFiles.length).toBe(0); - - done(); - }).catch(done.fail); - }); - it('scrolls to top of page', (done) => { store.dispatch('commitChanges', { payload, newMr: false }) .then(() => { @@ -337,7 +332,9 @@ describe('Multi-file store actions', () => { describe('failed', () => { beforeEach(() => { spyOn(service, 'commit').and.returnValue(Promise.resolve({ - message: 'failed message', + data: { + message: 'failed message', + }, })); }); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js index 947a60587df..6e204ef0404 100644 --- a/spec/javascripts/repo/stores/mutations/file_spec.js +++ b/spec/javascripts/repo/stores/mutations/file_spec.js @@ -117,7 +117,7 @@ describe('Multi-file store file mutations', () => { describe('CREATE_TMP_FILE', () => { it('adds file into parent tree', () => { - const f = file(); + const f = file('tmpFile'); mutations.CREATE_TMP_FILE(localState, { file: f, diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js index cf1248ba28b..e6ca8ea139e 100644 --- a/spec/javascripts/repo/stores/mutations/tree_spec.js +++ b/spec/javascripts/repo/stores/mutations/tree_spec.js @@ -57,7 +57,7 @@ describe('Multi-file store tree mutations', () => { describe('CREATE_TMP_TREE', () => { it('adds tree into parent tree', () => { - const tmpEntry = file(); + const tmpEntry = file('tmpTree'); mutations.CREATE_TMP_TREE(localState, { tmpEntry, diff --git a/spec/javascripts/search_spec.js b/spec/javascripts/search_spec.js new file mode 100644 index 00000000000..38e94d45e55 --- /dev/null +++ b/spec/javascripts/search_spec.js @@ -0,0 +1,40 @@ +import Api from '~/api'; +import Search from '~/pages/search/show/search'; + +describe('Search', () => { + const fixturePath = 'search/show.html.raw'; + const searchTerm = 'some search'; + const fillDropdownInput = (dropdownSelector) => { + const dropdownElement = document.querySelector(dropdownSelector).parentNode; + const inputElement = dropdownElement.querySelector('.dropdown-input-field'); + inputElement.value = searchTerm; + return inputElement; + }; + + preloadFixtures(fixturePath); + + beforeEach(() => { + loadFixtures(fixturePath); + new Search(); // eslint-disable-line no-new + }); + + it('requests groups from backend when filtering', (done) => { + spyOn(Api, 'groups').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-group-dropdown'); + + $(inputElement).trigger('input'); + }); + + it('requests projects from backend when filtering', (done) => { + spyOn(Api, 'projects').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-project-dropdown'); + + $(inputElement).trigger('input'); + }); +}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 3b094d20838..d9e84e35f69 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -15,7 +15,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-02-02T21: 49: 49.664Z', updated_at: '2017-05-03T22: 26: 03.760Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, @@ -28,7 +27,7 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/user0', }, { @@ -36,7 +35,7 @@ const RESPONSE_MAP = { username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/tajuana', }, { @@ -44,7 +43,7 @@ const RESPONSE_MAP = { username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/michaele.will', }, ], @@ -73,24 +72,24 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', }, { name: 'Marguerite Bartell', username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', }, { name: 'Laureen Ritchie', username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', }, ], human_time_estimate: null, @@ -101,24 +100,24 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', }, { name: 'Marguerite Bartell', username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', }, { name: 'Laureen Ritchie', username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', }, ], subscribed: true, @@ -153,7 +152,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-06-27T19:54:42.437Z', updated_at: '2017-08-18T03:39:49.222Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, @@ -184,7 +182,7 @@ const mockData = { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, rootPath: '/', fullPath: '/gitlab-org/gitlab-shell', @@ -196,7 +194,7 @@ const mockData = { human_total_time_spent: null, }, user: { - avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', id: 1, name: 'Administrator', username: 'root', diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index b97e24d9dcf..6bb6d639f24 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 9efd109b996..afa18cc127e 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 8b0d51bbcc8..97f762d07a7 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index ea4eae1e23f..3591f96ff87 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -6,14 +6,14 @@ const ASSIGNEE = { id: 2, name: 'gitlab user 2', username: 'gitlab2', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }; const ANOTHER_ASSINEE = { id: 3, name: 'gitlab user 3', username: 'gitlab3', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }; const PARTICIPANT = { @@ -38,7 +38,7 @@ describe('Sidebar store', () => { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, editable: true, rootPath: '/', diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js index 9b33dd02fb9..79db05f04ed 100644 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ b/spec/javascripts/sidebar/subscriptions_spec.js @@ -20,23 +20,23 @@ describe('Subscriptions', function () { subscribed: undefined, }); - expect(vm.$refs.loadingButton.loading).toBe(true); - expect(vm.$refs.loadingButton.label).toBeUndefined(); + expect(vm.$refs.toggleButton.isLoading).toBe(true); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-loading'); }); - it('has "Subscribe" text when currently not subscribed', () => { + it('is toggled "off" when currently not subscribed', () => { vm = mountComponent(Subscriptions, { subscribed: false, }); - expect(vm.$refs.loadingButton.label).toBe('Subscribe'); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass('is-checked'); }); - it('has "Unsubscribe" text when currently not subscribed', () => { + it('is toggled "on" when currently subscribed', () => { vm = mountComponent(Subscriptions, { subscribed: true, }); - expect(vm.$refs.loadingButton.label).toBe('Unsubscribe'); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked'); }); }); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index a53e8a94d89..b1b03ef1e09 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -1,5 +1,5 @@ import AccessorUtilities from '~/lib/utils/accessor'; -import SigninTabsMemoizer from '~/signin_tabs_memoizer'; +import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer'; (() => { describe('SigninTabsMemoizer', () => { @@ -53,6 +53,13 @@ import SigninTabsMemoizer from '~/signin_tabs_memoizer'; expect(memo.readData()).toEqual('#standard'); }); + it('overrides last selected tab with hash tag when given', () => { + window.location.hash = '#ldap'; + createMemoizer(); + + expect(memo.readData()).toEqual('#ldap'); + }); + describe('class constructor', () => { beforeEach(() => { memo = createMemoizer(); diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 1c87fcec245..7265e1b6cb5 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import SmartInterval from '~/smart_interval'; describe('SmartInterval', function () { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 6897c991066..9b2a5379855 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,12 +1,13 @@ /* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; -import _ from 'underscore'; import 'jasmine-jquery'; import '~/commons'; import Vue from 'vue'; import VueResource from 'vue-resource'; +import { getDefaultAdapter } from '~/lib/utils/axios_utils'; + const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent); Vue.config.devtools = !isHeadlessChrome; Vue.config.productionTip = false; @@ -31,7 +32,6 @@ jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; // globalize common libraries window.$ = window.jQuery = $; -window._ = _; // stub expected globals window.gl = window.gl || {}; @@ -61,6 +61,8 @@ beforeEach(() => { Vue.http.interceptors = builtinVueHttpInterceptors.slice(); }); +const axiosDefaultAdapter = getDefaultAdapter(); + // render all of our tests const testsContext = require.context('.', true, /_spec$/); testsContext.keys().forEach(function (path) { @@ -96,6 +98,12 @@ describe('test errors', () => { it('has no Vue error', () => { expect(hasVueErrors).toBe(false); }); + + it('restores axios adapter after mocking', () => { + if (getDefaultAdapter() !== axiosDefaultAdapter) { + fail('axios adapter is not restored! Did you forget a restore() on MockAdapter?'); + } + }); }); // if we're generating coverage reports, make sure to include all files so diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 59e16f0786e..35871dddf89 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,5 +1,5 @@ import * as urlUtils from '~/lib/utils/url_utility'; -import Todos from '~/todos'; +import Todos from '~/pages/dashboard/todos/index/todos'; import '~/lib/utils/common_utils'; describe('Todos', () => { diff --git a/spec/javascripts/toggle_buttons_spec.js b/spec/javascripts/toggle_buttons_spec.js new file mode 100644 index 00000000000..205e396d682 --- /dev/null +++ b/spec/javascripts/toggle_buttons_spec.js @@ -0,0 +1,120 @@ +import setupToggleButtons from '~/toggle_buttons'; +import getSetTimeoutPromise from './helpers/set_timeout_promise_helper'; + +function generateMarkup(isChecked = true) { + return ` + <button type="button" class="${isChecked ? 'is-checked' : ''} js-project-feature-toggle"> + <input type="hidden" class="js-project-feature-toggle-input" value="${isChecked}" /> + </button> + `; +} + +function setupFixture(isChecked, clickCallback) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = generateMarkup(isChecked); + + setupToggleButtons(wrapper, clickCallback); + + return wrapper; +} + +describe('ToggleButtons', () => { + describe('when input value is true', () => { + it('should initialize as checked', () => { + const wrapper = setupFixture(true); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }); + + it('should toggle to unchecked when clicked', (done) => { + const wrapper = setupFixture(true); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when input value is false', () => { + it('should initialize as unchecked', () => { + const wrapper = setupFixture(false); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }); + + it('should toggle to checked when clicked', (done) => { + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }) + .then(done) + .catch(done.fail); + }); + }); + + it('should emit `trigger-change` event', (done) => { + const changeSpy = jasmine.createSpy('changeEventHandler'); + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + const input = wrapper.querySelector('.js-project-feature-toggle-input'); + + $(input).on('trigger-change', changeSpy); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(changeSpy).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('clickCallback', () => { + it('should show loading indicator while waiting', (done) => { + const isChecked = true; + const clickCallback = (newValue, toggleButton) => { + const input = toggleButton.querySelector('.js-project-feature-toggle-input'); + + expect(newValue).toEqual(false); + + // Check for the loading state + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(true); + expect(toggleButton.disabled).toEqual(true); + expect(input.value).toEqual('true'); + + // After the callback finishes, check that the loading state is gone + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(false); + expect(toggleButton.disabled).toEqual(false); + expect(input.value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }; + + const wrapper = setupFixture(isChecked, clickCallback); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js index a750bc78f36..f14d5f6f76c 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js @@ -1,39 +1,39 @@ import Vue from 'vue'; -import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author'; - -const author = { - webUrl: 'http://foo.bar', - avatarUrl: 'http://gravatar.com/foo', - name: 'fatihacet', -}; -const createComponent = () => { - const Component = Vue.extend(authorComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { author }, - }); -}; +import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetAuthor', () => { - describe('props', () => { - it('should have props', () => { - const authorProp = authorComponent.props.author; + let vm; + + beforeEach(() => { + const Component = Vue.extend(authorComponent); + + vm = mountComponent(Component, { + author: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, - expect(authorProp).toBeDefined(); - expect(authorProp.type instanceof Object).toBeTruthy(); - expect(authorProp.required).toBeTruthy(); }); }); - describe('template', () => { - it('should have correct elements', () => { - const el = createComponent().$el; + afterEach(() => { + vm.$destroy(); + }); - expect(el.tagName).toEqual('A'); - expect(el.getAttribute('href')).toEqual(author.webUrl); - expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl); - expect(el.querySelector('.author').innerText.trim()).toEqual(author.name); - }); + it('renders link with the author web url', () => { + expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root'); + }); + + it('renders image with avatar url', () => { + expect( + vm.$el.querySelector('img').getAttribute('src'), + ).toEqual('http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon'); + }); + + it('renders author name', () => { + expect(vm.$el.textContent.trim()).toEqual('Administrator'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js index 515ddcbb875..8c55622b15e 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -1,61 +1,40 @@ import Vue from 'vue'; -import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time'; - -const props = { - actionText: 'Merged by', - author: { - webUrl: 'http://foo.bar', - avatarUrl: 'http://gravatar.com/foo', - name: 'fatihacet', - }, - dateTitle: '2017-03-23T23:02:00.807Z', - dateReadable: '12 hours ago', -}; -const createComponent = () => { - const Component = Vue.extend(authorTimeComponent); - - return new Component({ - el: document.createElement('div'), - propsData: props, - }); -}; +import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetAuthorTime', () => { - describe('props', () => { - it('should have props', () => { - const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props; - const ActionTextClass = actionText.type; - const DateTitleClass = dateTitle.type; - const DateReadableClass = dateReadable.type; - - expect(new ActionTextClass() instanceof String).toBeTruthy(); - expect(actionText.required).toBeTruthy(); - - expect(author.type instanceof Object).toBeTruthy(); - expect(author.required).toBeTruthy(); - - expect(new DateTitleClass() instanceof String).toBeTruthy(); - expect(dateTitle.required).toBeTruthy(); - - expect(new DateReadableClass() instanceof String).toBeTruthy(); - expect(dateReadable.required).toBeTruthy(); + let vm; + + beforeEach(() => { + const Component = Vue.extend(authorTimeComponent); + + vm = mountComponent(Component, { + actionText: 'Merged by', + author: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + dateTitle: '2017-03-23T23:02:00.807Z', + dateReadable: '12 hours ago', }); }); - describe('components', () => { - it('should have components', () => { - expect(authorTimeComponent.components['mr-widget-author']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); + }); + + it('renders provided action text', () => { + expect(vm.$el.textContent).toContain('Merged by'); }); - describe('template', () => { - it('should have correct elements', () => { - const el = createComponent().$el; + it('renders author', () => { + expect(vm.$el.textContent).toContain('Administrator'); + }); - expect(el.tagName).toEqual('H4'); - expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl); - expect(el.querySelector('time').innerText).toContain(props.dateReadable); - expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle); - }); + it('renders provided time', () => { + expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z'); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 06f89fabf42..13e5595bbfc 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -1,104 +1,219 @@ import Vue from 'vue'; -import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header'; - -const createComponent = (mr) => { - const Component = Vue.extend(headerComponent); - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetHeader', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = headerComponent.props; + let vm; + let Component; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(headerComponent); + }); + + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { - let vm; - beforeEach(() => { - vm = createComponent({ - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '/foo/bar/mr-widget-refactor', - targetBranch: 'master', + describe('shouldShowCommitsBehindText', () => { + it('return true when there are divergedCommitsCount', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); + + expect(vm.shouldShowCommitsBehindText).toEqual(true); + }); + + it('returns false where there are no divergedComits count', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 0, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); + expect(vm.shouldShowCommitsBehindText).toEqual(false); }); }); - it('shouldShowCommitsBehindText', () => { - expect(vm.shouldShowCommitsBehindText).toBeTruthy(); + describe('commitsText', () => { + it('returns singular when there is one commit', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 1, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); - vm.mr.divergedCommitsCount = 0; - expect(vm.shouldShowCommitsBehindText).toBeFalsy(); - }); + expect(vm.commitsText).toEqual('1 commit behind'); + }); - it('commitsText', () => { - expect(vm.commitsText).toEqual('commits'); + it('returns plural when there is more than one commit', () => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 2, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + } }); - vm.mr.divergedCommitsCount = 1; - expect(vm.commitsText).toEqual('commit'); + expect(vm.commitsText).toEqual('2 commits behind'); + }); }); }); describe('template', () => { - let vm; - let el; - const sourceBranchPath = '/foo/bar/mr-widget-refactor'; - const mr = { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - }; - - beforeEach(() => { - vm = createComponent(mr); - el = vm.$el; + describe('common elements', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('renders source branch link', () => { + expect( + vm.$el.querySelector('.js-source-branch').innerHTML, + ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>'); + }); + + it('renders clipboard button', () => { + expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null); + }); + + it('renders target branch', () => { + expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master'); + }); + }); + + describe('with an open merge request', () => { + afterEach(() => { + vm.$destroy(); + }); + + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('renders checkout branch button with modal trigger', () => { + const button = vm.$el.querySelector('.js-check-out-branch'); + + expect(button.textContent.trim()).toEqual('Check out branch'); + expect(button.getAttribute('data-target')).toEqual('#modal_merge_info'); + expect(button.getAttribute('data-toggle')).toEqual('modal'); + }); + + it('renders download dropdown with links', () => { + expect( + vm.$el.querySelector('.js-download-email-patches').textContent.trim(), + ).toEqual('Email patches'); + + expect( + vm.$el.querySelector('.js-download-email-patches').getAttribute('href'), + ).toEqual('/mr/email-patches'); + + expect( + vm.$el.querySelector('.js-download-plain-diff').textContent.trim(), + ).toEqual('Plain diff'); + + expect( + vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'), + ).toEqual('/mr/plainDiffPath'); + }); }); - it('should render template elements correctly', () => { - expect(el.classList.contains('mr-source-target')).toBeTruthy(); - const sourceBranchLink = el.querySelectorAll('.label-branch')[0]; - const targetBranchLink = el.querySelectorAll('.label-branch')[1]; - const commitsCount = el.querySelector('.diverged-commits-count'); - - expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); - expect(targetBranchLink.textContent).toContain(mr.targetBranch); - expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); - expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath); - expect(commitsCount.textContent).toContain('12 commits behind'); - expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath); - - expect(el.textContent).toContain('Check out branch'); - expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath); - expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath); + describe('with a closed merge request', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: false, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('does not render checkout branch button with modal trigger', () => { + const button = vm.$el.querySelector('.js-check-out-branch'); + + expect(button).toEqual(null); + }); + + it('does not render download dropdown with links', () => { + expect( + vm.$el.querySelector('.js-download-email-patches'), + ).toEqual(null); + + expect( + vm.$el.querySelector('.js-download-plain-diff'), + ).toEqual(null); + }); }); - it('should not have right action links if the MR state is not open', (done) => { - vm.mr.isOpen = false; - Vue.nextTick(() => { - expect(el.textContent).not.toContain('Check out branch'); - expect(el.querySelectorAll('.dropdown li a').length).toEqual(0); - done(); + describe('without diverged commits', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 0, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('does not render diverged commits info', () => { + expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null); }); }); - it('should not render diverged commits count if the MR has no diverged commits', (done) => { - vm.mr.divergedCommitsCount = null; - Vue.nextTick(() => { - expect(el.textContent).not.toContain('commits behind'); - expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0); - done(); + describe('with diverged commits', () => { + beforeEach(() => { + vm = mountComponent(Component, { mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + } }); + }); + + it('renders diverged commits info', () => { + expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js index 4da4fc82c26..cc43639f576 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js @@ -1,51 +1,56 @@ import Vue from 'vue'; -import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help'; - -const props = { - missingBranch: 'this-is-not-the-branch-you-are-looking-for', -}; -const text = `If the ${props.missingBranch} branch exists in your local repository`; - -const createComponent = () => { - const Component = Vue.extend(mergeHelpComponent); - return new Component({ - el: document.createElement('div'), - propsData: props, - }); -}; +import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('MRWidgetMergeHelp', () => { - describe('props', () => { - it('should have props', () => { - const { missingBranch } = mergeHelpComponent.props; - const MissingBranchTypeClass = missingBranch.type; - - expect(new MissingBranchTypeClass() instanceof String).toBeTruthy(); - expect(missingBranch.required).toBeFalsy(); - expect(missingBranch.default).toEqual(''); - }); + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(mergeHelpComponent); }); - describe('template', () => { - let vm; - let el; + afterEach(() => { + vm.$destroy(); + }); + describe('with missing branch', () => { beforeEach(() => { - vm = createComponent(); - el = vm.$el; + vm = mountComponent(Component, { + missingBranch: 'this-is-not-the-branch-you-are-looking-for', + }); }); - it('should have the correct elements', () => { - expect(el.classList.contains('mr-widget-help')).toBeTruthy(); - expect(el.textContent).toContain(text); + it('renders missing branch information', () => { + expect( + vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '), + ).toEqual( + 'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line', + ); }); - it('should not show missing branch name if missingBranch props is not provided', (done) => { - vm.missingBranch = null; - Vue.nextTick(() => { - expect(el.textContent).not.toContain(text); - done(); - }); + it('renders button to open help modal', () => { + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info'); + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal'); + }); + }); + + describe('without missing branch', () => { + beforeEach(() => { + vm = mountComponent(Component); + }); + + it('renders information about how to merge manually', () => { + expect( + vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '), + ).toEqual( + 'You can merge this merge request manually using the command line', + ); + }); + + it('renders element to open a modal', () => { + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info'); + expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js index f86fb6a0b4b..637bf483deb 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -1,117 +1,82 @@ import Vue from 'vue'; -import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links'; +import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; -const createComponent = (data) => { - const Component = Vue.extend(relatedLinksComponent); +describe('MRWidgetRelatedLinks', () => { + let vm; - return new Component({ - el: document.createElement('div'), - propsData: data, - }); -}; + const createComponent = (data) => { + const Component = Vue.extend(relatedLinksComponent); -describe('MRWidgetRelatedLinks', () => { - describe('props', () => { - it('should have props', () => { - const { relatedLinks } = relatedLinksComponent.props; + return mountComponent(Component, data); + }; - expect(relatedLinks).toBeDefined(); - expect(relatedLinks.type instanceof Object).toBeTruthy(); - expect(relatedLinks.required).toBeTruthy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { - const data = { - relatedLinks: { - closing: '/foo', - mentioned: '/foo', - assignToMe: '/foo', - }, - }; - - describe('hasLinks', () => { - it('should return correct value when we have links reference', () => { - const vm = createComponent(data); - expect(vm.hasLinks).toBeTruthy(); - - vm.relatedLinks.closing = null; - expect(vm.hasLinks).toBeTruthy(); - - vm.relatedLinks.mentioned = null; - expect(vm.hasLinks).toBeTruthy(); - - vm.relatedLinks.assignToMe = null; - expect(vm.hasLinks).toBeFalsy(); - }); - }); - describe('closesText', () => { - it('returns correct text for open merge request', () => { - data.state = 'open'; - const vm = createComponent(data); + it('returns Closes text for open merge request', () => { + vm = createComponent({ state: 'open', relatedLinks: {} }); expect(vm.closesText).toEqual('Closes'); }); it('returns correct text for closed merge request', () => { - data.state = 'closed'; - const vm = createComponent(data); + vm = createComponent({ state: 'closed', relatedLinks: {} }); expect(vm.closesText).toEqual('Did not close'); }); it('returns correct tense for merged request', () => { - data.state = 'merged'; - const vm = createComponent(data); + vm = createComponent({ state: 'merged', relatedLinks: {} }); expect(vm.closesText).toEqual('Closed'); }); }); }); - describe('template', () => { - it('should have only have closing issues text', () => { - const vm = createComponent({ - relatedLinks: { - closing: '<a href="#">#23</a> and <a>#42</a>', - }, - }); - const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - - expect(content).toContain('Closes #23 and #42'); - expect(content).not.toContain('Mentions'); + it('should have only have closing issues text', () => { + vm = createComponent({ + relatedLinks: { + closing: '<a href="#">#23</a> and <a>#42</a>', + }, }); + const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - it('should have only have mentioned issues text', () => { - const vm = createComponent({ - relatedLinks: { - mentioned: '<a href="#">#7</a>', - }, - }); + expect(content).toContain('Closes #23 and #42'); + expect(content).not.toContain('Mentions'); + }); - expect(vm.$el.innerText).toContain('Mentions #7'); - expect(vm.$el.innerText).not.toContain('Closes'); + it('should have only have mentioned issues text', () => { + vm = createComponent({ + relatedLinks: { + mentioned: '<a href="#">#7</a>', + }, }); - it('should have closing and mentioned issues at the same time', () => { - const vm = createComponent({ - relatedLinks: { - closing: '<a href="#">#7</a>', - mentioned: '<a href="#">#23</a> and <a>#42</a>', - }, - }); - const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); + expect(vm.$el.innerText).toContain('Mentions #7'); + expect(vm.$el.innerText).not.toContain('Closes'); + }); - expect(content).toContain('Closes #7'); - expect(content).toContain('Mentions #23 and #42'); + it('should have closing and mentioned issues at the same time', () => { + vm = createComponent({ + relatedLinks: { + closing: '<a href="#">#7</a>', + mentioned: '<a href="#">#23</a> and <a>#42</a>', + }, }); + const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - it('should have assing issues link', () => { - const vm = createComponent({ - relatedLinks: { - assignToMe: '<a href="#">Assign yourself to these issues</a>', - }, - }); + expect(content).toContain('Closes #7'); + expect(content).toContain('Mentions #23 and #42'); + }); - expect(vm.$el.innerText).toContain('Assign yourself to these issues'); + it('should have assing issues link', () => { + vm = createComponent({ + relatedLinks: { + assignToMe: '<a href="#">Assign yourself to these issues</a>', + }, }); + + expect(vm.$el.innerText).toContain('Assign yourself to these issues'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js new file mode 100644 index 00000000000..c39fcda0071 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -0,0 +1,44 @@ +import Vue from 'vue'; +import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('MR widget status icon component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(mrStatusIcon); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('while loading', () => { + it('renders loading icon', () => { + vm = mountComponent(Component, { status: 'loading' }); + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + }); + + describe('with status icon', () => { + it('renders ci status icon', () => { + vm = mountComponent(Component, { status: 'failed' }); + expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull(); + }); + }); + + describe('with disabled button', () => { + it('renders a disabled button', () => { + vm = mountComponent(Component, { status: 'failed', showDisabledButton: true }); + expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge'); + }); + }); + + describe('without disabled button', () => { + it('does not render a disabled button', () => { + vm = mountComponent(Component, { status: 'failed' }); + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js index 4869fb17d96..f98ebdb38e6 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -1,18 +1,31 @@ import Vue from 'vue'; -import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived'; +import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetArchived', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(archivedComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('This project is archived, write access has been disabled'); - }); + beforeEach(() => { + const Component = Vue.extend(archivedComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a ci status failed icon', () => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull(); + }); + + it('renders a disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Merge'); + }); + + it('renders information', () => { + expect( + vm.$el.querySelector('.bold').textContent.trim(), + ).toEqual('This project is archived, write access has been disabled'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index 6042d7384d5..95c94e95e3a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -1,32 +1,47 @@ import Vue from 'vue'; -import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed'; - -const mergeError = 'This is the merge error'; +import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetAutoMergeFailed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = autoMergeFailedComponent.props.mr; + let vm; + const mergeError = 'This is the merge error'; - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); + beforeEach(() => { + const Component = Vue.extend(autoMergeFailedComponent); + vm = mountComponent(Component, { + mr: { mergeError }, }); }); - describe('template', () => { - const Component = Vue.extend(autoMergeFailedComponent); - const vm = new Component({ - el: document.createElement('div'), - propsData: { - mr: { mergeError }, - }, - }); + afterEach(() => { + vm.$destroy(); + }); + + it('renders failed message', () => { + expect(vm.$el.textContent).toContain('This merge request failed to be merged automatically'); + }); + + it('renders merge error provided', () => { + expect(vm.$el.innerText).toContain(mergeError); + }); + + it('render refresh button', () => { + expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Refresh'); + }); + + it('emits event and shows loading icon when button is clicked', (done) => { + spyOn(eventHub, '$emit'); + vm.$el.querySelector('button').click(); + + expect(eventHub.$emit.calls.argsFor(0)[0]).toEqual('MRWidgetUpdateRequested'); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeFalsy(); - expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically'); - expect(vm.$el.innerText).toContain(mergeError); + Vue.nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + expect( + vm.$el.querySelector('button i').classList, + ).toContain('fa-spinner'); + done(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 6b7aa935ad3..658cadddb81 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -1,19 +1,29 @@ import Vue from 'vue'; -import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking'; +import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetChecking', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(checkingComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let Component; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('Checking ability to merge automatically'); - expect(el.querySelector('i')).toBeDefined(); - }); + beforeEach(() => { + Component = Vue.extend(checkingComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + + it('renders information about merging', () => { + expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual('Checking ability to merge automatically'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 1bf97bbf093..51a34739ee9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -1,74 +1,58 @@ import Vue from 'vue'; -import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed'; - -const mr = { - targetBranch: 'good-branch', - targetBranchPath: '/good-branch', - metrics: { - mergedBy: {}, - mergedAt: 'mergedUpdatedAt', - closedBy: { - name: 'Fatih Acet', - username: 'fatihacet', - }, - closedAt: 'closedEventUpdatedAt', - readableMergedAt: '', - readableClosedAt: '', - }, - updatedAt: 'mrUpdatedAt', - closedAt: '1 day ago', -}; - -const createComponent = () => { - const Component = Vue.extend(closedComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetClosed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = closedComponent.props.mr; - - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); - }); + let vm; + + beforeEach(() => { + const Component = Vue.extend(closedComponent); + vm = mountComponent(Component, { mr: { + metrics: { + mergedBy: {}, + closedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + readableClosedAt: 'less than a minute ago', + }, + targetBranchPath: '/twitter/flight/commits/so_long_jquery', + targetBranch: 'so_long_jquery', + } }); }); - describe('components', () => { - it('should have components added', () => { - expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); - describe('template', () => { - let vm; - let el; + it('renders warning icon', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders closed by information with author and time', () => { + expect( + vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain( + 'Closed by Administrator less than a minute ago', + ); + }); - afterEach(() => { - vm.$destroy(); - }); + it('links to the user that closed the MR', () => { + expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual('http://localhost:3000/root'); + }); - it('should have correct elements', () => { - expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name); - expect(el.textContent).toContain('The changes were not merged into'); - expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); - }); + it('renders information about the changes not being merged', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain('The changes were not merged into so_long_jquery'); + }); - it('should use closedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('closedEventUpdatedAt'); - }); + it('renders link for target branch', () => { + expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual('/twitter/flight/commits/so_long_jquery'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 5d4c7ec09dc..a7d69fdcdb9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,105 +1,85 @@ import Vue from 'vue'; -import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; +import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; import mountComponent from '../../../helpers/vue_mount_component_helper'; -const ConflictsComponent = Vue.extend(conflictsComponent); -const path = '/conflicts'; - describe('MRWidgetConflicts', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = conflictsComponent.props; + let Component; + let vm; + const path = '/conflicts'; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(conflictsComponent); }); - describe('template', () => { - describe('when allowed to merge', () => { - let vm; - - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: true, - conflictResolutionPath: path, - }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(vm.$el.textContent).toContain('There are merge conflicts'); - expect(vm.$el.textContent).not.toContain('ask someone with write access'); - }); - - it('should allow you to resolve the conflicts', () => { - const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); + afterEach(() => { + vm.$destroy(); + }); - expect(resolveButton.textContent).toContain('Resolve conflicts'); - expect(resolveButton.getAttribute('href')).toEqual(path); + describe('when allowed to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, }); + }); - it('should have merge buttons', () => { - const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); - const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - - expect(mergeButton.textContent).toContain('Merge'); - expect(mergeButton.disabled).toBeTruthy(); - expect(mergeButton.classList.contains('btn-success')).toEqual(true); - expect(mergeLocallyButton.textContent).toContain('Merge locally'); - }); + it('should tell you about conflicts without bothering other people', () => { + expect(vm.$el.textContent).toContain('There are merge conflicts'); + expect(vm.$el.textContent).not.toContain('ask someone with write access'); }); - describe('when user does not have permission to merge', () => { - let vm; + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: false, - }, - }); - }); + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + }); - afterEach(() => { - vm.$destroy(); - }); + it('should have merge buttons', () => { + const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); + const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - it('should show proper message', () => { - expect(vm.$el.textContent).toContain('ask someone with write access'); - }); + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeButton.disabled).toBeTruthy(); + expect(mergeButton.classList.contains('btn-success')).toEqual(true); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); + }); - it('should not have action buttons', () => { - expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); - expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); - expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + describe('when user does not have permission to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: false, + }, }); }); - describe('when fast-forward or semi-linear merge enabled', () => { - let vm; + it('should show proper message', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('ask someone with write access'); + }); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - shouldBeRebased: true, - }, - }); - }); + it('should not have action buttons', () => { + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); + expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + }); + }); - afterEach(() => { - vm.$destroy(); + describe('when fast-forward or semi-linear merge enabled', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + shouldBeRebased: true, + }, }); + }); - it('should tell you to rebase locally', () => { - expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.'); - expect(vm.$el.textContent).toContain('To merge this request, first rebase locally'); - }); + it('should tell you to rebase locally', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index cef365eec8a..a57b9811e08 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -1,45 +1,37 @@ import Vue from 'vue'; -import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge'; +import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; - -const mr = { - mergeError: 'Merge error happened.', -}; -const createComponent = () => { - const Component = Vue.extend(failedToMergeComponent); - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetFailedToMerge', () => { - describe('data', () => { - it('should have default data', () => { - const data = failedToMergeComponent.data(); + let Component; + let vm; + + beforeEach(() => { + Component = Vue.extend(failedToMergeComponent); + spyOn(eventHub, '$emit'); + vm = mountComponent(Component, { mr: { + mergeError: 'Merge error happened.', + } }); + }); - expect(data.timer).toEqual(10); - expect(data.isRefreshing).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('timerText', () => { it('should return correct timer text', () => { - const vm = createComponent(); - expect(vm.timerText).toEqual('10 seconds'); + expect(vm.timerText).toEqual('Refreshing in 10 seconds to show the updated status...'); vm.timer = 1; - expect(vm.timerText).toEqual('a second'); + expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...'); }); }); }); describe('created', () => { it('should disable polling', () => { - spyOn(eventHub, '$emit'); - createComponent(); - expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling'); }); }); @@ -47,13 +39,10 @@ describe('MRWidgetFailedToMerge', () => { describe('methods', () => { describe('refresh', () => { it('should emit event to request component refresh', () => { - spyOn(eventHub, '$emit'); - const vm = createComponent(); - - expect(vm.isRefreshing).toBeFalsy(); + expect(vm.isRefreshing).toEqual(false); vm.refresh(); - expect(vm.isRefreshing).toBeTruthy(); + expect(vm.isRefreshing).toEqual(true); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling'); }); @@ -61,12 +50,11 @@ describe('MRWidgetFailedToMerge', () => { describe('updateTimer', () => { it('should update timer and emit event when timer end', () => { - const vm = createComponent(); spyOn(vm, 'refresh'); expect(vm.timer).toEqual(10); - for (let i = 0; i < 10; i++) { // eslint-disable-line + for (let i = 0; i < 10; i += 1) { expect(vm.timer).toEqual(10 - i); vm.updateTimer(); } @@ -76,47 +64,54 @@ describe('MRWidgetFailedToMerge', () => { }); }); - describe('template', () => { - let vm; - let el; + describe('while it is refreshing', () => { + it('renders Refresing now', (done) => { + vm.isRefreshing = true; - beforeEach(() => { - vm = createComponent(); - el = vm.$el; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual('Refreshing now'); + done(); + }); }); + }); - it('should have correct elements', (done) => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('Merge error happened.'); - expect(el.innerText).toContain('Refreshing in 10 seconds'); - expect(el.innerText).not.toContain('Merge failed.'); - expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(el.querySelector('button').innerText).toContain('Merge'); - expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now'); - expect(el.querySelector('.js-refresh-label')).toEqual(null); - expect(el.innerText).not.toContain('Refreshing now'); - setTimeout(() => { - expect(el.innerText).toContain('Refreshing in 9 seconds'); - done(); - }, 1010); + describe('while it is not regresing', () => { + it('renders warning icon and disabled merge button', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('renders given error', () => { + expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual('Merge error happened..'); }); - it('should just generic merge failed message if merge_error is not available', (done) => { - vm.mr.mergeError = null; + it('renders refresh button', () => { + expect(vm.$el.querySelector('.js-refresh-button').textContent.trim()).toEqual('Refresh now'); + }); - Vue.nextTick(() => { - expect(el.innerText).toContain('Merge failed.'); - expect(el.innerText).not.toContain('Merge error happened.'); - done(); - }); + it('renders remaining time', () => { + expect( + vm.$el.querySelector('.has-custom-error').textContent.trim(), + ).toEqual('Refreshing in 10 seconds to show the updated status...'); + }); + }); + + it('should just generic merge failed message if merge_error is not available', (done) => { + vm.mr.mergeError = null; + + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('Merge failed.'); + expect(vm.$el.innerText).not.toContain('Merge error happened.'); + done(); }); + }); - it('should show refresh label when refresh requested', () => { - vm.refresh(); - Vue.nextTick(() => { - expect(el.innerText).not.toContain('Merge failed. Refreshing'); - expect(el.innerText).toContain('Refreshing now'); - }); + it('should show refresh label when refresh requested', (done) => { + vm.refresh(); + Vue.nextTick(() => { + expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing'); + expect(vm.$el.innerText).toContain('Refreshing now'); + done(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js deleted file mode 100644 index 237035648cf..00000000000 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging'; - -describe('MRWidgetMerging', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = mergingComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); - }); - - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(mergingComponent); - const mr = { - targetBranchPath: '/branch-path', - targetBranch: 'branch', - }; - const el = new Component({ - el: document.createElement('div'), - propsData: { mr }, - }).$el; - - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('This merge request is in the process of being merged'); - expect(el.innerText).toContain('changes will be merged into'); - expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js index 5f4df15bcd6..df56c4e2c5c 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js @@ -1,77 +1,50 @@ import Vue from 'vue'; -import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds'; +import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; - -const targetBranchPath = '/foo/bar'; -const targetBranch = 'foo'; -const sha = '1EA2EZ34'; - -const createComponent = () => { - const Component = Vue.extend(mwpsComponent); - const mr = { - shouldRemoveSourceBranch: false, - canRemoveSourceBranch: true, - canCancelAutomaticMerge: true, - mergeUserId: 1, - currentUserId: 1, - setToMWPSBy: {}, - sha, - targetBranchPath, - targetBranch, - }; - - const service = { - cancelAutomaticMerge() {}, - mergeResource: { - save() {}, - }, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr, service }, - }); -}; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetMergeWhenPipelineSucceeds', () => { - describe('props', () => { - it('should have props', () => { - const { mr, service } = mwpsComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); + let vm; + const targetBranchPath = '/foo/bar'; + const targetBranch = 'foo'; + const sha = '1EA2EZ34'; + + beforeEach(() => { + const Component = Vue.extend(mwpsComponent); + spyOn(eventHub, '$emit'); + + vm = mountComponent(Component, { + mr: { + shouldRemoveSourceBranch: false, + canRemoveSourceBranch: true, + canCancelAutomaticMerge: true, + mergeUserId: 1, + currentUserId: 1, + setToMWPSBy: {}, + sha, + targetBranchPath, + targetBranch, + }, + service: { + cancelAutomaticMerge() {}, + mergeResource: { + save() {}, + }, + }, }); }); - describe('components', () => { - it('should have components added', () => { - expect(mwpsComponent.components['mr-widget-author']).toBeDefined(); - }); - }); - - describe('data', () => { - it('should have default data', () => { - const data = mwpsComponent.data(); - - expect(data.isCancellingAutoMerge).toBeFalsy(); - expect(data.isRemovingSourceBranch).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('canRemoveSourceBranch', () => { it('should return true when user is able to remove source branch', () => { - const vm = createComponent(); - expect(vm.canRemoveSourceBranch).toBeTruthy(); }); it('should return false when user id is not the same with who set the MWPS', () => { - const vm = createComponent(); - vm.mr.mergeUserId = 2; expect(vm.canRemoveSourceBranch).toBeFalsy(); @@ -83,15 +56,11 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { }); it('should return false when shouldRemoveSourceBranch set to false', () => { - const vm = createComponent(); - vm.mr.shouldRemoveSourceBranch = true; expect(vm.canRemoveSourceBranch).toBeFalsy(); }); it('should return false if user is not able to remove the source branch', () => { - const vm = createComponent(); - vm.mr.canRemoveSourceBranch = false; expect(vm.canRemoveSourceBranch).toBeFalsy(); }); @@ -101,11 +70,9 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { describe('methods', () => { describe('cancelAutomaticMerge', () => { it('should set flag and call service then tell main component to update the widget with data', (done) => { - const vm = createComponent(); const mrObj = { is_new_mr_data: true, }; - spyOn(eventHub, '$emit'); spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => { resolve({ data: mrObj, @@ -123,8 +90,6 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { describe('removeSourceBranch', () => { it('should set flag and call service then request main component to update the widget', (done) => { - const vm = createComponent(); - spyOn(eventHub, '$emit'); spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => { resolve({ data: { @@ -148,31 +113,23 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { }); describe('template', () => { - let vm; - let el; - - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); - it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds'); - expect(el.innerText).toContain('The changes will be merged into'); - expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch will not be removed'); - expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge'); - expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); - expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch'); - expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy(); + expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds'); + expect(vm.$el.innerText).toContain('The changes will be merged into'); + expect(vm.$el.innerText).toContain(targetBranch); + expect(vm.$el.innerText).toContain('The source branch will not be removed'); + expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge'); + expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); + expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch'); + expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy(); }); it('should disable cancel auto merge button when the action is in progress', (done) => { vm.isCancellingAutoMerge = true; Vue.nextTick(() => { - expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); + expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); done(); }); }); @@ -181,7 +138,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { vm.mr.shouldRemoveSourceBranch = true; Vue.nextTick(() => { - const normalizedText = el.innerText.replace(/\s+/g, ' '); + const normalizedText = vm.$el.innerText.replace(/\s+/g, ' '); expect(normalizedText).toContain('The source branch will be removed'); expect(normalizedText).not.toContain('The source branch will not be removed'); done(); @@ -192,7 +149,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { vm.mr.currentUserId = 4; Vue.nextTick(() => { - expect(el.querySelector('.js-remove-source-branch')).toEqual(null); + expect(vm.$el.querySelector('.js-remove-source-branch')).toEqual(null); done(); }); }); @@ -201,7 +158,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { vm.isRemovingSourceBranch = true; Vue.nextTick(() => { - expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy(); + expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy(); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index 2dc3b72ea40..43a989393ba 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -1,108 +1,99 @@ import Vue from 'vue'; -import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged'; +import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; - -const targetBranch = 'foo'; - -const createComponent = () => { - const Component = Vue.extend(mergedComponent); - const mr = { - isRemovingSourceBranch: false, - cherryPickInForkPath: false, - canCherryPickInCurrentMR: true, - revertInForkPath: false, - canRevertInCurrentMR: true, - canRemoveSourceBranch: true, - sourceBranchRemoved: true, - metrics: { - mergedBy: {}, - mergedAt: 'mergedUpdatedAt', - readableMergedAt: '', - closedBy: {}, - closedAt: 'mergedUpdatedAt', - readableClosedAt: '', - }, - updatedAt: 'mrUpdatedAt', - targetBranch, - }; - - const service = { - removeSourceBranch() {}, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr, service }, - }); -}; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetMerged', () => { - describe('props', () => { - it('should have props', () => { - const { mr, service } = mergedComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); - }); - }); - - describe('components', () => { - it('should have components added', () => { - expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + let vm; + const targetBranch = 'foo'; + + beforeEach(() => { + const Component = Vue.extend(mergedComponent); + const mr = { + isRemovingSourceBranch: false, + cherryPickInForkPath: false, + canCherryPickInCurrentMR: true, + revertInForkPath: false, + canRevertInCurrentMR: true, + canRemoveSourceBranch: true, + sourceBranchRemoved: true, + metrics: { + mergedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + closedBy: {}, + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableClosedAt: '', + }, + updatedAt: 'mergedUpdatedAt', + targetBranch, + }; + + const service = { + removeSourceBranch() {}, + }; + + spyOn(eventHub, '$emit'); + + vm = mountComponent(Component, { mr, service }); }); - describe('data', () => { - it('should have default data', () => { - const data = mergedComponent.data(); - - expect(data.isMakingRequest).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('shouldShowRemoveSourceBranch', () => { - it('should correct value when fields changed', () => { - const vm = createComponent(); + it('returns true when sourceBranchRemoved is false', () => { vm.mr.sourceBranchRemoved = false; - expect(vm.shouldShowRemoveSourceBranch).toBeTruthy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(true); + }); + it('returns false wehn sourceBranchRemoved is true', () => { vm.mr.sourceBranchRemoved = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns false when canRemoveSourceBranch is false', () => { vm.mr.sourceBranchRemoved = false; vm.mr.canRemoveSourceBranch = false; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns false when is making request', () => { vm.mr.canRemoveSourceBranch = true; vm.isMakingRequest = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns true when all are true', () => { vm.mr.isRemovingSourceBranch = true; vm.mr.canRemoveSourceBranch = true; vm.isMakingRequest = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); }); }); + describe('shouldShowSourceBranchRemoving', () => { it('should correct value when fields changed', () => { - const vm = createComponent(); vm.mr.sourceBranchRemoved = false; - expect(vm.shouldShowSourceBranchRemoving).toBeFalsy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(false); vm.mr.sourceBranchRemoved = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); vm.mr.sourceBranchRemoved = false; vm.isMakingRequest = true; - expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(true); vm.isMakingRequest = false; vm.mr.isRemovingSourceBranch = true; - expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(true); }); }); }); @@ -110,8 +101,6 @@ describe('MRWidgetMerged', () => { describe('methods', () => { describe('removeSourceBranch', () => { it('should set flag and call service then request main component to update the widget', (done) => { - const vm = createComponent(); - spyOn(eventHub, '$emit'); spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => { resolve({ data: { @@ -123,7 +112,7 @@ describe('MRWidgetMerged', () => { vm.removeSourceBranch(); setTimeout(() => { const args = eventHub.$emit.calls.argsFor(0); - expect(vm.isMakingRequest).toBeTruthy(); + expect(vm.isMakingRequest).toEqual(true); expect(args[0]).toEqual('MRWidgetUpdateRequested'); expect(args[1]).not.toThrow(); done(); @@ -132,53 +121,50 @@ describe('MRWidgetMerged', () => { }); }); - describe('template', () => { - let vm; - let el; + it('has merged by information', () => { + expect(vm.$el.textContent).toContain('Merged by'); + expect(vm.$el.textContent).toContain('Administrator'); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders branch information', () => { + expect(vm.$el.textContent).toContain('The changes were merged into'); + expect(vm.$el.textContent).toContain(targetBranch); + }); - it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('.js-mr-widget-author')).toBeDefined(); - expect(el.innerText).toContain('The changes were merged into'); - expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch has been removed'); - expect(el.innerText).toContain('Revert'); - expect(el.innerText).toContain('Cherry-pick'); - expect(el.innerText).not.toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch is being removed'); - }); + it('renders information about branch being removed', () => { + expect(vm.$el.textContent).toContain('The source branch has been removed'); + }); - it('should not show source branch removed text', (done) => { - vm.mr.sourceBranchRemoved = false; + it('shows revert and cherry-pick buttons', () => { + expect(vm.$el.textContent).toContain('Revert'); + expect(vm.$el.textContent).toContain('Cherry-pick'); + }); - Vue.nextTick(() => { - expect(el.innerText).toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch has been removed'); - done(); - }); + it('should not show source branch removed text', (done) => { + vm.mr.sourceBranchRemoved = false; + + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('You can remove source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + done(); }); + }); - it('should show source branch removing text', (done) => { - vm.mr.isRemovingSourceBranch = true; - vm.mr.sourceBranchRemoved = false; + it('should show source branch removing text', (done) => { + vm.mr.isRemovingSourceBranch = true; + vm.mr.sourceBranchRemoved = false; - Vue.nextTick(() => { - expect(el.innerText).toContain('The source branch is being removed'); - expect(el.innerText).not.toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch has been removed'); - done(); - }); + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('The source branch is being removed'); + expect(vm.$el.innerText).not.toContain('You can remove source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + done(); }); + }); - it('should use mergedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('mergedUpdatedAt'); - }); + it('should use mergedEvent mergedAt as tooltip title', () => { + expect( + vm.$el.querySelector('time').getAttribute('title'), + ).toBe('Jan 24, 2018 1:02pm GMT+0000'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js new file mode 100644 index 00000000000..0b2ed2d4086 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('MRWidgetMerging', () => { + let vm; + beforeEach(() => { + const Component = Vue.extend(mergingComponent); + + vm = mountComponent(Component, { mr: { + targetBranchPath: '/branch-path', + targetBranch: 'branch', + } }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders information about merge request being merged', () => { + expect( + vm.$el.querySelector('.media-body').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '), + ).toContain('This merge request is in the process of being merged'); + }); + + it('renders branch information', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '), + ).toEqual('The changes will be merged into branch'); + expect( + vm.$el.querySelector('a').getAttribute('href'), + ).toEqual('/branch-path'); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 1127576617b..073f26cc78f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -170,14 +170,14 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.iconClass).toEqual('success'); }); - it('shows x for failed status', () => { + it('shows warning icon for failed status', () => { vm.mr.hasCI = true; - expect(vm.iconClass).toEqual('failed'); + expect(vm.iconClass).toEqual('warning'); }); - it('shows x for merge not allowed', () => { + it('shows warning icon for merge not allowed', () => { vm.mr.hasCI = true; - expect(vm.iconClass).toEqual('failed'); + expect(vm.iconClass).toEqual('warning'); }); }); @@ -371,6 +371,10 @@ describe('MRWidgetReadyToMerge', () => { }); }); + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + }); + it('should call start and stop polling when MR merged', (done) => { spyOn(eventHub, '$emit'); spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); @@ -392,6 +396,47 @@ describe('MRWidgetReadyToMerge', () => { }, 333); }); + it('updates status box', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + const statusBox = document.querySelector('.status-box'); + expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy(); + expect(statusBox.textContent).toContain('Merged'); + + done(); + }); + }); + + it('hides close button', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.btn-close').classList.contains('hidden')).toBeTruthy(); + + done(); + }); + }); + + it('updates merge request count badge', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.js-merge-counter').textContent).toBe('0'); + + done(); + }); + }); + it('should continue polling until MR is merged', (done) => { spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state')); spyOn(vm, 'initiateRemoveSourceBranchPolling'); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index ca29c9fee32..3dd75307484 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -14,7 +14,6 @@ export default { "updated_by_id": null, "created_at": "2017-04-07T12:27:26.718Z", "updated_at": "2017-04-07T15:39:25.852Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, @@ -39,7 +38,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "merged_at": "2017-04-07T15:39:25.696Z", @@ -51,7 +50,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "merge_user": null, @@ -65,7 +64,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "active": false, @@ -160,10 +159,10 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, - "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d", "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d" }, diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js new file mode 100644 index 00000000000..08e4e1f8337 --- /dev/null +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('clipboard button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(clipboardButton); + vm = mountComponent(Component, { + text: 'copy me', + title: 'Copy this value into Clipboard!', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a button for clipboard', () => { + expect(vm.$el.tagName).toEqual('BUTTON'); + expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me'); + expect(vm.$el.querySelector('i').className).toEqual('fa fa-clipboard'); + }); + + it('should have a tooltip with default values', () => { + expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!'); + expect(vm.$el.getAttribute('data-placement')).toEqual('top'); + expect(vm.$el.getAttribute('data-container')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js new file mode 100644 index 00000000000..a6a12614e77 --- /dev/null +++ b/spec/javascripts/vue_shared/components/confirmation_input_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import confirmationInput from '~/vue_shared/components/confirmation_input.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Confirmation input component', () => { + const Component = Vue.extend(confirmationInput); + const props = { + inputId: 'dummy-id', + confirmationKey: 'confirmation-key', + confirmationValue: 'confirmation-value', + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('sets id of the input field to inputId', () => { + expect(vm.$refs.enteredValue.id).toBe(props.inputId); + }); + + it('sets name of the input field to confirmationKey', () => { + expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey); + }); + }); + + describe('computed', () => { + describe('inputLabel', () => { + it('escapes confirmationValue by default', () => { + vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' }); + expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); + }); + + it('does not escape confirmationValue if escapeValue is false', () => { + vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false }); + expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); + }); + }); + }); + + describe('methods', () => { + describe('hasCorrectValue', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('returns false if entered value is incorrect', () => { + vm.$refs.enteredValue.value = 'incorrect'; + expect(vm.hasCorrectValue()).toBe(false); + }); + + it('returns true if entered value is correct', () => { + vm.$refs.enteredValue.value = props.confirmationValue; + expect(vm.hasCorrectValue()).toBe(true); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/loading_icon_spec.js b/spec/javascripts/vue_shared/components/loading_icon_spec.js index 1baf3537741..5cd3466f501 100644 --- a/spec/javascripts/vue_shared/components/loading_icon_spec.js +++ b/spec/javascripts/vue_shared/components/loading_icon_spec.js @@ -16,7 +16,8 @@ describe('Loading Icon Component', () => { ).toEqual('fa fa-spin fa-spinner fa-1x'); expect(component.$el.tagName).toEqual('DIV'); - expect(component.$el.classList.contains('text-center')).toEqual(true); + expect(component.$el.classList).toContain('text-center'); + expect(component.$el.classList).toContain('loading-container'); }); it('should render accessibility attributes', () => { diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 24209be83fe..5f980bbf36c 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -12,14 +12,14 @@ describe('Markdown field component', () => { beforeEach((done) => { vm = new Vue({ + components: { + fieldComponent, + }, data() { return { text: 'testing\n123', }; }, - components: { - fieldComponent, - }, template: ` <field-component markdown-preview-path="/preview" diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index fe75a86cac8..a5f9c75be4e 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -25,7 +25,7 @@ describe('Modal', () => { }); describe('with id', () => { - it('does not render a primary button', () => { + describe('does not render a primary button', () => { beforeEach(() => { vm = mountComponent(modalComponent, { id: 'my-modal', diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 20363e78094..2de108da2ac 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -21,22 +21,21 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('toggleCollapse events', () => { - const toggleCollapse = jasmine.createSpy(); - + describe('toggleCollapse events', () => { beforeEach((done) => { + spyOn(vm, 'toggleSidebar'); vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); it('should emit when sidebar is toggled', () => { vm.$el.querySelector('.gutter-toggle').click(); - expect(toggleCollapse).toHaveBeenCalled(); + expect(vm.toggleSidebar).toHaveBeenCalled(); }); it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); - expect(toggleCollapse).toHaveBeenCalled(); + expect(vm.toggleSidebar).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js new file mode 100644 index 00000000000..6940b04573e --- /dev/null +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -0,0 +1,77 @@ +import Vue from 'vue'; + +import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (config) => { + const Component = Vue.extend(stackedProgressBarComponent); + const defaultConfig = Object.assign({}, { + successLabel: 'Synced', + failureLabel: 'Failed', + neutralLabel: 'Out of sync', + successCount: 10, + failureCount: 5, + totalCount: 20, + }, config); + + return mountComponent(Component, defaultConfig); +}; + +describe('StackedProgressBarComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('neutralCount', () => { + it('returns neutralCount based on totalCount, successCount and failureCount', () => { + expect(vm.neutralCount).toBe(5); // 20 - 10 - 5 + }); + }); + }); + + describe('methods', () => { + describe('getPercent', () => { + it('returns percentage from provided count based on `totalCount`', () => { + expect(vm.getPercent(10)).toBe(50); + }); + }); + + describe('barStyle', () => { + it('returns style string based on percentage provided', () => { + expect(vm.barStyle(50)).toBe('width: 50%;'); + }); + }); + + describe('getTooltip', () => { + it('returns label string based on label and count provided', () => { + expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10'); + }); + }); + }); + + describe('template', () => { + it('renders container element', () => { + expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy(); + }); + + it('renders empty state when count is unavailable', () => { + const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 }); + expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0); + vmX.$destroy(); + }); + + it('renders bar elements when count is available', () => { + expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 1465ef5855f..c63f15e5880 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -32,7 +32,7 @@ describe('Pagination component', () => { change: spy, }); - expect(component.$el.innerHTML).not.toBeDefined(); + expect(component.$el.childNodes.length).toEqual(0); }); describe('prev button', () => { @@ -72,7 +72,6 @@ describe('Pagination component', () => { }); component.$el.querySelector('.js-previous-button a').click(); - expect(spy).toHaveBeenCalledWith(1); }); }); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 8450ad9dbcb..adf80d0c2bb 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 45a0bb0650f..8edba1f47a3 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import Dropzone from 'dropzone'; import ZenMode from '~/zen_mode'; |