summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-04-07 11:13:23 +0000
committerAlfredo Sumaran <alfredo@gitlab.com>2017-04-11 01:18:29 -0500
commit23a775f6bd7efb61e1c97be8742d35baf7bbfdb4 (patch)
treee1fe88326010292c675e66ae3c2ad1ce598c18fa
parent3b9a4c3827501686a151e25452315b971a06126b (diff)
downloadgitlab-ce-27724-fe-update-trace-handling-code-against-9-0-stable-as.tar.gz
FE: Resolve "Performance issues when processing large build traces with Ansi2html"27724-fe-update-trace-handling-code-against-9-0-stable-as
# Conflicts: # spec/javascripts/build_spec.js
-rw-r--r--app/assets/javascripts/build.js268
-rw-r--r--app/assets/stylesheets/pages/builds.scss32
-rw-r--r--app/views/projects/builds/show.html.haml5
-rw-r--r--spec/javascripts/build_spec.js106
4 files changed, 239 insertions, 172 deletions
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 7cb022c848a..9e7df9ad350 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,24 +1,28 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
+/* eslint-disable func-names, wrap-iife, no-use-before-define,
+consistent-return, prefer-rest-params */
/* global Breakpoints */
-var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-var AUTO_SCROLL_OFFSET = 75;
-var DOWN_BUILD_TRACE = '#down-build-trace';
+const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
+const AUTO_SCROLL_OFFSET = 75;
+const DOWN_BUILD_TRACE = '#down-build-trace';
-window.Build = (function() {
+window.Build = (function () {
Build.timeout = null;
Build.state = null;
function Build(options) {
- options = options || $('.js-build-options').data();
- this.pageUrl = options.pageUrl;
- this.buildUrl = options.buildUrl;
- this.buildStatus = options.buildStatus;
- this.state = options.logState;
- this.buildStage = options.buildStage;
- this.updateDropdown = bind(this.updateDropdown, this);
+ this.options = options || $('.js-build-options').data();
+
+ this.pageUrl = this.options.pageUrl;
+ this.buildUrl = this.options.buildUrl;
+ this.buildStatus = this.options.buildStatus;
+ this.state = this.options.logState;
+ this.buildStage = this.options.buildStage;
this.$document = $(document);
+
+ this.updateDropdown = bind(this.updateDropdown, this);
+
this.$body = $('body');
this.$buildTrace = $('#build-trace');
this.$autoScrollContainer = $('.autoscroll-container');
@@ -29,112 +33,110 @@ window.Build = (function() {
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
+ this.$buildScroll = $('#js-build-scroll');
+ this.$truncatedInfo = $('.js-truncated-info');
clearTimeout(Build.timeout);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar();
- this.$buildScroll = $('#js-build-scroll');
-
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
- 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);
+ 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);
+
this.$document.on('scroll', this.initScrollMonitor.bind(this));
- $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
- $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', 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.initScrollButtonAffix();
- }
+ this.initScrollButtonAffix();
this.invokeBuildTrace();
}
- Build.prototype.initSidebar = function() {
+ Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll();
- this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+ this.$document
+ .off('click', '.js-sidebar-build-toggle')
+ .on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
};
- Build.prototype.location = function() {
- return window.location.href.split("#")[0];
+ Build.prototype.invokeBuildTrace = function () {
+ return this.getBuildTrace();
};
- Build.prototype.invokeBuildTrace = function() {
- var continueRefreshStatuses = ['running', 'pending'];
- // Continue to update build trace when build is running or pending
- if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- Build.timeout = setTimeout((function(_this) {
- return function() {
- if (_this.location() === _this.pageUrl) {
- return _this.getBuildTrace();
- }
- };
- })(this), 4000);
- }
- };
-
- Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
-
+ Build.prototype.getBuildTrace = function () {
return $.ajax({
- url: this.pageUrl + "/trace.json",
+ url: `${this.pageUrl}/trace.json`,
dataType: 'json',
- success: function(buildData) {
- $('.js-build-output').html(buildData.html);
- gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
- if (window.location.hash === DOWN_BUILD_TRACE) {
- $("html,body").scrollTop(this.$buildTrace.height());
+ data: {
+ state: this.state,
+ },
+ success: ((log) => {
+ const $buildContainer = $('.js-build-output');
+
+ if (log.state) {
+ this.state = log.state;
}
- if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
+
+ if (log.append) {
+ $buildContainer.append(log.html);
+ } else {
+ $buildContainer.html(log.html);
+ if (log.truncated) {
+ $('.js-truncated-info-size').html(` ${log.size} `);
+ this.$truncatedInfo.removeClass('hidden');
+ this.initAffixTruncatedInfo();
+ } else {
+ this.$truncatedInfo.addClass('hidden');
+ }
+ }
+
+ this.checkAutoscroll();
+
+ if (!log.complete) {
+ Build.timeout = setTimeout(() => {
+ this.invokeBuildTrace();
+ }, 4000);
+ } else {
this.$buildRefreshAnimation.remove();
- return this.initScrollMonitor();
}
- }.bind(this)
- });
- };
- Build.prototype.getBuildTrace = function() {
- return $.ajax({
- url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
- dataType: "json",
- success: (function(_this) {
- return function(log) {
- var pageUrl;
-
- if (log.state) {
- _this.state = log.state;
- }
- _this.invokeBuildTrace();
- if (log.status === "running") {
- if (log.append) {
- $('.js-build-output').append(log.html);
- } else {
- $('.js-build-output').html(log.html);
- }
- return _this.checkAutoscroll();
- } else if (log.status !== _this.buildStatus) {
- pageUrl = _this.pageUrl;
- if (_this.$autoScrollStatus.data('state') === 'enabled') {
- pageUrl += DOWN_BUILD_TRACE;
- }
-
- return gl.utils.visitUrl(pageUrl);
+ if (log.status !== this.buildStatus) {
+ let pageUrl = this.pageUrl;
+
+ if (this.$autoScrollStatus.data('state') === 'enabled') {
+ pageUrl += DOWN_BUILD_TRACE;
}
- };
- })(this)
+
+ gl.utils.visitUrl(pageUrl);
+ }
+ }),
+ error: () => {
+ this.$buildRefreshAnimation.remove();
+ return this.initScrollMonitor();
+ },
});
};
- Build.prototype.checkAutoscroll = function() {
- if (this.$autoScrollStatus.data("state") === "enabled") {
- return $("html,body").scrollTop(this.$buildTrace.height());
+ Build.prototype.checkAutoscroll = function () {
+ if (this.$autoScrollStatus.data('state') === 'enabled') {
+ return $('html,body').scrollTop(this.$buildTrace.height());
}
// Handle a situation where user started new build
@@ -146,7 +148,7 @@ window.Build = (function() {
}
};
- Build.prototype.initScrollButtonAffix = function() {
+ Build.prototype.initScrollButtonAffix = function () {
// Hide everything initially
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
@@ -167,15 +169,17 @@ window.Build = (function() {
// - Show Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
- Build.prototype.initScrollMonitor = function() {
- if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ Build.prototype.initScrollMonitor = function () {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is somewhere in middle of Build Log
this.$scrollTopBtn.show();
if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
this.$scrollBottomBtn.show();
- } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
+ } else if (this.$buildRefreshAnimation.is(':visible') &&
+ !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
} else {
this.$scrollBottomBtn.hide();
@@ -186,10 +190,13 @@ window.Build = (function() {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else {
- this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollContainer.css({
+ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
+ }).show();
this.$autoScrollStatusText.addClass('animate');
}
- } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is at Top of Build Log
this.$scrollTopBtn.hide();
@@ -197,17 +204,22 @@ window.Build = (function() {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
- } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
- (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
+ gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') &&
+ gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
// User is at Bottom of Build Log
this.$scrollTopBtn.show();
this.$scrollBottomBtn.hide();
// Show and Reposition Autoscroll Status Indicator
- this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollContainer.css({
+ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
+ }).show();
this.$autoScrollStatusText.addClass('animate');
- } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
+ gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// Build Log height is small
this.$scrollTopBtn.hide();
@@ -218,65 +230,81 @@ window.Build = (function() {
this.$autoScrollStatusText.removeClass('animate');
}
- if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ if (this.buildStatus === 'running' || this.buildStatus === 'pending') {
// Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
- this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ this.$autoScrollStatus.data(
+ 'state',
+ gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled',
+ );
}
};
- Build.prototype.shouldHideSidebarForViewport = function() {
- var bootstrapBreakpoint;
- bootstrapBreakpoint = this.bp.getBreakpointSize();
+ Build.prototype.shouldHideSidebarForViewport = function () {
+ const bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
- Build.prototype.toggleSidebar = function(shouldHide) {
- var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ Build.prototype.toggleSidebar = function (shouldHide) {
+ const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
+ this.$truncatedInfo.toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
};
- Build.prototype.sidebarOnResize = function() {
+ Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
};
- Build.prototype.sidebarOnClick = function() {
+ Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
- Build.prototype.updateArtifactRemoveDate = function() {
- var $date, date;
- $date = $('.js-artifacts-remove');
+ Build.prototype.updateArtifactRemoveDate = function () {
+ const $date = $('.js-artifacts-remove');
if ($date.length) {
- date = $date.text();
- return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+ const date = $date.text();
+ return $date.text(
+ gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
+ );
}
};
- Build.prototype.populateJobs = function(stage) {
+ Build.prototype.populateJobs = function (stage) {
$('.build-job').hide();
- $('.build-job[data-stage="' + stage + '"]').show();
+ $(`.build-job[data-stage="${stage}"]`).show();
};
- Build.prototype.updateStageDropdownText = function(stage) {
+ Build.prototype.updateStageDropdownText = function (stage) {
$('.stage-selection').text(stage);
};
- Build.prototype.updateDropdown = function(e) {
+ Build.prototype.updateDropdown = function (e) {
e.preventDefault();
- var stage = e.currentTarget.text;
+ const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
};
- Build.prototype.stepTrace = function(e) {
- var $currentTarget;
+ Build.prototype.stepTrace = function (e) {
e.preventDefault();
- $currentTarget = $(e.currentTarget);
+
+ const $currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
- offset: 0
+ offset: 0,
+ });
+ };
+
+ Build.prototype.initAffixTruncatedInfo = function () {
+ const offsetTop = this.$buildTrace.offset().top;
+
+ this.$truncatedInfo.affix({
+ offset: {
+ top: offsetTop,
+ },
});
};
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index a24292a7c8c..889b257fc73 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -57,6 +57,37 @@
margin-right: 5px;
}
}
+
+ .truncated-info {
+ text-align: center;
+ border-bottom: 1px solid;
+ background-color: $black-transparent;
+ height: 45px;
+
+ &.affix {
+ top: 0;
+ }
+
+ // with sidebar
+ &.affix.sidebar-expanded {
+ right: 312px;
+ left: 22px;
+ }
+
+ // without sidebar
+ &.affix.sidebar-collapsed {
+ right: 20px;
+ left: 20px;
+ }
+
+ &.affix-top {
+ position: absolute;
+ top: 0;
+ margin: 0 auto;
+ right: 5px;
+ left: 5px;
+ }
+ }
}
.scroll-controls {
@@ -186,6 +217,7 @@
white-space: pre;
overflow-x: auto;
font-size: 12px;
+ position: relative;
.fa-refresh {
font-size: 24px;
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 307010edb58..668563a5579 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -71,6 +71,11 @@
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
+ .js-truncated-info.truncated-info.hidden
+ %span<
+ Showing last
+ %span.js-truncated-info-size><
+ KiB of log
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index 2ee22812cf2..edd4b3c1440 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -17,7 +17,7 @@ describe('Build', () => {
spyOn($, 'ajax');
});
- describe('constructor', () => {
+ describe('class constructor', () => {
beforeEach(() => {
jasmine.clock().install();
});
@@ -64,56 +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}/trace.json`,
- );
- expect(dataType).toBe('json');
- expect(success).toEqual(jasmine.any(Function));
-
- success.call(context, { 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));
-
- success.call(context, {
+ 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($, {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
+ complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
@@ -121,17 +98,20 @@ 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));
+ expect($.ajax.calls.count()).toBe(2);
+
+ args = $.ajax.calls.argsFor(1)[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));
- success.call(context, {
+ args.success.call($, {
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
+ complete: true,
});
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
@@ -139,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(1)[0];
+ args.success.call($, {
html: '<span>Different</span>',
status: 'running',
append: false,
@@ -161,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);