diff options
author | Mike Greiling <mike@pixelcog.com> | 2018-02-16 16:00:03 -0600 |
---|---|---|
committer | Mike Greiling <mike@pixelcog.com> | 2018-02-16 16:00:03 -0600 |
commit | 8e65c13a586031928c681c4926d059df23ad5753 (patch) | |
tree | df99f6a592a2d3f7f5fabb4c85c6b90f0343ca68 /spec/javascripts | |
parent | fa260ac8400b16bc19acc5740b47c596c1c903c0 (diff) | |
parent | b236348388c46c0550ec6844df35ec2689c4060b (diff) | |
download | gitlab-ce-8e65c13a586031928c681c4926d059df23ad5753.tar.gz |
Merge branch 'master' into chart.html.haml-refactorchart.html.haml-refactor
* master: (484 commits)
migrate admin:users:* to static bundle
correct for missing break statement in dispatcher.js
alias create and update actions to new and edit
migrate projects:merge_requests:edit to static bundle
migrate projects:merge_requests:creations:diffs to static bundle
migrate projects:merge_requests:creations:new to static bundle
migrate projects:issues:new and projects:issues:edit to static bundle
migrate projects:branches:index to static bundle
migrate projects:branches:new to static bundle
migrate projects:compare:show to static bundle
migrate projects:environments:metrics to static bundle
migrate projects:milestones:* and groups:milestones:* to static bundle
migrate explore:groups:index to static bundle
migrate explore:projects:* to static bundle
migrate dashboard:projects:* to static bundle
migrate admin:jobs:index to static bundle
migrate dashboard:todos:index to static bundle
migrate groups:merge_requests to static bundle
migrate groups:issues to static bundle
migrate dashboard:merge_requests to static bundle
...
Diffstat (limited to 'spec/javascripts')
42 files changed, 1226 insertions, 373 deletions
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 268b5b83b73..1c1b3f3ced3 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -79,7 +79,7 @@ import '~/lib/utils/common_utils'; return expect($emojiMenu.length).toBe(1); }); }); - return it('should remove emoji menu when body is clicked', function(done) { + it('should remove emoji menu when body is clicked', function(done) { $('.js-add-award').eq(0).click(); return lazyAssert(done, function() { var $emojiMenu; @@ -90,6 +90,17 @@ import '~/lib/utils/common_utils'; return expect($('.js-awards-block.current').length).toBe(0); }); }); + it('should not remove emoji menu when search is clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + $('.emoji-search').click(); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + return expect($('.js-awards-block.current').length).toBe(1); + }); + }); }); describe('::addAwardToEmojiBar', function() { it('should add emoji to votes block', function() { diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index a5fcb10b9dd..03df6c06691 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -5,7 +5,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Sortable from 'vendor/Sortable'; -import BoardList from '~/boards/components/board_list'; +import BoardList from '~/boards/components/board_list.vue'; import eventHub from '~/boards/eventhub'; import '~/boards/mixins/sortable_default_options'; import '~/boards/models/issue'; diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js new file mode 100644 index 00000000000..ee457a9c48c --- /dev/null +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -0,0 +1,213 @@ +import $ from 'jquery'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list'; + +const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables'; +const HIDE_CLASS = 'hide'; + +describe('AjaxFormVariableList', () => { + preloadFixtures('projects/ci_cd_settings.html.raw'); + preloadFixtures('projects/ci_cd_settings_with_variables.html.raw'); + + let container; + let saveButton; + let errorBox; + + let mock; + let ajaxVariableList; + + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html.raw'); + container = document.querySelector('.js-ci-variable-list-section'); + + mock = new MockAdapter(axios); + + const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); + saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + errorBox = container.querySelector('.js-ci-variable-error-box'); + ajaxVariableList = new AjaxFormVariableList({ + container, + formField: 'variables', + saveButton, + errorBox, + saveEndpoint: container.dataset.saveEndpoint, + }); + + spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough(); + spyOn(ajaxVariableList.variableList, 'toggleEnableRow').and.callThrough(); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('onSaveClicked', () => { + it('shows loading spinner while waiting for the request', (done) => { + const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); + + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false); + + return [200, {}]; + }); + + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true); + }) + .then(done) + .catch(done.fail); + }); + + it('calls `updateRowsWithPersistedVariables` with the persisted variables', (done) => { + const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }]; + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, { + variables: variablesResponse, + }); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(ajaxVariableList.updateRowsWithPersistedVariables) + .toHaveBeenCalledWith(variablesResponse); + }) + .then(done) + .catch(done.fail); + }); + + it('hides any previous error box', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200); + + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + }) + .then(done) + .catch(done.fail); + }); + + it('disables remove buttons while waiting for the request', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { + expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false); + + return [200, {}]; + }); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true); + }) + .then(done) + .catch(done.fail); + }); + + it('hides secret values', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {}); + + const row = container.querySelector('.js-row:first-child'); + const valueInput = row.querySelector('.js-ci-variable-input-value'); + const valuePlaceholder = row.querySelector('.js-secret-value-placeholder'); + + valueInput.value = 'bar'; + $(valueInput).trigger('input'); + + expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true); + expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false); + expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + + it('shows error box with validation errors', (done) => { + const validationError = 'some validation error'; + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [ + validationError, + ]); + + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false); + expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`); + }) + .then(done) + .catch(done.fail); + }); + + it('shows flash message when request fails', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500); + + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('updateRowsWithPersistedVariables', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings_with_variables.html.raw'); + container = document.querySelector('.js-ci-variable-list-section'); + + const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); + saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + errorBox = container.querySelector('.js-ci-variable-error-box'); + ajaxVariableList = new AjaxFormVariableList({ + container, + formField: 'variables', + saveButton, + errorBox, + saveEndpoint: container.dataset.saveEndpoint, + }); + }); + + it('removes variable that was removed', () => { + expect(container.querySelectorAll('.js-row').length).toBe(3); + + container.querySelector('.js-row-remove-button').click(); + + expect(container.querySelectorAll('.js-row').length).toBe(3); + + ajaxVariableList.updateRowsWithPersistedVariables([]); + + expect(container.querySelectorAll('.js-row').length).toBe(2); + }); + + it('updates new variable row with persisted ID', () => { + const row = container.querySelector('.js-row:last-child'); + const idInput = row.querySelector('.js-ci-variable-input-id'); + const keyInput = row.querySelector('.js-ci-variable-input-key'); + const valueInput = row.querySelector('.js-ci-variable-input-value'); + + keyInput.value = 'foo'; + $(keyInput).trigger('input'); + valueInput.value = 'bar'; + $(valueInput).trigger('input'); + + expect(idInput.value).toEqual(''); + + ajaxVariableList.updateRowsWithPersistedVariables([{ + id: 3, + key: 'foo', + value: 'bar', + }]); + + expect(idInput.value).toEqual('3'); + expect(row.dataset.isPersisted).toEqual('true'); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 0170ab458d4..cac785fd3c6 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -1,9 +1,12 @@ import VariableList from '~/ci_variable_list/ci_variable_list'; import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; +const HIDE_CLASS = 'hide'; + describe('VariableList', () => { preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + preloadFixtures('projects/ci_cd_settings.html.raw'); let $wrapper; let variableList; @@ -91,51 +94,22 @@ describe('VariableList', () => { const $inputValue = $row.find('.js-ci-variable-input-value'); const $placeholder = $row.find('.js-secret-value-placeholder'); - expect($placeholder.hasClass('hide')).toBe(false); - expect($inputValue.hasClass('hide')).toBe(true); + expect($placeholder.hasClass(HIDE_CLASS)).toBe(false); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(true); // Reveal values $wrapper.find('.js-secret-value-reveal-button').click(); - expect($placeholder.hasClass('hide')).toBe(true); - expect($inputValue.hasClass('hide')).toBe(false); + expect($placeholder.hasClass(HIDE_CLASS)).toBe(true); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(false); }); }); }); describe('with all inputs(key, value, protected)', () => { beforeEach(() => { - // This markup will be replaced with a fixture when we can render the - // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 - $wrapper = $(`<form class="js-variable-list"> - <ul> - <li class="js-row"> - <div class="ci-variable-body-item"> - <input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]"> - </div> - - <div class="ci-variable-body-item"> - <textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea> - </div> - - <div class="ci-variable-body-item ci-variable-protected-item"> - <button type="button" class="js-project-feature-toggle project-feature-toggle"> - <input - type="hidden" - class="js-ci-variable-input-protected js-project-feature-toggle-input" - name="variables[variables_attributes][][protected]" - value="true" - /> - </button> - </div> - - <button type="button" class="js-row-remove-button"></button> - </li> - </ul> - <button type="button" class="js-secret-value-reveal-button"> - Reveal values - </button> - </form>`); + loadFixtures('projects/ci_cd_settings.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ container: $wrapper, @@ -154,10 +128,88 @@ describe('VariableList', () => { // Check for the correct default in the new row const $protectedInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-protected'); - expect($protectedInput.val()).toBe('true'); + expect($protectedInput.val()).toBe('false'); }) .then(done) .catch(done.fail); }); }); + + describe('toggleEnableRow method', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should disable all key inputs', () => { + expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); + + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3); + }); + + it('should disable all remove buttons', () => { + expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3); + + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3); + }); + + it('should enable all remove buttons', () => { + variableList.toggleEnableRow(false); + expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3); + + variableList.toggleEnableRow(true); + + expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3); + }); + + it('should enable all key inputs', () => { + variableList.toggleEnableRow(false); + expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3); + + variableList.toggleEnableRow(true); + + expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); + }); + }); + + describe('hideValues', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should hide value input and show placeholder stars', () => { + const $row = $wrapper.find('.js-row'); + const $inputValue = $row.find('.js-ci-variable-input-value'); + const $placeholder = $row.find('.js-secret-value-placeholder'); + + $row.find('.js-ci-variable-input-value') + .val('foo') + .trigger('input'); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(true); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(false); + + variableList.hideValues(); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(false); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(true); + }); + }); }); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 44ec9e4eabf..1daccc8dd02 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -4,6 +4,8 @@ import axios from '~/lib/utils/axios_utils'; import CommitsList from '~/commits'; describe('Commits List', () => { + let commitsList; + beforeEach(() => { setFixtures(` <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master"> @@ -11,6 +13,7 @@ describe('Commits List', () => { </form> <ol id="commits-list"></ol> `); + commitsList = new CommitsList(25); }); it('should be defined', () => { @@ -19,7 +22,7 @@ describe('Commits List', () => { describe('processCommits', () => { it('should join commit headers', () => { - CommitsList.$contentList = $(` + commitsList.$contentList = $(` <div> <li class="commit-header" data-day="2016-09-20"> <span class="day">20 Sep, 2016</span> @@ -39,7 +42,7 @@ describe('Commits List', () => { // The last commit header should be removed // since the previous one has the same data-day value. - expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); + expect(commitsList.processCommits(data).find('li.commit-header').length).toBe(0); }); }); @@ -48,8 +51,7 @@ describe('Commits List', () => { let mock; beforeEach(() => { - CommitsList.init(25); - CommitsList.searchField.val(''); + commitsList.searchField.val(''); spyOn(history, 'replaceState').and.stub(); mock = new MockAdapter(axios); @@ -66,11 +68,11 @@ describe('Commits List', () => { }); it('should save the last search string', (done) => { - CommitsList.searchField.val('GitLab'); - CommitsList.filterResults() + commitsList.searchField.val('GitLab'); + commitsList.filterResults() .then(() => { expect(ajaxSpy).toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual('GitLab'); + expect(commitsList.lastSearch).toEqual('GitLab'); done(); }) @@ -78,10 +80,10 @@ describe('Commits List', () => { }); it('should not make ajax call if the input does not change', (done) => { - CommitsList.filterResults() + commitsList.filterResults() .then(() => { expect(ajaxSpy).not.toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual(''); + expect(commitsList.lastSearch).toEqual(''); done(); }) diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 2e5b65f5610..a8d09202154 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -105,4 +105,68 @@ describe('dateInWords', () => { it('should return abbreviated month name', () => { expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); }); + + it('should return date in words without year', () => { + expect(datetimeUtility.dateInWords(date, true, true)).toEqual('Jul 1'); + }); +}); + +describe('monthInWords', () => { + const date = new Date('2017-01-20'); + + it('returns month name from provided date', () => { + expect(datetimeUtility.monthInWords(date)).toBe('January'); + }); + + it('returns abbreviated month name from provided date', () => { + expect(datetimeUtility.monthInWords(date, true)).toBe('Jan'); + }); +}); + +describe('totalDaysInMonth', () => { + it('returns number of days in a month for given date', () => { + // 1st Feb, 2016 (leap year) + expect(datetimeUtility.totalDaysInMonth(new Date(2016, 1, 1))).toBe(29); + + // 1st Feb, 2017 + expect(datetimeUtility.totalDaysInMonth(new Date(2017, 1, 1))).toBe(28); + + // 1st Jan, 2017 + expect(datetimeUtility.totalDaysInMonth(new Date(2017, 0, 1))).toBe(31); + }); +}); + +describe('getSundays', () => { + it('returns array of dates representing all Sundays of the month', () => { + // December, 2017 (it has 5 Sundays) + const dateOfSundays = [3, 10, 17, 24, 31]; + const sundays = datetimeUtility.getSundays(new Date(2017, 11, 1)); + + expect(sundays.length).toBe(5); + sundays.forEach((sunday, index) => { + expect(sunday.getDate()).toBe(dateOfSundays[index]); + }); + }); +}); + +describe('getTimeframeWindow', () => { + it('returns array of dates representing a timeframe based on provided length and date', () => { + const date = new Date(2018, 0, 1); + const mockTimeframe = [ + new Date(2017, 9, 1), + new Date(2017, 10, 1), + new Date(2017, 11, 1), + new Date(2018, 0, 1), + new Date(2018, 1, 1), + new Date(2018, 2, 31), + ]; + const timeframe = datetimeUtility.getTimeframeWindow(6, date); + + expect(timeframe.length).toBe(6); + timeframe.forEach((timeframeItem, index) => { + expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBeTruthy(); + expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBeTruthy(); + expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy(); + }); + }); }); diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js index 1225fe2cb66..896a04a1a07 100644 --- a/spec/javascripts/droplab/drop_down_spec.js +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -1,8 +1,8 @@ import DropDown from '~/droplab/drop_down'; import utils from '~/droplab/utils'; -import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants'; +import { SELECTED_CLASS } from '~/droplab/constants'; -describe('DropDown', function () { +describe('DropLab DropDown', function () { describe('class constructor', function () { beforeEach(function () { spyOn(DropDown.prototype, 'getItems'); @@ -128,93 +128,131 @@ describe('DropDown', function () { beforeEach(function () { this.classList = jasmine.createSpyObj('classList', ['contains']); this.list = { dispatchEvent: () => {} }; - this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} }; - this.event = { preventDefault: () => {}, target: { classList: this.classList } }; + this.dropdown = { + hideOnClick: true, + hide: () => {}, + list: this.list, + addSelectedClass: () => {}, + }; + this.event = { + preventDefault: () => {}, + target: { + classList: this.classList, + closest: () => null, + }, + }; this.customEvent = {}; - this.closestElement = {}; + this.dummyListItem = document.createElement('li'); + spyOn(this.event.target, 'closest').and.callFake((selector) => { + if (selector === 'li') { + return this.dummyListItem; + } + + return null; + }); spyOn(this.dropdown, 'hide'); spyOn(this.dropdown, 'addSelectedClass'); spyOn(this.list, 'dispatchEvent'); spyOn(this.event, 'preventDefault'); spyOn(window, 'CustomEvent').and.returnValue(this.customEvent); - spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined); this.classList.contains.and.returnValue(false); + }); + it('should call event.target.closest', function () { DropDown.prototype.clickEvent.call(this.dropdown, this.event); - }); - it('should call utils.closest', function () { - expect(utils.closest).toHaveBeenCalledWith(this.event.target, 'LI'); + expect(this.event.target.closest).toHaveBeenCalledWith('.droplab-item-ignore'); + expect(this.event.target.closest).toHaveBeenCalledWith('li'); }); it('should call addSelectedClass', function () { - expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement); + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + + expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.dummyListItem); }); it('should call .preventDefault', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.event.preventDefault).toHaveBeenCalled(); }); it('should call .hide', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.dropdown.hide).toHaveBeenCalled(); }); it('should construct CustomEvent', function () { - expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); - }); + DropDown.prototype.clickEvent.call(this.dropdown, this.event); - it('should call .classList.contains checking for IGNORE_CLASS', function () { - expect(this.classList.contains).toHaveBeenCalledWith(IGNORE_CLASS); + expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); }); it('should call .dispatchEvent with the customEvent', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent); }); describe('if the target is a UL element', function () { beforeEach(function () { - this.event = { preventDefault: () => {}, target: { tagName: 'UL', classList: this.classList } }; - - spyOn(this.event, 'preventDefault'); - utils.closest.calls.reset(); + this.event.target = document.createElement('ul'); - DropDown.prototype.clickEvent.call(this.dropdown, this.event); + spyOn(this.event.target, 'closest'); }); it('should return immediately', function () { - expect(utils.closest).not.toHaveBeenCalled(); + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + + expect(this.event.target.closest).not.toHaveBeenCalled(); + expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled(); }); }); - describe('if the target has the IGNORE_CLASS class', function () { + describe('if the target has the droplab-item-ignore class', function () { beforeEach(function () { - this.event = { preventDefault: () => {}, target: { tagName: 'LI', classList: this.classList } }; + this.ignoredButton = document.createElement('button'); + this.ignoredButton.classList.add('droplab-item-ignore'); + this.event.target = this.ignoredButton; - spyOn(this.event, 'preventDefault'); - this.classList.contains.and.returnValue(true); - utils.closest.calls.reset(); + spyOn(this.ignoredButton, 'closest').and.callThrough(); + }); + it('does not select element', function () { DropDown.prototype.clickEvent.call(this.dropdown, this.event); - }); - it('should return immediately', function () { - expect(utils.closest).not.toHaveBeenCalled(); + expect(this.ignoredButton.closest.calls.count()).toBe(1); + expect(this.ignoredButton.closest).toHaveBeenCalledWith('.droplab-item-ignore'); + expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled(); }); }); describe('if no selected element exists', function () { beforeEach(function () { this.event.preventDefault.calls.reset(); - this.clickEvent = DropDown.prototype.clickEvent.call(this.dropdown, this.event); - }); - - it('should return undefined', function () { - expect(this.clickEvent).toBe(undefined); + this.dummyListItem = null; }); it('should return before .preventDefault is called', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + expect(this.event.preventDefault).not.toHaveBeenCalled(); + expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled(); + }); + }); + + describe('if hideOnClick is false', () => { + beforeEach(function () { + this.dropdown.hideOnClick = false; + this.dropdown.hide.calls.reset(); + }); + + it('should not call .hide', function () { + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + + expect(this.dropdown.hide).not.toHaveBeenCalled(); }); }); }); @@ -278,20 +316,23 @@ describe('DropDown', function () { describe('addEvents', function () { beforeEach(function () { - this.list = { addEventListener: () => {} }; + this.list = { + addEventListener: () => {}, + querySelectorAll: () => [], + }; this.dropdown = { list: this.list, clickEvent: () => {}, closeDropdown: () => {}, eventWrapper: {}, }; + }); + it('should call .addEventListener', function () { spyOn(this.list, 'addEventListener'); DropDown.prototype.addEvents.call(this.dropdown); - }); - it('should call .addEventListener', function () { expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function)); }); diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js index 79447787fc9..4a516c517ef 100644 --- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import eventHub from '~/filtered_search/event_hub'; import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content'; -import '~/filtered_search/filtered_search_token_keys'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; const createComponent = (propsData) => { const Component = Vue.extend(RecentSearchesDropdownContent); @@ -19,14 +19,14 @@ const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); describe('RecentSearchesDropdownContent', () => { const propsDataWithoutItems = { items: [], - allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), + allowedKeys: FilteredSearchTokenKeys.getKeys(), }; const propsDataWithItems = { items: [ 'foo', 'author:@root label:~foo bar', ], - allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), + allowedKeys: FilteredSearchTokenKeys.getKeys(), }; let vm; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index 02415485d19..f1e6119253e 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -3,6 +3,8 @@ import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown'; import '~/filtered_search/dropdown_user'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; + describe('Dropdown User', () => { describe('getSearchInput', () => { let dropdownUser; @@ -14,7 +16,7 @@ describe('Dropdown User', () => { spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new gl.DropdownUser({ - tokenKeys: gl.FilteredSearchTokenKeys, + tokenKeys: FilteredSearchTokenKeys, }); }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index b1b3d43f241..d6e1af105f1 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,6 +1,7 @@ import '~/filtered_search/dropdown_utils'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Dropdown Utils', () => { @@ -137,7 +138,7 @@ describe('Dropdown Utils', () => { `); input = document.getElementById('test'); - allowedKeys = gl.FilteredSearchTokenKeys.getKeys(); + allowedKeys = FilteredSearchTokenKeys.getKeys(); }); function config() { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index b8890e4cda1..0ed9a587dc1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -3,8 +3,8 @@ import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searche import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import '~/lib/utils/common_utils'; -import '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; import '~/filtered_search/filtered_search_manager'; @@ -14,6 +14,7 @@ describe('Filtered Search Manager', () => { let input; let manager; let tokensContainer; + const page = 'issues'; const placeholder = 'Search or filter results...'; function dispatchBackspaceEvent(element, eventType) { @@ -62,7 +63,7 @@ describe('Filtered Search Manager', () => { input = document.querySelector('.filtered-search'); tokensContainer = document.querySelector('.tokens-container'); - manager = new gl.FilteredSearchManager(); + manager = new gl.FilteredSearchManager({ page }); manager.setup(); }; @@ -80,19 +81,19 @@ describe('Filtered Search Manager', () => { }); it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { - manager = new gl.FilteredSearchManager(); + manager = new gl.FilteredSearchManager({ page }); expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ isLocalStorageAvailable, - allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), + allowedKeys: FilteredSearchTokenKeys.getKeys(), }); }); }); describe('setup', () => { beforeEach(() => { - manager = new gl.FilteredSearchManager(); + manager = new gl.FilteredSearchManager({ page }); }); it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index 69b424c3af5..fbc3926d332 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,11 +1,11 @@ -import '~/filtered_search/filtered_search_token_keys'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; describe('Filtered Search Token Keys', () => { describe('get', () => { let tokenKeys; beforeEach(() => { - tokenKeys = gl.FilteredSearchTokenKeys.get(); + tokenKeys = FilteredSearchTokenKeys.get(); }); it('should return tokenKeys', () => { @@ -21,7 +21,7 @@ describe('Filtered Search Token Keys', () => { let conditions; beforeEach(() => { - conditions = gl.FilteredSearchTokenKeys.getConditions(); + conditions = FilteredSearchTokenKeys.getConditions(); }); it('should return conditions', () => { @@ -35,71 +35,71 @@ describe('Filtered Search Token Keys', () => { describe('searchByKey', () => { it('should return null when key not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + const tokenKey = FilteredSearchTokenKeys.searchByKey('notakey'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by key', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + const tokenKeys = FilteredSearchTokenKeys.get(); + const result = FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchBySymbol', () => { it('should return null when symbol not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + const tokenKey = FilteredSearchTokenKeys.searchBySymbol('notasymbol'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by symbol', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + const tokenKeys = FilteredSearchTokenKeys.get(); + const result = FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchByKeyParam', () => { it('should return null when key param not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + const tokenKey = FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + const tokenKeys = FilteredSearchTokenKeys.get(); + const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]); }); it('should return alternative tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + const tokenKeys = FilteredSearchTokenKeys.getAlternatives(); + const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchByConditionUrl', () => { it('should return null when condition url not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + const condition = FilteredSearchTokenKeys.searchByConditionUrl(null); expect(condition === null).toBe(true); }); it('should return condition when found by url', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + const conditions = FilteredSearchTokenKeys.getConditions(); + const result = FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); expect(result).toBe(conditions[0]); }); }); describe('searchByConditionKeyValue', () => { it('should return null when condition tokenKey and value not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + const condition = FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); expect(condition === null).toBe(true); }); it('should return condition when found by tokenKey and value', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys + const conditions = FilteredSearchTokenKeys.getConditions(); + const result = FilteredSearchTokenKeys .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); expect(result).toEqual(conditions[0]); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index 585bea9b499..bf8b66f1110 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,8 +1,8 @@ -import '~/filtered_search/filtered_search_token_keys'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; describe('Filtered Search Tokenizer', () => { - const allowedKeys = gl.FilteredSearchTokenKeys.getKeys(); + const allowedKeys = FilteredSearchTokenKeys.getKeys(); describe('processTokens', () => { it('returns for input containing only search value', () => { diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb new file mode 100644 index 00000000000..35be52fbf97 --- /dev/null +++ b/spec/javascripts/fixtures/groups.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Groups (JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:group) { create(:group, name: 'frontend-fixtures-group' )} + + render_views + + before(:all) do + clean_frontend_fixtures('groups/') + end + + before do + group.add_master(admin) + sign_in(admin) + end + + describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do + it 'groups/ci_cd_settings.html.raw' do |example| + get :show, + group_id: group + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 2a100e7fab5..b344b389241 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe ProjectsController, '(JavaScript fixtures)', type: :controller do +describe 'Projects (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'builds-project') } + let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') } + let!(:variable1) { create(:ci_variable, project: project_variable_populated) } + let!(:variable2) { create(:ci_variable, project: project_variable_populated) } render_views @@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do end before do + # EE-specific start + # EE specific end + project.add_master(admin) sign_in(admin) end @@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'projects/dashboard.html.raw' do |example| - get :show, - namespace_id: project.namespace.to_param, - id: project + describe ProjectsController, '(JavaScript fixtures)', type: :controller do + it 'projects/dashboard.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + id: project - expect(response).to be_success - store_frontend_fixture(response, example.description) + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + + it 'projects/edit.html.raw' do |example| + get :edit, + namespace_id: project.namespace.to_param, + id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do + it 'projects/ci_cd_settings.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + project_id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + + it 'projects/ci_cd_settings_with_variables.html.raw' do |example| + get :show, + namespace_id: project_variable_populated.namespace.to_param, + project_id: project_variable_populated + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end end end diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js index 5a8009e57fd..9c1fc0fda9e 100644 --- a/spec/javascripts/gl_form_spec.js +++ b/spec/javascripts/gl_form_spec.js @@ -1,10 +1,8 @@ -import Autosize from 'autosize'; +import autosize from 'autosize'; import GLForm from '~/gl_form'; import '~/lib/utils/text_utility'; import '~/lib/utils/common_utils'; -window.autosize = Autosize; - describe('GLForm', () => { describe('when instantiated', function () { beforeEach((done) => { @@ -13,14 +11,12 @@ describe('GLForm', () => { spyOn($.prototype, 'off').and.returnValue(this.textarea); spyOn($.prototype, 'on').and.returnValue(this.textarea); spyOn($.prototype, 'css'); - spyOn(window, 'autosize'); - this.glForm = new GLForm(this.form); + this.glForm = new GLForm(this.form, false); setTimeout(() => { $.prototype.off.calls.reset(); $.prototype.on.calls.reset(); $.prototype.css.calls.reset(); - window.autosize.calls.reset(); done(); }); }); @@ -43,10 +39,6 @@ describe('GLForm', () => { expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function)); }); - it('should autosize the textarea', () => { - expect(window.autosize).toHaveBeenCalledWith(jasmine.any(Object)); - }); - it('should set the resize css property to vertical', () => { expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical'); }); @@ -74,7 +66,7 @@ describe('GLForm', () => { spyOn($.prototype, 'data'); spyOn($.prototype, 'outerHeight').and.returnValue(200); spyOn(window, 'outerHeight').and.returnValue(400); - spyOn(window.autosize, 'destroy'); + spyOn(autosize, 'destroy'); this.glForm.destroyAutosize(); }); @@ -88,7 +80,7 @@ describe('GLForm', () => { }); it('should call autosize destroy', () => { - expect(window.autosize.destroy).toHaveBeenCalledWith(this.textarea); + expect(autosize.destroy).toHaveBeenCalledWith(this.textarea); }); it('should set the data-height attribute', () => { @@ -107,9 +99,9 @@ describe('GLForm', () => { it('should return undefined if the data-height equals the outerHeight', () => { spyOn($.prototype, 'outerHeight').and.returnValue(200); spyOn($.prototype, 'data').and.returnValue(200); - spyOn(window.autosize, 'destroy'); + spyOn(autosize, 'destroy'); expect(this.glForm.destroyAutosize()).toBeUndefined(); - expect(window.autosize.destroy).not.toHaveBeenCalled(); + expect(autosize.destroy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js index 7a826487bf9..5decb5e6bbd 100644 --- a/spec/javascripts/gpg_badges_spec.js +++ b/spec/javascripts/gpg_badges_spec.js @@ -1,6 +1,9 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import GpgBadges from '~/gpg_badges'; describe('GpgBadges', () => { + let mock; const dummyCommitSha = 'n0m0rec0ffee'; const dummyBadgeHtml = 'dummy html'; const dummyResponse = { @@ -11,38 +14,43 @@ describe('GpgBadges', () => { }; beforeEach(() => { + mock = new MockAdapter(axios); setFixtures(` + <form + class="commits-search-form" data-signatures-path="/hello" action="/hello" + method="get"> + <input name="utf8" type="hidden" value="✓"> + <input type="search" name="search" id="commits-search"class="form-control search-text-input input-short"> + </form> <div class="parent-container"> <div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div> </div> `); }); - it('displays a loading spinner', () => { - spyOn($, 'get').and.returnValue({ - done() { - // intentionally left blank - }, - }); + afterEach(() => { + mock.restore(); + }); - GpgBadges.fetch(); + it('displays a loading spinner', (done) => { + mock.onGet('/hello').reply(200); - expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); - const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); - expect(spinners.length).toBe(1); + GpgBadges.fetch().then(() => { + expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); + const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); + expect(spinners.length).toBe(1); + done(); + }).catch(done.fail); }); - it('replaces the loading spinner', () => { - spyOn($, 'get').and.returnValue({ - done(callback) { - callback(dummyResponse); - }, - }); - - GpgBadges.fetch(); + it('replaces the loading spinner', (done) => { + mock.onGet('/hello').reply(200, dummyResponse); - expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); - const parentContainer = document.querySelector('.parent-container'); - expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + GpgBadges.fetch().then(() => { + expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); + const parentContainer = document.querySelector('.parent-container'); + expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + done(); + }).catch(done.fail); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 8338efe915b..3adc29262f3 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -268,10 +268,10 @@ describe('AppComponent', () => { it('updates props which show modal confirmation dialog', () => { const group = Object.assign({}, mockParentGroupItem); - expect(vm.showModal).toBeFalsy(); + expect(vm.updateModal).toBeFalsy(); expect(vm.groupLeaveConfirmationMessage).toBe(''); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.showModal).toBeTruthy(); + expect(vm.updateModal).toBeTruthy(); expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); }); }); @@ -280,9 +280,9 @@ describe('AppComponent', () => { it('hides modal confirmation which is shown before leaving the group', () => { const group = Object.assign({}, mockParentGroupItem); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.showModal).toBeTruthy(); + expect(vm.updateModal).toBeTruthy(); vm.hideLeaveGroupModal(); - expect(vm.showModal).toBeFalsy(); + expect(vm.updateModal).toBeFalsy(); }); }); @@ -307,7 +307,7 @@ describe('AppComponent', () => { spyOn($, 'scrollTo'); vm.leaveGroup(); - expect(vm.showModal).toBeFalsy(); + expect(vm.updateModal).toBeFalsy(); expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { @@ -475,7 +475,7 @@ describe('AppComponent', () => { it('renders modal confirmation dialog', () => { vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; - vm.showModal = true; + vm.updateModal = true; const modalDialogEl = vm.$el.querySelector('.modal'); expect(modalDialogEl).not.toBe(null); expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); diff --git a/spec/javascripts/importer_status_spec.js b/spec/javascripts/importer_status_spec.js new file mode 100644 index 00000000000..71a2cd51f63 --- /dev/null +++ b/spec/javascripts/importer_status_spec.js @@ -0,0 +1,107 @@ +import { ImporterStatus } from '~/importer_status'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; + +describe('Importer Status', () => { + let instance; + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('addToImport', () => { + const importUrl = '/import_url'; + + beforeEach(() => { + setFixtures(` + <tr id="repo_123"> + <td class="import-target"></td> + <td class="import-actions job-status"> + <button name="button" type="submit" class="btn btn-import js-add-to-import"> + </button> + </td> + </tr> + `); + spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); + spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); + instance = new ImporterStatus('', importUrl); + }); + + it('sets table row to active after post request', (done) => { + mock.onPost(importUrl).reply(200, { + id: 1, + full_path: '/full_path', + }); + + instance.addToImport({ + currentTarget: document.querySelector('.js-add-to-import'), + }) + .then(() => { + expect(document.querySelector('tr').classList.contains('active')).toEqual(true); + done(); + }) + .catch(done.fail); + }); + }); + + describe('autoUpdate', () => { + const jobsUrl = '/jobs_url'; + + beforeEach(() => { + const div = document.createElement('div'); + div.innerHTML = ` + <div id="project_1"> + <div class="job-status"> + </div> + </div> + `; + + document.body.appendChild(div); + + spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); + spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); + instance = new ImporterStatus(jobsUrl); + }); + + function setupMock(importStatus) { + mock.onGet(jobsUrl).reply(200, [{ + id: 1, + import_status: importStatus, + }]); + } + + function expectJobStatus(done, status) { + instance.autoUpdate() + .then(() => { + expect(document.querySelector('#project_1').innerText.trim()).toEqual(status); + done(); + }) + .catch(done.fail); + } + + it('sets the job status to done', (done) => { + setupMock('finished'); + expectJobStatus(done, 'done'); + }); + + it('sets the job status to scheduled', (done) => { + setupMock('scheduled'); + expectJobStatus(done, 'scheduled'); + }); + + it('sets the job status to started', (done) => { + setupMock('started'); + expectJobStatus(done, 'started'); + }); + + it('sets the job status to custom status', (done) => { + setupMock('custom status'); + expectJobStatus(done, 'custom status'); + }); + }); +}); diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js index 8ff93c4f918..365e9fe6a4b 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js +++ b/spec/javascripts/issuable_time_tracker_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; -import timeTracker from '~/sidebar/components/time_tracking/time_tracker'; +import timeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; function initTimeTrackingComponent(opts) { setFixtures(` diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 03b58e9c1d0..b4599688c6d 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -10,6 +10,7 @@ describe('Job', () => { const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; let mock; let response; + let job; function waitForPromise() { return new Promise(resolve => requestAnimationFrame(resolve)); @@ -22,6 +23,8 @@ describe('Job', () => { spyOn(urlUtils, 'visitUrl'); + response = {}; + mock = new MockAdapter(axios); mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); @@ -30,7 +33,7 @@ describe('Job', () => { afterEach(() => { mock.restore(); - response = {}; + clearTimeout(job.timeout); }); describe('class constructor', () => { @@ -43,15 +46,19 @@ describe('Job', () => { }); describe('setup', () => { - beforeEach(function () { - this.job = new Job(); + beforeEach(function (done) { + job = new Job(); + + waitForPromise() + .then(done) + .catch(done.fail); }); it('copies build options', function () { - expect(this.job.pagePath).toBe(JOB_URL); - expect(this.job.buildStatus).toBe('success'); - expect(this.job.buildStage).toBe('test'); - expect(this.job.state).toBe(''); + expect(job.pagePath).toBe(JOB_URL); + expect(job.buildStatus).toBe('success'); + expect(job.buildStage).toBe('test'); + expect(job.state).toBe(''); }); it('only shows the jobs matching the current stage', () => { @@ -84,12 +91,12 @@ describe('Job', () => { complete: false, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.job.state).toBe('newstate'); + expect(job.state).toBe('newstate'); response = { html: '<span>More</span>', @@ -103,7 +110,7 @@ describe('Job', () => { .then(waitForPromise) .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); - expect(this.job.state).toBe('finalstate'); + expect(job.state).toBe('finalstate'); }) .then(done) .catch(done.fail); @@ -117,7 +124,7 @@ describe('Job', () => { complete: false, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -151,7 +158,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -172,7 +179,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -191,9 +198,10 @@ describe('Job', () => { append: false, size: 50, total: 100, + complete: false, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -207,6 +215,7 @@ describe('Job', () => { append: true, size: 10, total: 100, + complete: true, }; }) .then(() => jasmine.clock().tick(4001)) @@ -229,7 +238,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); expect( document.querySelector('.js-raw-link').textContent.trim(), @@ -247,7 +256,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -269,7 +278,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(done) @@ -296,7 +305,7 @@ describe('Job', () => { it('should request build trace with state parameter', (done) => { spyOn(axios, 'get').and.callThrough(); // eslint-disable-next-line no-new - new Job(); + job = new Job(); setTimeout(() => { expect(axios.get).toHaveBeenCalledWith( diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 80430011aed..49799c31995 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -480,4 +480,33 @@ describe('common_utils', () => { expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); }); }); + + describe('convertObjectPropsToCamelCase', () => { + it('returns new object with camelCase property names by converting object with snake_case names', () => { + const snakeRegEx = /(_\w)/g; + const mockObj = { + id: 1, + group_name: 'GitLab.org', + absolute_web_url: 'https://gitlab.com/gitlab-org/', + }; + const mappings = { + id: 'id', + groupName: 'group_name', + absoluteWebUrl: 'absolute_web_url', + }; + + const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); + + Object.keys(convertedObj).forEach((prop) => { + expect(snakeRegEx.test(prop)).toBeFalsy(); + expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); + }); + }); + + it('return empty object if method is called with null or undefined', () => { + expect(Object.keys(commonUtils.convertObjectPropsToCamelCase(null)).length).toBe(0); + expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0); + expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0); + }); + }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 69a23d7b2f3..e57a55fa71a 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -72,4 +72,10 @@ describe('text_utility', () => { expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); }); }); + + describe('convertToCamelCase', () => { + it('converts snake_case string to camelCase string', () => { + expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase'); + }); + }); }); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js index 3319eeb3f31..df3198dd3e2 100644 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ b/spec/javascripts/monitoring/dashboard_state_spec.js @@ -29,34 +29,6 @@ describe('EmptyState', () => { expect(component.currentState).toBe(component.states.gettingStarted); }); - it('buttonPath returns settings path for the state "gettingStarted"', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - settingsPath: statePaths.settingsPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: 'foo', - emptyLoadingSvgPath: 'foo', - emptyUnableToConnectSvgPath: 'foo', - }); - - expect(component.buttonPath).toEqual(statePaths.settingsPath); - expect(component.buttonPath).not.toEqual(statePaths.documentationPath); - }); - - it('buttonPath returns documentation path for any of the other states', () => { - const component = createComponent({ - selectedState: 'loading', - settingsPath: statePaths.settingsPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: 'foo', - emptyLoadingSvgPath: 'foo', - emptyUnableToConnectSvgPath: 'foo', - }); - - expect(component.buttonPath).toEqual(statePaths.documentationPath); - expect(component.buttonPath).not.toEqual(statePaths.settingsPath); - }); - it('showButtonDescription returns a description with a link for the unableToConnect state', () => { const component = createComponent({ selectedState: 'unableToConnect', @@ -88,6 +60,7 @@ describe('EmptyState', () => { const component = createComponent({ selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, + clustersPath: statePaths.clustersPath, documentationPath: statePaths.documentationPath, emptyGettingStartedSvgPath: 'foo', emptyLoadingSvgPath: 'foo', diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 2bbe963e393..f30208b27b6 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -2471,6 +2471,7 @@ export const deploymentData = [ export const statePaths = { settingsPath: '/root/hello-prometheus/services/prometheus/edit', + clustersPath: '/root/hello-prometheus/clusters', documentationPath: '/help/administration/monitoring/prometheus/index.md', }; diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index 02304bf5d7d..8f8ba231ae8 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import MarkdownComponent from '~/notebook/cells/markdown.vue'; -import katex from 'vendor/katex'; +import katex from 'katex'; const Component = Vue.extend(MarkdownComponent); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 36c56cd3862..12d180137a0 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -2,14 +2,29 @@ import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; +import '~/render_gfm'; import * as mockData from '../mock_data'; -import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +const vueMatchers = { + toIncludeElement() { + return { + compare(vm, selector) { + const result = { + pass: vm.$el.querySelector(selector) !== null, + }; + return result; + }, + }; + }, +}; describe('note_app', () => { let mountComponent; let vm; beforeEach(() => { + jasmine.addMatchers(vueMatchers); + const IssueNotesApp = Vue.extend(notesApp); mountComponent = (data) => { @@ -105,7 +120,7 @@ describe('note_app', () => { }); it('should render loading icon', () => { - expect(vm.$el.querySelector('.js-loading')).toBeDefined(); + expect(vm).toIncludeElement('.js-loading'); }); it('should render form', () => { @@ -118,10 +133,14 @@ describe('note_app', () => { describe('update note', () => { describe('individual note', () => { - beforeEach(() => { + beforeEach((done) => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(done); + }, 0); }); afterEach(() => { @@ -131,40 +150,32 @@ describe('note_app', () => { ); }); - it('renders edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); - done(); - }); - }, 0); + it('renders edit form', () => { + expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); it('calls the service to update the note', (done) => { - getSetTimeoutPromise() - .then(() => { - vm.$el.querySelector('.js-note-edit').click(); - }) - .then(Vue.nextTick) - .then(() => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - expect(service.updateNote).toHaveBeenCalled(); - }) - // Wait for the requests to finish before destroying - .then(Vue.nextTick) + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + // Wait for the requests to finish before destroying + Vue.nextTick() .then(done) .catch(done.fail); }); }); - describe('dicussion note', () => { - beforeEach(() => { + describe('discussion note', () => { + beforeEach((done) => { Vue.http.interceptors.push(mockData.discussionNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); + + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(done); + }, 0); }); afterEach(() => { @@ -174,30 +185,17 @@ describe('note_app', () => { ); }); - it('renders edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); - done(); - }); - }, 0); + it('renders edit form', () => { + expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); it('updates the note and resets the edit form', (done) => { - getSetTimeoutPromise() - .then(() => { - vm.$el.querySelector('.js-note-edit').click(); - }) - .then(Vue.nextTick) - .then(() => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - expect(service.updateNote).toHaveBeenCalled(); - }) - // Wait for the requests to finish before destroying - .then(Vue.nextTick) + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + // Wait for the requests to finish before destroying + Vue.nextTick() .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index cb63b64724d..88a7ffb0b9c 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -56,4 +56,25 @@ describe('issue_note', () => { done(); }, 0); }); + + describe('cancel edit', () => { + it('restores content of updated note', (done) => { + const noteBody = 'updated note text'; + vm.updateNote = () => Promise.resolve(); + + vm.formUpdateHandler(noteBody, null, $.noop); + + setTimeout(() => { + expect(vm.note.note_html).toEqual(noteBody); + + vm.formCancelHandler(); + + setTimeout(() => { + expect(vm.note.note_html).toEqual(noteBody); + + done(); + }); + }); + }); + }); }); diff --git a/spec/javascripts/notes/helpers.js b/spec/javascripts/notes/helpers.js new file mode 100644 index 00000000000..a7663710a56 --- /dev/null +++ b/spec/javascripts/notes/helpers.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line import/prefer-default-export +export const resetStore = (store) => { + store.replaceState({ + notes: [], + targetNoteHash: null, + lastFetchedAt: null, + + notesData: {}, + userData: {}, + noteableData: {}, + }); +}; diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index f0c800c759d..ccf4bd070c2 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -7,6 +7,8 @@ export const notesDataMock = { notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes', quickActionsDocsPath: '/help/user/project/quick_actions', registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane', + closeIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close', + reopenIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen', }; export const userDataMock = { diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index e092320f9a3..ab80ed7bbfb 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,8 +1,16 @@ +import Vue from 'vue'; +import _ from 'underscore'; import * as actions from '~/notes/stores/actions'; +import store from '~/notes/stores'; import testAction from '../../helpers/vuex_action_helper'; +import { resetStore } from '../helpers'; import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; describe('Actions Notes Store', () => { + afterEach(() => { + resetStore(store); + }); + describe('setNotesData', () => { it('should set received notes data', (done) => { testAction(actions.setNotesData, null, { notesData: {} }, [ @@ -58,4 +66,67 @@ describe('Actions Notes Store', () => { ], done); }); }); + + describe('async methods', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify({}), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + describe('closeIssue', () => { + it('sets state as closed', (done) => { + store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } }) + .then(() => { + expect(store.state.noteableData.state).toEqual('closed'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('reopenIssue', () => { + it('sets state as reopened', (done) => { + store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } }) + .then(() => { + expect(store.state.noteableData.state).toEqual('reopened'); + done(); + }) + .catch(done.fail); + }); + }); + }); + + describe('emitStateChangedEvent', () => { + it('emits an event on the document', () => { + document.addEventListener('issuable_vue_app:change', (event) => { + expect(event.detail.data).toEqual({ id: '1', state: 'closed' }); + expect(event.detail.isClosed).toEqual(false); + }); + + store.dispatch('emitStateChangedEvent', { id: '1', state: 'closed' }); + }); + }); + + describe('toggleIssueLocalState', () => { + it('sets issue state as closed', (done) => { + testAction(actions.toggleIssueLocalState, 'closed', {}, [ + { type: 'CLOSE_ISSUE', payload: 'closed' }, + ], done); + }); + + it('sets issue state as reopened', (done) => { + testAction(actions.toggleIssueLocalState, 'reopened', {}, [ + { type: 'REOPEN_ISSUE', payload: 'reopened' }, + ], done); + }); + }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index c5a84b71788..919ffbfdef0 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -55,4 +55,10 @@ describe('Getters Notes Store', () => { expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); }); }); + + describe('issueState', () => { + it('should return the issue state', () => { + expect(getters.issueState(state)).toEqual(noteableDataMock.state); + }); + }); }); diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js index 040d14efed2..4655e29eed0 100644 --- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue'; +import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; Vue.use(Translate); diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js index ed481cb60a1..f95a7cef18a 100644 --- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; -import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue'; +import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue'; const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); const cookieKey = 'pipeline_schedules_callout_dismissed'; diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index d010d897642..8ce33d410a7 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -15,6 +15,7 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'repeat', cssClass: 'bar', + id: 123, }, }).$mount(); }); @@ -38,9 +39,8 @@ describe('Pipelines Async Button', () => { describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { - spyOn(window, 'confirm').and.returnValue(true); - eventHub.$on('postAction', (endpoint) => { - expect(endpoint).toEqual('/foo'); + eventHub.$on('actionConfirmationModal', (data) => { + expect(data.id).toEqual(123); }); component = new AsyncButtonComponent({ @@ -49,7 +49,7 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - confirmActionMessage: 'bar', + id: 123, }, }).$mount(); diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js new file mode 100644 index 00000000000..d433f8c3e07 --- /dev/null +++ b/spec/javascripts/settings_panels_spec.js @@ -0,0 +1,29 @@ +import initSettingsPanels from '~/settings_panels'; + +describe('Settings Panels', () => { + preloadFixtures('projects/ci_cd_settings.html.raw'); + + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html.raw'); + }); + + describe('initSettingsPane', () => { + afterEach(() => { + location.hash = ''; + }); + + it('should expand linked hash fragment panel', () => { + location.hash = '#js-general-pipeline-settings'; + + const pipelineSettingsPanel = document.querySelector('#js-general-pipeline-settings'); + // Our test environment automatically expands everything so we need to clear that out first + pipelineSettingsPanel.classList.remove('expanded'); + + expect(pipelineSettingsPanel.classList.contains('expanded')).toBe(false); + + initSettingsPanels(); + + expect(pipelineSettingsPanel.classList.contains('expanded')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js index 720effb5c1c..3d7f4abd420 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -1,38 +1,22 @@ import Vue from 'vue'; -import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch'; - -const createComponent = () => { - const Component = Vue.extend(missingBranchComponent); - const mr = { - sourceBranchRemoved: true, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetMissingBranch', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = missingBranchComponent.props.mr; + let vm; - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); - }); + beforeEach(() => { + const Component = Vue.extend(missingBranchComponent); + vm = mountComponent(Component, { mr: { sourceBranchRemoved: true } }); }); - describe('components', () => { - it('should have components added', () => { - expect(missingBranchComponent.components['mr-widget-merge-help']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('missingBranchName', () => { it('should return proper branch name', () => { - const vm = createComponent(); expect(vm.missingBranchName).toEqual('source'); vm.mr.sourceBranchRemoved = false; @@ -43,7 +27,7 @@ describe('MRWidgetMissingBranch', () => { describe('template', () => { it('should have correct elements', () => { - const el = createComponent().$el; + const el = vm.$el; const content = el.textContent.replace(/\n(\s)+/g, ' ').trim(); expect(el.classList.contains('mr-widget-body')).toBeTruthy(); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js index 33f20ab132d..c89e863d904 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js @@ -1,17 +1,24 @@ import Vue from 'vue'; -import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed'; +import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetNotAllowed', () => { - describe('template', () => { + let vm; + beforeEach(() => { const Component = Vue.extend(notAllowedComponent); - const vm = new Component({ - el: document.createElement('div'), - }); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('Ready to be merged automatically.'); - expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request'); - }); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders success icon', () => { + expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null); + }); + + it('renders informative text', () => { + expect(vm.$el.innerText).toContain('Ready to be merged automatically.'); + expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index d0702f9f503..edab26286bc 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -1,16 +1,23 @@ import Vue from 'vue'; -import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked'; +import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetPipelineBlocked', () => { - describe('template', () => { + let vm; + beforeEach(() => { const Component = Vue.extend(pipelineBlockedComponent); - const vm = new Component({ - el: document.createElement('div'), - }); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); - }); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders warning icon', () => { + expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null); + }); + + it('renders information text', () => { + expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index cd00d0a39a3..45035effe81 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -295,6 +295,15 @@ describe('mrWidgetOptions', () => { expect(notify.notifyMe).not.toHaveBeenCalled(); }); + + it('should not notify if no pipeline provided', () => { + vm.handleNotification({ + ...data, + pipeline: undefined, + }); + + expect(notify.notifyMe).not.toHaveBeenCalled(); + }); }); describe('resumePolling', () => { diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js deleted file mode 100644 index a6a12614e77..00000000000 --- a/spec/javascripts/vue_shared/components/confirmation_input_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -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/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js new file mode 100644 index 00000000000..d6148cb785b --- /dev/null +++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js @@ -0,0 +1,192 @@ +import Vue from 'vue'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const modalComponent = Vue.extend(GlModal); + +describe('GlModal', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('with id', () => { + const props = { + id: 'my-modal', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.id).toBe(props.id); + }); + }); + + describe('without id', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { }); + }); + + it('does not add an id attribute to the modal', () => { + expect(vm.$el.hasAttribute('id')).toBe(false); + }); + }); + + describe('with headerTitleText', () => { + const props = { + headerTitleText: 'my title text', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('sets the modal title', () => { + const modalTitle = vm.$el.querySelector('.modal-title'); + expect(modalTitle.innerHTML.trim()).toBe(props.headerTitleText); + }); + }); + + describe('with footerPrimaryButtonVariant', () => { + const props = { + footerPrimaryButtonVariant: 'danger', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('sets the primary button class', () => { + const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type'); + expect(primaryButton).toHaveClass(`btn-${props.footerPrimaryButtonVariant}`); + }); + }); + + describe('with footerPrimaryButtonText', () => { + const props = { + footerPrimaryButtonText: 'my button text', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('sets the primary button text', () => { + const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type'); + expect(primaryButton.innerHTML.trim()).toBe(props.footerPrimaryButtonText); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> + <div id="modal-container"></div> + `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + $(vm.$el).on('shown.bs.modal', () => done()); + + modalButton.click(); + }); + + describe('methods', () => { + const dummyEvent = 'not really an event'; + + beforeEach(() => { + vm = mountComponent(modalComponent, { }); + spyOn(vm, '$emit'); + }); + + describe('emitCancel', () => { + it('emits a cancel event', () => { + vm.emitCancel(dummyEvent); + + expect(vm.$emit).toHaveBeenCalledWith('cancel', dummyEvent); + }); + }); + + describe('emitSubmit', () => { + it('emits a submit event', () => { + vm.emitSubmit(dummyEvent); + + expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent); + }); + }); + }); + + describe('slots', () => { + const slotContent = 'this should go into the slot'; + const modalWithSlot = (slotName) => { + let template; + if (slotName) { + template = ` + <gl-modal> + <template slot="${slotName}">${slotContent}</template> + </gl-modal> + `; + } else { + template = `<gl-modal>${slotContent}</gl-modal>`; + } + + return Vue.extend({ + components: { + GlModal, + }, + template, + }); + }; + + describe('default slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot()); + }); + + it('sets the modal body', () => { + const modalBody = vm.$el.querySelector('.modal-body'); + expect(modalBody.innerHTML).toBe(slotContent); + }); + }); + + describe('header slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot('header')); + }); + + it('sets the modal header', () => { + const modalHeader = vm.$el.querySelector('.modal-header'); + expect(modalHeader.innerHTML).toBe(slotContent); + }); + }); + + describe('title slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot('title')); + }); + + it('sets the modal title', () => { + const modalTitle = vm.$el.querySelector('.modal-title'); + expect(modalTitle.innerHTML).toBe(slotContent); + }); + }); + + describe('footer slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot('footer')); + }); + + it('sets the modal footer', () => { + const modalFooter = vm.$el.querySelector('.modal-footer'); + expect(modalFooter.innerHTML).toBe(slotContent); + }); + }); + }); +}); |