Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

fix($timeout): verifyNoPendingTasks() throws even for non-timeout pending tasks #14336

Closed
gkalpak opened this issue Mar 29, 2016 · 12 comments
Closed

Comments

@gkalpak
Copy link
Member

gkalpak commented Mar 29, 2016

Do you want to request a feature or report a bug?

Bug

What is the current behavior?

ngMock's $timeout.verifyNoPendingTasks() throws if there are deferred tasks that are not related to $timeout.
Reproduction

What is the expected behavior?

ngMock's $timeout.verifyNoPendingTasks() should not throw if there are deferred tasks that are not related to $timeout.

What is the motivation / use case for changing the behavior?

It makes it difficult/unintuitive to test code that uses $timeout wrt pending tasks.

Which versions of Angular, and which browser / OS are affected by this issue? Did this work in previous versions of Angular? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.

Angular versions: 1.4 and 1.5 (those I tested with, but should affect oldrer versions too)
Browsers: All

Other information (e.g. stacktraces, related issues, suggestions how to fix)

When this method was introduced (back in f0c6ebc), $timeout was probably the only service that used $browser.defer(), so all $browser.deferredFns came from $timeout (thus an non-empty array, meant pending timeouts).
This is no longer the case. E.g. $browser.defer() is used in $evalAsync() and $applyAsync(), which means it transitively used throughout Angular (in watchers, directives etc).

@gkalpak
Copy link
Member Author

gkalpak commented Mar 29, 2016

This has been broken for so long, that fixing it should probably be considered a breaking change 😞

@mbarakaja
Copy link

Hello @gkalpak

I made a demo to reproduce the error.

So, if I flush a queued function with $timeout and then that queued function makes an HTTP request, which I have to flush with $httpBackend as well, to satisfy the HTTP request expectation in my test, $timeout.verifyNoPendingTasks() fails. If I don't flush the HTTP request, it passes.

I don't know how ngMock works internally, but is seen that a call to $httpBackend.flush(); adds an extra task that $timeout.verifyNoPendingTasks() thinks is related to a $timeout queued function.

@gkalpak
Copy link
Member Author

gkalpak commented Mar 29, 2016

@mbarakaja, thx for the demo. So, it is the same issue after all (an unexpected, non-timeout related deferred task, created after $httpBackend.flush()).

@petebacondarwin
Copy link
Contributor

The problem in the reproduction by @mbarakaja is that the call to $http.post creates a $q deferred object that in turn adds a new item to the $browser pending tasks queue. It seems wrong to me that the call to $http.flush() does not remove this pending task from the $browser queue.

I think we need to find a way to segment calls to $browser.defer in ngMock so that we can then verify only particular kinds of pending tasks.

@rbevers
Copy link

rbevers commented May 9, 2016

That sounds good, @petebacondarwin . I understand it has been this way a long time, @gkalpak, but it still eats dev time when tests involve a combination of $http and $timeout.

I was dealing with an asynch select directive the other day that required both of these mocked in a test and banged my head testing all the cases. I was glad to see this reported already.

@eraye1
Copy link

eraye1 commented Dec 14, 2016

Running into this on 1.5.9. :(

@jackyliang
Copy link

I am curious if you ever fixed this issue? I am running into (what I think) is a similar issue. If I use applyAsync instead of timeout, call verifyNoPendingTask(), then flush, the test would pass as if applyAsync is the same as timeout. Not quite sure how to get around this issue either.

@scottohara
Copy link

This issue started causing failures in my test suite when upgrading between 1.6.1 and 1.6.2.

I suspect that it may be due to #14159, which now adds a pending task for any pending route resolves.

Although I don't have a repro to prove this yet, looking at the set of items listed in the 1.6.2 changelog, I'm basing my suspicion on the fact that $route: make asynchronous tasks count as pending requests change sounds like it could be related, given the similar comments above about $http.post creating pending task.

What I can confirm is that if I don't load the ui.router module in my test suite, the test that checks $timeout.verifyNoPendingTasks() starts passing again. Curiously, even without any $stateProvider states configured (i.e. no routes, and nothing to resolve), the error still occurs if ui.router is loaded.

If, as discussed above, $timeout.verifyNoPendingTasks() is unable to distinguish between these pending resolve tasks and $timeout tasks, then this issue will potentially start to impact more users as they upgrade to 1.6.2 and beyond.

For now, I'm working around the issue by performing an extra preventative $timeout.flush() before my failing test runs.

@gkalpak
Copy link
Member Author

gkalpak commented Mar 12, 2017

The $route: make asynchronous tasks count as pending requests part does not affect ui.router, only ngRoute. So, it must be something else that is causing the new behavior.

@scottohara
Copy link

The $route: make asynchronous tasks count as pending requests part does not affect ui.router, only ngRoute

@gkalpak That is true.

However looking at the commit, there were also some changes made to ngMock/angular-mocks.js; specifically to:

  • $browser.$$incOutstandingRequestCount() (previously a noop)
  • $browser.$$completeOutstandingRequest() (previously a noop)
  • $browser.notifyWhenNoOutstandingRequests()

So it is not only ngRoute that is affected by the change, but also ngMock.

Perhaps ui.router was already doing something similar to what ngRoute now does (i.e. calling $browser.$$incOutstandingRequestCount()), and since the function is no longer a noop in 1.6.2, the problem arises?

I'll keep looking to see if I can pinpoint what is creating the pending tasks.

@gkalpak
Copy link
Member Author

gkalpak commented Mar 12, 2017

These are private methods and external libraries should not be using or relying on them. If ui.router does, then it might indeed cause your tests to break. If it doesn't, then it must be something else.

@scottohara
Copy link

OK, thanks @gkalpak. I think I've tracked down where the pending task is being created.

The issue seems to be that ui.router.state.$state.$get() immediately creates a rejected promise (which it will later return if it determines the state being transitioned to is no longer current):

function $get($rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) {
    var TransitionSupersededError = new Error('transition superseded');
    var TransitionSuperseded = silenceUncaughtInPromise($q.reject(TransitionSupersededError));
    ...
}   

It is the call to $q.reject that creates a pending task, i.e.

qFactory.reject() -> qFactory.rejectPromise() -> qFactory.$$reject() -> scheduleProcessQueue(promise.$$state)

scheduleProcessQueue(state) checks if state.status === 2, and if so calls nextTick(processChecks) -> $rootScope.$evalAsync() -> $browser.defer() -> which pushes an item onto $browser.deferredFns making it’s length > 0...hence a pending task.

I'll refer this issue to the ui.router project, and check if they are aware that they're inadvertently creating a pending task by setting up a rejected promise in their provider's $get() function.

@Narretz Narretz added this to the 1.7.x milestone Apr 12, 2018
@petebacondarwin petebacondarwin self-assigned this May 14, 2018
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jun 16, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to angular#14336.
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jun 27, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jun 27, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to angular#14336.
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jun 27, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 2, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to angular#14336.
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 2, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 2, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 7, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to angular#14336.
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 7, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 10, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to angular#14336.
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jul 10, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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
gkalpak added a commit that referenced this issue Jul 13, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to #14336.
gkalpak added a commit that referenced this issue Jul 13, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to #14336.
gkalpak added a commit that referenced this issue Jul 13, 2018

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
`$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 #14336
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.