test(change detect): Port more change detect tests

Move more change detector unit tests to exercise the Dart pre-generated
change detectors in addition to `dynamic` and `JIT` change detectors.

See #502
This commit is contained in:
Tim Blasi 2015-06-08 15:23:27 -07:00
parent 7611f92f5b
commit 0409b4ca49
2 changed files with 318 additions and 341 deletions

View File

@ -61,8 +61,7 @@ export function main() {
describe(`${cdType} Change Detector`, () => { describe(`${cdType} Change Detector`, () => {
function _getProtoChangeDetector(def: ChangeDetectorDefinition) { function _getProtoChangeDetector(def: ChangeDetectorDefinition, registry = null) {
var registry = null;
switch (cdType) { switch (cdType) {
case 'dynamic': case 'dynamic':
return new DynamicProtoChangeDetector(registry, def); return new DynamicProtoChangeDetector(registry, def);
@ -75,15 +74,20 @@ export function main() {
} }
} }
function _bindSimpleValue(expression: string, context = _DEFAULT_CONTEXT) { function _createChangeDetector(expression: string, context = _DEFAULT_CONTEXT,
registry = null) {
var dispatcher = new TestDispatcher(); var dispatcher = new TestDispatcher();
var testDef = getDefinition(expression); var testDef = getDefinition(expression);
var protoCd = _getProtoChangeDetector(testDef.cdDef); var protoCd = _getProtoChangeDetector(testDef.cdDef, registry);
var cd = protoCd.instantiate(dispatcher); var cd = protoCd.instantiate(dispatcher);
cd.hydrate(context, testDef.locals, null); cd.hydrate(context, testDef.locals, null);
cd.detectChanges(); return new _ChangeDetectorAndDispatcher(cd, dispatcher);
return dispatcher.log; }
function _bindSimpleValue(expression: string, context = _DEFAULT_CONTEXT) {
var val = _createChangeDetector(expression, context);
val.changeDetector.detectChanges();
return val.dispatcher.log;
} }
it('should support literals', it('should support literals',
@ -235,7 +239,104 @@ export function main() {
expect(_bindSimpleValue('a.sayHi("Jim")', td)).toEqual(['propName=Hi, Jim']); expect(_bindSimpleValue('a.sayHi("Jim")', td)).toEqual(['propName=Hi, Jim']);
}); });
describe("Locals", () => { it('should do simple watching', () => {
var person = new Person('misko');
var val = _createChangeDetector('name', person);
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=misko']);
val.dispatcher.clear();
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual([]);
val.dispatcher.clear();
person.name = 'Misko';
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=Misko']);
});
it('should support literal array', () => {
var val = _createChangeDetector('[1, 2]');
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues).toEqual([[1, 2]]);
val = _createChangeDetector('[1, a]', new TestData(2));
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues).toEqual([[1, 2]]);
});
it('should support literal maps', () => {
var val = _createChangeDetector('{z: 1}');
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues[0]['z']).toEqual(1);
val = _createChangeDetector('{z: a}', new TestData(1));
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues[0]['z']).toEqual(1);
});
// TODO(kegluneq): Insert it('should support interpolation', ...) testcase.
describe('change notification', () => {
describe('simple checks', () => {
it('should pass a change record to the dispatcher', () => {
var person = new Person('bob');
var val = _createChangeDetector('name', person);
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues).toEqual(['bob']);
});
});
describe('pipes', () => {
it('should pass a change record to the dispatcher', () => {
var registry = new FakePipeRegistry('pipe', () => new CountingPipe());
var person = new Person('bob');
var val = _createChangeDetector('name | pipe', person, registry);
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues).toEqual(['bob state:0']);
});
});
// TODO(kegluneq): Insert describe('change notification', ...) testcases.
});
// TODO(kegluneq): Insert describe('reading directives', ...) testcases.
describe('enforce no new changes', () => {
it('should throw when a record gets changed after it has been checked', () => {
var val = _createChangeDetector('a', new TestData('value'));
expect(() => { val.changeDetector.checkNoChanges(); })
.toThrowError(new RegExp(
'Expression [\'"]a in location[\'"] has changed after it was checked'));
});
it('should not break the next run', () => {
var val = _createChangeDetector('a', new TestData('value'));
expect(() => val.changeDetector.checkNoChanges())
.toThrowError(new RegExp(
'Expression [\'"]a in location[\'"] has changed after it was checked.'));
val.changeDetector.detectChanges();
expect(val.dispatcher.loggedValues).toEqual(['value']);
});
});
// TODO vsavkin: implement it
describe('error handling', () => {
xit('should wrap exceptions into ChangeDetectionError', () => {
var val = _createChangeDetector('invalidProp');
try {
val.changeDetector.detectChanges();
throw new BaseException('fail');
} catch (e) {
expect(e).toBeAnInstanceOf(ChangeDetectionError);
expect(e.location).toEqual('invalidProp in someComponent');
}
});
});
describe('Locals', () => {
it('should read a value from locals', it('should read a value from locals',
() => { expect(_bindSimpleValue('valueFromLocals')).toEqual(['propName=value']); }); () => { expect(_bindSimpleValue('valueFromLocals')).toEqual(['propName=value']); });
@ -245,10 +346,10 @@ export function main() {
it('should handle nested locals', it('should handle nested locals',
() => { expect(_bindSimpleValue('nestedLocals')).toEqual(['propName=value']); }); () => { expect(_bindSimpleValue('nestedLocals')).toEqual(['propName=value']); });
it("should fall back to a regular field read when the locals map" + it('should fall back to a regular field read when the locals map' +
"does not have the requested field", 'does not have the requested field',
() => { () => {
expect(_bindSimpleValue('fallbackLocals', new Person("Jim"))) expect(_bindSimpleValue('fallbackLocals', new Person('Jim')))
.toEqual(['propName=Jim']); .toEqual(['propName=Jim']);
}); });
@ -262,6 +363,200 @@ export function main() {
.toEqual(['propName=MTV']); .toEqual(['propName=MTV']);
}); });
}); });
describe('handle children', () => {
var parent, child;
beforeEach(() => {
parent = _createChangeDetector('10').changeDetector;
child = _createChangeDetector('"str"').changeDetector;
});
it('should add light dom children', () => {
parent.addChild(child);
expect(parent.lightDomChildren.length).toEqual(1);
expect(parent.lightDomChildren[0]).toBe(child);
});
it('should add shadow dom children', () => {
parent.addShadowDomChild(child);
expect(parent.shadowDomChildren.length).toEqual(1);
expect(parent.shadowDomChildren[0]).toBe(child);
});
it('should remove light dom children', () => {
parent.addChild(child);
parent.removeChild(child);
expect(parent.lightDomChildren).toEqual([]);
});
it('should remove shadow dom children', () => {
parent.addShadowDomChild(child);
parent.removeShadowDomChild(child);
expect(parent.shadowDomChildren.length).toEqual(0);
});
});
// TODO(kegluneq): Insert describe('mode', ...) testcases.
describe('markPathToRootAsCheckOnce', () => {
function changeDetector(mode, parent) {
var val = _createChangeDetector('10');
val.changeDetector.mode = mode;
if (isPresent(parent)) parent.addChild(val.changeDetector);
return val.changeDetector;
}
it('should mark all checked detectors as CHECK_ONCE until reaching a detached one', () => {
var root = changeDetector(CHECK_ALWAYS, null);
var disabled = changeDetector(DETACHED, root);
var parent = changeDetector(CHECKED, disabled);
var checkAlwaysChild = changeDetector(CHECK_ALWAYS, parent);
var checkOnceChild = changeDetector(CHECK_ONCE, checkAlwaysChild);
var checkedChild = changeDetector(CHECKED, checkOnceChild);
checkedChild.markPathToRootAsCheckOnce();
expect(root.mode).toEqual(CHECK_ALWAYS);
expect(disabled.mode).toEqual(DETACHED);
expect(parent.mode).toEqual(CHECK_ONCE);
expect(checkAlwaysChild.mode).toEqual(CHECK_ALWAYS);
expect(checkOnceChild.mode).toEqual(CHECK_ONCE);
expect(checkedChild.mode).toEqual(CHECK_ONCE);
});
});
describe('hydration', () => {
it('should be able to rehydrate a change detector', () => {
var cd = _createChangeDetector('name').changeDetector;
cd.hydrate('some context', null, null);
expect(cd.hydrated()).toBe(true);
cd.dehydrate();
expect(cd.hydrated()).toBe(false);
cd.hydrate('other context', null, null);
expect(cd.hydrated()).toBe(true);
});
it('should destroy all active pipes during dehyration', () => {
var pipe = new OncePipe();
var registry = new FakePipeRegistry('pipe', () => pipe);
var cd = _createChangeDetector('name | pipe', new Person('bob'), registry).changeDetector;
cd.detectChanges();
cd.dehydrate();
expect(pipe.destroyCalled).toBe(true);
});
it('should throw when detectChanges is called on a dehydrated detector', () => {
var context = new Person('Bob');
var val = _createChangeDetector('name', context);
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=Bob']);
val.changeDetector.dehydrate();
var dehydratedException = new DehydratedException();
expect(() => {val.changeDetector.detectChanges()})
.toThrowError(dehydratedException.toString());
expect(val.dispatcher.log).toEqual(['propName=Bob']);
});
});
describe('pipes', () => {
it('should support pipes', () => {
var registry = new FakePipeRegistry('pipe', () => new CountingPipe());
var ctx = new Person('Megatron');
var val = _createChangeDetector('name | pipe', ctx, registry);
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=Megatron state:0']);
val.dispatcher.clear();
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=Megatron state:1']);
});
it('should lookup pipes in the registry when the context is not supported', () => {
var registry = new FakePipeRegistry('pipe', () => new OncePipe());
var ctx = new Person('Megatron');
var cd = _createChangeDetector('name | pipe', ctx, registry).changeDetector;
cd.detectChanges();
expect(registry.numberOfLookups).toEqual(1);
ctx.name = 'Optimus Prime';
cd.detectChanges();
expect(registry.numberOfLookups).toEqual(2);
});
it('should invoke onDestroy on a pipe before switching to another one', () => {
var pipe = new OncePipe();
var registry = new FakePipeRegistry('pipe', () => pipe);
var ctx = new Person('Megatron');
var cd = _createChangeDetector('name | pipe', ctx, registry).changeDetector;
cd.detectChanges();
ctx.name = 'Optimus Prime';
cd.detectChanges();
expect(pipe.destroyCalled).toEqual(true);
});
it('should inject the ChangeDetectorRef ' +
'of the encompassing component into a pipe',
() => {
var registry = new FakePipeRegistry('pipe', () => new IdentityPipe());
var cd =
_createChangeDetector('name | pipe', new Person('bob'), registry).changeDetector;
cd.detectChanges();
expect(registry.cdRef).toBe(cd.ref);
});
});
it('should do nothing when no change', () => {
var registry = new FakePipeRegistry('pipe', () => new IdentityPipe());
var ctx = new Person('Megatron');
var val = _createChangeDetector('name | pipe', ctx, registry);
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=Megatron']);
val.dispatcher.clear();
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual([]);
});
it('should unwrap the wrapped value', () => {
var registry = new FakePipeRegistry('pipe', () => new WrappedPipe());
var ctx = new Person('Megatron');
var val = _createChangeDetector('name | pipe', ctx, registry);
val.changeDetector.detectChanges();
expect(val.dispatcher.log).toEqual(['propName=Megatron']);
});
}); });
}); });
@ -320,46 +615,6 @@ export function main() {
beforeEach(() => { dispatcher = new TestDispatcher(); }); beforeEach(() => { dispatcher = new TestDispatcher(); });
it('should do simple watching', () => {
var person = new Person("misko");
var c = createChangeDetector('name', 'name', person);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.log).toEqual(['name=misko']);
dispatcher.clear();
cd.detectChanges();
expect(dispatcher.log).toEqual([]);
dispatcher.clear();
person.name = "Misko";
cd.detectChanges();
expect(dispatcher.log).toEqual(['name=Misko']);
});
it("should support literal array", () => {
var c = createChangeDetector('array', '[1,2]');
c["changeDetector"].detectChanges();
expect(c["dispatcher"].loggedValues).toEqual([[1, 2]]);
c = createChangeDetector('array', '[1,a]', new TestData(2));
c["changeDetector"].detectChanges();
expect(c["dispatcher"].loggedValues).toEqual([[1, 2]]);
});
it("should support literal maps", () => {
var c = createChangeDetector('map', '{z:1}');
c["changeDetector"].detectChanges();
expect(c["dispatcher"].loggedValues[0]['z']).toEqual(1);
c = createChangeDetector('map', '{z:a}', new TestData(1));
c["changeDetector"].detectChanges();
expect(c["dispatcher"].loggedValues[0]['z']).toEqual(1);
});
it("should support interpolation", () => { it("should support interpolation", () => {
var ast = parser.parseInterpolation("B{{a}}A", "location"); var ast = parser.parseInterpolation("B{{a}}A", "location");
var pcd = createProtoChangeDetector([BindingRecord.createForElement(ast, 0, "memo")]); var pcd = createProtoChangeDetector([BindingRecord.createForElement(ast, 0, "memo")]);
@ -373,34 +628,6 @@ export function main() {
}); });
describe("change notification", () => { describe("change notification", () => {
describe("simple checks", () => {
it("should pass a change record to the dispatcher", () => {
var person = new Person('bob');
var c = createChangeDetector('name', 'name', person);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.loggedValues).toEqual(['bob']);
});
});
describe("pipes", () => {
it("should pass a change record to the dispatcher", () => {
var registry = new FakePipeRegistry('pipe', () => new CountingPipe());
var person = new Person('bob');
var c = createChangeDetector('name', 'name | pipe', person, registry);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.loggedValues).toEqual(['bob state:0']);
});
});
describe("updating directives", () => { describe("updating directives", () => {
var dirRecord1 = new DirectiveRecord({ var dirRecord1 = new DirectiveRecord({
directiveIndex: new DirectiveIndex(0, 0), directiveIndex: new DirectiveIndex(0, 0),
@ -651,95 +878,6 @@ export function main() {
expect(dispatcher.loggedValues).toEqual(['aaa']); expect(dispatcher.loggedValues).toEqual(['aaa']);
}); });
}); });
describe("enforce no new changes", () => {
it("should throw when a record gets changed after it has been checked", () => {
var pcd =
createProtoChangeDetector([BindingRecord.createForElement(ast("a"), 0, "a")]);
var dispatcher = new TestDispatcher();
var cd = pcd.instantiate(dispatcher);
cd.hydrate(new TestData('value'), null, null);
expect(() => { cd.checkNoChanges(); })
.toThrowError(
new RegExp("Expression 'a in location' has changed after it was checked"));
});
it("should not break the next run", () => {
var pcd =
createProtoChangeDetector([BindingRecord.createForElement(ast("a"), 0, "a")]);
var dispatcher = new TestDispatcher();
var cd = pcd.instantiate(dispatcher);
cd.hydrate(new TestData('value'), null, null);
expect(() => cd.checkNoChanges())
.toThrowError(
new RegExp("Expression 'a in location' has changed after it was checked."));
cd.detectChanges();
expect(dispatcher.loggedValues).toEqual(['value']);
});
});
// TODO vsavkin: implement it
describe("error handling", () => {
xit("should wrap exceptions into ChangeDetectionError", () => {
var pcd = createProtoChangeDetector();
var cd = pcd.instantiate(
new TestDispatcher(),
[BindingRecord.createForElement(ast("invalidProp"), 0, "a")], null, []);
cd.hydrate(_DEFAULT_CONTEXT, null);
try {
cd.detectChanges();
throw new BaseException("fail");
} catch (e) {
expect(e).toBeAnInstanceOf(ChangeDetectionError);
expect(e.location).toEqual("invalidProp in someComponent");
}
});
});
describe("handle children", () => {
var parent, child;
beforeEach(() => {
parent = createProtoChangeDetector([]).instantiate(null);
child = createProtoChangeDetector([]).instantiate(null);
});
it("should add light dom children", () => {
parent.addChild(child);
expect(parent.lightDomChildren.length).toEqual(1);
expect(parent.lightDomChildren[0]).toBe(child);
});
it("should add shadow dom children", () => {
parent.addShadowDomChild(child);
expect(parent.shadowDomChildren.length).toEqual(1);
expect(parent.shadowDomChildren[0]).toBe(child);
});
it("should remove light dom children", () => {
parent.addChild(child);
parent.removeChild(child);
expect(parent.lightDomChildren).toEqual([]);
});
it("should remove shadow dom children", () => {
parent.addShadowDomChild(child);
parent.removeShadowDomChild(child);
expect(parent.shadowDomChildren.length).toEqual(0);
});
});
}); });
describe("mode", () => { describe("mode", () => {
@ -848,174 +986,6 @@ export function main() {
}); });
}); });
}); });
describe("markPathToRootAsCheckOnce", () => {
function changeDetector(mode, parent) {
var cd = createProtoChangeDetector([]).instantiate(null);
cd.mode = mode;
if (isPresent(parent)) parent.addChild(cd);
return cd;
}
it("should mark all checked detectors as CHECK_ONCE " + "until reaching a detached one",
() => {
var root = changeDetector(CHECK_ALWAYS, null);
var disabled = changeDetector(DETACHED, root);
var parent = changeDetector(CHECKED, disabled);
var checkAlwaysChild = changeDetector(CHECK_ALWAYS, parent);
var checkOnceChild = changeDetector(CHECK_ONCE, checkAlwaysChild);
var checkedChild = changeDetector(CHECKED, checkOnceChild);
checkedChild.markPathToRootAsCheckOnce();
expect(root.mode).toEqual(CHECK_ALWAYS);
expect(disabled.mode).toEqual(DETACHED);
expect(parent.mode).toEqual(CHECK_ONCE);
expect(checkAlwaysChild.mode).toEqual(CHECK_ALWAYS);
expect(checkOnceChild.mode).toEqual(CHECK_ONCE);
expect(checkedChild.mode).toEqual(CHECK_ONCE);
});
});
describe("hydration", () => {
it("should be able to rehydrate a change detector", () => {
var c = createChangeDetector("memo", "name");
var cd = c["changeDetector"];
cd.hydrate("some context", null, null);
expect(cd.hydrated()).toBe(true);
cd.dehydrate();
expect(cd.hydrated()).toBe(false);
cd.hydrate("other context", null, null);
expect(cd.hydrated()).toBe(true);
});
it("should destroy all active pipes during dehyration", () => {
var pipe = new OncePipe();
var registry = new FakePipeRegistry('pipe', () => pipe);
var c = createChangeDetector("memo", "name | pipe", new Person('bob'), registry);
var cd = c["changeDetector"];
cd.detectChanges();
cd.dehydrate();
expect(pipe.destroyCalled).toBe(true);
});
it("should throw when detectChanges is called on a dehydrated detector", () => {
var context = new Person('Bob');
var c = createChangeDetector("propName", "name", context);
var cd = c["changeDetector"];
var log = c["dispatcher"].log;
cd.detectChanges();
expect(log).toEqual(["propName=Bob"]);
cd.dehydrate();
var dehydratedException = new DehydratedException();
expect(() => {cd.detectChanges()}).toThrowError(dehydratedException.toString());
expect(log).toEqual(["propName=Bob"]);
});
});
describe("pipes", () => {
it("should support pipes", () => {
var registry = new FakePipeRegistry('pipe', () => new CountingPipe());
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, registry);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Megatron state:0']);
dispatcher.clear();
cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Megatron state:1']);
});
it("should lookup pipes in the registry when the context is not supported", () => {
var registry = new FakePipeRegistry('pipe', () => new OncePipe());
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, registry);
var cd = c["changeDetector"];
cd.detectChanges();
expect(registry.numberOfLookups).toEqual(1);
ctx.name = "Optimus Prime";
cd.detectChanges();
expect(registry.numberOfLookups).toEqual(2);
});
it("should invoke onDestroy on a pipe before switching to another one", () => {
var pipe = new OncePipe();
var registry = new FakePipeRegistry('pipe', () => pipe);
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, registry);
var cd = c["changeDetector"];
cd.detectChanges();
ctx.name = "Optimus Prime";
cd.detectChanges();
expect(pipe.destroyCalled).toEqual(true);
});
it("should inject the ChangeDetectorRef " + "of the encompassing component into a pipe",
() => {
var registry = new FakePipeRegistry('pipe', () => new IdentityPipe());
var c = createChangeDetector("memo", "name | pipe", new Person('bob'), registry);
var cd = c["changeDetector"];
cd.detectChanges();
expect(registry.cdRef).toBe(cd.ref);
});
});
it("should do nothing when no change", () => {
var registry = new FakePipeRegistry('pipe', () => new IdentityPipe());
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, registry);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Megatron']);
dispatcher.clear();
cd.detectChanges();
expect(dispatcher.log).toEqual([]);
});
it("should unwrap the wrapped value", () => {
var registry = new FakePipeRegistry('pipe', () => new WrappedPipe());
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, registry);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Megatron']);
});
}); });
}); });
} }
@ -1133,8 +1103,7 @@ class Person {
} }
class Address { class Address {
city: string; constructor(public city: string) {}
constructor(city: string) { this.city = city; }
toString(): string { return isBlank(this.city) ? '-' : this.city } toString(): string { return isBlank(this.city) ? '-' : this.city }
} }
@ -1144,9 +1113,7 @@ class Uninitialized {
} }
class TestData { class TestData {
a; constructor(public a: any) {}
constructor(a) { this.a = a; }
} }
class FakeDirectives { class FakeDirectives {
@ -1179,3 +1146,7 @@ class TestDispatcher extends ChangeDispatcher {
_asString(value) { return (isBlank(value) ? 'null' : value.toString()); } _asString(value) { return (isBlank(value) ? 'null' : value.toString()); }
} }
class _ChangeDetectorAndDispatcher {
constructor(public changeDetector: any, public dispatcher: TestDispatcher) {}
}

View File

@ -108,6 +108,12 @@ var _availableDefinitions = [
'1 > 2 ? 1 : 2', '1 > 2 ? 1 : 2',
'["foo", "bar"][0]', '["foo", "bar"][0]',
'{"foo": "bar"}["foo"]', '{"foo": "bar"}["foo"]',
'name',
'[1, 2]',
'[1, a]',
'{z: 1}',
'{z: a}',
'name | pipe',
'value', 'value',
'a', 'a',
'address.city', 'address.city',