Skip to content

Commit

Permalink
feat(ngMock): add $flushPendingTasks() and $verifyNoPendingTasks()
Browse files Browse the repository at this point in the history
`$flushPendingTasks([delay])` allows flushing all pending tasks (or up
to a specific delay). This includes `$timeout`s, `$q` promises and tasks
scheduled via `$rootScope.$applyAsync()` and `$rootScope.$evalAsync()`.
(ATM, it only flushes tasks scheduled via `$browser.defer()`, which does
not include `$http` requests and `$route` transitions.)

`$verifyNoPendingTasks([taskType])` allows verifying that there are no
pending tasks (in general or of a specific type). This includes tasks
flushed by `$flushPendingTasks()` as well as pending `$http` requests
and in-progress `$route` transitions.

Background:
`ngMock/$timeout` has `flush()` and `verifyNoPendingTasks()` methods,
but they take all kinds of tasks into account which is confusing. For
example, `$timeout.verifyNoPendingTasks()` can fail (even if there are
no pending timeouts) because of an unrelated pending `$http` request.

This behavior is retained for backwards compatibility, but the new
methods are more generic (and thus less confusing) and also allow
more fine-grained control (when appropriate).

Closes angular#14336
  • Loading branch information
gkalpak committed Jul 13, 2018
1 parent 411e354 commit b14f67f
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 7 deletions.
116 changes: 113 additions & 3 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ angular.mock.$Browser = function($log, $$taskTrackerFactory) {
* @description
* Flushes all pending requests and executes the defer callbacks.
*
* See {@link ngMock.$flushPendingsTasks} for more info.
*
* @param {number=} number of milliseconds to flush. See {@link #defer.now}
*/
self.defer.flush = function(delay) {
Expand Down Expand Up @@ -155,7 +157,9 @@ angular.mock.$Browser = function($log, $$taskTrackerFactory) {
* Verifies that there are no pending tasks that need to be flushed.
* You can check for a specific type of tasks only, by specifying a `taskType`.
*
* @param {string=} taskType - The type task to check for.
* See {@link $verifyNoPendingTasks} for more info.
*
* @param {string=} taskType - The type tasks to check for.
*/
self.defer.verifyNoPendingTasks = function(taskType) {
var pendingTasks = !taskType
Expand Down Expand Up @@ -212,6 +216,82 @@ angular.mock.$Browser.prototype = {
}
};

/**
* @ngdoc function
* @name $flushPendingTasks
*
* @description
* Flushes all currently pending tasks and executes the corresponding callbacks.
*
* Optionally, you can also pass a `delay` argument to only flush tasks that are scheduled to be
* executed within `delay` milliseconds. Currently, `delay` only applies to timeouts, since all
* other tasks have a delay of 0 (i.e. they are scheduled to be executed as soon as possible, but
* still asynchronously).
*
* If no delay is specified, it uses a delay such that all currently pending tasks are flushed.
*
* The types of tasks that are flushed include:
*
* - Pending timeouts (via {@link $timeout}).
* - Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}.
* - Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
*
* <div class="alert alert-info">
* Periodic tasks scheduled via {@link $interval} use a different queue and are not flushed by
* `$flushPendingTasks()`. Use {@link ngMock.$interval#flush $interval.flush([millis])} instead.
* </div>
*
* @param {number=} delay - The number of milliseconds to flush.
*/
angular.mock.$FlushPendingTasksProvider = function() {
this.$get = [
'$browser',
function($browser) {
return function $flushPendingTasks(delay) {
return $browser.defer.flush(delay);
};
}
];
};

/**
* @ngdoc function
* @name $verifyNoPendingTasks
*
* @description
* Verifies that there are no pending tasks that need to be flushed. It throws an error if there are
* still pending tasks.
*
* You can check for a specific type of tasks only, by specifying a `taskType`.
*
* Available task types:
*
* - `$timeout`: Pending timeouts (via {@link $timeout}).
* - `$http`: Pending HTTP requests (via {@link $http}).
* - `$route`: In-progress route transitions (via {@link $route}).
* - `$applyAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}.
* - `$evalAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
*
* <div class="alert alert-info">
* Periodic tasks scheduled via {@link $interval} use a different queue and are not taken into
* account by `$verifyNoPendingTasks()`. There is currently no way to verify that there are no
* pending {@link $interval} tasks.
* </div>
*
* @param {string=} taskType - The type of tasks to check for.
*/
angular.mock.$VerifyNoPendingTasksProvider = function() {
this.$get = [
'$browser',
function($browser) {
return function $verifyNoPendingTasks(taskType) {
return $browser.defer.verifyNoPendingTasks(taskType);
};
}
];
};

/**
* @ngdoc provider
Expand Down Expand Up @@ -2179,6 +2259,15 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $
*
* Flushes the queue of pending tasks.
*
* _This method is essentially an alias of {@link ngMock.$flushPendingTasks}._
*
* <div class="alert alert-warning">
* For historical reasons, this method will also flush non-`$timeout` pending tasks, such as
* {@link $q} promises and tasks scheduled via
* {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and
* {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
* </div>
*
* @param {number=} delay maximum timeout amount to flush up until
*/
$delegate.flush = function(delay) {
Expand All @@ -2193,7 +2282,26 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $
* @name $timeout#verifyNoPendingTasks
* @description
*
* Verifies that there are no pending tasks that need to be flushed.
* Verifies that there are no pending tasks that need to be flushed. It throws an error if there
* are still pending tasks.
*
* _This method is essentially an alias of {@link ngMock.$verifyNoPendingTasks} (called with no
* arguments)._
*
* <div class="alert alert-warning">
* <p>
* For historical reasons, this method will also verify non-`$timeout` pending tasks, such as
* pending {@link $http} requests, in-progress {@link $route} transitions, unresolved
* {@link $q} promises and tasks scheduled via
* {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and
* {@link ng.$rootScope.Scope#$evalAsync $evalAsync}.
* </p>
* <p>
* It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally
* supports verifying a specific type of tasks. For example, you can verify there are no
* pending timeouts with `$verifyNoPendingTasks('$timeout')`.
* </p>
* </div>
*/
$delegate.verifyNoPendingTasks = function() {
// For historical reasons, `$timeout.verifyNoPendingTasks()` takes all types of pending tasks
Expand Down Expand Up @@ -2422,7 +2530,9 @@ angular.module('ngMock', ['ng']).provider({
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
$rootElement: angular.mock.$RootElementProvider,
$componentController: angular.mock.$ComponentControllerProvider
$componentController: angular.mock.$ComponentControllerProvider,
$flushPendingTasks: angular.mock.$FlushPendingTasksProvider,
$verifyNoPendingTasks: angular.mock.$VerifyNoPendingTasksProvider
}).config(['$provide', '$compileProvider', function($provide, $compileProvider) {
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
Expand Down
101 changes: 97 additions & 4 deletions test/ngMock/angular-mocksSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,16 +626,17 @@ describe('ngMock', function() {

it('should flush delayed', function() {
browser.defer(logFn('A'));
browser.defer(logFn('B'), 10, 'taskType');
browser.defer(logFn('C'), 20);
browser.defer(logFn('B'), 0, 'taskTypeB');
browser.defer(logFn('C'), 10, 'taskTypeC');
browser.defer(logFn('D'), 20);
expect(log).toEqual('');
expect(browser.defer.now).toEqual(0);

browser.defer.flush(0);
expect(log).toEqual('A;');
expect(log).toEqual('A;B;');

browser.defer.flush();
expect(log).toEqual('A;B;C;');
expect(log).toEqual('A;B;C;D;');
});

it('should defer and flush over time', function() {
Expand Down Expand Up @@ -663,6 +664,62 @@ describe('ngMock', function() {
it('should not throw an exception when passing a specific delay', function() {
expect(function() {browser.defer.flush(100);}).not.toThrow();
});

describe('tasks scheduled during flushing', function() {
it('should be flushed if they do not exceed the target delay (when no delay specified)',
function() {
browser.defer(function() {
logFn('1')();
browser.defer(function() {
logFn('3')();
browser.defer(logFn('4'), 1);
}, 2);
}, 1);
browser.defer(function() {
logFn('2')();
browser.defer(logFn('6'), 4);
}, 2);
browser.defer(logFn('5'), 5);

browser.defer.flush(0);
expect(browser.defer.now).toEqual(0);
expect(log).toEqual('');

browser.defer.flush();
expect(browser.defer.now).toEqual(5);
expect(log).toEqual('1;2;3;4;5;');
}
);

it('should be flushed if they do not exceed the specified delay',
function() {
browser.defer(function() {
logFn('1')();
browser.defer(function() {
logFn('3')();
browser.defer(logFn('4'), 1);
}, 2);
}, 1);
browser.defer(function() {
logFn('2')();
browser.defer(logFn('6'), 4);
}, 2);
browser.defer(logFn('5'), 5);

browser.defer.flush(0);
expect(browser.defer.now).toEqual(0);
expect(log).toEqual('');

browser.defer.flush(4);
expect(browser.defer.now).toEqual(4);
expect(log).toEqual('1;2;3;4;');

browser.defer.flush(6);
expect(browser.defer.now).toEqual(10);
expect(log).toEqual('1;2;3;4;5;6;');
}
);
});
});

describe('defer.cancel', function() {
Expand Down Expand Up @@ -811,6 +868,42 @@ describe('ngMock', function() {
});


describe('$flushPendingTasks', function() {
var $flushPendingTasks;
var browserDeferFlushSpy;

beforeEach(inject(function($browser, _$flushPendingTasks_) {
$flushPendingTasks = _$flushPendingTasks_;
browserDeferFlushSpy = spyOn($browser.defer, 'flush').and.returnValue('flushed');
}));

it('should delegate to `$browser.defer.flush()`', function() {
var result = $flushPendingTasks(42);

expect(browserDeferFlushSpy).toHaveBeenCalledOnceWith(42);
expect(result).toBe('flushed');
});
});


describe('$verifyNoPendingTasks', function() {
var $verifyNoPendingTasks;
var browserDeferVerifySpy;

beforeEach(inject(function($browser, _$verifyNoPendingTasks_) {
$verifyNoPendingTasks = _$verifyNoPendingTasks_;
browserDeferVerifySpy = spyOn($browser.defer, 'verifyNoPendingTasks').and.returnValue('verified');
}));

it('should delegate to `$browser.defer.verifyNoPendingTasks()`', function() {
var result = $verifyNoPendingTasks('fortyTwo');

expect(browserDeferVerifySpy).toHaveBeenCalledOnceWith('fortyTwo');
expect(result).toBe('verified');
});
});


describe('$exceptionHandler', function() {
it('should rethrow exceptions', inject(function($exceptionHandler) {
expect(function() { $exceptionHandler('myException'); }).toThrow('myException');
Expand Down

0 comments on commit b14f67f

Please sign in to comment.