summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/build.js97
-rw-r--r--app/assets/javascripts/dispatcher.js.es63
-rw-r--r--app/assets/stylesheets/pages/builds.scss22
-rw-r--r--app/helpers/builds_helper.rb10
-rw-r--r--app/views/projects/builds/show.html.haml28
-rw-r--r--package.json1
-rw-r--r--spec/javascripts/.eslintrc11
-rw-r--r--spec/javascripts/build_spec.js.es6175
-rw-r--r--spec/javascripts/fixtures/build.html.haml57
9 files changed, 325 insertions, 79 deletions
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index d63125ce44b..5133e361001 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -8,56 +8,55 @@
Build.state = null;
function Build(options) {
- this.page_url = options.page_url;
- this.build_url = options.build_url;
- this.build_status = options.build_status;
+ options = options || $('.js-build-options').data();
+ this.pageUrl = options.pageUrl;
+ this.buildUrl = options.buildUrl;
+ this.buildStatus = options.buildStatus;
this.state = options.state1;
- this.build_stage = options.build_stage;
- this.hideSidebar = bind(this.hideSidebar, this);
- this.toggleSidebar = bind(this.toggleSidebar, this);
+ this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
+
this.initSidebar();
+ this.$buildScroll = $('#js-build-scroll');
- this.populateJobs(this.build_stage);
- this.updateStageDropdownText(this.build_stage);
+ this.populateJobs(this.buildStage);
+ this.updateStageDropdownText(this.buildStage);
+ this.sidebarOnResize();
- $(window).off('resize.build').on('resize.build', this.hideSidebar);
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
- $('#js-build-scroll > a').off('click').on('click', this.stepTrace);
+ $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
+ $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
- this.initScrollButtons();
+ this.initScrollButtonAffix();
}
- if (this.build_status === "running" || this.build_status === "pending") {
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Bind autoscroll button to follow build output
$('#autoscroll-button').on('click', function() {
var state;
state = $(this).data("state");
if ("enabled" === state) {
$(this).data("state", "disabled");
- return $(this).text("enable autoscroll");
+ return $(this).text("Enable autoscroll");
} else {
$(this).data("state", "enabled");
- return $(this).text("disable autoscroll");
+ return $(this).text("Disable autoscroll");
}
- //
- // Bind autoscroll button to follow build output
- //
});
Build.interval = setInterval((function(_this) {
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
return function() {
- if (window.location.href.split("#").first() === _this.page_url) {
+ if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
- //
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- //
})(this), 4000);
}
}
@@ -72,20 +71,23 @@
top: this.sidebarTranslationLimits.max
});
this.$sidebar.niceScroll();
- this.hideSidebar();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
};
+ Build.prototype.location = function() {
+ return window.location.href.split("#")[0];
+ };
+
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
return $.ajax({
- url: this.build_url,
+ url: this.buildUrl,
dataType: 'json',
- success: function(build_data) {
- $('.js-build-output').html(build_data.trace_html);
- if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
+ success: function(buildData) {
+ $('.js-build-output').html(buildData.trace_html);
+ if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
@@ -94,7 +96,7 @@
Build.prototype.getBuildTrace = function() {
return $.ajax({
- url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
+ url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
@@ -108,8 +110,8 @@
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
- } else if (log.status !== _this.build_status) {
- return Turbolinks.visit(_this.page_url);
+ } else if (log.status !== _this.buildStatus) {
+ return Turbolinks.visit(_this.pageUrl);
}
};
})(this)
@@ -122,12 +124,11 @@
}
};
- Build.prototype.initScrollButtons = function() {
- var $body, $buildScroll, $buildTrace;
- $buildScroll = $('#js-build-scroll');
+ Build.prototype.initScrollButtonAffix = function() {
+ var $body, $buildTrace;
$body = $('body');
$buildTrace = $('#build-trace');
- return $buildScroll.affix({
+ return this.$buildScroll.affix({
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
@@ -136,18 +137,12 @@
});
};
- Build.prototype.shouldHideSidebar = function() {
+ Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
- Build.prototype.toggleSidebar = function() {
- if (this.shouldHideSidebar()) {
- return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
- }
- };
-
Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
@@ -156,12 +151,20 @@
});
};
- Build.prototype.hideSidebar = function() {
- if (this.shouldHideSidebar()) {
- return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- }
+ Build.prototype.toggleSidebar = function(shouldHide) {
+ var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+ this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
+ .toggleClass('right-sidebar-collapsed', shouldHide);
+ };
+
+ Build.prototype.sidebarOnResize = function() {
+ this.toggleSidebar(this.shouldHideSidebarForViewport());
+ };
+
+ Build.prototype.sidebarOnClick = function() {
+ if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 8e4fd1f19ba..756a24cc0fc 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -29,6 +29,9 @@
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:builds:show':
+ new Build();
+ break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6300ac9662f..f1d311cabbe 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -14,18 +14,10 @@
}
}
- .autoscroll-container {
- position: fixed;
- bottom: 20px;
- right: 20px;
- z-index: 100;
- }
-
.scroll-controls {
- &.affix-top {
- position: absolute;
- top: 10px;
- right: 25px;
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
}
&.affix-bottom {
@@ -34,13 +26,13 @@
}
&.affix {
- right: 30px;
+ right: 25px;
bottom: 15px;
z-index: 1;
+ }
- @media (min-width: $screen-md-min) {
- right: 26%;
- }
+ &.sidebar-expanded {
+ right: #{$gutter_width + ($gl-padding * 2)};
}
a {
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index f3aaff9140d..fde297c588e 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -5,4 +5,14 @@ module BuildsHelper
build_class += ' retried' if build.retried?
build_class
end
+
+ def javascript_build_options
+ {
+ page_url: namespace_project_build_url(@project.namespace, @project, @build),
+ build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
+ build_status: @build.status,
+ build_stage: @build.stage,
+ state1: @build.trace_with_state[:state]
+ }
+ end
end
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b5e8b0bf6eb..ae7a7ecb392 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
-- trace_with_state = @build.trace_with_state
- header_title project_title(@project, "Builds", project_builds_path(@project))
= render "projects/pipelines/head", build_subnav: true
@@ -28,32 +27,27 @@
Runners page
.prepend-top-default
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ .scroll-step
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
+ Enable autoscroll
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
- #down-build-trace
+ #down-build-trace
= render "sidebar"
- :javascript
- new Build({
- page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
- build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
- build_status: "#{@build.status}",
- build_stage: "#{@build.stage}",
- state1: "#{trace_with_state[:state]}"
- })
+.js-build-options{ data: javascript_build_options }
diff --git a/package.json b/package.json
index a303c9c1eac..e75e070451b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"eslint-config-airbnb": "^12.0.0",
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.0.1",
+ "eslint-plugin-jasmine": "^1.8.1",
"eslint-plugin-jsx-a11y": "^2.2.3",
"eslint-plugin-react": "^6.4.1"
}
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
new file mode 100644
index 00000000000..90388929612
--- /dev/null
+++ b/spec/javascripts/.eslintrc
@@ -0,0 +1,11 @@
+{
+ "plugins": ["jasmine"],
+ "env": {
+ "jasmine": true
+ },
+ "extends": "plugin:jasmine/recommended",
+ "rules": {
+ "prefer-arrow-callback": 0,
+ "func-names": 0
+ }
+}
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
new file mode 100644
index 00000000000..370944b6a8c
--- /dev/null
+++ b/spec/javascripts/build_spec.js.es6
@@ -0,0 +1,175 @@
+/* global Build */
+/* eslint-disable no-new */
+//= require build
+//= require breakpoints
+//= require jquery.nicescroll
+//= require turbolinks
+
+(() => {
+ describe('Build', () => {
+ fixture.preload('build.html');
+
+ beforeEach(function () {
+ fixture.load('build.html');
+ spyOn($, 'ajax');
+ });
+
+ describe('constructor', () => {
+ beforeEach(function () {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ describe('setup', function () {
+ beforeEach(function () {
+ this.build = new Build();
+ });
+
+ it('copies build options', function () {
+ expect(this.build.pageUrl).toBe('http://example.com/root/test-build/builds/2');
+ expect(this.build.buildUrl).toBe('http://example.com/root/test-build/builds/2.json');
+ expect(this.build.buildStatus).toBe('passed');
+ expect(this.build.buildStage).toBe('test');
+ expect(this.build.state).toBe('buildstate');
+ });
+
+ it('only shows the jobs matching the current stage', function () {
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('selects the current stage in the build dropdown menu', function () {
+ expect($('.stage-selection').text()).toBe('test');
+ });
+
+ it('updates the jobs when the build dropdown changes', function () {
+ $('.stage-item:contains("build")').click();
+
+ expect($('.stage-selection').text()).toBe('build');
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+ });
+
+ describe('initial build trace', function () {
+ beforeEach(function () {
+ new Build();
+ });
+
+ it('displays the initial build trace', function () {
+ expect($.ajax.calls.count()).toBe(1);
+ const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
+ expect(url).toBe('http://example.com/root/test-build/builds/2.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', function () {
+ 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', function () {
+ beforeEach(function () {
+ $('.js-build-options').data('buildStatus', 'running');
+ this.build = new Build();
+ spyOn(this.build, 'location')
+ .and.returnValue('http://example.com/root/test-build/builds/2');
+ });
+
+ it('updates the build trace on an interval', function () {
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(2);
+ let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
+ expect(url).toBe(
+ 'http://example.com/root/test-build/builds/2/trace.json?state=buildstate'
+ );
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>Update<span>',
+ status: 'running',
+ state: 'newstate',
+ append: true,
+ });
+
+ 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);
+ [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
+ expect(url).toBe(
+ 'http://example.com/root/test-build/builds/2/trace.json?state=newstate'
+ );
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.build.state).toBe('finalstate');
+ });
+
+ it('replaces the entire build trace', function () {
+ jasmine.clock().tick(4001);
+ let [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Update</span>',
+ status: 'running',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ jasmine.clock().tick(4001);
+ [{ success, context }] = $.ajax.calls.argsFor(2);
+ success.call(context, {
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ });
+
+ 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', function () {
+ spyOn(Turbolinks, 'visit');
+
+ jasmine.clock().tick(4001);
+ const [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Final</span>',
+ status: 'passed',
+ append: true,
+ });
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(
+ 'http://example.com/root/test-build/builds/2'
+ );
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/fixtures/build.html.haml b/spec/javascripts/fixtures/build.html.haml
new file mode 100644
index 00000000000..a2bc81c6be7
--- /dev/null
+++ b/spec/javascripts/fixtures/build.html.haml
@@ -0,0 +1,57 @@
+.build-page
+ .prepend-top-default
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ #js-build-scroll.scroll-controls
+ %a.btn{href: '#build-trace'}
+ %i.fa.fa-angle-up
+ %a.btn{href: '#down-build-trace'}
+ %i.fa.fa-angle-down
+ %pre.build-trace#build-trace
+ %code.bash.js-build-output
+ %i.fa.fa-refresh.fa-spin.js-build-refresh
+
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+ .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
+ Build
+ %strong #1
+ %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
+ %i.fa.fa-angle-double-right
+ .blocks-container
+ .dropdown.build-dropdown
+ .title Stage
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.stage-selection More
+ %i.fa.fa-caret-down
+ %ul.dropdown-menu
+ %li
+ %a.stage-item build
+ %li
+ %a.stage-item test
+ %li
+ %a.stage-item deploy
+ .builds-container
+ .build-job{data: {stage: 'build'}}
+ %a{href: 'http://example.com/root/test-build/builds/1'}
+ %i.fa.fa-check
+ %i.fa.fa-check-circle-o
+ %span
+ Setup
+ .build-job{data: {stage: 'test'}}
+ %a{href: 'http://example.com/root/test-build/builds/2'}
+ %i.fa.fa-check
+ %i.fa.fa-check-circle-o
+ %span
+ Tests
+ .build-job{data: {stage: 'deploy'}}
+ %a{href: 'http://example.com/root/test-build/builds/3'}
+ %i.fa.fa-check
+ %i.fa.fa-check-circle-o
+ %span
+ Deploy
+
+.js-build-options{ data: { page_url: 'http://example.com/root/test-build/builds/2',
+ build_url: 'http://example.com/root/test-build/builds/2.json',
+ build_status: 'passed',
+ build_stage: 'test',
+ state1: 'buildstate' }}