fix(view): remove dynamic components when the parent view is dehydrated

Also adds a bunch of unit tests for affected parts.

Fixes #1201
This commit is contained in:
Tobias Bosch
2015-04-14 18:01:44 -07:00
parent 6ecaa9aebb
commit 213dabdceb
16 changed files with 813 additions and 117 deletions
+131 -19
View File
@@ -1,25 +1,69 @@
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
dispatchEvent,
expect,
iit,
inject,
beforeEachBindings,
it,
xit,
SpyObject, proxy
} from 'angular2/test_lib';
import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {ViewFactory} from 'angular2/src/render/dom/view/view_factory';
import {RenderProtoView} from 'angular2/src/render/dom/view/proto_view';
import {RenderView} from 'angular2/src/render/dom/view/view';
import {ElementBinder} from 'angular2/src/render/dom/view/element_binder';
import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy';
import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom'
import {EventManager} from 'angular2/src/render/dom/events/event_manager';
export function main() {
function createViewFactory({capacity}):ViewFactory {
return new ViewFactory(capacity, null, null);
}
function createPv() {
return new RenderProtoView({
element: el('<div></div>'),
isRootView: false,
elementBinders: []
});
}
describe('RenderViewFactory', () => {
var eventManager;
var shadowDomStrategy;
function createViewFactory({capacity}):ViewFactory {
return new ViewFactory(capacity, eventManager, shadowDomStrategy);
}
function createProtoView(rootEl=null, binders=null) {
if (isBlank(rootEl)) {
rootEl = el('<div></div>');
}
if (isBlank(binders)) {
binders = [];
}
return new RenderProtoView({
element: rootEl,
isRootView: false,
elementBinders: binders
});
}
function createComponentElBinder(componentId, nestedProtoView = null) {
var binder = new ElementBinder({
componentId: componentId,
textNodeIndices: []
});
binder.nestedProtoView = nestedProtoView;
return binder;
}
beforeEach( () => {
eventManager = new SpyEventManager();
shadowDomStrategy = new SpyShadowDomStrategy();
});
it('should create views', () => {
var pv = createPv();
var pv = createProtoView();
var vf = createViewFactory({
capacity: 1
});
@@ -29,8 +73,8 @@ export function main() {
describe('caching', () => {
it('should support multiple RenderProtoViews', () => {
var pv1 = createPv();
var pv2 = createPv();
var pv1 = createProtoView();
var pv2 = createProtoView();
var vf = createViewFactory({ capacity: 2 });
var view1 = vf.getView(pv1);
var view2 = vf.getView(pv2);
@@ -42,7 +86,7 @@ export function main() {
});
it('should reuse the newest view that has been returned', () => {
var pv = createPv();
var pv = createProtoView();
var vf = createViewFactory({ capacity: 2 });
var view1 = vf.getView(pv);
var view2 = vf.getView(pv);
@@ -53,7 +97,7 @@ export function main() {
});
it('should not add views when the capacity has been reached', () => {
var pv = createPv();
var pv = createProtoView();
var vf = createViewFactory({ capacity: 2 });
var view1 = vf.getView(pv);
var view2 = vf.getView(pv);
@@ -68,5 +112,73 @@ export function main() {
});
describe('child components', () => {
var vf, log;
beforeEach(() => {
vf = createViewFactory({capacity: 1});
log = [];
shadowDomStrategy.spy('attachTemplate').andCallFake( (el, view) => {
ListWrapper.push(log, ['attachTemplate', el, view]);
});
shadowDomStrategy.spy('constructLightDom').andCallFake( (lightDomView, shadowDomView, el) => {
ListWrapper.push(log, ['constructLightDom', lightDomView, shadowDomView, el]);
return new SpyLightDom();
});
});
it('should create static child component views', () => {
var hostPv = createProtoView(el('<div><div class="ng-binding"></div></div>'), [
createComponentElBinder(
'someComponent',
createProtoView()
)
]);
var hostView = vf.getView(hostPv);
var shadowView = hostView.componentChildViews[0];
expect(shadowView).toBeTruthy();
expect(hostView.lightDoms[0]).toBeTruthy();
expect(log[0]).toEqual(['constructLightDom', hostView, shadowView, hostView.boundElements[0]]);
expect(log[1]).toEqual(['attachTemplate', hostView.boundElements[0], shadowView]);
});
it('should not create dynamic child component views', () => {
var hostPv = createProtoView(el('<div><div class="ng-binding"></div></div>'), [
createComponentElBinder(
'someComponent',
null
)
]);
var hostView = vf.getView(hostPv);
var shadowView = hostView.componentChildViews[0];
expect(shadowView).toBeFalsy();
expect(hostView.lightDoms[0]).toBeFalsy();
expect(log).toEqual([]);
});
});
});
}
@proxy
@IMPLEMENTS(EventManager)
class SpyEventManager extends SpyObject {
constructor(){super(EventManager);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
@proxy
@IMPLEMENTS(ShadowDomStrategy)
class SpyShadowDomStrategy extends SpyObject {
constructor(){super(ShadowDomStrategy);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
@proxy
@IMPLEMENTS(LightDom)
class SpyLightDom extends SpyObject {
constructor(){super(LightDom);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
+164 -49
View File
@@ -1,53 +1,161 @@
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
import {ListWrapper} from 'angular2/src/facade/collection';
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
dispatchEvent,
expect,
iit,
inject,
beforeEachBindings,
it,
xit,
SpyObject, proxy
} from 'angular2/test_lib';
import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang';
import {RenderProtoView} from 'angular2/src/render/dom/view/proto_view';
import {ElementBinder} from 'angular2/src/render/dom/view/element_binder';
import {RenderView} from 'angular2/src/render/dom/view/view';
import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy';
import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom';
import {EventManager} from 'angular2/src/render/dom/events/event_manager';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
function createView() {
var proto = new RenderProtoView({element: el('<div></div>'), isRootView: false, elementBinders: []});
var rootNodes = [el('<div></div>')];
var boundTextNodes = [];
var boundElements = [el('<div></div>')];
var viewContainers = [];
var contentTags = [];
var eventManager = null;
return new RenderView(proto, rootNodes,
boundTextNodes, boundElements, viewContainers, contentTags, eventManager);
}
function createShadowDomStrategy(log) {
return new FakeShadowDomStrategy(log);
}
describe('RenderView', () => {
var log, strategy;
var shadowDomStrategy;
var eventManager;
function createProtoView({rootEl, binders}={}) {
if (isBlank(rootEl)) {
rootEl = el('<div></div>');
}
if (isBlank(binders)) {
binders = [];
}
return new RenderProtoView({
element: rootEl,
isRootView: false,
elementBinders: binders
});
}
function createComponentElBinder(componentId, nestedProtoView = null) {
var binder = new ElementBinder({
componentId: componentId,
textNodeIndices: []
});
binder.nestedProtoView = nestedProtoView;
return binder;
}
function createHostProtoView(nestedProtoView) {
return createProtoView({
binders: [
createComponentElBinder(
'someComponent',
nestedProtoView
)
]
});
}
function createEmptyView() {
var root = el('<div><div></div></div>');
return new RenderView(createProtoView(), [DOM.childNodes(root)[0]],
[], [], [], [], eventManager);
}
function createHostView(pv, shadowDomView) {
var view = new RenderView(pv, [el('<div></div>')],
[], [el('<div></div>')], [], [], eventManager);
view.setComponentView(shadowDomStrategy, 0, shadowDomView);
return view;
}
beforeEach( () => {
log = [];
strategy = createShadowDomStrategy(log);
eventManager = new SpyEventManager();
shadowDomStrategy = new SpyShadowDomStrategy();
shadowDomStrategy.spy('constructLightDom').andCallFake( (lightDomView, shadowDomView, el) => {
return new SpyLightDom();
});
});
describe('setComponentView', () => {
it('should redistribute when a component is added to a hydrated view', () => {
var hostView = createView();
var childView = createView();
var shadowView = new SpyRenderView();
var hostPv = createHostProtoView(createProtoView());
var hostView = createHostView(hostPv, shadowView);
hostView.hydrate(null);
hostView.setComponentView(strategy, 0, childView);
expect(log[0]).toEqual(['redistribute']);
hostView.setComponentView(shadowDomStrategy, 0, shadowView);
var lightDomSpy:SpyLightDom = hostView.lightDoms[0];
expect(lightDomSpy.spy('redistribute')).toHaveBeenCalled();
});
it('should not redistribute when a component is added to a dehydrated view', () => {
var hostView = createView();
var childView = createView();
hostView.setComponentView(strategy, 0, childView);
expect(log).toEqual([]);
var shadowView = new SpyRenderView();
var hostPv = createHostProtoView(createProtoView());
var hostView = createHostView(hostPv, shadowView);
hostView.setComponentView(shadowDomStrategy, 0, shadowView);
var lightDomSpy:SpyLightDom = hostView.lightDoms[0];
expect(lightDomSpy.spy('redistribute')).not.toHaveBeenCalled();
});
});
describe('hydrate', () => {
it('should hydrate existing child components', () => {
var hostPv = createHostProtoView(createProtoView());
var shadowView = new SpyRenderView();
var hostView = createHostView(hostPv, shadowView);
hostView.hydrate(null);
expect(shadowView.spy('hydrate')).toHaveBeenCalled();
});
});
describe('dehydrate', () => {
var hostView;
function createAndHydrate(nestedProtoView, shadowView) {
var hostPv = createHostProtoView(nestedProtoView);
hostView = createHostView(hostPv, shadowView);
hostView.hydrate(null);
}
it('should dehydrate child components', () => {
var shadowView = new SpyRenderView();
createAndHydrate(createProtoView(), shadowView);
hostView.dehydrate();
expect(shadowView.spy('dehydrate')).toHaveBeenCalled();
});
it('should not clear static child components', () => {
var shadowView = createEmptyView();
createAndHydrate(createProtoView(), shadowView);
hostView.dehydrate();
expect(hostView.componentChildViews[0]).toBe(shadowView);
expect(shadowView.rootNodes[0].parentNode).toBeTruthy();
});
it('should clear dynamic child components', () => {
var shadowView = createEmptyView();
createAndHydrate(null, shadowView);
hostView.dehydrate();
expect(hostView.componentChildViews[0]).toBe(null);
expect(shadowView.rootNodes[0].parentNode).toBe(null);
});
});
@@ -55,24 +163,31 @@ export function main() {
});
}
class FakeShadowDomStrategy extends ShadowDomStrategy {
log;
constructor(log) {
super();
this.log = log;
}
constructLightDom(lightDomView:RenderView, shadowDomView:RenderView, element): LightDom {
return new FakeLightDom(this.log, lightDomView, shadowDomView, element);
}
@proxy
@IMPLEMENTS(EventManager)
class SpyEventManager extends SpyObject {
constructor(){super(EventManager);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
@proxy
@IMPLEMENTS(ShadowDomStrategy)
class SpyShadowDomStrategy extends SpyObject {
constructor(){super(ShadowDomStrategy);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
@proxy
@IMPLEMENTS(LightDom)
class SpyLightDom extends SpyObject {
constructor(){super(LightDom);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
@proxy
@IMPLEMENTS(RenderView)
class SpyRenderView extends SpyObject {
constructor(){super(RenderView);}
noSuchMethod(m){return super.noSuchMethod(m)}
}
class FakeLightDom extends LightDom {
log;
constructor(log, lightDomView:RenderView, shadowDomView:RenderView, element) {
super(lightDomView, shadowDomView, element);
this.log = log;
}
redistribute() {
ListWrapper.push(this.log, ['redistribute']);
}
}