summaryrefslogtreecommitdiff
path: root/spec/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'spec/javascripts')
-rw-r--r--spec/javascripts/build_spec.js308
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js3
-rw-r--r--spec/javascripts/fixtures/jobs.rb (renamed from spec/javascripts/fixtures/builds.rb)2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js306
-rw-r--r--spec/javascripts/issue_show/components/edit_actions_spec.js147
-rw-r--r--spec/javascripts/issue_show/components/fields/description_spec.js56
-rw-r--r--spec/javascripts/issue_show/components/fields/description_template_spec.js49
-rw-r--r--spec/javascripts/issue_show/components/fields/project_move_spec.js38
-rw-r--r--spec/javascripts/issue_show/components/fields/title_spec.js30
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js68
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js8
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js2
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js9
-rw-r--r--spec/javascripts/merge_request_spec.js4
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js45
-rw-r--r--spec/javascripts/notes_spec.js5
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js6
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js4
-rw-r--r--spec/javascripts/pipelines/graph/graph_component_spec.js59
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js24
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js2
-rw-r--r--spec/javascripts/raven/raven_config_spec.js18
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js37
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js82
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js121
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js67
-rw-r--r--spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js68
29 files changed, 1448 insertions, 208 deletions
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index 8ec96bdb583..461908f3fde 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -8,13 +8,12 @@ import '~/breakpoints';
import 'vendor/jquery.nicescroll';
describe('Build', () => {
- const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
- spyOn($, 'ajax');
});
describe('class constructor', () => {
@@ -33,7 +32,6 @@ describe('Build', () => {
it('copies build options', function () {
expect(this.build.pageUrl).toBe(BUILD_URL);
- expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
expect(this.build.buildStatus).toBe('success');
expect(this.build.buildStage).toBe('test');
expect(this.build.state).toBe('');
@@ -65,27 +63,14 @@ describe('Build', () => {
});
describe('running build', () => {
- beforeEach(function () {
- this.build = new Build();
- });
-
it('updates the build trace on an interval', function () {
+ const deferred1 = $.Deferred();
+ const deferred2 = $.Deferred();
+ const deferred3 = $.Deferred();
+ spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
spyOn(gl.utils, 'visitUrl');
- jasmine.clock().tick(4001);
-
- 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];
-
- expect(args.url).toBe(`${BUILD_URL}/trace.json`);
- expect(args.dataType).toBe('json');
- expect(args.success).toEqual(jasmine.any(Function));
-
- args.success.call($, {
+ deferred1.resolve({
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
@@ -93,20 +78,9 @@ describe('Build', () => {
complete: false,
});
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(this.build.state).toBe('newstate');
-
- jasmine.clock().tick(4001);
-
- expect($.ajax.calls.count()).toBe(3);
-
- 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));
+ deferred2.resolve();
- args.success.call($, {
+ deferred3.resolve({
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
@@ -114,150 +88,222 @@ describe('Build', () => {
complete: true,
});
+ this.build = new Build();
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.build.state).toBe('newstate');
+
+ jasmine.clock().tick(4001);
+
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(this.build.state).toBe('finalstate');
});
it('replaces the entire build trace', () => {
+ const deferred1 = $.Deferred();
+ const deferred2 = $.Deferred();
+ const deferred3 = $.Deferred();
+
+ spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
+
spyOn(gl.utils, 'visitUrl');
- jasmine.clock().tick(4001);
- let args = $.ajax.calls.argsFor(0)[0];
- args.success.call($, {
- html: '<span>Update</span>',
+ deferred1.resolve({
+ html: '<span>Update<span>',
status: 'running',
append: false,
complete: false,
});
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ deferred2.resolve();
- jasmine.clock().tick(4001);
- args = $.ajax.calls.argsFor(2)[0];
- args.success.call($, {
+ deferred3.resolve({
html: '<span>Different</span>',
status: 'running',
append: false,
});
+ this.build = new Build();
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ jasmine.clock().tick(4001);
+
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl');
+ const deferred = $.Deferred();
- jasmine.clock().tick(4001);
- const [{ success }] = $.ajax.calls.argsFor(0);
- success.call($, {
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
html: '<span>Final</span>',
status: 'passed',
append: true,
complete: true,
});
+ this.build = new Build();
+
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
});
+ });
- describe('truncated information', () => {
- describe('when size is less than total', () => {
- 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,
- size: 50,
- total: 100,
- });
-
- expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ describe('truncated information', () => {
+ describe('when size is less than total', () => {
+ it('shows information about truncated log', () => {
+ spyOn(gl.utils, 'visitUrl');
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
});
- it('shows the size in KiB', () => {
- jasmine.clock().tick(4001);
- const [{ success }] = $.ajax.calls.argsFor(0);
- const size = 50;
-
- success.call($, {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(size)}`);
+ this.build = new Build();
+
+ expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ });
+
+ it('shows the size in KiB', () => {
+ const size = 50;
+ spyOn(gl.utils, 'visitUrl');
+ const deferred = $.Deferred();
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size,
+ total: 100,
});
- it('shows incremented size', () => {
- jasmine.clock().tick(4001);
- let args = $.ajax.calls.argsFor(0)[0];
- args.success.call($, {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(50)}`);
-
- jasmine.clock().tick(4001);
- args = $.ajax.calls.argsFor(2)[0];
- args.success.call($, {
- html: '<span>Update</span>',
- status: 'success',
- append: true,
- size: 10,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(60)}`);
+ this.build = new Build();
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(size)}`);
+ });
+
+ it('shows incremented size', () => {
+ const deferred1 = $.Deferred();
+ const deferred2 = $.Deferred();
+ const deferred3 = $.Deferred();
+
+ spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
+
+ spyOn(gl.utils, 'visitUrl');
+
+ deferred1.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
});
- it('renders the raw link', () => {
- jasmine.clock().tick(4001);
- const [{ success }] = $.ajax.calls.argsFor(0);
-
- success.call($, {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-raw-link').textContent.trim(),
- ).toContain('Complete Raw');
+ deferred2.resolve();
+
+ this.build = new Build();
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(50)}`);
+
+ jasmine.clock().tick(4001);
+
+ deferred3.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: true,
+ size: 10,
+ total: 100,
});
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(60)}`);
});
- describe('when size is equal than total', () => {
- it('does not show the trunctated information', () => {
- jasmine.clock().tick(4001);
- const [{ success }] = $.ajax.calls.argsFor(0);
+ it('renders the raw link', () => {
+ const deferred = $.Deferred();
+ spyOn(gl.utils, 'visitUrl');
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
- success.call($, {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 100,
- total: 100,
- });
+ this.build = new Build();
- expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ expect(
+ document.querySelector('.js-raw-link').textContent.trim(),
+ ).toContain('Complete Raw');
+ });
+ });
+
+ describe('when size is equal than total', () => {
+ it('does not show the trunctated information', () => {
+ const deferred = $.Deferred();
+ spyOn(gl.utils, 'visitUrl');
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 100,
+ total: 100,
});
+
+ this.build = new Build();
+
+ expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ });
+ });
+ });
+
+ describe('output trace', () => {
+ beforeEach(() => {
+ const deferred = $.Deferred();
+ spyOn(gl.utils, 'visitUrl');
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
});
+
+ this.build = new Build();
+ });
+
+ it('should render trace controls', () => {
+ const controllers = document.querySelector('.controllers');
+
+ expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined();
+ expect(controllers.querySelector('.js-erase-link')).toBeDefined();
+ expect(controllers.querySelector('.js-scroll-up')).toBeDefined();
+ expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
+ });
+
+ it('should render received output', () => {
+ expect(
+ document.querySelector('.js-build-output').innerHTML,
+ ).toEqual('<span>Update</span>');
});
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 8688332782d..6e59ee96c6b 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -57,6 +57,7 @@ describe('Filtered Search Manager', () => {
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager();
+ manager.setup();
});
afterEach(() => {
@@ -72,6 +73,7 @@ describe('Filtered Search Manager', () => {
spyOn(recentSearchesStoreSrc, 'default');
filteredSearchManager = new gl.FilteredSearchManager();
+ filteredSearchManager.setup();
return filteredSearchManager;
});
@@ -89,6 +91,7 @@ describe('Filtered Search Manager', () => {
spyOn(window, 'Flash');
filteredSearchManager = new gl.FilteredSearchManager();
+ filteredSearchManager.setup();
expect(window.Flash).not.toHaveBeenCalled();
});
diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/jobs.rb
index 320de791b08..dc7dde1138c 100644
--- a/spec/javascripts/fixtures/builds.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do
+describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index ee456869c53..0030a953119 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import '~/render_math';
import '~/render_gfm';
import issuableApp from '~/issue_show/components/app.vue';
+import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data';
const issueShowInterceptor = data => (request, next) => {
@@ -22,14 +23,25 @@ describe('Issuable output', () => {
const IssuableDescriptionComponent = Vue.extend(issuableApp);
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
+ spyOn(eventHub, '$emit');
+
vm = new IssuableDescriptionComponent({
propsData: {
canUpdate: true,
+ canDestroy: true,
+ canMove: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1',
- initialTitle: '',
+ initialTitleHtml: '',
+ initialTitleText: '',
initialDescriptionHtml: '',
initialDescriptionText: '',
+ markdownPreviewUrl: '/',
+ markdownDocs: '/',
+ projectsAutocompleteUrl: '/',
+ isConfidential: false,
+ projectNamespace: '/',
+ projectPath: '/',
},
}).$mount();
});
@@ -57,4 +69,296 @@ describe('Issuable output', () => {
});
});
});
+
+ it('shows actions if permissions are correct', (done) => {
+ vm.showForm = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('does not show actions if permissions are incorrect', (done) => {
+ vm.showForm = true;
+ vm.canUpdate = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+
+ it('does not update formState if form is already open', (done) => {
+ vm.openForm();
+
+ vm.state.titleText = 'testing 123';
+
+ vm.openForm();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.store.formState.title,
+ ).not.toBe('testing 123');
+
+ done();
+ });
+ });
+
+ describe('updateIssuable', () => {
+ it('fetches new data after update', (done) => {
+ spyOn(vm.service, 'getData');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ confidential: false,
+ web_url: location.pathname,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.service.getData,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('reloads the page if the confidential status has changed', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ confidential: true,
+ web_url: location.pathname,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).toHaveBeenCalledWith(location.pathname);
+
+ done();
+ });
+ });
+
+ it('correctly updates issuable data', (done) => {
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve();
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.service.updateIssuable,
+ ).toHaveBeenCalledWith(vm.formState);
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+
+ done();
+ });
+ });
+
+ it('does not redirect if issue has not moved', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ web_url: location.pathname,
+ confidential: vm.isConfidential,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('redirects if issue is moved', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ web_url: '/testing-issue-move',
+ confidential: vm.isConfidential,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).toHaveBeenCalledWith('/testing-issue-move');
+
+ done();
+ });
+ });
+
+ it('does not update issuable if project move confirm is false', (done) => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ spyOn(vm.service, 'updateIssuable');
+
+ vm.store.formState.move_to_project_id = 1;
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.service.updateIssuable,
+ ).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('closes form on error', (done) => {
+ spyOn(window, 'Flash').and.callThrough();
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
+ reject();
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error updating issue');
+
+ done();
+ });
+ });
+ });
+
+ describe('deleteIssuable', () => {
+ it('changes URL when deleted', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { web_url: '/test' };
+ },
+ });
+ }));
+
+ vm.deleteIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).toHaveBeenCalledWith('/test');
+
+ done();
+ });
+ });
+
+ it('stops polling when deleting', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.poll, 'stop');
+ spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { web_url: '/test' };
+ },
+ });
+ }));
+
+ vm.deleteIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.poll.stop,
+ ).toHaveBeenCalledWith();
+
+ done();
+ });
+ });
+
+ it('closes form on error', (done) => {
+ spyOn(window, 'Flash').and.callThrough();
+ spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve, reject) => {
+ reject();
+ }));
+
+ vm.deleteIssuable();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error deleting issue');
+
+ done();
+ });
+ });
+ });
+
+ describe('open form', () => {
+ it('shows locked warning if form is open & data is different', (done) => {
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
+
+ Vue.nextTick()
+ .then(() => new Promise((resolve) => {
+ setTimeout(resolve);
+ }))
+ .then(() => {
+ vm.openForm();
+
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
+
+ return new Promise((resolve) => {
+ setTimeout(resolve);
+ });
+ })
+ .then(() => {
+ expect(
+ vm.formState.lockedWarningVisible,
+ ).toBeTruthy();
+
+ expect(
+ vm.$el.querySelector('.alert'),
+ ).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js
new file mode 100644
index 00000000000..f6625b748b6
--- /dev/null
+++ b/spec/javascripts/issue_show/components/edit_actions_spec.js
@@ -0,0 +1,147 @@
+import Vue from 'vue';
+import editActions from '~/issue_show/components/edit_actions.vue';
+import eventHub from '~/issue_show/event_hub';
+import Store from '~/issue_show/stores';
+
+describe('Edit Actions components', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(editActions);
+ const store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ store.formState.title = 'test';
+
+ spyOn(eventHub, '$emit');
+
+ vm = new Component({
+ propsData: {
+ canDestroy: true,
+ formState: store.formState,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders all buttons as enabled', () => {
+ expect(
+ vm.$el.querySelectorAll('.disabled').length,
+ ).toBe(0);
+
+ expect(
+ vm.$el.querySelectorAll('[disabled]').length,
+ ).toBe(0);
+ });
+
+ it('does not render delete button if canUpdate is false', (done) => {
+ vm.canDestroy = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-danger'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+
+ it('disables submit button when title is blank', (done) => {
+ vm.formState.title = '';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-save').getAttribute('disabled'),
+ ).toBe('disabled');
+
+ done();
+ });
+ });
+
+ describe('updateIssuable', () => {
+ it('sends update.issauble event when clicking save button', () => {
+ vm.$el.querySelector('.btn-save').click();
+
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('update.issuable');
+ });
+
+ it('shows loading icon after clicking save button', (done) => {
+ vm.$el.querySelector('.btn-save').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-save .fa'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('disabled button after clicking save button', (done) => {
+ vm.$el.querySelector('.btn-save').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-save').getAttribute('disabled'),
+ ).toBe('disabled');
+
+ done();
+ });
+ });
+ });
+
+ describe('closeForm', () => {
+ it('emits close.form when clicking cancel', () => {
+ vm.$el.querySelector('.btn-default').click();
+
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ });
+ });
+
+ describe('deleteIssuable', () => {
+ it('sends delete.issuable event when clicking save button', () => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ vm.$el.querySelector('.btn-danger').click();
+
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('delete.issuable');
+ });
+
+ it('shows loading icon after clicking delete button', (done) => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ vm.$el.querySelector('.btn-danger').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-danger .fa'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('does no actions when confirm is false', (done) => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ vm.$el.querySelector('.btn-danger').click();
+
+ Vue.nextTick(() => {
+ expect(
+ eventHub.$emit,
+ ).not.toHaveBeenCalledWith('delete.issuable');
+ expect(
+ vm.$el.querySelector('.btn-danger .fa'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js
new file mode 100644
index 00000000000..f5b35b1e8b0
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/description_spec.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import Store from '~/issue_show/stores';
+import descriptionField from '~/issue_show/components/fields/description.vue';
+
+describe('Description field component', () => {
+ let vm;
+ let store;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(descriptionField);
+ const el = document.createElement('div');
+ store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ store.formState.description = 'test';
+
+ document.body.appendChild(el);
+
+ vm = new Component({
+ el,
+ propsData: {
+ markdownPreviewUrl: '/',
+ markdownDocs: '/',
+ formState: store.formState,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders markdown field with description', () => {
+ expect(
+ vm.$el.querySelector('.md-area textarea').value,
+ ).toBe('test');
+ });
+
+ it('renders markdown field with a markdown description', (done) => {
+ store.formState.description = '**test**';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.md-area textarea').value,
+ ).toBe('**test**');
+
+ done();
+ });
+ });
+
+ it('focuses field when mounted', () => {
+ expect(
+ document.activeElement,
+ ).toBe(vm.$refs.textarea);
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js
new file mode 100644
index 00000000000..2b7ee65094b
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
+import '~/templates/issuable_template_selector';
+import '~/templates/issuable_template_selectors';
+
+describe('Issue description template component', () => {
+ let vm;
+ let formState;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(descriptionTemplate);
+ formState = {
+ description: 'test',
+ };
+
+ vm = new Component({
+ propsData: {
+ formState,
+ issuableTemplates: [{ name: 'test' }],
+ projectPath: '/',
+ projectNamespace: '/',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders templates as JSON array in data attribute', () => {
+ expect(
+ vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data'),
+ ).toBe('[{"name":"test"}]');
+ });
+
+ it('updates formState when changing template', () => {
+ vm.issuableTemplate.editor.setValue('test new template');
+
+ expect(
+ formState.description,
+ ).toBe('test new template');
+ });
+
+ it('returns formState description with editor getValue', () => {
+ formState.description = 'testing new template';
+
+ expect(
+ vm.issuableTemplate.editor.getValue(),
+ ).toBe('testing new template');
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js
new file mode 100644
index 00000000000..86d35c33ff4
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import projectMove from '~/issue_show/components/fields/project_move.vue';
+
+describe('Project move field component', () => {
+ let vm;
+ let formState;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(projectMove);
+
+ formState = {
+ move_to_project_id: 0,
+ };
+
+ vm = new Component({
+ propsData: {
+ formState,
+ projectsAutocompleteUrl: '/autocomplete',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('mounts select2 element', () => {
+ expect(
+ vm.$el.querySelector('.select2-container'),
+ ).not.toBeNull();
+ });
+
+ it('updates formState on change', () => {
+ $(vm.$refs['move-dropdown']).val(2).trigger('change');
+
+ expect(
+ formState.move_to_project_id,
+ ).toBe(2);
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js
new file mode 100644
index 00000000000..53ae038a6a2
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/title_spec.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import Store from '~/issue_show/stores';
+import titleField from '~/issue_show/components/fields/title.vue';
+
+describe('Title field component', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ const Component = Vue.extend(titleField);
+ store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ store.formState.title = 'test';
+
+ vm = new Component({
+ propsData: {
+ formState: store.formState,
+ },
+ }).$mount();
+ });
+
+ it('renders form control with formState title', () => {
+ expect(
+ vm.$el.querySelector('.form-control').value,
+ ).toBe('test');
+ });
+});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
new file mode 100644
index 00000000000..9a85223208c
--- /dev/null
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import formComponent from '~/issue_show/components/form.vue';
+import '~/templates/issuable_template_selector';
+import '~/templates/issuable_template_selectors';
+
+describe('Inline edit form component', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(formComponent);
+
+ vm = new Component({
+ propsData: {
+ canDestroy: true,
+ canMove: true,
+ formState: {
+ title: 'b',
+ description: 'a',
+ lockedWarningVisible: false,
+ },
+ markdownPreviewUrl: '/',
+ markdownDocs: '/',
+ projectsAutocompleteUrl: '/',
+ projectPath: '/',
+ projectNamespace: '/',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('does not render template selector if no templates exist', () => {
+ expect(
+ vm.$el.querySelector('.js-issuable-selector-wrap'),
+ ).toBeNull();
+ });
+
+ it('renders template selector when templates exists', (done) => {
+ spyOn(gl, 'IssuableTemplateSelectors');
+ vm.issuableTemplates = ['test'];
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.js-issuable-selector-wrap'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('hides locked warning by default', () => {
+ expect(
+ vm.$el.querySelector('.alert'),
+ ).toBeNull();
+ });
+
+ it('shows locked warning if formState is different', (done) => {
+ vm.formState.lockedWarningVisible = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.alert'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
index 2f953e7e92e..a2d90a9b9f5 100644
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import Store from '~/issue_show/stores';
import titleComponent from '~/issue_show/components/title.vue';
describe('Title component', () => {
@@ -6,11 +7,18 @@ describe('Title component', () => {
beforeEach(() => {
const Component = Vue.extend(titleComponent);
+ const store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
vm = new Component({
propsData: {
issuableRef: '#1',
titleHtml: 'Testing <img />',
titleText: 'Testing',
+ showForm: false,
+ formState: store.formState,
},
}).$mount();
});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index e9bffd74d90..e3938a77680 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -356,7 +356,7 @@ import '~/lib/utils/common_utils';
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 BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
const FAVICON_PATH = '//icon_status_success';
const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub();
const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub();
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index 90b12c9f115..83c92deccdc 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,4 @@
-import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils';
+import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -45,4 +45,11 @@ describe('Number Utils', () => {
expect(bytesToKiB(1000)).toEqual(0.9765625);
});
});
+
+ describe('bytesToMiB', () => {
+ it('calculates MiB for the given bytes', () => {
+ expect(bytesToMiB(1048576)).toEqual(1);
+ expect(bytesToMiB(1000000)).toEqual(0.95367431640625);
+ });
+ });
});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 1173fa40947..f444bcaf847 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -13,7 +13,9 @@ import '~/merge_request';
});
it('modifies the Markdown field', function() {
spyOn(jQuery, 'ajax').and.stub();
- $('input[type=checkbox]').attr('checked', true).trigger('change');
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', true, true);
+ $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
return it('submits an ajax request on tasklist:changed', function() {
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 3d1706aab68..7b910282cc8 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,4 +1,5 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
+/* global Notes */
import '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
@@ -7,6 +8,7 @@ import '~/lib/utils/common_utils';
import '~/diff';
import '~/single_file_diff';
import '~/files_comment_button';
+import '~/notes';
import 'vendor/jquery.scrollTo';
(function () {
@@ -29,7 +31,7 @@ import 'vendor/jquery.scrollTo';
};
$.extend(stubLocation, defaults, stubs || {});
};
- preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw');
beforeEach(function () {
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
@@ -286,8 +288,49 @@ import 'vendor/jquery.scrollTo';
spyOn($, 'ajax').and.callFake(function (options) {
expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json');
});
+
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
});
+
+ describe('with note fragment hash', () => {
+ beforeEach(() => {
+ loadFixtures('merge_requests/diff_comment.html.raw');
+ spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests');
+ window.notes = new Notes('', []);
+ spyOn(window.notes, 'toggleDiffNote').and.callThrough();
+ });
+
+ afterEach(() => {
+ delete window.notes;
+ });
+
+ it('should expand and scroll to linked fragment hash #note_xxx', function () {
+ const noteId = 'note_1';
+ spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId);
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ html: `<div id="${noteId}">foo</div>` });
+ });
+
+ this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+
+ expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({
+ target: jasmine.any(Object),
+ lineType: 'old',
+ forceShow: true,
+ });
+ });
+
+ it('should gracefully ignore non-existant fragment hash', function () {
+ spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ html: '' });
+ });
+
+ this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+
+ expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
+ });
+ });
});
});
}).call(window);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 025f08ee332..17aa70ff3f1 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -34,7 +34,9 @@ import '~/notes';
});
it('modifies the Markdown field', function() {
- $('input[type=checkbox]').attr('checked', true).trigger('change');
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', true, true);
+ $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
@@ -128,7 +130,6 @@ import '~/notes';
beforeEach(() => {
note = {
id: 1,
- discussion_html: null,
valid: true,
note: 'heya',
html: '<div>heya</div>',
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index f033956c071..85bd87318db 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -4,7 +4,7 @@ import actionComponent from '~/pipelines/components/graph/action_component.vue';
describe('pipeline graph action component', () => {
let component;
- beforeEach(() => {
+ beforeEach((done) => {
const ActionComponent = Vue.extend(actionComponent);
component = new ActionComponent({
propsData: {
@@ -14,6 +14,8 @@ describe('pipeline graph action component', () => {
actionIcon: 'icon_action_cancel',
},
}).$mount();
+
+ Vue.nextTick(done);
});
it('should render a link', () => {
@@ -27,7 +29,7 @@ describe('pipeline graph action component', () => {
it('should update bootstrap tooltip when title changes', (done) => {
component.tooltipText = 'changed';
- Vue.nextTick(() => {
+ setTimeout(() => {
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
done();
});
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
index 14ff1b0d25c..25fd18b197e 100644
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
@@ -4,7 +4,7 @@ import dropdownActionComponent from '~/pipelines/components/graph/dropdown_actio
describe('action component', () => {
let component;
- beforeEach(() => {
+ beforeEach((done) => {
const DropdownActionComponent = Vue.extend(dropdownActionComponent);
component = new DropdownActionComponent({
propsData: {
@@ -14,6 +14,8 @@ describe('action component', () => {
actionIcon: 'icon_action_cancel',
},
}).$mount();
+
+ Vue.nextTick(done);
});
it('should render a link', () => {
diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js
index 6bd0eb86263..713baa65a17 100644
--- a/spec/javascripts/pipelines/graph/graph_component_spec.js
+++ b/spec/javascripts/pipelines/graph/graph_component_spec.js
@@ -14,49 +14,42 @@ describe('graph component', () => {
describe('while is loading', () => {
it('should render a loading icon', () => {
- const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
+ const component = new GraphComponent({
+ propsData: {
+ isLoading: true,
+ pipeline: {},
+ },
+ }).$mount('#js-pipeline-graph-vue');
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
});
});
- describe('with a successfull response', () => {
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(graphJSON), {
- status: 200,
- }));
- };
+ describe('with data', () => {
+ it('should render the graph', () => {
+ const component = new GraphComponent({
+ propsData: {
+ isLoading: false,
+ pipeline: graphJSON,
+ },
+ }).$mount('#js-pipeline-graph-vue');
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
-
- it('should render the graph', (done) => {
- const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
-
- setTimeout(() => {
- expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
+ expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
- expect(
- component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'),
- ).toEqual(true);
+ expect(
+ component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'),
+ ).toEqual(true);
- expect(
- component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'),
- ).toEqual(true);
+ expect(
+ component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'),
+ ).toEqual(true);
- expect(
- component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
- ).toEqual(true);
+ expect(
+ component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
+ ).toEqual(true);
- expect(component.$el.querySelector('loading-icon')).toBe(null);
+ expect(component.$el.querySelector('loading-icon')).toBe(null);
- expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
- done();
- }, 0);
+ expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
});
});
});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 63986b6c0db..e90593e0f40 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -27,26 +27,30 @@ describe('pipeline graph job component', () => {
});
describe('name with link', () => {
- it('should render the job name and status with a link', () => {
+ it('should render the job name and status with a link', (done) => {
const component = new JobComponent({
propsData: {
job: mockJob,
},
}).$mount();
- const link = component.$el.querySelector('a');
+ Vue.nextTick(() => {
+ const link = component.$el.querySelector('a');
- expect(link.getAttribute('href')).toEqual(mockJob.status.details_path);
+ expect(link.getAttribute('href')).toEqual(mockJob.status.details_path);
- expect(
- link.getAttribute('data-original-title'),
- ).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
+ expect(
+ link.getAttribute('data-original-title'),
+ ).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
- expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
+ expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
- expect(
- component.$el.querySelector('.ci-status-text').textContent.trim(),
- ).toEqual(mockJob.name);
+ expect(
+ component.$el.querySelector('.ci-status-text').textContent.trim(),
+ ).toEqual(mockJob.name);
+
+ done();
+ });
});
});
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 0bcc3905702..d74b1281668 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import pipelineUrlComp from '~/pipelines/components/pipeline_url';
+import pipelineUrlComp from '~/pipelines/components/pipeline_url.vue';
describe('Pipeline Url Component', () => {
let PipelineUrlComponent;
diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
index b31a7c28ebe..c82658b9262 100644
--- a/spec/javascripts/raven/raven_config_spec.js
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -140,24 +140,6 @@ describe('RavenConfig', () => {
});
});
- describe('bindRavenErrors', () => {
- let $document;
- let $;
-
- beforeEach(() => {
- $document = jasmine.createSpyObj('$document', ['on']);
- $ = jasmine.createSpy('$').and.returnValue($document);
-
- window.$ = $;
-
- RavenConfig.bindRavenErrors();
- });
-
- it('should call .on', function () {
- expect($document.on).toHaveBeenCalledWith('ajaxError.raven', RavenConfig.handleRavenErrors);
- });
- });
-
describe('handleRavenErrors', () => {
let event;
let req;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index da9dff18ada..2c3d0ddff28 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -7,6 +7,18 @@ const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
const metricsMockData = {
success: true,
metrics: {
+ memory_before: [
+ {
+ metric: {},
+ value: [1495785220.607, '9572875.906976745'],
+ },
+ ],
+ memory_after: [
+ {
+ metric: {},
+ value: [1495787020.607, '4485853.130206379'],
+ },
+ ],
memory_values: [
{
metric: {},
@@ -39,7 +51,7 @@ const createComponent = () => {
const messages = {
loadingMetrics: 'Loading deployment statistics.',
- hasMetrics: 'Deployment memory usage:',
+ hasMetrics: 'Memory usage unchanged from 0MB to 0MB',
loadFailed: 'Failed to load deployment statistics.',
metricsUnavailable: 'Deployment statistics are not available currently.',
};
@@ -89,17 +101,52 @@ describe('MemoryUsage', () => {
});
});
+ describe('computed', () => {
+ describe('memoryChangeType', () => {
+ it('should return "increased" if memoryFrom value is less than memoryTo value', () => {
+ vm.memoryFrom = 4.28;
+ vm.memoryTo = 9.13;
+
+ expect(vm.memoryChangeType).toEqual('increased');
+ });
+
+ it('should return "decreased" if memoryFrom value is less than memoryTo value', () => {
+ vm.memoryFrom = 9.13;
+ vm.memoryTo = 4.28;
+
+ expect(vm.memoryChangeType).toEqual('decreased');
+ });
+
+ it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => {
+ vm.memoryFrom = 1;
+ vm.memoryTo = 1;
+
+ expect(vm.memoryChangeType).toEqual('unchanged');
+ });
+ });
+ });
+
describe('methods', () => {
const { metrics, deployment_time } = metricsMockData;
+ describe('getMegabytes', () => {
+ it('should return Megabytes from provided Bytes value', () => {
+ const memoryInBytes = '9572875.906976745';
+
+ expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13');
+ });
+ });
+
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
vm.computeGraphData(metrics, deployment_time);
- const { hasMetrics, memoryMetrics, deploymentTime } = vm;
+ const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
expect(hasMetrics).toBeTruthy();
expect(memoryMetrics.length > 0).toBeTruthy();
expect(deploymentTime).toEqual(deployment_time);
+ expect(memoryFrom).toEqual('9.13');
+ expect(memoryTo).toEqual('4.28');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index d043ad38b8b..732b516badd 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -5,7 +5,7 @@ import * as simplePoll from '~/lib/utils/simple_poll';
const commitMessage = 'This is the commit message';
const commitMessageWithDescription = 'This is the commit message description';
-const createComponent = () => {
+const createComponent = (customConfig = {}) => {
const Component = Vue.extend(readyToMergeComponent);
const mr = {
isPipelineActive: false,
@@ -17,8 +17,12 @@ const createComponent = () => {
sha: '12345678',
commitMessage,
commitMessageWithDescription,
+ shouldRemoveSourceBranch: true,
+ canRemoveSourceBranch: false,
};
+ Object.assign(mr, customConfig.mr);
+
const service = {
merge() {},
poll() {},
@@ -51,7 +55,6 @@ describe('MRWidgetReadyToMerge', () => {
describe('data', () => {
it('should have default data', () => {
- expect(vm.removeSourceBranch).toBeTruthy(true);
expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
@@ -166,6 +169,36 @@ describe('MRWidgetReadyToMerge', () => {
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
});
+
+ describe('Remove source branch checkbox', () => {
+ describe('when user can merge but cannot delete branch', () => {
+ it('isRemoveSourceBranchButtonDisabled should be true', () => {
+ expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true);
+ });
+
+ it('should be disabled in the rendered output', () => {
+ const checkboxElement = vm.$el.querySelector('#remove-source-branch-input');
+ expect(checkboxElement.getAttribute('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('when user can merge and can delete branch', () => {
+ beforeEach(() => {
+ this.customVm = createComponent({
+ mr: { canRemoveSourceBranch: true },
+ });
+ });
+
+ it('isRemoveSourceBranchButtonDisabled should be false', () => {
+ expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ });
+
+ it('should be enabled in rendered output', () => {
+ const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
+ expect(checkboxElement.getAttribute('disabled')).toBeNull();
+ });
+ });
+ });
});
describe('methods', () => {
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index bdc18243a15..3a0c50b750f 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import notify from '~/lib/utils/notify';
import mockData from './mock_data';
const createComponent = () => {
@@ -107,6 +108,8 @@ describe('mrWidgetOptions', () => {
it('should tell service to check status', (done) => {
spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData));
spyOn(vm.mr, 'setData');
+ spyOn(vm, 'handleNotification');
+
let isCbExecuted = false;
const cb = () => {
isCbExecuted = true;
@@ -117,6 +120,7 @@ describe('mrWidgetOptions', () => {
setTimeout(() => {
expect(vm.service.checkStatus).toHaveBeenCalled();
expect(vm.mr.setData).toHaveBeenCalled();
+ expect(vm.handleNotification).toHaveBeenCalledWith(mockData);
expect(isCbExecuted).toBeTruthy();
done();
}, 333);
@@ -254,6 +258,39 @@ describe('mrWidgetOptions', () => {
});
});
+ describe('handleNotification', () => {
+ const data = {
+ ci_status: 'running',
+ title: 'title',
+ pipeline: { details: { status: { label: 'running-label' } } },
+ };
+
+ beforeEach(() => {
+ spyOn(notify, 'notifyMe');
+
+ vm.mr.ciStatus = 'failed';
+ vm.mr.gitlabLogo = 'logo.png';
+ });
+
+ it('should call notifyMe', () => {
+ vm.handleNotification(data);
+
+ expect(notify.notifyMe).toHaveBeenCalledWith(
+ 'Pipeline running-label',
+ 'Pipeline running-label for "title"',
+ 'logo.png',
+ );
+ });
+
+ it('should not call notifyMe if the status has not changed', () => {
+ vm.mr.ciStatus = data.ci_status;
+
+ vm.handleNotification(data);
+
+ expect(notify.notifyMe).not.toHaveBeenCalled();
+ });
+ });
+
describe('resumePolling', () => {
it('should call stopTimer on pollingInterval', () => {
spyOn(vm.pollingInterval, 'resume');
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
new file mode 100644
index 00000000000..1bf8916b3d0
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import headerCi from '~/vue_shared/components/header_ci_component.vue';
+
+describe('Header CI Component', () => {
+ let HeaderCi;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderCi = Vue.extend(headerCi);
+
+ props = {
+ status: {
+ group: 'failed',
+ icon: 'ci-status-failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ itemName: 'job',
+ itemId: 123,
+ time: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ actions: [
+ {
+ label: 'Retry',
+ path: 'path',
+ type: 'button',
+ cssClass: 'btn',
+ },
+ {
+ label: 'Go',
+ path: 'path',
+ type: 'link',
+ cssClass: 'link',
+ },
+ ],
+ };
+
+ vm = new HeaderCi({
+ propsData: props,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render status badge', () => {
+ expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
+ expect(
+ vm.$el.querySelector('.ci-failed').getAttribute('href'),
+ ).toEqual(props.status.details_path);
+ });
+
+ it('should render item name and id', () => {
+ expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ });
+
+ it('should render timeago date', () => {
+ expect(vm.$el.querySelector('time')).toBeDefined();
+ });
+
+ it('should render user icon and name', () => {
+ expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ });
+
+ it('should render provided actions', () => {
+ expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
+ expect(vm.$el.querySelector('.link').tagName).toEqual('A');
+ expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
+ expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
new file mode 100644
index 00000000000..4bbaff561fc
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -0,0 +1,121 @@
+import Vue from 'vue';
+import fieldComponent from '~/vue_shared/components/markdown/field.vue';
+
+describe('Markdown field component', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = new Vue({
+ render(createElement) {
+ return createElement(
+ fieldComponent,
+ {
+ props: {
+ markdownPreviewUrl: '/preview',
+ markdownDocs: '/docs',
+ },
+ },
+ [
+ createElement('textarea', {
+ slot: 'textarea',
+ }),
+ ],
+ );
+ },
+ });
+ });
+
+ it('creates a new instance of GL form', (done) => {
+ spyOn(gl, 'GLForm');
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ gl.GLForm,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ describe('mounted', () => {
+ beforeEach((done) => {
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders textarea inside backdrop', () => {
+ expect(
+ vm.$el.querySelector('.zen-backdrop textarea'),
+ ).not.toBeNull();
+ });
+
+ describe('markdown preview', () => {
+ let previewLink;
+
+ beforeEach(() => {
+ spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ body: '<p>markdown preview</p>',
+ };
+ },
+ });
+ }));
+
+ previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a');
+ });
+
+ it('sets preview link as active', (done) => {
+ previewLink.click();
+
+ Vue.nextTick(() => {
+ expect(
+ previewLink.parentNode.classList.contains('active'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('shows preview loading text', (done) => {
+ previewLink.click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.md-preview').textContent.trim(),
+ ).toContain('Loading...');
+
+ done();
+ });
+ });
+
+ it('renders markdown preview', (done) => {
+ previewLink.click();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.md-preview').innerHTML,
+ ).toContain('<p>markdown preview</p>');
+
+ done();
+ });
+ });
+
+ it('renders GFM with jQuery', (done) => {
+ spyOn($.fn, 'renderGFM');
+ previewLink.click();
+
+ setTimeout(() => {
+ expect(
+ $.fn.renderGFM,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
new file mode 100644
index 00000000000..7110ff36937
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import headerComponent from '~/vue_shared/components/markdown/header.vue';
+
+describe('Markdown field header component', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(headerComponent);
+
+ vm = new Component({
+ propsData: {
+ previewMarkdown: false,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders markdown buttons', () => {
+ expect(
+ vm.$el.querySelectorAll('.js-md').length,
+ ).toBe(7);
+ });
+
+ it('renders `write` link as active when previewMarkdown is false', () => {
+ expect(
+ vm.$el.querySelector('li:nth-child(1)').classList.contains('active'),
+ ).toBeTruthy();
+ });
+
+ it('renders `preview` link as active when previewMarkdown is true', (done) => {
+ vm.previewMarkdown = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('li:nth-child(2)').classList.contains('active'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('emits toggle markdown event when clicking preview', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('li:nth-child(2) a').click();
+
+ expect(
+ vm.$emit,
+ ).toHaveBeenCalledWith('toggle-markdown');
+ });
+
+ it('blurs preview link after click', (done) => {
+ const link = vm.$el.querySelector('li:nth-child(2) a');
+ spyOn(HTMLElement.prototype, 'blur');
+
+ link.click();
+
+ setTimeout(() => {
+ expect(
+ link.blur,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
new file mode 100644
index 00000000000..bf28019ef24
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import '~/lib/utils/datetime_utility';
+
+describe('Time ago with tooltip component', () => {
+ let TimeagoTooltip;
+ let vm;
+
+ beforeEach(() => {
+ TimeagoTooltip = Vue.extend(timeagoTooltip);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render timeago with a bootstrap tooltip', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ },
+ }).$mount();
+
+ expect(vm.$el.tagName).toEqual('TIME');
+ expect(vm.$el.classList.contains('js-timeago')).toEqual(true);
+ expect(
+ vm.$el.getAttribute('data-original-title'),
+ ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));
+ expect(vm.$el.getAttribute('data-placement')).toEqual('top');
+
+ const timeago = gl.utils.getTimeago();
+
+ expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z'));
+ });
+
+ it('should render tooltip placed in bottom', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ tooltipPlacement: 'bottom',
+ },
+ }).$mount();
+
+ expect(vm.$el.getAttribute('data-placement')).toEqual('bottom');
+ });
+
+ it('should render short format class', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ shortFormat: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.classList.contains('js-short-timeago')).toEqual(true);
+ });
+
+ it('should render provided html class', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ cssClass: 'foo',
+ },
+ }).$mount();
+
+ expect(vm.$el.classList.contains('foo')).toEqual(true);
+ });
+});