fix(core): Add an error state for ChangeDetectors that is set when bindings or lifecycle events throw exceptions and prevents further detection.

- Changes the `alreadyChecked` flag of AbstractChangeDetector to a new `state` flag.
- Changes all checks of alreadyChecked to check that the state is NeverChecked.
- Set state to Errored if an error is thrown during detection.
- Skip change detection for a detector and its children when the state is Errored.
- Add a test to validate this fixes issue #4323.

Closes #4953
This commit is contained in:
Alex Rickabaugh
2015-10-27 10:55:01 -07:00
parent c930a533d1
commit d1b54d6807
12 changed files with 110 additions and 27 deletions

View File

@ -431,10 +431,12 @@ export function main() {
describe('updating directives', () => {
var directive1;
var directive2;
var directive3;
beforeEach(() => {
directive1 = new TestDirective();
directive2 = new TestDirective();
directive3 = new TestDirective(null, null, true);
});
it('should happen directly, without invoking the dispatcher', () => {
@ -510,6 +512,30 @@ export function main() {
expect(directive1.onInitCalled).toBe(false);
});
it('should not call onInit again if it throws', () => {
var cd = _createWithoutHydrate('directiveOnInit').changeDetector;
cd.hydrate(_DEFAULT_CONTEXT, null, new FakeDirectives([directive3], []), null);
var errored = false;
// First pass fails, but onInit should be called.
try {
cd.detectChanges();
} catch (e) {
errored = true;
}
expect(errored).toBe(true);
expect(directive3.onInitCalled).toBe(true);
directive3.onInitCalled = false;
// Second change detection also fails, but this time onInit should not be called.
try {
cd.detectChanges();
} catch (e) {
throw new BaseException("Second detectChanges() should not have run detection.");
}
expect(directive3.onInitCalled).toBe(false);
});
});
describe('afterContentInit', () => {
@ -1336,13 +1362,19 @@ class TestDirective {
afterViewCheckedCalled = false;
event;
constructor(public afterContentCheckedSpy = null, public afterViewCheckedSpy = null) {}
constructor(public afterContentCheckedSpy = null, public afterViewCheckedSpy = null,
public throwOnInit = false) {}
onEvent(event) { this.event = event; }
doCheck() { this.doCheckCalled = true; }
onInit() { this.onInitCalled = true; }
onInit() {
this.onInitCalled = true;
if (this.throwOnInit) {
throw "simulated onInit failure";
}
}
onChanges(changes) {
var r = {};