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

fix(ngEventDirs): check for $rootScope.$$phase in event handler and don't $apply if already in $digest #16629

Merged
merged 2 commits into from
Jul 25, 2018

Conversation

Narretz
Copy link
Contributor

@Narretz Narretz commented Jul 13, 2018

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

What is the current behavior? (You can also link to an open issue here)

What is the new behavior (if this is a feature change)?

Does this PR introduce a breaking change?

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

Other information:

@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

1 similar comment
@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

$rootScope.$apply(function() {
element.triggerHandler('click');
});
}).toThrowError('listener error');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test surprises me. AFAICT the callback in the $apply should be called wrapped in a try-catch block. I see that errors in the $digest() call get re-thrown; so perhaps the click() method on the scope is being called in the digest?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this error would be caught in the $apply phase try/catch 🤔

Also I think we could use another test which invokes the handler within a watcher, so it would be within the $digest phase

But note that in both of these cases it does not re-throw, so why does it in this test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't confirmed, but I suspect it is because $exceptionHandler throws by default in tests. In order to test the "real" behavior, you have to set $exceptionHandler in log mode.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, that sounds right. I guess we should have tests for both cases?

Copy link
Contributor

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for that surprising test, this LGTM

scope.$evalAsync(callback);
} else {
scope.$apply(callback);
callback();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we are not wrapping this in try { ... } catch (err) { $exceptionHandler(Err); }?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should make the following more consistent...

dom.click();
otherStuff();

... when dom.click() invokes ng-click and does vs does-not throw. SGTM...

@Narretz
Copy link
Contributor Author

Narretz commented Jul 17, 2018

I've updated the PR and added the additional try catch ... however, I think it's not actually necessary. See the two new tests: when we trigger an event handler in a watch expression fn, the error thrown gets logged twice because the watch loop in the digest never sees the error.

If we simply throw in the watch expression fn, the watch loop in the digest catches the error and logs it (only once).

At least it feels wrong that this leads to two different results.

$rootScope.$apply(function() {
element.triggerHandler('click');
});
}).toThrowError('listener error');
}));
$rootScope.do();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking this would be one line up, within the $apply. Or maybe it would be more clear if we did:

$rootScope.do = function() {
    element.triggerHandler('click');
    $log.log('done');
}

Then invoke $rootScope.do within vs outside $apply. Those 2 cases should be consistent, both with $exceptionHandler throwing (the done never runs) vs only logging (the done always runs). I think this commit is required for those to be consistent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if we do

      $rootScope.do = function() {
          element.triggerHandler('click');
          $log.log('done');
        };

        $rootScope.$apply(function() {
          $rootScope.do();
        });

then we need the try-catch.
I guess the question is now for which behavior it's more important to be consistent? Because the try catch changes the behavior when we throw inside a digest. I assume that behavior is not as important.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show an example of that change?

Copy link
Contributor Author

@Narretz Narretz Jul 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the tests

});


it('should not stop the digest when the event is fired in a watch expression fn', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbedard if you trigger a click event handler inside a watch expression fn, then the additional try catch changes how the error gets logged, and in this case, anything that comes after the handler is called, whereas when you call the handler directly and throw, what comes after the handler is not called. See the test above for the different outcome.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is caused by the click() vs triggerHandler('click')? I didn't think that would change anything...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's caused by the new try catch. To clarify, ij the watcher expression fn tests I test direct call to scope fn against event handler call to scope fn. The first test doesn't use the event handler at all. It's a bit of a digression from the original testing but it's an example where the new try catch leads to different behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the watch expression fn test set up like you intended it to be? Because you mentioned that you think such a test would make sense.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... I understand these two tests now. Sorry I was misreading $rootScope.click() as $element.click() for some reason (my fault 😅). I think this difference is ok, definitely worth the two do() tests behaving the same imo.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test is correct and good as-is 👍

@@ -1,6 +1,6 @@
'use strict';

describe('event directives', function() {
fdescribe('event directives', function() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now, but making a note of it...

});
});

it('should not stop the digest when the handler is called in a watch expression fn', function() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this one is necessary since it doesn't actually trigger a DOM event

@jbedard
Copy link
Collaborator

jbedard commented Jul 18, 2018

LGTM

Could potentially also test dom.click() (the native click method) in addition to the the jQuery version. I think they should behave the same.

});


it('should not stop the digest when the handler throws an error', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description seems wrong.

Copy link
Contributor Author

@Narretz Narretz Jul 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what are we actually testing? "it should pass an error in the handler to the $exceptionHandler if the handler event is triggered called during a digest"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, we are testing what happens when the event is triggered outside a digest. I would expected something along the lines:

should not stop normal execution when the handler is triggered outside a digest and throws an error

});
});

it('should not stop the digest when the handler is called in a watch expression fn', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this test (and the ones below) test that the digest is not stopped? I would expect a second watch expression.
(What am I missing?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, not correct. It tests if the second part of the watch expression fn gets called after the event handler throws

};

$rootScope.$watch(function() {
$rootScope.click();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean element.triggerHandler('click')?
If yes, wouldn't this test be identical with the following one?
If no, then what does this test have to do with event handlers?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's what the next test is for, which is why I don't think this one is necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test exists because I was looking for something that would behave differently when adding the try ... catch in the event directive.

});
});

describe('click', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have similar test for forceAsyncEvents? If not, we should 😁

@Narretz Narretz force-pushed the fix-phase branch 2 times, most recently from a489fda to b389161 Compare July 23, 2018 10:06
markgardner and others added 2 commits July 23, 2018 19:31
…n $digest

Digest cycle already in progress error can inadvertently be caused when triggering an
element's click event while within an active digest cycle. This is due to the ngEventsDirs
event handler always calling $rootScope.$apply regardless of the status of $rootScope.$$phase.
Checking the phase and calling the function immediately if within an active digest cycle
will prevent the problem without reducing current functionality.

Closes angular#14673
Closes angular#14674
…nt was triggered in a digest

This ensures that the error handling is the same for events triggered inside and outside a digest.
@Narretz Narretz merged commit a42f8a0 into angular:master Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants