diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index cc990d915f..51697086d5 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -325,6 +325,10 @@ export class ElementInjector extends TreeNode { return pb !== _undefined && isPresent(pb); } + forElement(el):boolean { + return this._preBuiltObjects.element.domElement === el; + } + getComponent() { if (this._proto._binding0IsComponent) { return this._obj0; diff --git a/modules/core/src/compiler/shadow_dom_emulation/content_tag.js b/modules/core/src/compiler/shadow_dom_emulation/content_tag.js index f79a63f236..91e62dcb74 100644 --- a/modules/core/src/compiler/shadow_dom_emulation/content_tag.js +++ b/modules/core/src/compiler/shadow_dom_emulation/content_tag.js @@ -2,50 +2,98 @@ import {Decorator} from '../../annotations/annotations'; import {SourceLightDom, DestinationLightDom, LightDom} from './light_dom'; import {Inject} from 'di/di'; import {Element, Node, DOM} from 'facade/dom'; +import {isPresent} from 'facade/lang'; import {List, ListWrapper} from 'facade/collection'; import {NgElement} from 'core/dom/element'; var _scriptTemplate = DOM.createScriptTag('type', 'ng/content') -@Decorator({ - selector: 'content' -}) -export class Content { - _destinationLightDom:LightDom; +class ContentStrategy { + nodes; + insert(nodes:List){} +} - _beginScript:Element; - _endScript:Element; +/** + * An implementation of the content tag that is used by transcluding components. + * It is used when the content tag is not a direct child of another component, + * and thus does not affect redistribution. + */ +class RenderedContent extends ContentStrategy { + beginScript:Element; + endScript:Element; + nodes:List; - select:string; - - constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) { - this._destinationLightDom = destinationLightDom; - - this.select = contentEl.getAttribute('select'); - - this._replaceContentElementWithScriptTags(contentEl.domElement); + constructor(el:Element) { + this._replaceContentElementWithScriptTags(el); + this.nodes = []; } insert(nodes:List) { - DOM.insertAllBefore(this._endScript, nodes); - this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this._endScript : nodes[0]); + this.nodes = nodes; + DOM.insertAllBefore(this.endScript, nodes); + this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]); } _replaceContentElementWithScriptTags(contentEl:Element) { - this._beginScript = DOM.clone(_scriptTemplate); - this._endScript = DOM.clone(_scriptTemplate); + this.beginScript = DOM.clone(_scriptTemplate); + this.endScript = DOM.clone(_scriptTemplate); - DOM.insertBefore(contentEl, this._beginScript); - DOM.insertBefore(contentEl, this._endScript); + DOM.insertBefore(contentEl, this.beginScript); + DOM.insertBefore(contentEl, this.endScript); DOM.removeChild(DOM.parentElement(contentEl), contentEl); } _removeNodesUntil(node:Node) { - var p = DOM.parentElement(this._beginScript); - for (var next = DOM.nextSibling(this._beginScript); + var p = DOM.parentElement(this.beginScript); + for (var next = DOM.nextSibling(this.beginScript); next !== node; - next = DOM.nextSibling(this._beginScript)) { + next = DOM.nextSibling(this.beginScript)) { DOM.removeChild(p, next); } } +} + +/** + * An implementation of the content tag that is used by transcluding components. + * It is used when the content tag is a direct child of another component, + * and thus does not get rendered but only affect the distribution of its parent component. + */ +class IntermediateContent extends ContentStrategy { + destinationLightDom:LightDom; + nodes:List; + + constructor(destinationLightDom:LightDom) { + this.destinationLightDom = destinationLightDom; + this.nodes = []; + } + + insert(nodes:List) { + this.nodes = nodes; + this.destinationLightDom.redistribute(); + } +} + + +@Decorator({ + selector: 'content' +}) +export class Content { + _element:Element; + select:string; + _strategy:ContentStrategy; + + constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) { + this.select = contentEl.getAttribute('select'); + this._strategy = isPresent(destinationLightDom) ? + new IntermediateContent(destinationLightDom) : + new RenderedContent(contentEl.domElement); + } + + nodes():List { + return this._strategy.nodes; + } + + insert(nodes:List) { + this._strategy.insert(nodes); + } } \ No newline at end of file diff --git a/modules/core/src/compiler/shadow_dom_emulation/light_dom.js b/modules/core/src/compiler/shadow_dom_emulation/light_dom.js index 2a3f7febd5..cdf0e7ea22 100644 --- a/modules/core/src/compiler/shadow_dom_emulation/light_dom.js +++ b/modules/core/src/compiler/shadow_dom_emulation/light_dom.js @@ -10,22 +10,37 @@ import {Content} from './content_tag'; export class SourceLightDom {} export class DestinationLightDom {} + +class _Root { + node:Node; + injector:ElementInjector; + + constructor(node, injector) { + this.node = node; + this.injector = injector; + } +} + // TODO: LightDom should implement SourceLightDom and DestinationLightDom // once interfaces are supported export class LightDom { lightDomView:View; shadowDomView:View; - roots:List; + nodes:List; + roots:List<_Root>; constructor(lightDomView:View, shadowDomView:View, element:Element) { this.lightDomView = lightDomView; this.shadowDomView = shadowDomView; - this.roots = DOM.childNodesAsList(element); - DOM.clearNodes(element); + this.nodes = DOM.childNodesAsList(element); + this.roots = null; } redistribute() { - redistributeNodes(this.contentTags(), this.expandedDomNodes()); + var tags = this.contentTags(); + if (isPresent(tags)) { + redistributeNodes(tags, this.expandedDomNodes()); + } } contentTags(): List { @@ -33,7 +48,11 @@ export class LightDom { } _collectAllContentTags(item, acc:List):List { - ListWrapper.forEach(item.elementInjectors, (ei) => { + var eis = item.elementInjectors; + for (var i = 0; i < eis.length; ++i) { + var ei = eis[i]; + if (isBlank(ei)) continue; + if (ei.hasDirective(Content)) { ListWrapper.push(acc, ei.get(Content)); @@ -43,23 +62,43 @@ export class LightDom { this._collectAllContentTags(c, acc); }); } - }); + } return acc; } expandedDomNodes():List { var res = []; - ListWrapper.forEach(this.roots, (root) => { - // TODO: vsavkin calculcate this info statically when creating light dom - var viewPort = this.lightDomView.getViewPortByTemplateElement(root); - if (isPresent(viewPort)) { - res = ListWrapper.concat(res, viewPort.nodes()); + + var roots = this._roots(); + for (var i = 0; i < roots.length; ++i) { + + var root = roots[i]; + var ei = root.injector; + + if (isPresent(ei) && ei.hasPreBuiltObject(ViewPort)) { + var vp = root.injector.get(ViewPort); + res = ListWrapper.concat(res, vp.nodes()); + + } else if (isPresent(ei) && ei.hasDirective(Content)) { + var content = root.injector.get(Content); + res = ListWrapper.concat(res, content.nodes()); + } else { - ListWrapper.push(res, root); + ListWrapper.push(res, root.node); } - }); + } return res; } + + _roots() { + if (isPresent(this.roots)) return this.roots; + + var viewInj = this.lightDomView.elementInjectors; + this.roots = ListWrapper.map(this.nodes, (n) => + new _Root(n, ListWrapper.find(viewInj, (inj) => inj.forElement(n)))); + + return this.roots; + } } function redistributeNodes(contents:List, nodes:List) { diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index fe11cf9723..145db61f60 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -155,9 +155,17 @@ export class View { if (isPresent(componentDirective)) { this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector, elementInjector, elementInjector.getComponent()); + } + } + // this should be moved into DOM write queue + for (var i = 0; i < binders.length; ++i) { + var componentDirective = binders[i].componentDirective; + if (isPresent(componentDirective)) { var lightDom = this.preBuiltObjects[i].lightDom; - if (isPresent(lightDom)) lightDom.redistribute(); + if (isPresent(lightDom)) { + lightDom.redistribute(); + } } } } @@ -192,16 +200,6 @@ export class View { } } - getViewPortByTemplateElement(node):ViewPort { - if (!(node instanceof Element)) return null; - - for (var i = 0; i < this.viewPorts.length; ++i) { - if (this.viewPorts[i].templateElement === node) return this.viewPorts[i]; - } - - return null; - } - _invokeMementoForRecords(records:List) { for(var i = 0; i < records.length; ++i) { this._invokeMementoFor(records[i]); diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js index 7bb9fa9aac..244c637079 100644 --- a/modules/core/test/compiler/integration_spec.js +++ b/modules/core/test/compiler/integration_spec.js @@ -109,50 +109,9 @@ export function main() { }); }); }); - - it('should emulate content tag', (done) => { - var temp = `` + - `
Light
` + - `
DOM
` + - `
`; - - function createView(pv) { - var view = pv.instantiate(null); - view.hydrate(new Injector([]), null, {}); - return view; - } - - compiler.compile(MyComp, el(temp)). - then(createView). - then((view) => { - expect(DOM.getText(view.nodes[0])).toEqual('Before LightDOM After'); - done(); - }); - }); }); } -@Template({ - selector: '[trivial-template]' -}) -class TrivialTemplateDirective { - constructor(viewPort:ViewPort) { - viewPort.create(); - } -} - -@Component({ - selector: 'emulated-shadow-dom-component', - template: new TemplateConfig({ - inline: 'Before After', - directives: [] - }), - shadowDom: ShadowDomEmulated -}) -class EmulatedShadowDomCmp { - -} - @Decorator({ selector: '[my-dir]', bind: {'elprop':'dirProp'} @@ -166,7 +125,7 @@ class MyDir { @Component({ template: new TemplateConfig({ - directives: [MyDir, ChildComp, SomeTemplate, EmulatedShadowDomCmp, TrivialTemplateDirective] + directives: [MyDir, ChildComp, SomeTemplate] }) }) class MyComp { @@ -207,3 +166,4 @@ class MyService { this.greeting = 'hello'; } } + diff --git a/modules/core/test/compiler/shadow_dom/content_tag_spec.js b/modules/core/test/compiler/shadow_dom/content_tag_spec.js index 004fbf9286..cf5359d65c 100644 --- a/modules/core/test/compiler/shadow_dom/content_tag_spec.js +++ b/modules/core/test/compiler/shadow_dom/content_tag_spec.js @@ -14,22 +14,20 @@ var _script = ``; export function main() { describe('Content', function() { it("should insert the nodes", () => { - var lightDom = new DummyLightDom(); var parent = el("
"); var content = DOM.firstChild(parent); - var c = new Content(lightDom, new NgElement(content)); + var c = new Content(null, new NgElement(content)); c.insert([el(""), el("")]) expect(DOM.getInnerHTML(parent)).toEqual(`${_script}${_script}`); }); it("should remove the nodes from the previous insertion", () => { - var lightDom = new DummyLightDom(); var parent = el("
"); var content = DOM.firstChild(parent); - var c = new Content(lightDom, new NgElement(content)); + var c = new Content(null, new NgElement(content)); c.insert([el("")]); c.insert([el("")]); @@ -37,11 +35,10 @@ export function main() { }); it("should insert empty list", () => { - var lightDom = new DummyLightDom(); var parent = el("
"); var content = DOM.firstChild(parent); - var c = new Content(lightDom, new NgElement(content)); + var c = new Content(null, new NgElement(content)); c.insert([el("")]); c.insert([]); diff --git a/modules/core/test/compiler/shadow_dom/light_dom_spec.js b/modules/core/test/compiler/shadow_dom/light_dom_spec.js index 73ea4d0204..da73706605 100644 --- a/modules/core/test/compiler/shadow_dom/light_dom_spec.js +++ b/modules/core/test/compiler/shadow_dom/light_dom_spec.js @@ -15,10 +15,12 @@ import {ProtoRecordRange} from 'change_detection/change_detection'; class FakeElementInjector { content; viewPort; + element; - constructor(content, viewPort) { + constructor(content = null, viewPort = null, element = null) { this.content = content; this.viewPort = viewPort; + this.element = element; } hasDirective(type) { @@ -29,6 +31,10 @@ class FakeElementInjector { return this.viewPort != null; } + forElement(n) { + return this.element == n; + } + get(t) { if (t === Content) return this.content; if (t === ViewPort) return this.viewPort; @@ -44,16 +50,9 @@ class FakeElementInjector { @IMPLEMENTS(View) class FakeView { elementInjectors; - ports; - constructor(elementInjectors = null, ports = null) { + constructor(elementInjectors = null) { this.elementInjectors = elementInjectors; - this.ports = ports; - } - - getViewPortByTemplateElement(el) { - if (isBlank(this.ports)) return null; - return MapWrapper.get(this.ports, el); } noSuchMethod(i) { @@ -67,7 +66,7 @@ class FakeViewPort { _nodes; _contentTagContainers; - constructor(nodes, views) { + constructor(nodes = null, views = null) { this._nodes = nodes; this._contentTagContainers = views; } @@ -90,14 +89,19 @@ class FakeViewPort { @IMPLEMENTS(Content) class FakeContentTag { select; - nodes; + _nodes; - constructor(select = null) { + constructor(select = null, nodes = null) { this.select = select; + this._nodes = nodes; } insert(nodes){ - this.nodes = ListWrapper.clone(nodes); + this._nodes = ListWrapper.clone(nodes); + } + + nodes() { + return this._nodes; } noSuchMethod(i) { @@ -111,13 +115,13 @@ export function main() { var lightDomView; beforeEach(() => { - lightDomView = new FakeView([], MapWrapper.create()); + lightDomView = new FakeView([]); }); describe("contentTags", () => { it("should collect content tags from element injectors", () => { var tag = new FakeContentTag(); - var shadowDomView = new FakeView([new FakeElementInjector(tag, null)]); + var shadowDomView = new FakeView([new FakeElementInjector(tag)]); var lightDom = new LightDom(lightDomView, shadowDomView, el("
")); @@ -147,15 +151,34 @@ export function main() { it("should include view port nodes", () => { var lightDomEl = el("
") - var template = lightDomEl.childNodes[0]; - var lightDomView = new FakeView([], - MapWrapper.createFromPairs([ - [template, new FakeViewPort([el("")], null)] - ]) - ); + var lightDomView = new FakeView([ + new FakeElementInjector( + null, + new FakeViewPort([el("")]), + DOM.firstChild(lightDomEl))]); - var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl); + var lightDom = new LightDom( + lightDomView, + new FakeView(), + lightDomEl); + + expect(toHtml(lightDom.expandedDomNodes())).toEqual([""]); + }); + + it("should include content nodes", () => { + var lightDomEl = el("
") + + var lightDomView = new FakeView([ + new FakeElementInjector( + new FakeContentTag(null, [el("")]), + null, + DOM.firstChild(lightDomEl))]); + + var lightDom = new LightDom( + lightDomView, + new FakeView(), + lightDomEl); expect(toHtml(lightDom.expandedDomNodes())).toEqual([""]); }); @@ -175,8 +198,8 @@ export function main() { lightDom.redistribute(); - expect(toHtml(contentA.nodes)).toEqual(["1", "3"]); - expect(toHtml(contentB.nodes)).toEqual(["2"]); + expect(toHtml(contentA.nodes())).toEqual(["1", "3"]); + expect(toHtml(contentB.nodes())).toEqual(["2"]); }); it("should support wildcard content tags", () => { @@ -192,8 +215,8 @@ export function main() { lightDom.redistribute(); - expect(toHtml(wildcard.nodes)).toEqual(["1", "2", "3"]); - expect(toHtml(contentB.nodes)).toEqual([]); + expect(toHtml(wildcard.nodes())).toEqual(["1", "2", "3"]); + expect(toHtml(contentB.nodes())).toEqual([]); }); }); }); diff --git a/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js b/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js new file mode 100644 index 0000000000..f421ff8a4d --- /dev/null +++ b/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js @@ -0,0 +1,311 @@ +import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_lib/test_lib'; + +import {DOM} from 'facade/dom'; + +import {Injector} from 'di/di'; +import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; + +import {Compiler, CompilerCache} from 'core/compiler/compiler'; +import {LifeCycle} from 'core/life_cycle/life_cycle'; +import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; +import {ShadowDomStrategy, ShadowDomNative, ShadowDomEmulated} from 'core/compiler/shadow_dom'; + +import {Decorator, Component, Template} from 'core/annotations/annotations'; +import {TemplateConfig} from 'core/annotations/template_config'; + +import {ViewPort} from 'core/compiler/viewport'; +import {StringMapWrapper, MapWrapper} from 'facade/collection'; + +export function main() { + describe('integration tests', function() { + + StringMapWrapper.forEach( + {"native" : ShadowDomNative, "emulated" : ShadowDomEmulated}, (strategy, name) => { + + describe(`${name} shadow dom strategy`, () => { + var compiler; + + beforeEach( () => { + compiler = new Compiler(null, new TestDirectiveMetadataReader(strategy), + new Parser(new Lexer()), new CompilerCache()); + }); + + function compile(template, assertions) { + compiler.compile(MyComp, el(template)). + then(createView). + then((view) => { + var lc = new LifeCycle(new ChangeDetector(view.recordRange)); + assertions(view, lc); + }); + } + + it('should support multiple content tags', (done) => { + var temp = '' + + '
B
' + + '
C
' + + '
A
' + + '
'; + + compile(temp, (view, lc) => { + expect(view.nodes).toHaveText('(A, BC)'); + done(); + }); + }); + + it('should redistribute only direct children', (done) => { + var temp = '' + + '
B
A
' + + '
C
' + + '
'; + + compile(temp, (view, lc) => { + expect(view.nodes).toHaveText('(, BAC)'); + done(); + }); + }); + + it("should redistribute when the light dom changes", (done) => { + var temp = '' + + '
A
' + + '
B
' + + '
'; + + compile(temp, (view, lc) => { + var dir = view.elementInjectors[1].get(ManualTemplateDirective); + + expect(view.nodes).toHaveText('(, B)'); + + dir.show(); + lc.tick(); + + expect(view.nodes).toHaveText('(A, B)'); + + dir.hide(); + lc.tick(); + + expect(view.nodes).toHaveText('(, B)'); + + done(); + }); + }); + + it("should support nested components", (done) => { + var temp = '' + + '
A
' + + '
B
' + + '
'; + + compile(temp, (view, lc) => { + expect(view.nodes).toHaveText('OUTER(SIMPLE(AB))'); + + done(); + }); + }); + + it("should support nesting with content being direct child of a nested component", (done) => { + var temp = '' + + '
A
' + + '
B
' + + '
C
' + + '
'; + + compile(temp, (view, lc) => { + var dir = view.elementInjectors[1].get(ManualTemplateDirective); + + expect(view.nodes).toHaveText('OUTER(INNER(INNERINNER(,BC)))'); + + dir.show(); + lc.tick(); + + expect(view.nodes).toHaveText('OUTER(INNER(INNERINNER(A,BC)))'); + done(); + }); + }); + + // Enable once dom-write queue is implemented and onDehydrate is implemented + //it('should redistribute when the shadow dom changes', (done) => { + // var temp = '' + + // '
A
' + + // '
B
' + + // '
C
' + + // '
'; + // + // + // compile(temp, (view, lc) => { + // var cmp = view.elementInjectors[0].get(ConditionalContentComponent); + // + // expect(view.nodes).toHaveText('(, ABC)'); + // + // cmp.showLeft(); + // lc.tick(); + // + // expect(view.nodes).toHaveText('(A, BC)'); + // + // cmp.hideLeft() + // lc.tick(); + // + // expect(view.nodes).toHaveText('(, ABC)'); + // + // done(); + // }); + //}); + + //Implement once NgElement support changing a class + //it("should redistribute when a class has been added or removed"); + //it('should not lose focus', () => { + // var temp = `aaa bbb`; + // + // compile(temp, (view, lc) => { + // var input = view.nodes[1]; + // input.focus(); + // + // expect(document.activeElement.id).toEqual("focused-input"); + // + // // update class of input + // + // expect(document.activeElement.id).toEqual("focused-input"); + // }); + //}); + }); + }); + + }); +} + +class TestDirectiveMetadataReader extends DirectiveMetadataReader { + shadowDomStrategy; + + constructor(shadowDomStrategy) { + this.shadowDomStrategy = shadowDomStrategy; + } + + parseShadowDomStrategy(annotation:Component):ShadowDomStrategy{ + return this.shadowDomStrategy; + } +} + +@Template({ + selector: '[manual]' +}) +class ManualTemplateDirective { + viewPort; + constructor(viewPort:ViewPort) { + this.viewPort = viewPort; + } + + show() { this.viewPort.create(); } + hide() { this.viewPort.remove(0); } +} + +@Template({ + selector: '[auto]', + bind: { + 'auto': 'auto' + } +}) +class AutoTemplateDirective { + viewPort; + constructor(viewPort:ViewPort) { + this.viewPort = viewPort; + } + + set auto(newValue:boolean) { + if (newValue) { + this.viewPort.create(); + } else { + this.viewPort.remove(0); + } + } +} + +@Component({ + selector: 'simple', + template: new TemplateConfig({ + inline: 'SIMPLE()' + }) +}) +class Simple { +} + +@Component({ + selector: 'multiple-content-tags', + template: new TemplateConfig({ + inline: '(, )' + }) +}) +class MultipleContentTagsComponent { +} + + +@Component({ + selector: 'conditional-content', + template: new TemplateConfig({ + inline: '
(
, )
', + directives: [AutoTemplateDirective] + }) +}) +class ConditionalContentComponent { + cond:boolean; + + constructor() { + this.cond = false; + } + + showLeft() { this.cond = true; } + hideLeft() { this.cond = false; } +} + +@Component({ + selector: 'outer-with-indirect-nested', + template: new TemplateConfig({ + inline: 'OUTER(
)', + directives: [Simple] + }) +}) +class OuterWithIndirectNestedComponent { +} + +@Component({ + selector: 'outer', + template: new TemplateConfig({ + inline: 'OUTER()', + directives: [InnerComponent] + }) +}) +class OuterComponent { +} + +@Component({ + selector: 'inner', + template: new TemplateConfig({ + inline: 'INNER()', + directives: [InnerInnerComponent] + }) +}) +class InnerComponent { +} + +@Component({ + selector: 'innerinner', + template: new TemplateConfig({ + inline: 'INNERINNER(,)' + }) +}) +class InnerInnerComponent { +} + + +@Component({ + template: new TemplateConfig({ + directives: [MultipleContentTagsComponent, ManualTemplateDirective, + ConditionalContentComponent, OuterWithIndirectNestedComponent, OuterComponent] + }) +}) +class MyComp { +} + +function createView(pv) { + var view = pv.instantiate(null); + view.hydrate(new Injector([]), null, {}); + return view; +} \ No newline at end of file diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index 80cb42004b..d0bddb215b 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -69,25 +69,6 @@ export function main() { }); }); - describe("getViewPortByTemplateElement", () => { - var view, viewPort, templateElement; - - beforeEach(() => { - templateElement = el(""); - view = new View(null, null, new ProtoRecordRange(), MapWrapper.create()); - viewPort = new FakeViewPort(templateElement); - view.viewPorts = [viewPort]; - }); - - it("should return null when the given element is not an element", () => { - expect(view.getViewPortByTemplateElement("not an element")).toBeNull(); - }); - - it("should return a view port with the matching template element", () => { - expect(view.getViewPortByTemplateElement(templateElement)).toBe(viewPort); - }); - }); - describe('with locals', function() { var view; beforeEach(() => { @@ -204,7 +185,7 @@ export function main() { it('should be off by default.', () => { var template = el('
') var view = new ProtoView(template, new ProtoRecordRange()) - .instantiate(null); + .instantiate(null); view.hydrate(null, null, null); expect(view.nodes[0]).not.toBe(template); }); @@ -333,24 +314,24 @@ export function main() { }); it('should expose component services and component instance to directives in the shadow Dom', - () => { - var subpv = new ProtoView( + () => { + var subpv = new ProtoView( el('
hello shadow dom
'), new ProtoRecordRange()); - subpv.bindElement( + subpv.bindElement( new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); - var pv = createComponentWithSubPV(subpv); + var pv = createComponentWithSubPV(subpv); - var view = createNestedView(pv); + var view = createNestedView(pv); - var subView = view.componentChildViews[0]; - var subInj = subView.rootElementInjectors[0]; - var subDecorator = subInj.get(ServiceDependentDecorator); - var comp = view.rootElementInjectors[0].get(SomeComponent); + var subView = view.componentChildViews[0]; + var subInj = subView.rootElementInjectors[0]; + var subDecorator = subInj.get(ServiceDependentDecorator); + var comp = view.rootElementInjectors[0].get(SomeComponent); - expect(subDecorator).toBeAnInstanceOf(ServiceDependentDecorator); - expect(subDecorator.service).toBe(comp.service); - expect(subDecorator.component).toBe(comp); - }); + expect(subDecorator).toBeAnInstanceOf(ServiceDependentDecorator); + expect(subDecorator.service).toBe(comp.service); + expect(subDecorator.component).toBe(comp); + }); function expectViewHasNoDirectiveInstances(view) { view.elementInjectors.forEach((inj) => expect(inj.hasInstances()).toBe(false)); @@ -358,9 +339,9 @@ export function main() { it('dehydration should dehydrate child component views too', () => { var subpv = new ProtoView( - el('
hello shadow dom
'), new ProtoRecordRange()); + el('
hello shadow dom
'), new ProtoRecordRange()); subpv.bindElement( - new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); + new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); var pv = createComponentWithSubPV(subpv); var view = createNestedView(pv); @@ -369,7 +350,7 @@ export function main() { expect(view.hydrated()).toBe(false); expectViewHasNoDirectiveInstances(view); view.componentChildViews.forEach( - (view) => expectViewHasNoDirectiveInstances(view)); + (view) => expectViewHasNoDirectiveInstances(view)); }); it('should create shadow dom', () => { @@ -398,7 +379,7 @@ export function main() { describe('with template views', () => { function createViewWithTemplate() { var templateProtoView = new ProtoView( - el('
'), new ProtoRecordRange()); + el('
'), new ProtoRecordRange()); var pv = new ProtoView(el(''), new ProtoRecordRange()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeTemplate])); binder.templateDirective = someTemplateDirective; @@ -422,37 +403,6 @@ export function main() { }); }); - describe('event handlers', () => { - var view, ctx, called; - - function createViewAndContext(protoView) { - view = createView(protoView); - ctx = view.context; - called = 0; - ctx.callMe = () => called += 1; - } - - function dispatchClick(el) { - DOM.dispatchEvent(el, DOM.createMouseEvent('click')); - } - - it('should fire on non-bubbling native events', () => { - var pv = new ProtoView(el('
'), - new ProtoRecordRange()); - pv.bindElement(null); - pv.bindEvent('click', parser.parseBinding('callMe()', null)); - createViewAndContext(pv); - - dispatchClick(view.nodes[0]); - dispatchClick(view.nodes[0].firstChild); - - // the bubbled event does not execute the expression. - // It is trivially passing on webkit browsers due to - // https://bugs.webkit.org/show_bug.cgi?id=122755 - expect(called).toEqual(1); - }); - }); - describe('react to record changes', () => { var view, cd, ctx; @@ -633,7 +583,6 @@ class MyEvaluationContext { foo:string; a; b; - callMe; constructor() { this.foo = 'bar'; }; diff --git a/modules/test_lib/src/test_lib.dart b/modules/test_lib/src/test_lib.dart index d04d30ed59..5624ffc37e 100644 --- a/modules/test_lib/src/test_lib.dart +++ b/modules/test_lib/src/test_lib.dart @@ -36,6 +36,7 @@ class NotExpect extends gns.NotExpect { } beforeEach(fn) { + gns.guinnessEnableHtmlMatchers(); gns.beforeEach(_enableReflection(fn)); } diff --git a/modules/test_lib/src/test_lib.es6 b/modules/test_lib/src/test_lib.es6 index 47d97281b8..b979c5c5b0 100644 --- a/modules/test_lib/src/test_lib.es6 +++ b/modules/test_lib/src/test_lib.es6 @@ -77,6 +77,20 @@ window.beforeEach(function() { }; }, + toHaveText: function() { + return { + compare: function(actual, expectedText) { + var actualText = elementText(actual); + return { + pass: actualText == expectedText, + get message() { + return 'Expected ' + actualText + ' to be equal to ' + expectedText; + } + }; + } + }; + }, + toImplement: function() { return { compare: function(actualObject, expectedInterface) { @@ -131,6 +145,20 @@ function mapToString(m) { return `{ ${res.join(',')} }`; } +function elementText(n) { + var hasShadowRoot = (n) => n instanceof Element && n.shadowRoot; + var hasNodes = (n) => n.childNodes && n.childNodes.length > 0; + + if (n instanceof Comment) return ''; + + if (n instanceof Array) return n.map((nn) => elementText(nn)).join(""); + if (n instanceof Element && n.tagName == 'CONTENT') + return elementText(Array.prototype.slice.apply(n.getDistributedNodes())); + if (hasShadowRoot(n)) return elementText(DOM.childNodesAsList(n.shadowRoot)); + if (hasNodes(n)) return elementText(DOM.childNodesAsList(n)); + + return n.textContent; +} export function el(html) { return DOM.createTemplate(html).content.firstChild;