diff options
Diffstat (limited to 'spec/javascripts')
42 files changed, 2663 insertions, 295 deletions
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index ea7753c7a1d..d6d50304086 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -3,6 +3,8 @@ import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; +require('~/lib/utils/common_utils'); + (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; @@ -28,7 +30,7 @@ import AwardsHandler from '~/awards_handler'; loadFixtures('issues/issue_with_comment.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { - return function(url, emoji, cb) { + return function(button, url, emoji, cb) { return cb(); }; })(this)); @@ -115,6 +117,27 @@ import AwardsHandler from '~/awards_handler'; return expect($emojiButton.next('.js-counter').text()).toBe('4'); }); }); + describe('::userAuthored', function() { + it('should update tooltip to user authored title', function() { + var $thumbsUpEmoji, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); + $thumbsUpEmoji.attr('data-title', 'sam'); + awardsHandler.userAuthored($thumbsUpEmoji); + return expect($thumbsUpEmoji.data("original-title")).toBe("You cannot vote on your own issue, MR and note"); + }); + it('should restore tooltip back to initial vote list', function() { + var $thumbsUpEmoji, $votesBlock; + jasmine.clock().install(); + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); + $thumbsUpEmoji.attr('data-title', 'sam'); + awardsHandler.userAuthored($thumbsUpEmoji); + jasmine.clock().tick(2801); + jasmine.clock().uninstall(); + return expect($thumbsUpEmoji.data("original-title")).toBe("sam"); + }); + }); describe('::getAwardUrl', function() { return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); diff --git a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js new file mode 100644 index 00000000000..d1ebae33dab --- /dev/null +++ b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js @@ -0,0 +1,42 @@ +import { + BoxGeometry, +} from 'three/build/three.module'; +import MeshObject from '~/blob/3d_viewer/mesh_object'; + +describe('Mesh object', () => { + it('defaults to non-wireframe material', () => { + const object = new MeshObject( + new BoxGeometry(10, 10, 10), + ); + + expect(object.material.wireframe).toBeFalsy(); + }); + + it('changes to wirefame material', () => { + const object = new MeshObject( + new BoxGeometry(10, 10, 10), + ); + + object.changeMaterial('wireframe'); + + expect(object.material.wireframe).toBeTruthy(); + }); + + it('scales object down', () => { + const object = new MeshObject( + new BoxGeometry(10, 10, 10), + ); + const radius = object.geometry.boundingSphere.radius; + + expect(radius).not.toBeGreaterThan(4); + }); + + it('does not scale object down', () => { + const object = new MeshObject( + new BoxGeometry(1, 1, 1), + ); + const radius = object.geometry.boundingSphere.radius; + + expect(radius).toBeLessThan(1); + }); +}); diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js new file mode 100644 index 00000000000..d3a4d04345b --- /dev/null +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -0,0 +1,80 @@ +import renderPDF from '~/blob/pdf'; +import testPDF from './test.pdf'; + +describe('PDF renderer', () => { + let viewer; + let app; + + const checkLoaded = (done) => { + if (app.loading) { + setTimeout(() => { + checkLoaded(done); + }, 100); + } else { + done(); + } + }; + + preloadFixtures('static/pdf_viewer.html.raw'); + + beforeEach(() => { + loadFixtures('static/pdf_viewer.html.raw'); + viewer = document.getElementById('js-pdf-viewer'); + viewer.dataset.endpoint = testPDF; + }); + + it('shows loading icon', () => { + renderPDF(); + + expect( + document.querySelector('.loading'), + ).not.toBeNull(); + }); + + describe('successful response', () => { + beforeEach((done) => { + app = renderPDF(); + + checkLoaded(done); + }); + + it('does not show loading icon', () => { + expect( + document.querySelector('.loading'), + ).toBeNull(); + }); + + it('renders the PDF', () => { + expect( + document.querySelector('.pdf-viewer'), + ).not.toBeNull(); + }); + + it('renders the PDF page', () => { + expect( + document.querySelector('.pdf-page'), + ).not.toBeNull(); + }); + }); + + describe('error getting file', () => { + beforeEach((done) => { + viewer.dataset.endpoint = 'invalid/endpoint'; + app = renderPDF(); + + checkLoaded(done); + }); + + it('does not show loading icon', () => { + expect( + document.querySelector('.loading'), + ).toBeNull(); + }); + + it('shows error message', () => { + expect( + document.querySelector('.md').textContent.trim(), + ).toBe('An error occured whilst loading the file. Please try again later.'); + }); + }); +}); diff --git a/spec/javascripts/blob/pdf/test.pdf b/spec/javascripts/blob/pdf/test.pdf Binary files differnew file mode 100644 index 00000000000..eb3d147fde3 --- /dev/null +++ b/spec/javascripts/blob/pdf/test.pdf diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js new file mode 100644 index 00000000000..0e4431548c4 --- /dev/null +++ b/spec/javascripts/blob/sketch/index_spec.js @@ -0,0 +1,118 @@ +/* eslint-disable no-new */ +import JSZip from 'jszip'; +import SketchLoader from '~/blob/sketch'; + +describe('Sketch viewer', () => { + const generateZipFileArrayBuffer = (zipFile, resolve, done) => { + zipFile + .generateAsync({ type: 'arrayBuffer' }) + .then((content) => { + resolve(content); + + setTimeout(() => { + done(); + }, 100); + }); + }; + + preloadFixtures('static/sketch_viewer.html.raw'); + + beforeEach(() => { + loadFixtures('static/sketch_viewer.html.raw'); + }); + + describe('with error message', () => { + beforeEach((done) => { + spyOn(SketchLoader.prototype, 'getZipFile').and.callFake(() => new Promise((resolve, reject) => { + reject(); + + setTimeout(() => { + done(); + }); + })); + + new SketchLoader(document.getElementById('js-sketch-viewer')); + }); + + it('renders error message', () => { + expect( + document.querySelector('#js-sketch-viewer p'), + ).not.toBeNull(); + + expect( + document.querySelector('#js-sketch-viewer p').textContent.trim(), + ).toContain('Cannot show preview.'); + }); + + it('removes render the loading icon', () => { + expect( + document.querySelector('.js-loading-icon'), + ).toBeNull(); + }); + }); + + describe('success', () => { + beforeEach((done) => { + spyOn(SketchLoader.prototype, 'getZipFile').and.callFake(() => new Promise((resolve) => { + const zipFile = new JSZip(); + zipFile.folder('previews') + .file('preview.png', 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAA1JREFUeNoBAgD9/wAAAAIAAVMrnDAAAAAASUVORK5CYII=', { + base64: true, + }); + + generateZipFileArrayBuffer(zipFile, resolve, done); + })); + + new SketchLoader(document.getElementById('js-sketch-viewer')); + }); + + it('does not render error message', () => { + expect( + document.querySelector('#js-sketch-viewer p'), + ).toBeNull(); + }); + + it('removes render the loading icon', () => { + expect( + document.querySelector('.js-loading-icon'), + ).toBeNull(); + }); + + it('renders preview img', () => { + const img = document.querySelector('#js-sketch-viewer img'); + + expect(img).not.toBeNull(); + expect(img.classList.contains('img-responsive')).toBeTruthy(); + }); + + it('renders link to image', () => { + const img = document.querySelector('#js-sketch-viewer img'); + const link = document.querySelector('#js-sketch-viewer a'); + + expect(link.href).toBe(img.src); + expect(link.target).toBe('_blank'); + }); + }); + + describe('incorrect file', () => { + beforeEach((done) => { + spyOn(SketchLoader.prototype, 'getZipFile').and.callFake(() => new Promise((resolve) => { + const zipFile = new JSZip(); + + generateZipFileArrayBuffer(zipFile, resolve, done); + })); + + new SketchLoader(document.getElementById('js-sketch-viewer')); + }); + + it('renders error message', () => { + expect( + document.querySelector('#js-sketch-viewer p'), + ).not.toBeNull(); + + expect( + document.querySelector('#js-sketch-viewer p').textContent.trim(), + ).toContain('Cannot show preview.'); + }); + }); +}); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js new file mode 100644 index 00000000000..3f598887603 --- /dev/null +++ b/spec/javascripts/boards/board_list_spec.js @@ -0,0 +1,201 @@ +/* global BoardService */ +/* global boardsMockInterceptor */ +/* global List */ +/* global listObj */ +/* global ListIssue */ +import Vue from 'vue'; +import _ from 'underscore'; +import Sortable from 'vendor/Sortable'; +import BoardList from '~/boards/components/board_list'; +import eventHub from '~/boards/eventhub'; +import '~/boards/mixins/sortable_default_options'; +import '~/boards/models/issue'; +import '~/boards/models/list'; +import '~/boards/stores/boards_store'; +import './mock_data'; + +window.Sortable = Sortable; + +describe('Board list component', () => { + let component; + + beforeEach((done) => { + const el = document.createElement('div'); + + document.body.appendChild(el); + Vue.http.interceptors.push(boardsMockInterceptor); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.issueBoards.BoardsStore.create(); + gl.IssueBoardsApp = new Vue(); + + const BoardListComp = Vue.extend(BoardList); + const list = new List(listObj); + const issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [], + }); + list.issuesSize = 1; + list.issues.push(issue); + + component = new BoardListComp({ + el, + propsData: { + disabled: false, + list, + issues: list.issues, + loading: false, + issueLinkBase: '/issues', + rootPath: '/', + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + + it('renders component', () => { + expect( + component.$el.classList.contains('board-list-component'), + ).toBe(true); + }); + + it('renders loading icon', (done) => { + component.loading = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-loading'), + ).not.toBeNull(); + + done(); + }); + }); + + it('renders issues', () => { + expect( + component.$el.querySelectorAll('.card').length, + ).toBe(1); + }); + + it('sets data attribute with issue id', () => { + expect( + component.$el.querySelector('.card').getAttribute('data-issue-id'), + ).toBe('1'); + }); + + it('shows new issue form', (done) => { + component.toggleForm(); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-new-issue-form'), + ).not.toBeNull(); + + expect( + component.$el.querySelector('.is-smaller'), + ).not.toBeNull(); + + done(); + }); + }); + + it('shows new issue form after eventhub event', (done) => { + eventHub.$emit(`hide-issue-form-${component.list.id}`); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-new-issue-form'), + ).not.toBeNull(); + + expect( + component.$el.querySelector('.is-smaller'), + ).not.toBeNull(); + + done(); + }); + }); + + it('does not show new issue form for closed list', (done) => { + component.list.type = 'closed'; + component.toggleForm(); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-new-issue-form'), + ).toBeNull(); + + done(); + }); + }); + + it('shows count list item', (done) => { + component.showCount = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count'), + ).not.toBeNull(); + + expect( + component.$el.querySelector('.board-list-count').textContent.trim(), + ).toBe('Showing all issues'); + + done(); + }); + }); + + it('shows how many more issues to load', (done) => { + component.showCount = true; + component.list.issuesSize = 20; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count').textContent.trim(), + ).toBe('Showing 1 of 20 issues'); + + done(); + }); + }); + + it('loads more issues after scrolling', (done) => { + spyOn(component.list, 'nextPage'); + 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; + component.list.issues.push(issue); + } + + Vue.nextTick(() => { + component.$refs.list.scrollTop = 20000; + + setTimeout(() => { + expect(component.list.nextPage).toHaveBeenCalled(); + + done(); + }); + }); + }); + + it('shows loading more spinner', (done) => { + component.showCount = true; + component.list.loadingMore = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count .fa-spinner'), + ).not.toBeNull(); + + done(); + }); + }); +}); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 549c7af8ea8..7174bf1e041 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -64,54 +64,33 @@ describe('Build', () => { }); }); - describe('initial build trace', () => { - beforeEach(() => { - new Build(); - }); - - it('displays the initial build trace', () => { - expect($.ajax.calls.count()).toBe(1); - const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); - expect(url).toBe(`${BUILD_URL}.json`); - expect(dataType).toBe('json'); - expect(success).toEqual(jasmine.any(Function)); - - success.call(context, { trace_html: '<span>Example</span>', status: 'running' }); - - expect($('#build-trace .js-build-output').text()).toMatch(/Example/); - }); - - it('removes the spinner', () => { - const [{ success, context }] = $.ajax.calls.argsFor(0); - success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); - - expect($('.js-build-refresh').length).toBe(0); - }); - }); - describe('running build', () => { beforeEach(function () { - $('.js-build-options').data('buildStatus', 'running'); this.build = new Build(); - spyOn(this.build, 'location').and.returnValue(BUILD_URL); }); it('updates the build trace on an interval', function () { + spyOn(gl.utils, 'visitUrl'); + jasmine.clock().tick(4001); - expect($.ajax.calls.count()).toBe(2); - let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); - expect(url).toBe( - `${BUILD_URL}/trace.json?state=`, - ); - expect(dataType).toBe('json'); - expect(success).toEqual(jasmine.any(Function)); + expect($.ajax.calls.count()).toBe(1); + + // We have to do it this way to prevent Webpack to fail to compile + // when destructuring assignments and reusing + // the same variables names inside the same scope + let args = $.ajax.calls.argsFor(0)[0]; - success.call(context, { + expect(args.url).toBe(`${BUILD_URL}/trace.json`); + expect(args.dataType).toBe('json'); + expect(args.success).toEqual(jasmine.any(Function)); + + args.success.call($, { html: '<span>Update<span>', status: 'running', state: 'newstate', append: true, + complete: false, }); expect($('#build-trace .js-build-output').text()).toMatch(/Update/); @@ -120,16 +99,19 @@ describe('Build', () => { jasmine.clock().tick(4001); expect($.ajax.calls.count()).toBe(3); - [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2); - expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`); - expect(dataType).toBe('json'); - expect(success).toEqual(jasmine.any(Function)); - success.call(context, { + args = $.ajax.calls.argsFor(2)[0]; + expect(args.url).toBe(`${BUILD_URL}/trace.json`); + expect(args.dataType).toBe('json'); + expect(args.data.state).toBe('newstate'); + expect(args.success).toEqual(jasmine.any(Function)); + + args.success.call($, { html: '<span>More</span>', status: 'running', state: 'finalstate', append: true, + complete: true, }); expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); @@ -137,19 +119,22 @@ describe('Build', () => { }); it('replaces the entire build trace', () => { + spyOn(gl.utils, 'visitUrl'); + jasmine.clock().tick(4001); - let [{ success, context }] = $.ajax.calls.argsFor(1); - success.call(context, { + let args = $.ajax.calls.argsFor(0)[0]; + args.success.call($, { html: '<span>Update</span>', status: 'running', - append: true, + append: false, + complete: false, }); expect($('#build-trace .js-build-output').text()).toMatch(/Update/); jasmine.clock().tick(4001); - [{ success, context }] = $.ajax.calls.argsFor(2); - success.call(context, { + args = $.ajax.calls.argsFor(2)[0]; + args.success.call($, { html: '<span>Different</span>', status: 'running', append: false, @@ -159,15 +144,34 @@ describe('Build', () => { expect($('#build-trace .js-build-output').text()).toMatch(/Different/); }); + it('shows information about truncated log', () => { + jasmine.clock().tick(4001); + const [{ success }] = $.ajax.calls.argsFor(0); + + success.call($, { + html: '<span>Update</span>', + status: 'success', + append: false, + truncated: true, + size: '50', + }); + + expect( + $('#build-trace .js-truncated-info').text().trim(), + ).toContain('Showing last 50 KiB of log'); + expect($('#build-trace .js-truncated-info-size').text()).toMatch('50'); + }); + it('reloads the page when the build is done', () => { spyOn(gl.utils, 'visitUrl'); jasmine.clock().tick(4001); - const [{ success, context }] = $.ajax.calls.argsFor(1); - success.call(context, { + const [{ success }] = $.ajax.calls.argsFor(0); + success.call($, { html: '<span>Final</span>', status: 'passed', append: true, + complete: true, }); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js new file mode 100644 index 00000000000..dfd0810d52e --- /dev/null +++ b/spec/javascripts/comment_type_toggle_spec.js @@ -0,0 +1,157 @@ +import CommentTypeToggle from '~/comment_type_toggle'; +import * as dropLabSrc from '~/droplab/drop_lab'; +import InputSetter from '~/droplab/plugins/input_setter'; + +describe('CommentTypeToggle', function () { + describe('class constructor', function () { + beforeEach(function () { + this.dropdownTrigger = {}; + this.dropdownList = {}; + this.noteTypeInput = {}; + this.submitButton = {}; + this.closeButton = {}; + + this.commentTypeToggle = new CommentTypeToggle({ + dropdownTrigger: this.dropdownTrigger, + dropdownList: this.dropdownList, + noteTypeInput: this.noteTypeInput, + submitButton: this.submitButton, + closeButton: this.closeButton, + }); + }); + + it('should set .dropdownTrigger', function () { + expect(this.commentTypeToggle.dropdownTrigger).toBe(this.dropdownTrigger); + }); + + it('should set .dropdownList', function () { + expect(this.commentTypeToggle.dropdownList).toBe(this.dropdownList); + }); + + it('should set .noteTypeInput', function () { + expect(this.commentTypeToggle.noteTypeInput).toBe(this.noteTypeInput); + }); + + it('should set .submitButton', function () { + expect(this.commentTypeToggle.submitButton).toBe(this.submitButton); + }); + + it('should set .closeButton', function () { + expect(this.commentTypeToggle.closeButton).toBe(this.closeButton); + }); + + it('should set .reopenButton', function () { + expect(this.commentTypeToggle.reopenButton).toBe(this.reopenButton); + }); + }); + + describe('initDroplab', function () { + beforeEach(function () { + this.commentTypeToggle = { + dropdownTrigger: {}, + dropdownList: {}, + noteTypeInput: {}, + submitButton: {}, + closeButton: {}, + setConfig: () => {}, + }; + this.config = {}; + + this.droplab = jasmine.createSpyObj('droplab', ['init']); + + spyOn(dropLabSrc, 'default').and.returnValue(this.droplab); + spyOn(this.commentTypeToggle, 'setConfig').and.returnValue(this.config); + + CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle); + }); + + it('should instantiate a DropLab instance', function () { + expect(dropLabSrc.default).toHaveBeenCalled(); + }); + + it('should set .droplab', function () { + expect(this.commentTypeToggle.droplab).toBe(this.droplab); + }); + + it('should call .setConfig', function () { + expect(this.commentTypeToggle.setConfig).toHaveBeenCalled(); + }); + + it('should call DropLab.prototype.init', function () { + expect(this.droplab.init).toHaveBeenCalledWith( + this.commentTypeToggle.dropdownTrigger, + this.commentTypeToggle.dropdownList, + [InputSetter], + this.config, + ); + }); + }); + + describe('setConfig', function () { + describe('if no .closeButton is provided', function () { + beforeEach(function () { + this.commentTypeToggle = { + dropdownTrigger: {}, + dropdownList: {}, + noteTypeInput: {}, + submitButton: {}, + reopenButton: {}, + }; + + this.setConfig = CommentTypeToggle.prototype.setConfig.call(this.commentTypeToggle); + }); + + it('should not add .closeButton related InputSetter config', function () { + expect(this.setConfig).toEqual({ + InputSetter: [{ + input: this.commentTypeToggle.noteTypeInput, + valueAttribute: 'data-value', + }, { + input: this.commentTypeToggle.submitButton, + valueAttribute: 'data-submit-text', + }, { + input: this.commentTypeToggle.reopenButton, + valueAttribute: 'data-reopen-text', + }, { + input: this.commentTypeToggle.reopenButton, + valueAttribute: 'data-reopen-text', + inputAttribute: 'data-alternative-text', + }], + }); + }); + }); + + describe('if no .reopenButton is provided', function () { + beforeEach(function () { + this.commentTypeToggle = { + dropdownTrigger: {}, + dropdownList: {}, + noteTypeInput: {}, + submitButton: {}, + closeButton: {}, + }; + + this.setConfig = CommentTypeToggle.prototype.setConfig.call(this.commentTypeToggle); + }); + + it('should not add .reopenButton related InputSetter config', function () { + expect(this.setConfig).toEqual({ + InputSetter: [{ + input: this.commentTypeToggle.noteTypeInput, + valueAttribute: 'data-value', + }, { + input: this.commentTypeToggle.submitButton, + valueAttribute: 'data-submit-text', + }, { + input: this.commentTypeToggle.closeButton, + valueAttribute: 'data-close-text', + }, { + input: this.commentTypeToggle.closeButton, + valueAttribute: 'data-close-text', + inputAttribute: 'data-alternative-text', + }], + }); + }); + }); + }); +}); diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js new file mode 100644 index 00000000000..35239e4fb8e --- /dev/null +++ b/spec/javascripts/droplab/constants_spec.js @@ -0,0 +1,29 @@ +/* eslint-disable */ + +import * as constants from '~/droplab/constants'; + +describe('constants', function () { + describe('DATA_TRIGGER', function () { + it('should be `data-dropdown-trigger`', function() { + expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger'); + }); + }); + + describe('DATA_DROPDOWN', function () { + it('should be `data-dropdown`', function() { + expect(constants.DATA_DROPDOWN).toBe('data-dropdown'); + }); + }); + + describe('SELECTED_CLASS', function () { + it('should be `droplab-item-selected`', function() { + expect(constants.SELECTED_CLASS).toBe('droplab-item-selected'); + }); + }); + + describe('ACTIVE_CLASS', function () { + it('should be `droplab-item-active`', function() { + expect(constants.ACTIVE_CLASS).toBe('droplab-item-active'); + }); + }); +}); diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js new file mode 100644 index 00000000000..802e2435672 --- /dev/null +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -0,0 +1,593 @@ +/* eslint-disable */ + +import DropDown from '~/droplab/drop_down'; +import utils from '~/droplab/utils'; +import { SELECTED_CLASS } from '~/droplab/constants'; + +describe('DropDown', function () { + describe('class constructor', function () { + beforeEach(function () { + spyOn(DropDown.prototype, 'getItems'); + spyOn(DropDown.prototype, 'initTemplateString'); + spyOn(DropDown.prototype, 'addEvents'); + + this.list = { innerHTML: 'innerHTML' }; + this.dropdown = new DropDown(this.list); + }); + + it('sets the .hidden property to true', function () { + expect(this.dropdown.hidden).toBe(true); + }) + + it('sets the .list property', function () { + expect(this.dropdown.list).toBe(this.list); + }); + + it('calls .getItems', function () { + expect(DropDown.prototype.getItems).toHaveBeenCalled(); + }); + + it('calls .initTemplateString', function () { + expect(DropDown.prototype.initTemplateString).toHaveBeenCalled(); + }); + + it('calls .addEvents', function () { + expect(DropDown.prototype.addEvents).toHaveBeenCalled(); + }); + + it('sets the .initialState property to the .list.innerHTML', function () { + expect(this.dropdown.initialState).toBe(this.list.innerHTML); + }); + + describe('if the list argument is a string', function () { + beforeEach(function () { + this.element = {}; + this.selector = '.selector'; + + spyOn(Document.prototype, 'querySelector').and.returnValue(this.element); + + this.dropdown = new DropDown(this.selector); + }); + + it('calls .querySelector with the selector string', function () { + expect(Document.prototype.querySelector).toHaveBeenCalledWith(this.selector); + }); + + it('sets the .list property element', function () { + expect(this.dropdown.list).toBe(this.element); + }); + }); + }); + + describe('getItems', function () { + beforeEach(function () { + this.list = { querySelectorAll: () => {} }; + this.dropdown = { list: this.list }; + this.nodeList = []; + + spyOn(this.list, 'querySelectorAll').and.returnValue(this.nodeList); + + this.getItems = DropDown.prototype.getItems.call(this.dropdown); + }); + + it('calls .querySelectorAll with a list item query', function () { + expect(this.list.querySelectorAll).toHaveBeenCalledWith('li'); + }); + + it('sets the .items property to the returned list items', function () { + expect(this.dropdown.items).toEqual(jasmine.any(Array)); + }); + + it('returns the .items', function () { + expect(this.getItems).toEqual(jasmine.any(Array)); + }); + }); + + describe('initTemplateString', function () { + beforeEach(function () { + this.items = [{ outerHTML: '<a></a>' }, { outerHTML: '<img>' }]; + this.dropdown = { items: this.items }; + + DropDown.prototype.initTemplateString.call(this.dropdown); + }); + + it('should set .templateString to the last items .outerHTML', function () { + expect(this.dropdown.templateString).toBe(this.items[1].outerHTML); + }); + + it('should not set .templateString to a non-last items .outerHTML', function () { + expect(this.dropdown.templateString).not.toBe(this.items[0].outerHTML); + }); + + describe('if .items is not set', function () { + beforeEach(function () { + this.dropdown = { getItems: () => {} }; + + spyOn(this.dropdown, 'getItems').and.returnValue([]); + + DropDown.prototype.initTemplateString.call(this.dropdown); + }); + + it('should call .getItems', function () { + expect(this.dropdown.getItems).toHaveBeenCalled(); + }); + }); + + describe('if items array is empty', function () { + beforeEach(function () { + this.dropdown = { items: [] }; + + DropDown.prototype.initTemplateString.call(this.dropdown); + }); + + it('should set .templateString to an empty string', function () { + expect(this.dropdown.templateString).toBe(''); + }); + }); + }); + + describe('clickEvent', function () { + beforeEach(function () { + this.list = { dispatchEvent: () => {} }; + this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} }; + this.event = { preventDefault: () => {}, target: {} }; + this.customEvent = {}; + this.closestElement = {}; + + 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); + + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + }); + + it('should call utils.closest', function () { + expect(utils.closest).toHaveBeenCalledWith(this.event.target, 'LI'); + }); + + it('should call addSelectedClass', function () { + expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement); + }) + + it('should call .preventDefault', function () { + expect(this.event.preventDefault).toHaveBeenCalled(); + }); + + it('should call .hide', function () { + expect(this.dropdown.hide).toHaveBeenCalled(); + }); + + it('should construct CustomEvent', function () { + expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object)); + }); + + it('should call .dispatchEvent with the customEvent', function () { + expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent); + }); + + describe('if the target is a UL element', function () { + beforeEach(function () { + this.event = { preventDefault: () => {}, target: { tagName: 'UL' } }; + + spyOn(this.event, 'preventDefault'); + utils.closest.calls.reset(); + + DropDown.prototype.clickEvent.call(this.dropdown, this.event); + }); + + it('should return immediately', function () { + expect(utils.closest).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); + }); + + it('should return before .preventDefault is called', function () { + expect(this.event.preventDefault).not.toHaveBeenCalled(); + }); + }); + }); + + describe('addSelectedClass', function () { + beforeEach(function () { + this.items = Array(4).forEach((item, i) => { + this.items[i] = { classList: { add: () => {} } }; + spyOn(this.items[i].classList, 'add'); + }); + this.selected = { classList: { add: () => {} } }; + this.dropdown = { removeSelectedClasses: () => {} }; + + spyOn(this.dropdown, 'removeSelectedClasses'); + spyOn(this.selected.classList, 'add'); + + DropDown.prototype.addSelectedClass.call(this.dropdown, this.selected); + }); + + it('should call .removeSelectedClasses', function () { + expect(this.dropdown.removeSelectedClasses).toHaveBeenCalled(); + }); + + it('should call .classList.add', function () { + expect(this.selected.classList.add).toHaveBeenCalledWith(SELECTED_CLASS); + }); + }); + + describe('removeSelectedClasses', function () { + beforeEach(function () { + this.items = Array(4); + this.items.forEach((item, i) => { + this.items[i] = { classList: { add: () => {} } }; + spyOn(this.items[i].classList, 'add'); + }); + this.dropdown = { items: this.items }; + + DropDown.prototype.removeSelectedClasses.call(this.dropdown); + }); + + it('should call .classList.remove for all items', function () { + this.items.forEach((item, i) => { + expect(this.items[i].classList.add).toHaveBeenCalledWith(SELECTED_CLASS); + }); + }); + + describe('if .items is not set', function () { + beforeEach(function () { + this.dropdown = { getItems: () => {} }; + + spyOn(this.dropdown, 'getItems').and.returnValue([]); + + DropDown.prototype.removeSelectedClasses.call(this.dropdown); + }); + + it('should call .getItems', function () { + expect(this.dropdown.getItems).toHaveBeenCalled(); + }); + }); + }); + + describe('addEvents', function () { + beforeEach(function () { + this.list = { addEventListener: () => {} }; + this.dropdown = { list: this.list, clickEvent: () => {}, eventWrapper: {} }; + + spyOn(this.list, 'addEventListener'); + + DropDown.prototype.addEvents.call(this.dropdown); + }); + + it('should call .addEventListener', function () { + expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function)); + }); + }); + + describe('toggle', function () { + beforeEach(function () { + this.dropdown = { hidden: true, show: () => {}, hide: () => {} }; + + spyOn(this.dropdown, 'show'); + spyOn(this.dropdown, 'hide'); + + DropDown.prototype.toggle.call(this.dropdown); + }); + + it('should call .show if hidden is true', function () { + expect(this.dropdown.show).toHaveBeenCalled(); + }); + + describe('if hidden is false', function () { + beforeEach(function () { + this.dropdown = { hidden: false, show: () => {}, hide: () => {} }; + + spyOn(this.dropdown, 'show'); + spyOn(this.dropdown, 'hide'); + + DropDown.prototype.toggle.call(this.dropdown); + }); + + it('should call .show if hidden is true', function () { + expect(this.dropdown.hide).toHaveBeenCalled(); + }); + }); + }); + + describe('setData', function () { + beforeEach(function () { + this.dropdown = { render: () => {} }; + this.data = ['data']; + + spyOn(this.dropdown, 'render'); + + DropDown.prototype.setData.call(this.dropdown, this.data); + }); + + it('should set .data', function () { + expect(this.dropdown.data).toBe(this.data); + }); + + it('should call .render with the .data', function () { + expect(this.dropdown.render).toHaveBeenCalledWith(this.data); + }); + }); + + describe('addData', function () { + beforeEach(function () { + this.dropdown = { render: () => {}, data: ['data1'] }; + this.data = ['data2']; + + spyOn(this.dropdown, 'render'); + spyOn(Array.prototype, 'concat').and.callThrough(); + + DropDown.prototype.addData.call(this.dropdown, this.data); + }); + + it('should call .concat with data', function () { + expect(Array.prototype.concat).toHaveBeenCalledWith(this.data); + }); + + it('should set .data with concatination', function () { + expect(this.dropdown.data).toEqual(['data1', 'data2']); + }); + + it('should call .render with the .data', function () { + expect(this.dropdown.render).toHaveBeenCalledWith(['data1', 'data2']); + }); + + describe('if .data is undefined', function () { + beforeEach(function () { + this.dropdown = { render: () => {}, data: undefined }; + this.data = ['data2']; + + spyOn(this.dropdown, 'render'); + + DropDown.prototype.addData.call(this.dropdown, this.data); + }); + + it('should set .data with concatination', function () { + expect(this.dropdown.data).toEqual(['data2']); + }); + }); + }); + + describe('render', function () { + beforeEach(function () { + this.list = { querySelector: () => {} }; + this.dropdown = { renderChildren: () => {}, list: this.list }; + this.renderableList = {}; + this.data = [0, 1]; + + spyOn(this.dropdown, 'renderChildren').and.callFake(data => data); + spyOn(this.list, 'querySelector').and.returnValue(this.renderableList); + spyOn(this.data, 'map').and.callThrough(); + + DropDown.prototype.render.call(this.dropdown, this.data); + }); + + it('should call .map', function () { + expect(this.data.map).toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it('should call .renderChildren for each data item', function() { + expect(this.dropdown.renderChildren.calls.count()).toBe(this.data.length); + }); + + it('sets the renderableList .innerHTML', function () { + expect(this.renderableList.innerHTML).toBe('01'); + }); + + describe('if no data argument is passed' , function () { + beforeEach(function () { + this.data.map.calls.reset(); + this.dropdown.renderChildren.calls.reset(); + + DropDown.prototype.render.call(this.dropdown, undefined); + }); + + it('should not call .map', function () { + expect(this.data.map).not.toHaveBeenCalled(); + }); + + it('should not call .renderChildren', function () { + expect(this.dropdown.renderChildren).not.toHaveBeenCalled(); + }); + }); + + describe('if no dynamic list is present', function () { + beforeEach(function () { + this.list = { querySelector: () => {} }; + this.dropdown = { renderChildren: () => {}, list: this.list }; + this.data = [0, 1]; + + spyOn(this.dropdown, 'renderChildren').and.callFake(data => data); + spyOn(this.list, 'querySelector'); + spyOn(this.data, 'map').and.callThrough(); + + DropDown.prototype.render.call(this.dropdown, this.data); + }); + + it('sets the .list .innerHTML', function () { + expect(this.list.innerHTML).toBe('01'); + }); + }); + }); + + describe('renderChildren', function () { + beforeEach(function () { + this.templateString = 'templateString'; + this.dropdown = { setImagesSrc: () => {}, templateString: this.templateString }; + this.data = { droplab_hidden: true }; + this.html = 'html'; + this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } }; + + spyOn(utils, 't').and.returnValue(this.html); + spyOn(document, 'createElement').and.returnValue(this.template); + spyOn(this.dropdown, 'setImagesSrc'); + + this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data); + }); + + it('should call utils.t with .templateString and data', function () { + expect(utils.t).toHaveBeenCalledWith(this.templateString, this.data); + }); + + it('should call document.createElement', function () { + expect(document.createElement).toHaveBeenCalledWith('div'); + }); + + it('should set the templates .innerHTML to the HTML', function () { + expect(this.template.innerHTML).toBe(this.html); + }); + + it('should call .setImagesSrc with the template', function () { + expect(this.dropdown.setImagesSrc).toHaveBeenCalledWith(this.template); + }); + + it('should set the template display to none', function () { + expect(this.template.firstChild.style.display).toBe('none'); + }); + + it('should return the templates .firstChild.outerHTML', function () { + expect(this.renderChildren).toBe(this.template.firstChild.outerHTML); + }); + + describe('if droplab_hidden is false', function () { + beforeEach(function () { + this.data = { droplab_hidden: false }; + this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data); + }); + + it('should set the template display to block', function () { + expect(this.template.firstChild.style.display).toBe('block'); + }); + }); + }); + + describe('setImagesSrc', function () { + beforeEach(function () { + this.dropdown = {}; + this.template = { querySelectorAll: () => {} }; + + spyOn(this.template, 'querySelectorAll').and.returnValue([]); + + DropDown.prototype.setImagesSrc.call(this.dropdown, this.template); + }); + + it('should call .querySelectorAll', function () { + expect(this.template.querySelectorAll).toHaveBeenCalledWith('img[data-src]'); + }); + }); + + describe('show', function () { + beforeEach(function () { + this.list = { style: {} }; + this.dropdown = { list: this.list, hidden: true }; + + DropDown.prototype.show.call(this.dropdown); + }); + + it('it should set .list display to block', function () { + expect(this.list.style.display).toBe('block'); + }); + + it('it should set .hidden to false', function () { + expect(this.dropdown.hidden).toBe(false); + }); + + describe('if .hidden is false', function () { + beforeEach(function () { + this.list = { style: {} }; + this.dropdown = { list: this.list, hidden: false }; + + this.show = DropDown.prototype.show.call(this.dropdown); + }); + + it('should return undefined', function () { + expect(this.show).toEqual(undefined); + }); + + it('should not set .list display to block', function () { + expect(this.list.style.display).not.toEqual('block'); + }); + }); + }); + + describe('hide', function () { + beforeEach(function () { + this.list = { style: {} }; + this.dropdown = { list: this.list }; + + DropDown.prototype.hide.call(this.dropdown); + }); + + it('it should set .list display to none', function () { + expect(this.list.style.display).toBe('none'); + }); + + it('it should set .hidden to true', function () { + expect(this.dropdown.hidden).toBe(true); + }); + }); + + describe('toggle', function () { + beforeEach(function () { + this.hidden = true + this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} }; + + spyOn(this.dropdown, 'show'); + spyOn(this.dropdown, 'hide'); + + DropDown.prototype.toggle.call(this.dropdown); + }); + + it('should call .show', function () { + expect(this.dropdown.show).toHaveBeenCalled(); + }); + + describe('if .hidden is false', function () { + beforeEach(function () { + this.hidden = false + this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} }; + + spyOn(this.dropdown, 'show'); + spyOn(this.dropdown, 'hide'); + + DropDown.prototype.toggle.call(this.dropdown); + }); + + it('should call .hide', function () { + expect(this.dropdown.hide).toHaveBeenCalled(); + }); + }); + }); + + describe('destroy', function () { + beforeEach(function () { + this.list = { removeEventListener: () => {} }; + this.eventWrapper = { clickEvent: 'clickEvent' }; + this.dropdown = { list: this.list, hide: () => {}, eventWrapper: this.eventWrapper }; + + spyOn(this.list, 'removeEventListener'); + spyOn(this.dropdown, 'hide'); + + DropDown.prototype.destroy.call(this.dropdown); + }); + + it('it should call .hide', function () { + expect(this.dropdown.hide).toHaveBeenCalled(); + }); + + it('it should call .removeEventListener', function () { + expect(this.list.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.clickEvent); + }); + }); +}); diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js new file mode 100644 index 00000000000..8ebdcdd1404 --- /dev/null +++ b/spec/javascripts/droplab/hook_spec.js @@ -0,0 +1,82 @@ +/* eslint-disable */ + +import Hook from '~/droplab/hook'; +import * as dropdownSrc from '~/droplab/drop_down'; + +describe('Hook', function () { + describe('class constructor', function () { + beforeEach(function () { + this.trigger = { id: 'id' }; + this.list = {}; + this.plugins = {}; + this.config = {}; + this.dropdown = {}; + + spyOn(dropdownSrc, 'default').and.returnValue(this.dropdown); + + this.hook = new Hook(this.trigger, this.list, this.plugins, this.config); + }); + + it('should set .trigger', function () { + expect(this.hook.trigger).toBe(this.trigger); + }); + + it('should set .list', function () { + expect(this.hook.list).toBe(this.dropdown); + }); + + it('should call DropDown constructor', function () { + expect(dropdownSrc.default).toHaveBeenCalledWith(this.list); + }); + + it('should set .type', function () { + expect(this.hook.type).toBe('Hook'); + }); + + it('should set .event', function () { + expect(this.hook.event).toBe('click'); + }); + + it('should set .plugins', function () { + expect(this.hook.plugins).toBe(this.plugins); + }); + + it('should set .config', function () { + expect(this.hook.config).toBe(this.config); + }); + + it('should set .id', function () { + expect(this.hook.id).toBe(this.trigger.id); + }); + + describe('if config argument is undefined', function () { + beforeEach(function () { + this.config = undefined; + + this.hook = new Hook(this.trigger, this.list, this.plugins, this.config); + }); + + it('should set .config to an empty object', function () { + expect(this.hook.config).toEqual({}); + }); + }); + + describe('if plugins argument is undefined', function () { + beforeEach(function () { + this.plugins = undefined; + + this.hook = new Hook(this.trigger, this.list, this.plugins, this.config); + }); + + it('should set .plugins to an empty array', function () { + expect(this.hook.plugins).toEqual([]); + }); + }); + }); + + describe('addEvents', function () { + it('should exist', function () { + expect(Hook.prototype.hasOwnProperty('addEvents')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/droplab/plugins/input_setter_spec.js b/spec/javascripts/droplab/plugins/input_setter_spec.js new file mode 100644 index 00000000000..bd625f4ae80 --- /dev/null +++ b/spec/javascripts/droplab/plugins/input_setter_spec.js @@ -0,0 +1,212 @@ +/* eslint-disable */ + +import InputSetter from '~/droplab/plugins/input_setter'; + +describe('InputSetter', function () { + describe('init', function () { + beforeEach(function () { + this.config = { InputSetter: {} }; + this.hook = { config: this.config }; + this.inputSetter = jasmine.createSpyObj('inputSetter', ['addEvents']); + + InputSetter.init.call(this.inputSetter, this.hook); + }); + + it('should set .hook', function () { + expect(this.inputSetter.hook).toBe(this.hook); + }); + + it('should set .config', function () { + expect(this.inputSetter.config).toBe(this.config.InputSetter); + }); + + it('should set .eventWrapper', function () { + expect(this.inputSetter.eventWrapper).toEqual({}); + }); + + it('should call .addEvents', function () { + expect(this.inputSetter.addEvents).toHaveBeenCalled(); + }); + + describe('if config.InputSetter is not set', function () { + beforeEach(function () { + this.config = { InputSetter: undefined }; + this.hook = { config: this.config }; + + InputSetter.init.call(this.inputSetter, this.hook); + }); + + it('should set .config to an empty object', function () { + expect(this.inputSetter.config).toEqual({}); + }); + + it('should set hook.config to an empty object', function () { + expect(this.hook.config.InputSetter).toEqual({}); + }); + }) + }); + + describe('addEvents', function () { + beforeEach(function () { + this.hook = { list: { list: jasmine.createSpyObj('list', ['addEventListener']) } }; + this.inputSetter = { eventWrapper: {}, hook: this.hook, setInputs: () => {} }; + + InputSetter.addEvents.call(this.inputSetter); + }); + + it('should set .eventWrapper.setInputs', function () { + expect(this.inputSetter.eventWrapper.setInputs).toEqual(jasmine.any(Function)); + }); + + it('should call .addEventListener', function () { + expect(this.hook.list.list.addEventListener) + .toHaveBeenCalledWith('click.dl', this.inputSetter.eventWrapper.setInputs); + }); + }); + + describe('removeEvents', function () { + beforeEach(function () { + this.hook = { list: { list: jasmine.createSpyObj('list', ['removeEventListener']) } }; + this.eventWrapper = jasmine.createSpyObj('eventWrapper', ['setInputs']); + this.inputSetter = { eventWrapper: this.eventWrapper, hook: this.hook }; + + InputSetter.removeEvents.call(this.inputSetter); + }); + + it('should call .removeEventListener', function () { + expect(this.hook.list.list.removeEventListener) + .toHaveBeenCalledWith('click.dl', this.eventWrapper.setInputs); + }); + }); + + describe('setInputs', function () { + beforeEach(function () { + this.event = { detail: { selected: {} } }; + this.config = [0, 1]; + this.inputSetter = { config: this.config, setInput: () => {} }; + + spyOn(this.inputSetter, 'setInput'); + + InputSetter.setInputs.call(this.inputSetter, this.event); + }); + + it('should call .setInput for each config element', function () { + const allArgs = this.inputSetter.setInput.calls.allArgs(); + + expect(allArgs.length).toEqual(2); + + allArgs.forEach((args, i) => { + expect(args[0]).toBe(this.config[i]); + expect(args[1]).toBe(this.event.detail.selected); + }); + }); + + describe('if config isnt an array', function () { + beforeEach(function () { + this.inputSetter = { config: {}, setInput: () => {} }; + + InputSetter.setInputs.call(this.inputSetter, this.event); + }); + + it('should set .config to an array with .config as the first element', function () { + expect(this.inputSetter.config).toEqual([{}]); + }); + }); + }); + + describe('setInput', function () { + beforeEach(function () { + this.selectedItem = { getAttribute: () => {} }; + this.input = { value: 'oldValue', tagName: 'INPUT', hasAttribute: () => {} }; + this.config = { valueAttribute: {}, input: this.input }; + this.inputSetter = { hook: { trigger: {} } }; + this.newValue = 'newValue'; + + spyOn(this.selectedItem, 'getAttribute').and.returnValue(this.newValue); + spyOn(this.input, 'hasAttribute').and.returnValue(false); + + InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem); + }); + + it('should call .getAttribute', function () { + expect(this.selectedItem.getAttribute).toHaveBeenCalledWith(this.config.valueAttribute); + }); + + it('should call .hasAttribute', function () { + expect(this.input.hasAttribute).toHaveBeenCalledWith(undefined); + }); + + it('should set the value of the input', function () { + expect(this.input.value).toBe(this.newValue); + }); + + describe('if no config.input is provided', function () { + beforeEach(function () { + this.config = { valueAttribute: {} }; + this.trigger = { value: 'oldValue', tagName: 'INPUT', hasAttribute: () => {} }; + this.inputSetter = { hook: { trigger: this.trigger } }; + + InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem); + }); + + it('should set the value of the hook.trigger', function () { + expect(this.trigger.value).toBe(this.newValue); + }); + }); + + describe('if the input tag is not INPUT', function () { + beforeEach(function () { + this.input = { textContent: 'oldValue', tagName: 'SPAN', hasAttribute: () => {} }; + this.config = { valueAttribute: {}, input: this.input }; + + InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem); + }); + + it('should set the textContent of the input', function () { + expect(this.input.textContent).toBe(this.newValue); + }); + }); + + describe('if there is an inputAttribute', function () { + beforeEach(function () { + this.selectedItem = { getAttribute: () => {} }; + this.input = { id: 'oldValue', hasAttribute: () => {}, setAttribute: () => {} }; + this.inputSetter = { hook: { trigger: {} } }; + this.newValue = 'newValue'; + this.inputAttribute = 'id'; + this.config = { + valueAttribute: {}, + input: this.input, + inputAttribute: this.inputAttribute, + }; + + spyOn(this.selectedItem, 'getAttribute').and.returnValue(this.newValue); + spyOn(this.input, 'hasAttribute').and.returnValue(true); + spyOn(this.input, 'setAttribute'); + + InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem); + }); + + it('should call setAttribute', function () { + expect(this.input.setAttribute).toHaveBeenCalledWith(this.inputAttribute, this.newValue); + }); + + it('should not set the value or textContent of the input', function () { + expect(this.input.value).not.toBe('newValue'); + expect(this.input.textContent).not.toBe('newValue'); + }); + }); + }); + + describe('destroy', function () { + beforeEach(function () { + this.inputSetter = jasmine.createSpyObj('inputSetter', ['removeEvents']); + + InputSetter.destroy.call(this.inputSetter); + }); + + it('should call .removeEvents', function () { + expect(this.inputSetter.removeEvents).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 13840b42bd6..6348d97b0a5 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -19,6 +19,11 @@ describe('Actions Component', () => { name: 'foo', play_path: '#', }, + { + name: 'foo bar', + play_path: 'url', + playable: false, + }, ]; spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); @@ -49,4 +54,14 @@ describe('Actions Component', () => { expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path); }); + + it('should render a disabled action when it\'s not playable', () => { + expect( + component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), + ).toEqual('disabled'); + + expect( + component.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'), + ).toEqual(true); + }); }); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index 9bcf617fcd8..9762688af1a 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import '~/flash'; import EnvironmentsComponent from '~/environments/components/environment'; -import { environment } from './mock_data'; +import { environment, folder } from './mock_data'; describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); @@ -83,9 +83,10 @@ describe('Environment', () => { it('should render a table with environments', (done) => { setTimeout(() => { + expect(component.$el.querySelectorAll('table')).toBeDefined(); expect( - component.$el.querySelectorAll('table tbody tr').length, - ).toEqual(1); + component.$el.querySelector('.environment-name').textContent.trim(), + ).toEqual(environment.name); done(); }, 0); }); @@ -179,4 +180,101 @@ describe('Environment', () => { }, 0); }); }); + + describe('expandable folders', () => { + const environmentsResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + environments: [folder], + stopped_count: 0, + available_count: 1, + }), { + status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsResponseInterceptor, + ); + }); + + it('should open a closed folder', (done) => { + setTimeout(() => { + component.$el.querySelector('.folder-name').click(); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'), + ).toContain('display: none'); + expect( + component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'), + ).not.toContain('display: none'); + done(); + }); + }); + }); + + it('should close an opened folder', (done) => { + setTimeout(() => { + // open folder + component.$el.querySelector('.folder-name').click(); + + Vue.nextTick(() => { + // close folder + component.$el.querySelector('.folder-name').click(); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'), + ).toContain('display: none'); + expect( + component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'), + ).not.toContain('display: none'); + done(); + }); + }); + }); + }); + + it('should show children environments and a button to show all environments', (done) => { + setTimeout(() => { + // open folder + component.$el.querySelector('.folder-name').click(); + + Vue.nextTick(() => { + const folderInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + environments: [environment], + }), { status: 200 })); + }; + + Vue.http.interceptors.push(folderInterceptor); + + // wait for next async request + setTimeout(() => { + expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); + expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all'); + + Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor); + done(); + }); + }); + }); + }); + }); }); diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js index 115d84b50f5..f617c4bdffe 100644 --- a/spec/javascripts/environments/environments_store_spec.js +++ b/spec/javascripts/environments/environments_store_spec.js @@ -1,38 +1,106 @@ import Store from '~/environments/stores/environments_store'; import { environmentsList, serverData } from './mock_data'; -(() => { - describe('Store', () => { - let store; +describe('Store', () => { + let store; - beforeEach(() => { - store = new Store(); - }); + beforeEach(() => { + store = new Store(); + }); - it('should start with a blank state', () => { - expect(store.state.environments.length).toEqual(0); - expect(store.state.stoppedCounter).toEqual(0); - expect(store.state.availableCounter).toEqual(0); - expect(store.state.paginationInformation).toEqual({}); - }); + it('should start with a blank state', () => { + expect(store.state.environments.length).toEqual(0); + expect(store.state.stoppedCounter).toEqual(0); + expect(store.state.availableCounter).toEqual(0); + expect(store.state.paginationInformation).toEqual({}); + }); + it('should store environments', () => { + store.storeEnvironments(serverData); + expect(store.state.environments.length).toEqual(serverData.length); + expect(store.state.environments[0]).toEqual(environmentsList[0]); + }); + + it('should store available count', () => { + store.storeAvailableCount(2); + expect(store.state.availableCounter).toEqual(2); + }); + + it('should store stopped count', () => { + store.storeStoppedCount(2); + expect(store.state.stoppedCounter).toEqual(2); + }); + + describe('store environments', () => { it('should store environments', () => { store.storeEnvironments(serverData); expect(store.state.environments.length).toEqual(serverData.length); - expect(store.state.environments[0]).toEqual(environmentsList[0]); }); - it('should store available count', () => { - store.storeAvailableCount(2); - expect(store.state.availableCounter).toEqual(2); + it('should add folder keys when environment is a folder', () => { + const environment = { + name: 'bar', + size: 3, + id: 2, + }; + + store.storeEnvironments([environment]); + expect(store.state.environments[0].isFolder).toEqual(true); + expect(store.state.environments[0].folderName).toEqual('bar'); + }); + + it('should extract content of `latest` key when provided', () => { + const environment = { + name: 'bar', + size: 3, + id: 2, + latest: { + last_deployment: {}, + isStoppable: true, + }, + }; + + store.storeEnvironments([environment]); + expect(store.state.environments[0].last_deployment).toEqual({}); + expect(store.state.environments[0].isStoppable).toEqual(true); }); - it('should store stopped count', () => { - store.storeStoppedCount(2); - expect(store.state.stoppedCounter).toEqual(2); + it('should store latest.name when the environment is not a folder', () => { + store.storeEnvironments(serverData); + expect(store.state.environments[0].name).toEqual(serverData[0].latest.name); }); - it('should store pagination information', () => { + it('should store root level name when environment is a folder', () => { + store.storeEnvironments(serverData); + expect(store.state.environments[1].folderName).toEqual(serverData[1].name); + }); + }); + + describe('toggleFolder', () => { + it('should toggle folder', () => { + store.storeEnvironments(serverData); + + store.toggleFolder(store.state.environments[1]); + expect(store.state.environments[1].isOpen).toEqual(true); + + store.toggleFolder(store.state.environments[1]); + expect(store.state.environments[1].isOpen).toEqual(false); + }); + }); + + describe('setfolderContent', () => { + it('should store folder content', () => { + store.storeEnvironments(serverData); + + store.setfolderContent(store.state.environments[1], serverData); + + expect(store.state.environments[1].children.length).toEqual(serverData.length); + expect(store.state.environments[1].children[0].isChildren).toEqual(true); + }); + }); + + describe('store pagination', () => { + it('should store normalized and integer pagination information', () => { const pagination = { 'X-nExt-pAge': '2', 'X-page': '1', @@ -55,4 +123,4 @@ import { environmentsList, serverData } from './mock_data'; expect(store.state.paginationInformation).toEqual(expectedResult); }); }); -})(); +}); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 43a217a67f5..72f3db29a66 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -47,9 +47,10 @@ describe('Environments Folder View', () => { it('should render a table with environments', (done) => { setTimeout(() => { + expect(component.$el.querySelectorAll('table')).toBeDefined(); expect( - component.$el.querySelectorAll('table tbody tr').length, - ).toEqual(2); + component.$el.querySelector('.environment-name').textContent.trim(), + ).toEqual(environmentsList[0].name); done(); }, 0); }); diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js index 30861481cc5..15e11aa686b 100644 --- a/spec/javascripts/environments/mock_data.js +++ b/spec/javascripts/environments/mock_data.js @@ -84,3 +84,19 @@ export const environment = { updated_at: '2017-01-31T10:53:46.894Z', }, }; + +export const folder = { + folderName: 'build', + size: 5, + id: 12, + name: 'build/update-README', + state: 'available', + external_url: null, + environment_type: 'build', + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', +}; 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 new file mode 100644 index 00000000000..2722882375f --- /dev/null +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -0,0 +1,166 @@ +import Vue from 'vue'; +import eventHub from '~/filtered_search/event_hub'; +import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content'; + +const createComponent = (propsData) => { + const Component = Vue.extend(RecentSearchesDropdownContent); + + return new Component({ + el: document.createElement('div'), + propsData, + }); +}; + +// Remove all the newlines and whitespace from the formatted markup +const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); + +describe('RecentSearchesDropdownContent', () => { + const propsDataWithoutItems = { + items: [], + }; + const propsDataWithItems = { + items: [ + 'foo', + 'author:@root label:~foo bar', + ], + }; + + let vm; + afterEach(() => { + if (vm) { + vm.$destroy(); + } + }); + + describe('with no items', () => { + let el; + + beforeEach(() => { + vm = createComponent(propsDataWithoutItems); + el = vm.$el; + }); + + it('should render empty state', () => { + expect(el.querySelector('.dropdown-info-note')).toBeDefined(); + + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + expect(items.length).toEqual(propsDataWithoutItems.items.length); + }); + }); + + describe('with items', () => { + let el; + + beforeEach(() => { + vm = createComponent(propsDataWithItems); + el = vm.$el; + }); + + it('should render clear recent searches button', () => { + expect(el.querySelector('.filtered-search-history-clear-button')).toBeDefined(); + }); + + it('should render recent search items', () => { + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + expect(items.length).toEqual(propsDataWithItems.items.length); + + expect(trimMarkupWhitespace(items[0].querySelector('.filtered-search-history-dropdown-search-token').textContent)).toEqual('foo'); + + const item1Tokens = items[1].querySelectorAll('.filtered-search-history-dropdown-token'); + expect(item1Tokens.length).toEqual(2); + expect(item1Tokens[0].querySelector('.name').textContent).toEqual('author:'); + expect(item1Tokens[0].querySelector('.value').textContent).toEqual('@root'); + expect(item1Tokens[1].querySelector('.name').textContent).toEqual('label:'); + expect(item1Tokens[1].querySelector('.value').textContent).toEqual('~foo'); + expect(trimMarkupWhitespace(items[1].querySelector('.filtered-search-history-dropdown-search-token').textContent)).toEqual('bar'); + }); + }); + + describe('computed', () => { + describe('processedItems', () => { + it('with items', () => { + vm = createComponent(propsDataWithItems); + const processedItems = vm.processedItems; + + expect(processedItems.length).toEqual(2); + + expect(processedItems[0].text).toEqual(propsDataWithItems.items[0]); + expect(processedItems[0].tokens).toEqual([]); + expect(processedItems[0].searchToken).toEqual('foo'); + + expect(processedItems[1].text).toEqual(propsDataWithItems.items[1]); + expect(processedItems[1].tokens.length).toEqual(2); + expect(processedItems[1].tokens[0].prefix).toEqual('author:'); + expect(processedItems[1].tokens[0].suffix).toEqual('@root'); + expect(processedItems[1].tokens[1].prefix).toEqual('label:'); + expect(processedItems[1].tokens[1].suffix).toEqual('~foo'); + expect(processedItems[1].searchToken).toEqual('bar'); + }); + + it('with no items', () => { + vm = createComponent(propsDataWithoutItems); + const processedItems = vm.processedItems; + + expect(processedItems.length).toEqual(0); + }); + }); + + describe('hasItems', () => { + it('with items', () => { + vm = createComponent(propsDataWithItems); + const hasItems = vm.hasItems; + expect(hasItems).toEqual(true); + }); + + it('with no items', () => { + vm = createComponent(propsDataWithoutItems); + const hasItems = vm.hasItems; + expect(hasItems).toEqual(false); + }); + }); + }); + + describe('methods', () => { + describe('onItemActivated', () => { + let onRecentSearchesItemSelectedSpy; + + beforeEach(() => { + onRecentSearchesItemSelectedSpy = jasmine.createSpy('spy'); + eventHub.$on('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); + + vm = createComponent(propsDataWithItems); + }); + + afterEach(() => { + eventHub.$off('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); + }); + + it('emits event', () => { + expect(onRecentSearchesItemSelectedSpy).not.toHaveBeenCalled(); + vm.onItemActivated('something'); + expect(onRecentSearchesItemSelectedSpy).toHaveBeenCalledWith('something'); + }); + }); + + describe('onRequestClearRecentSearches', () => { + let onRequestClearRecentSearchesSpy; + + beforeEach(() => { + onRequestClearRecentSearchesSpy = jasmine.createSpy('spy'); + eventHub.$on('requestClearRecentSearches', onRequestClearRecentSearchesSpy); + + vm = createComponent(propsDataWithItems); + }); + + afterEach(() => { + eventHub.$off('requestClearRecentSearches', onRequestClearRecentSearchesSpy); + }); + + it('emits event', () => { + expect(onRequestClearRecentSearchesSpy).not.toHaveBeenCalled(); + vm.onRequestClearRecentSearches({ stopPropagation: () => {} }); + expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index c16f77c53a2..2b1fe5e3eef 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -33,7 +33,7 @@ require('~/filtered_search/dropdown_user'); }); }); - describe('config droplabAjaxFilter\'s endpoint', () => { + describe('config AjaxFilter\'s endpoint', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); @@ -45,13 +45,13 @@ require('~/filtered_search/dropdown_user'); }; const dropdown = new gl.DropdownUser(); - expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); }); it('should return endpoint when relative_url_root is undefined', () => { const dropdown = new gl.DropdownUser(); - expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); }); it('should return endpoint with relative url when available', () => { @@ -60,7 +60,7 @@ require('~/filtered_search/dropdown_user'); }; const dropdown = new gl.DropdownUser(); - expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); }); afterEach(() => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 5f7c05e9014..97af681429b 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -29,7 +29,7 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper beforeEach(() => { setFixtures(` - <div class="filtered-search-input-container"> + <div class="filtered-search-box"> <form> <ul class="tokens-container list-unstyled"> ${FilteredSearchSpecHelper.createInputHTML(placeholder)} @@ -264,12 +264,12 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper describe('toggleInputContainerFocus', () => { it('toggles on focus', () => { input.focus(); - expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true); + expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); }); it('toggles on blur', () => { input.blur(); - expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false); + expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); }); }); }); diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js new file mode 100644 index 00000000000..2a58fb3a7df --- /dev/null +++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js @@ -0,0 +1,56 @@ +import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; + +describe('RecentSearchesService', () => { + let service; + + beforeEach(() => { + service = new RecentSearchesService(); + window.localStorage.removeItem(service.localStorageKey); + }); + + describe('fetch', () => { + it('should default to empty array', (done) => { + const fetchItemsPromise = service.fetch(); + + fetchItemsPromise + .then((items) => { + expect(items).toEqual([]); + done(); + }) + .catch((err) => { + done.fail('Shouldn\'t reject with empty localStorage key', err); + }); + }); + + it('should reject when unable to parse', (done) => { + window.localStorage.setItem(service.localStorageKey, 'fail'); + const fetchItemsPromise = service.fetch(); + + fetchItemsPromise + .catch(() => { + done(); + }); + }); + + it('should return items from localStorage', (done) => { + window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]'); + const fetchItemsPromise = service.fetch(); + + fetchItemsPromise + .then((items) => { + expect(items).toEqual(['foo', 'bar']); + done(); + }); + }); + }); + + describe('setRecentSearches', () => { + it('should save things in localStorage', () => { + const items = ['foo', 'bar']; + service.save(items); + const newLocalStorageValue = + window.localStorage.getItem(service.localStorageKey); + expect(JSON.parse(newLocalStorageValue)).toEqual(items); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js b/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js new file mode 100644 index 00000000000..1eebc6f2367 --- /dev/null +++ b/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js @@ -0,0 +1,59 @@ +import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; + +describe('RecentSearchesStore', () => { + let store; + + beforeEach(() => { + store = new RecentSearchesStore(); + }); + + describe('addRecentSearch', () => { + it('should add to the front of the list', () => { + store.addRecentSearch('foo'); + store.addRecentSearch('bar'); + + expect(store.state.recentSearches).toEqual(['bar', 'foo']); + }); + + it('should deduplicate', () => { + store.addRecentSearch('foo'); + store.addRecentSearch('bar'); + store.addRecentSearch('foo'); + + expect(store.state.recentSearches).toEqual(['foo', 'bar']); + }); + + it('only keeps track of 5 items', () => { + store.addRecentSearch('1'); + store.addRecentSearch('2'); + store.addRecentSearch('3'); + store.addRecentSearch('4'); + store.addRecentSearch('5'); + store.addRecentSearch('6'); + store.addRecentSearch('7'); + + expect(store.state.recentSearches).toEqual(['7', '6', '5', '4', '3']); + }); + }); + + describe('setRecentSearches', () => { + it('should override list', () => { + store.setRecentSearches([ + 'foo', + 'bar', + ]); + store.setRecentSearches([ + 'baz', + 'qux', + ]); + + expect(store.state.recentSearches).toEqual(['baz', 'qux']); + }); + + it('only keeps track of 5 items', () => { + store.setRecentSearches(['1', '2', '3', '4', '5', '6', '7']); + + expect(store.state.recentSearches).toEqual(['1', '2', '3', '4', '5']); + }); + }); +}); diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml index 483063fb889..e2dd9519898 100644 --- a/spec/javascripts/fixtures/environments/metrics.html.haml +++ b/spec/javascripts/fixtures/environments/metrics.html.haml @@ -1,12 +1,62 @@ -%div +.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' } .top-area .row .col-sm-6 %h3.page-title Metrics for environment - .row - .col-sm-12 - %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } - .row - .col-sm-12 - %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
\ No newline at end of file + .prometheus-state + .js-getting-started.hidden + .row + .col-md-4.col-md-offset-4.state-svg + %svg + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Get started with performance monitoring + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. Learn more about performance monitoring + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + %a.btn.btn-success + Configure Prometheus + .js-loading.hidden + .row + .col-md-4.col-md-offset-4.state-svg + %svg + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Waiting for performance data + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available. + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + %a.btn.btn-success + View documentation + .js-unable-to-connect.hidden + .row + .col-md-4.col-md-offset-4.state-svg + %svg + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Unable to connect to Prometheus server + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Ensure connectivity is available from the GitLab server to the Prometheus server + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + %a.btn.btn-success + View documentation + .prometheus-graphs + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index fddeaaf504d..47d904b865b 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -7,6 +7,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) } let(:pipeline) do create( :ci_pipeline, @@ -32,6 +33,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end + it 'merge_requests/merged_merge_request.html.raw' do |example| + allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true) + render_merge_request(example.description, merged_merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/fixtures/pdf_viewer.html.haml b/spec/javascripts/fixtures/pdf_viewer.html.haml new file mode 100644 index 00000000000..2e57beae54b --- /dev/null +++ b/spec/javascripts/fixtures/pdf_viewer.html.haml @@ -0,0 +1 @@ +.file-content#js-pdf-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/sketch_viewer.html.haml b/spec/javascripts/fixtures/sketch_viewer.html.haml new file mode 100644 index 00000000000..f01bd00925a --- /dev/null +++ b/spec/javascripts/fixtures/sketch_viewer.html.haml @@ -0,0 +1,2 @@ +.file-content#js-sketch-viewer{ data: { endpoint: '/test_sketch_file.sketch' } } + .js-loading-icon diff --git a/spec/javascripts/issue_show/issue_title_spec.js b/spec/javascripts/issue_show/issue_title_spec.js new file mode 100644 index 00000000000..806d728a874 --- /dev/null +++ b/spec/javascripts/issue_show/issue_title_spec.js @@ -0,0 +1,22 @@ +import Vue from 'vue'; +import issueTitle from '~/issue_show/issue_title'; + +describe('Issue Title', () => { + let IssueTitleComponent; + + beforeEach(() => { + IssueTitleComponent = Vue.extend(issueTitle); + }); + + it('should render a title', () => { + const component = new IssueTitleComponent({ + propsData: { + initialTitle: 'wow', + endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title', + }, + }).$mount(); + + expect(component.$el.classList).toContain('title'); + expect(component.$el.innerHTML).toContain('wow'); + }); +}); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index aabc8bea12f..9a2570ef7e9 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,18 +1,17 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ +/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ import Issue from '~/issue'; require('~/lib/utils/text_utility'); describe('Issue', function() { - var INVALID_URL = 'http://goesnowhere.nothing/whereami'; - var $boxClosed, $boxOpen, $btnClose, $btnReopen; + let $boxClosed, $boxOpen, $btnClose, $btnReopen; preloadFixtures('issues/closed-issue.html.raw'); preloadFixtures('issues/issue-with-task-list.html.raw'); preloadFixtures('issues/open-issue.html.raw'); function expectErrorMessage() { - var $flashMessage = $('div.flash-alert'); + const $flashMessage = $('div.flash-alert'); expect($flashMessage).toExist(); expect($flashMessage).toBeVisible(); expect($flashMessage).toHaveText('Unable to update this issue at this time.'); @@ -26,10 +25,28 @@ describe('Issue', function() { expectVisibility($btnReopen, !isIssueOpen); } - function expectPendingRequest(req, $triggeredButton) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe($triggeredButton.attr('href')); - expect($triggeredButton).toHaveProp('disabled', true); + function expectNewBranchButtonState(isPending, canCreate) { + if (Issue.$btnNewBranch.length === 0) { + return; + } + + const $available = Issue.$btnNewBranch.find('.available'); + expect($available).toHaveText('New branch'); + + if (!isPending && canCreate) { + expect($available).toBeVisible(); + } else { + expect($available).toBeHidden(); + } + + const $unavailable = Issue.$btnNewBranch.find('.unavailable'); + expect($unavailable).toHaveText('New branch unavailable'); + + if (!isPending && !canCreate) { + expect($unavailable).toBeVisible(); + } else { + expect($unavailable).toBeHidden(); + } } function expectVisibility($element, shouldBeVisible) { @@ -81,100 +98,107 @@ describe('Issue', function() { }); }); - describe('close issue', function() { - beforeEach(function() { - loadFixtures('issues/open-issue.html.raw'); - findElements(); - this.issue = new Issue(); - - expectIssueState(true); - }); + [true, false].forEach((isIssueInitiallyOpen) => { + describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() { + const action = isIssueInitiallyOpen ? 'close' : 'reopen'; + + function ajaxSpy(req) { + if (req.url === this.$triggeredButton.attr('href')) { + expect(req.type).toBe('PUT'); + expect(this.$triggeredButton).toHaveProp('disabled', true); + expectNewBranchButtonState(true, false); + return this.issueStateDeferred; + } else if (req.url === Issue.$btnNewBranch.data('path')) { + expect(req.type).toBe('get'); + expectNewBranchButtonState(true, false); + return this.canCreateBranchDeferred; + } + + expect(req.url).toBe('unexpected'); + return null; + } + + beforeEach(function() { + if (isIssueInitiallyOpen) { + loadFixtures('issues/open-issue.html.raw'); + } else { + loadFixtures('issues/closed-issue.html.raw'); + } + + findElements(); + this.issue = new Issue(); + expectIssueState(isIssueInitiallyOpen); + this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen; + + this.$projectIssuesCounter = $('.issue_counter'); + this.$projectIssuesCounter.text('1,001'); + + this.issueStateDeferred = new jQuery.Deferred(); + this.canCreateBranchDeferred = new jQuery.Deferred(); + + spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this)); + }); - it('closes an issue', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expectPendingRequest(req, $btnClose); - req.success({ + it(`${action}s the issue`, function() { + this.$triggeredButton.trigger('click'); + this.issueStateDeferred.resolve({ id: 34 }); - }); - - $btnClose.trigger('click'); + this.canCreateBranchDeferred.resolve({ + can_create_branch: !isIssueInitiallyOpen + }); - expectIssueState(false); - expect($btnClose).toHaveProp('disabled', false); - expect($('.issue_counter')).toHaveText(0); - }); + expectIssueState(!isIssueInitiallyOpen); + expect(this.$triggeredButton).toHaveProp('disabled', false); + expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); + expectNewBranchButtonState(false, !isIssueInitiallyOpen); + }); - it('fails to close an issue with success:false', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expectPendingRequest(req, $btnClose); - req.success({ + it(`fails to ${action} the issue if saved:false`, function() { + this.$triggeredButton.trigger('click'); + this.issueStateDeferred.resolve({ saved: false }); - }); - - $btnClose.attr('href', INVALID_URL); - $btnClose.trigger('click'); - - expectIssueState(true); - expect($btnClose).toHaveProp('disabled', false); - expectErrorMessage(); - expect($('.issue_counter')).toHaveText(1); - }); + this.canCreateBranchDeferred.resolve({ + can_create_branch: isIssueInitiallyOpen + }); - it('fails to closes an issue with HTTP error', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expectPendingRequest(req, $btnClose); - req.error(); + expectIssueState(isIssueInitiallyOpen); + expect(this.$triggeredButton).toHaveProp('disabled', false); + expectErrorMessage(); + expect(this.$projectIssuesCounter.text()).toBe('1,001'); + expectNewBranchButtonState(false, isIssueInitiallyOpen); }); - $btnClose.attr('href', INVALID_URL); - $btnClose.trigger('click'); - - expectIssueState(true); - expect($btnClose).toHaveProp('disabled', true); - expectErrorMessage(); - expect($('.issue_counter')).toHaveText(1); - }); - - it('updates counter', () => { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expectPendingRequest(req, $btnClose); - req.success({ - id: 34 + it(`fails to ${action} the issue if HTTP error occurs`, function() { + this.$triggeredButton.trigger('click'); + this.issueStateDeferred.reject(); + this.canCreateBranchDeferred.resolve({ + can_create_branch: isIssueInitiallyOpen }); - }); - expect($('.issue_counter')).toHaveText(1); - $('.issue_counter').text('1,001'); - expect($('.issue_counter').text()).toEqual('1,001'); - $btnClose.trigger('click'); - expect($('.issue_counter').text()).toEqual('1,000'); - }); - }); + expectIssueState(isIssueInitiallyOpen); + expect(this.$triggeredButton).toHaveProp('disabled', true); + expectErrorMessage(); + expect(this.$projectIssuesCounter.text()).toBe('1,001'); + expectNewBranchButtonState(false, isIssueInitiallyOpen); + }); - describe('reopen issue', function() { - beforeEach(function() { - loadFixtures('issues/closed-issue.html.raw'); - findElements(); - this.issue = new Issue(); + it('disables the new branch button if Ajax call fails', function() { + this.$triggeredButton.trigger('click'); + this.issueStateDeferred.reject(); + this.canCreateBranchDeferred.reject(); - expectIssueState(false); - }); - - it('reopens an issue', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expectPendingRequest(req, $btnReopen); - req.success({ - id: 34 - }); + expectNewBranchButtonState(false, false); }); - $btnReopen.trigger('click'); + it('does not trigger Ajax call if new branch button is missing', function() { + Issue.$btnNewBranch = $(); + this.canCreateBranchDeferred = null; - expectIssueState(true); - expect($btnReopen).toHaveProp('disabled', false); - expect($('.issue_counter')).toHaveText(1); + this.$triggeredButton.trigger('click'); + this.issueStateDeferred.reject(); + }); }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 5a93d479c1f..03f3c206f44 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -310,5 +310,56 @@ require('~/lib/utils/common_utils'); }); }, 10000); }); + + describe('gl.utils.setFavicon', () => { + it('should set page favicon to provided favicon', () => { + const faviconName = 'custom_favicon'; + const fakeLink = { + setAttribute() {}, + }; + + spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); + spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { + expect(attr).toEqual('href'); + expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true); + }); + gl.utils.setFavicon(faviconName); + }); + }); + + describe('gl.utils.resetFavicon', () => { + it('should reset page favicon to tanuki', () => { + const fakeLink = { + setAttribute() {}, + }; + + spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); + spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { + expect(attr).toEqual('href'); + expect(val).toMatch(/favicon/); + }); + gl.utils.resetFavicon(); + }); + }); + + describe('gl.utils.setCiStatusFavicon', () => { + it('should set page favicon to CI status favicon based on provided status', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`; + const FAVICON_PATH = 'ci_favicons/'; + const FAVICON = 'icon_status_success'; + const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); + const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ icon: FAVICON }); + expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON); + options.success(); + expect(spyResetFavicon).toHaveBeenCalled(); + options.error(); + expect(spyResetFavicon).toHaveBeenCalled(); + }); + + gl.utils.setCiStatusFavicon(BUILD_URL); + }); + }); }); })(); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js new file mode 100644 index 00000000000..5fde8be9123 --- /dev/null +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -0,0 +1,41 @@ +import { formatRelevantDigits } from '~/lib/utils/number_utils'; + +describe('Number Utils', () => { + describe('formatRelevantDigits', () => { + it('returns an empty string when the number is NaN', () => { + expect(formatRelevantDigits('fail')).toBe(''); + }); + + it('returns 4 decimals when there is 4 plus digits to the left', () => { + const formattedNumber = formatRelevantDigits('1000.1234567'); + const rightFromDecimal = formattedNumber.split('.')[1]; + const leftFromDecimal = formattedNumber.split('.')[0]; + expect(rightFromDecimal.length).toBe(4); + expect(leftFromDecimal.length).toBe(4); + }); + + it('returns 3 decimals when there is 1 digit to the left', () => { + const formattedNumber = formatRelevantDigits('0.1234567'); + const rightFromDecimal = formattedNumber.split('.')[1]; + const leftFromDecimal = formattedNumber.split('.')[0]; + expect(rightFromDecimal.length).toBe(3); + expect(leftFromDecimal.length).toBe(1); + }); + + it('returns 2 decimals when there is 2 digits to the left', () => { + const formattedNumber = formatRelevantDigits('10.1234567'); + const rightFromDecimal = formattedNumber.split('.')[1]; + const leftFromDecimal = formattedNumber.split('.')[0]; + expect(rightFromDecimal.length).toBe(2); + expect(leftFromDecimal.length).toBe(2); + }); + + it('returns 1 decimal when there is 3 digits to the left', () => { + const formattedNumber = formatRelevantDigits('100.1234567'); + const rightFromDecimal = formattedNumber.split('.')[1]; + const leftFromDecimal = formattedNumber.split('.')[0]; + expect(rightFromDecimal.length).toBe(1); + expect(leftFromDecimal.length).toBe(3); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index e3429c2a1cb..918b6d32c43 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -4,6 +4,20 @@ import Poll from '~/lib/utils/poll'; Vue.use(VueResource); +const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { + const timer = () => { + setTimeout(() => { + if (service.fetch.calls.count() === waitForCount) { + successCallback(); + } else { + timer(); + } + }, 5); + }; + + timer(); +}; + class ServiceMock { constructor(endpoint) { this.service = Vue.resource(endpoint); @@ -16,6 +30,7 @@ class ServiceMock { describe('Poll', () => { let callbacks; + let service; beforeEach(() => { callbacks = { @@ -23,8 +38,11 @@ describe('Poll', () => { error: () => {}, }; + service = new ServiceMock('endpoint'); + spyOn(callbacks, 'success'); spyOn(callbacks, 'error'); + spyOn(service, 'fetch').and.callThrough(); }); it('calls the success callback when no header for interval is provided', (done) => { @@ -35,19 +53,20 @@ describe('Poll', () => { Vue.http.interceptors.push(successInterceptor); new Poll({ - resource: new ServiceMock('endpoint'), + resource: service, method: 'fetch', successCallback: callbacks.success, errorCallback: callbacks.error, }).makeRequest(); - setTimeout(() => { + waitForAllCallsToFinish(service, 1, () => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); + + Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor); + done(); }, 0); - - Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor); }); it('calls the error callback whe the http request returns an error', (done) => { @@ -58,19 +77,19 @@ describe('Poll', () => { Vue.http.interceptors.push(errorInterceptor); new Poll({ - resource: new ServiceMock('endpoint'), + resource: service, method: 'fetch', successCallback: callbacks.success, errorCallback: callbacks.error, }).makeRequest(); - setTimeout(() => { + waitForAllCallsToFinish(service, 1, () => { expect(callbacks.success).not.toHaveBeenCalled(); expect(callbacks.error).toHaveBeenCalled(); - done(); - }, 0); + Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor); - Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor); + done(); + }); }); it('should call the success callback when the interval header is -1', (done) => { @@ -81,7 +100,7 @@ describe('Poll', () => { Vue.http.interceptors.push(intervalInterceptor); new Poll({ - resource: new ServiceMock('endpoint'), + resource: service, method: 'fetch', successCallback: callbacks.success, errorCallback: callbacks.error, @@ -90,10 +109,11 @@ describe('Poll', () => { setTimeout(() => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); + + Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor); + done(); }, 0); - - Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor); }); it('starts polling when http status is 200 and interval header is provided', (done) => { @@ -103,26 +123,28 @@ describe('Poll', () => { Vue.http.interceptors.push(pollInterceptor); - const service = new ServiceMock('endpoint'); - spyOn(service, 'fetch').and.callThrough(); - - new Poll({ + const Polling = new Poll({ resource: service, method: 'fetch', data: { page: 1 }, successCallback: callbacks.success, errorCallback: callbacks.error, - }).makeRequest(); + }); + + Polling.makeRequest(); + + waitForAllCallsToFinish(service, 2, () => { + Polling.stop(); - setTimeout(() => { expect(service.fetch.calls.count()).toEqual(2); expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - done(); - }, 5); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + + done(); + }); }); describe('stop', () => { @@ -133,9 +155,6 @@ describe('Poll', () => { Vue.http.interceptors.push(pollInterceptor); - const service = new ServiceMock('endpoint'); - spyOn(service, 'fetch').and.callThrough(); - const Polling = new Poll({ resource: service, method: 'fetch', @@ -150,14 +169,15 @@ describe('Poll', () => { Polling.makeRequest(); - setTimeout(() => { + waitForAllCallsToFinish(service, 1, () => { expect(service.fetch.calls.count()).toEqual(1); expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); expect(Polling.stop).toHaveBeenCalled(); - done(); - }, 100); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + + done(); + }); }); }); @@ -169,10 +189,6 @@ describe('Poll', () => { Vue.http.interceptors.push(pollInterceptor); - const service = new ServiceMock('endpoint'); - - spyOn(service, 'fetch').and.callThrough(); - const Polling = new Poll({ resource: service, method: 'fetch', @@ -187,17 +203,22 @@ describe('Poll', () => { }); spyOn(Polling, 'stop').and.callThrough(); + spyOn(Polling, 'restart').and.callThrough(); Polling.makeRequest(); - setTimeout(() => { + waitForAllCallsToFinish(service, 2, () => { + Polling.stop(); + expect(service.fetch.calls.count()).toEqual(2); expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); expect(Polling.stop).toHaveBeenCalled(); - done(); - }, 10); + expect(Polling.restart).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); + + done(); + }); }); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 4200e943121..daef9b93fa5 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,110 +1,108 @@ require('~/lib/utils/text_utility'); -(() => { - describe('text_utility', () => { - describe('gl.text.getTextWidth', () => { - it('returns zero width when no text is passed', () => { - expect(gl.text.getTextWidth('')).toBe(0); - }); +describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); - it('returns zero width when no text is passed and font is passed', () => { - expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); - }); + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); - it('returns width when text is passed', () => { - expect(gl.text.getTextWidth('foo') > 0).toBe(true); - }); + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); - it('returns bigger width when font is larger', () => { - const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); - const regular = gl.text.getTextWidth('foo', '10px sans-serif'); - expect(largeFont > regular).toBe(true); - }); + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); }); + }); - describe('gl.text.pluralize', () => { - it('returns pluralized', () => { - expect(gl.text.pluralize('test', 2)).toBe('tests'); - }); + describe('gl.text.pluralize', () => { + it('returns pluralized', () => { + expect(gl.text.pluralize('test', 2)).toBe('tests'); + }); - it('returns pluralized when count is 0', () => { - expect(gl.text.pluralize('test', 0)).toBe('tests'); - }); + it('returns pluralized when count is 0', () => { + expect(gl.text.pluralize('test', 0)).toBe('tests'); + }); - it('does not return pluralized', () => { - expect(gl.text.pluralize('test', 1)).toBe('test'); - }); + it('does not return pluralized', () => { + expect(gl.text.pluralize('test', 1)).toBe('test'); }); + }); - describe('gl.text.highCountTrim', () => { - it('returns 99+ for count >= 100', () => { - expect(gl.text.highCountTrim(105)).toBe('99+'); - expect(gl.text.highCountTrim(100)).toBe('99+'); - }); + describe('gl.text.highCountTrim', () => { + it('returns 99+ for count >= 100', () => { + expect(gl.text.highCountTrim(105)).toBe('99+'); + expect(gl.text.highCountTrim(100)).toBe('99+'); + }); - it('returns exact number for count < 100', () => { - expect(gl.text.highCountTrim(45)).toBe(45); - }); + it('returns exact number for count < 100', () => { + expect(gl.text.highCountTrim(45)).toBe(45); }); + }); - describe('gl.text.insertText', () => { - let textArea; + describe('gl.text.insertText', () => { + let textArea; - beforeAll(() => { - textArea = document.createElement('textarea'); - document.querySelector('body').appendChild(textArea); - }); + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + }); - afterAll(() => { - textArea.parentNode.removeChild(textArea); - }); + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); - describe('without selection', () => { - it('inserts the tag on an empty line', () => { - const initialValue = ''; + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); + }); - it('inserts the tag on a new line if the current one is not empty', () => { - const initialValue = 'some text'; + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}\n* `); - }); + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); - it('inserts the tag on the same line if the current line only contains spaces', () => { - const initialValue = ' '; + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); + }); - it('inserts the tag on the same line if the current line only contains tabs', () => { - const initialValue = '\t\t\t'; + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + gl.text.insertText(textArea, textArea.value, '*', null, '', false); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + expect(textArea.value).toEqual(`${initialValue}* `); }); }); }); -})(); +}); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7b9632be84e..e437333d522 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,8 +1,12 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ require('~/merge_request_tabs'); +require('~/commit/pipelines/pipelines_bundle.js'); require('~/breakpoints'); require('~/lib/utils/common_utils'); +require('~/diff'); +require('~/single_file_diff'); +require('~/files_comment_button'); require('vendor/jquery.scrollTo'); (function () { @@ -39,7 +43,8 @@ require('vendor/jquery.scrollTo'); }); afterEach(function () { - this.class.destroy(); + this.class.unbindEvents(); + this.class.destroyPipelinesView(); }); describe('#activateTab', function () { @@ -65,6 +70,7 @@ require('vendor/jquery.scrollTo'); expect($('#diffs')).toHaveClass('active'); }); }); + describe('#opensInNewTab', function () { var tabUrl; var windowTarget = '_blank'; @@ -116,6 +122,7 @@ require('vendor/jquery.scrollTo'); stopImmediatePropagation: function () {} }); }); + it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); @@ -129,6 +136,7 @@ require('vendor/jquery.scrollTo'); stopImmediatePropagation: function () {} }); }); + it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); @@ -149,6 +157,7 @@ require('vendor/jquery.scrollTo'); spyOn($, 'ajax').and.callFake(function () {}); this.subject = this.class.setCurrentAction; }); + it('changes from commits', function () { setLocation({ pathname: '/foo/bar/merge_requests/1/commits' @@ -156,13 +165,16 @@ require('vendor/jquery.scrollTo'); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); }); + it('changes from diffs', function () { setLocation({ pathname: '/foo/bar/merge_requests/1/diffs' }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); + it('changes from diffs.html', function () { setLocation({ pathname: '/foo/bar/merge_requests/1/diffs.html' @@ -170,6 +182,7 @@ require('vendor/jquery.scrollTo'); expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); + it('changes from notes', function () { setLocation({ pathname: '/foo/bar/merge_requests/1' @@ -177,6 +190,7 @@ require('vendor/jquery.scrollTo'); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); + it('includes search parameters and hash string', function () { setLocation({ pathname: '/foo/bar/merge_requests/1/diffs', @@ -185,6 +199,7 @@ require('vendor/jquery.scrollTo'); }); expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); }); + it('replaces the current history state', function () { var newState; setLocation({ @@ -197,6 +212,7 @@ require('vendor/jquery.scrollTo'); }, document.title, newState); } }); + it('treats "show" like "notes"', function () { setLocation({ pathname: '/foo/bar/merge_requests/1/commits' @@ -207,12 +223,16 @@ require('vendor/jquery.scrollTo'); describe('#tabShown', () => { beforeEach(function () { + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: '' }); + }); loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); }); describe('with "Side-by-side"/parallel diff view', () => { beforeEach(function () { this.class.diffViewType = () => 'parallel'; + gl.Diff.prototype.diffViewType = () => 'parallel'; }); it('maintains `container-limited` for pipelines tab', function (done) { @@ -224,7 +244,6 @@ require('vendor/jquery.scrollTo'); }); }); }; - asyncClick('.merge-request-tabs .pipelines-tab a') .then(() => asyncClick('.merge-request-tabs .diffs-tab a')) .then(() => asyncClick('.merge-request-tabs .pipelines-tab a')) @@ -237,6 +256,28 @@ require('vendor/jquery.scrollTo'); done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); }); }); + + it('maintains `container-limited` when switching from "Changes" tab before it loads', function (done) { + const asyncClick = function (selector) { + return new Promise((resolve) => { + setTimeout(() => { + document.querySelector(selector).click(); + resolve(); + }); + }); + }; + + asyncClick('.merge-request-tabs .diffs-tab a') + .then(() => asyncClick('.merge-request-tabs .notes-tab a')) + .then(() => { + const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); + expect(hasContainerLimitedClass).toBe(true); + }) + .then(done) + .catch((err) => { + done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); + }); + }); }); }); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index d5193b41c33..88dae8c3e06 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility'); it('should call showCIStatus even if a notification should not be displayed', function() { var spy; spy = spyOn(this["class"], 'showCIStatus').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); }); it('should call showCIStatus when a notification should be displayed', function() { var spy; spy = spyOn(this["class"], 'showCIStatus').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(true); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); }); it('should call showCICoverage when the coverage rate is set', function() { var spy; spy = spyOn(this["class"], 'showCICoverage').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); }); @@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility'); var spy; this.ciStatusData.coverage = null; spy = spyOn(this["class"], 'showCICoverage').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).not.toHaveBeenCalled(); }); it('should not display a notification on the first check after the widget has been created', function() { var spy; spy = spyOn(window, 'notify'); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"] = new window.gl.MergeRequestWidget(this.opts); this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); @@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility'); it('should update the pipeline URL when the pipeline changes', function() { var spy; spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); this.ciStatusData.pipeline += 1; this["class"].getCIStatus(false); @@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility'); it('should update the commit URL when the sha changes', function() { var spy; spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); this.ciStatusData.sha = "9b50b99a"; this["class"].getCIStatus(false); diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js new file mode 100644 index 00000000000..b5c5e60dd97 --- /dev/null +++ b/spec/javascripts/merged_buttons_spec.js @@ -0,0 +1,44 @@ +/* global MergedButtons */ + +import '~/merged_buttons'; + +describe('MergedButtons', () => { + const fixturesPath = 'merge_requests/merged_merge_request.html.raw'; + preloadFixtures(fixturesPath); + + beforeEach(() => { + loadFixtures(fixturesPath); + this.mergedButtons = new MergedButtons(); + this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)'); + this.$removeBranchProgress = $('.remove_source_branch_in_progress'); + this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); + this.$removeBranchButton = $('.remove_source_branch'); + }); + + describe('removeSourceBranch', () => { + it('shows loader', () => { + $('.remove_source_branch').trigger('click'); + expect(this.$removeBranchProgress).toBeVisible(); + expect(this.$removeBranchWidget).not.toBeVisible(); + }); + }); + + describe('removeBranchSuccess', () => { + it('refreshes page when branch removed', () => { + spyOn(gl.utils, 'refreshCurrentPage').and.stub(); + const response = { status: 200 }; + this.$removeBranchButton.trigger('ajax:success', response, 'xhr'); + expect(gl.utils.refreshCurrentPage).toHaveBeenCalled(); + }); + }); + + describe('removeBranchError', () => { + it('shows error message', () => { + const response = { status: 500 }; + this.$removeBranchButton.trigger('ajax:error', response, 'xhr'); + expect(this.$removeBranchFailed).toBeVisible(); + expect(this.$removeBranchProgress).not.toBeVisible(); + expect(this.$removeBranchWidget).not.toBeVisible(); + }); + }); +}); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index a3c1c5e1b7c..4b904fc2960 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,5 +1,4 @@ import 'jquery'; -import '~/lib/utils/common_utils'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; @@ -12,6 +11,7 @@ describe('PrometheusGraph', () => { beforeEach(() => { loadFixtures(fixtureName); + $('.prometheus-container').data('has-metrics', 'true'); this.prometheusGraph = new PrometheusGraph(); const self = this; const fakeInit = (metricsResponse) => { @@ -37,9 +37,11 @@ describe('PrometheusGraph', () => { it('transforms the data', () => { this.prometheusGraph.init(prometheusMockData.metrics); - expect(this.prometheusGraph.data).toBeDefined(); - expect(this.prometheusGraph.data.cpu_values.length).toBe(121); - expect(this.prometheusGraph.data.memory_values.length).toBe(121); + Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => { + const graphProps = this.prometheusGraph.graphSpecificProperties[key]; + expect(graphProps.data).toBeDefined(); + expect(graphProps.data.length).toBe(121); + }); }); it('creates two graphs', () => { @@ -68,8 +70,29 @@ describe('PrometheusGraph', () => { expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); - expect($axisLabelContainer.find('rect').length).toBe(2); + expect($axisLabelContainer.find('rect').length).toBe(3); expect($axisLabelContainer.find('text').length).toBe(4); }); }); }); + +describe('PrometheusGraphs UX states', () => { + const fixtureName = 'static/environments/metrics.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + this.prometheusGraph = new PrometheusGraph(); + }); + + it('shows a specified state', () => { + this.prometheusGraph.state = '.js-getting-started'; + this.prometheusGraph.updateState(); + const $state = $('.js-getting-started'); + expect($state).toBeDefined(); + expect($('.state-title', $state)).toBeDefined(); + expect($('.state-svg', $state)).toBeDefined(); + expect($('.state-description', $state)).toBeDefined(); + expect($('.state-button', $state)).toBeDefined(); + }); +}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index b30c5da8822..07dc51a7815 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -64,6 +64,7 @@ if (process.env.BABEL_ENV === 'coverage') { './snippet/snippet_bundle.js', './terminal/terminal_bundle.js', './users/users_bundle.js', + './issue_show/index.js', ]; describe('Uncovered files', function () { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0f390c8b980..3960759f7cb 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -22,7 +22,7 @@ require('./mock_u2f_device'); it('allows registering a U2F device', function() { var deviceResponse, inProgressMessage, registeredMessage, setupButton; setupButton = this.container.find("#js-setup-u2f-device"); - expect(setupButton.text()).toBe('Setup New U2F Device'); + expect(setupButton.text()).toBe('Setup new U2F device'); setupButton.trigger('click'); inProgressMessage = this.container.children("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js index bc8e504c413..6e910d2dc71 100644 --- a/spec/javascripts/vue_pipelines_index/async_button_spec.js +++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import asyncButtonComp from '~/vue_pipelines_index/components/async_button'; +import asyncButtonComp from '~/vue_pipelines_index/components/async_button.vue'; describe('Pipelines Async Button', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/vue_pipelines_index/empty_state_spec.js index 733337168dc..2b10d54babe 100644 --- a/spec/javascripts/vue_pipelines_index/empty_state_spec.js +++ b/spec/javascripts/vue_pipelines_index/empty_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import emptyStateComp from '~/vue_pipelines_index/components/empty_state'; +import emptyStateComp from '~/vue_pipelines_index/components/empty_state.vue'; describe('Pipelines Empty State', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/vue_pipelines_index/error_state_spec.js index 524e018b1fa..7999c15c18d 100644 --- a/spec/javascripts/vue_pipelines_index/error_state_spec.js +++ b/spec/javascripts/vue_pipelines_index/error_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import errorStateComp from '~/vue_pipelines_index/components/error_state'; +import errorStateComp from '~/vue_pipelines_index/components/error_state.vue'; describe('Pipelines Error State', () => { let component; diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js index dba998c7688..0910df61915 100644 --- a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js +++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js @@ -15,6 +15,11 @@ describe('Pipelines Actions dropdown', () => { name: 'stop_review', path: '/root/review-app/builds/1893/play', }, + { + name: 'foo', + path: '#', + playable: false, + }, ]; spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); @@ -59,4 +64,14 @@ describe('Pipelines Actions dropdown', () => { expect(component.$el.querySelector('.fa-spinner')).toEqual(null); }); + + it('should render a disabled action when it\'s not playable', () => { + expect( + component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), + ).toEqual('disabled'); + + expect( + component.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'), + ).toEqual(true); + }); }); |