refactor(Compiler): introduce ShimComponent to shim CSS & DOM in emulated mode

Closes #715
This commit is contained in:
Victor Berchet
2015-02-19 11:55:43 +01:00
committed by Misko Hevery
parent 5111f9ae37
commit d0ca07afaa
12 changed files with 486 additions and 705 deletions

View File

@ -1,128 +0,0 @@
import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib';
import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline';
import {ShadowDomTransformer} from 'angular2/src/core/compiler/pipeline/shadow_dom_transformer';
import {Component} from 'angular2/src/core/annotations/annotations';
import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata';
import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy';
import {shimCssText} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_css';
import {DOM} from 'angular2/src/facade/dom';
import {MapWrapper} from 'angular2/src/facade/collection';
export function main() {
describe('ShadowDomTransformer', () => {
function createPipeline(selector, strategy:ShadowDomStrategy, styleHost) {
var component = new Component({selector: selector});
var meta = new DirectiveMetadata(null, component);
var pipe = new ShadowDomTransformer(meta, strategy, styleHost);
pipe.clearCache();
return new CompilePipeline([pipe]);
}
it('it should set ignoreBindings to true for style elements', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(false, false), host);
var results = pipeline.process(el('<div><style></style></div>'));
expect(results[0].ignoreBindings).toBe(false);
expect(results[1].ignoreBindings).toBe(true);
});
describe('css', () => {
it('should not extract the styles when extractStyles() is false', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(false, false), host);
var template = el('<style>.s{}</style>');
pipeline.process(template);
expect(template).toHaveText('.s{}');
});
it('should move the styles to the host when extractStyles() is true', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(true, false), host);
var template = el('<div><style>.s{}</style></div>');
pipeline.process(template);
expect(template).toHaveText('');
expect(host).toHaveText('.s{}');
});
it('should preserve original content when moving styles', () => {
var host = el('<div>original content</div>');
var pipeline = createPipeline('foo', new FakeStrategy(true, false), host);
var template = el('<div><style>.s{}</style></div>');
pipeline.process(template);
expect(template).toHaveText('');
expect(host).toHaveText('.s{}original content');
});
it('should move the styles to the host in the original order', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(true, false), host);
var template = el('<div><style>.s1{}</style><style>.s2{}</style></div>');
pipeline.process(template);
expect(host).toHaveText('.s1{}.s2{}');
});
it('should shim the styles when shim() and extractStyles() are true', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(true, true), host);
var template = el('<div><style>.s1{}</style></div>');
pipeline.process(template);
expect(host).toHaveText(shimCssText('.s1{}', 'foo'));
});
it('should deduplicate styles before moving them when shim() is false', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(true, false), host);
var template = el('<div><style>.s1{}</style><style>.s1{}</style><style>.s1{}</style></div>');
pipeline.process(template);
expect(host).toHaveText('.s1{}');
});
});
describe('html', () => {
it('should add an attribute to all children when shim() is true', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(false, true), host);
var template = el('<div><span></span></div>');
pipeline.process(template);
expect(DOM.getOuterHTML(template)).toEqual('<div foo=""><span foo=""></span></div>')
});
it('should not modify the template when shim() is false', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo', new FakeStrategy(false, false), host);
var template = el('<div><span></span></div>');
pipeline.process(template);
expect(DOM.getOuterHTML(template)).toEqual('<div><span></span></div>')
});
it('should not throw with complex selectors', () => {
var host = DOM.createElement('div');
var pipeline = createPipeline('foo[bar]', new FakeStrategy(false, true), host);
var template = el('<div><span></span></div>');
expect(() => pipeline.process(template)).not.toThrow();
});
});
});
}
class FakeStrategy extends ShadowDomStrategy {
_extractStyles: boolean;
_shim: boolean;
constructor(extractStyles: boolean, shim: boolean) {
super();
this._extractStyles = extractStyles;
this._shim = shim;
}
extractStyles(): boolean {
return this._extractStyles;
}
shim(): boolean {
return this._shim;
}
}

View File

@ -0,0 +1,91 @@
import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib';
import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline';
import {ShimShadowCss} from 'angular2/src/core/compiler/pipeline/shim_shadow_css';
import {ShimComponent} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component';
import {Component} from 'angular2/src/core/annotations/annotations';
import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata';
import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy';
import {Type} from 'angular2/src/facade/lang';
export function main() {
describe('ShimShadowCss', () => {
function createPipeline(strategy:ShadowDomStrategy, styleHost) {
var component = new Component({selector: 'selector'});
var meta = new DirectiveMetadata(null, component);
var shimShadowCss = new ShimShadowCss(meta, strategy, styleHost);
return new CompilePipeline([shimShadowCss]);
}
it('it should set ignoreBindings to true for style elements', () => {
var host = el('<div></div>');
var pipeline = createPipeline(new FakeStrategy(false), host);
var results = pipeline.process(el('<div><style></style></div>'));
expect(results[0].ignoreBindings).toBe(false);
expect(results[1].ignoreBindings).toBe(true);
});
it('should not extract the styles when extractStyles() is false', () => {
var host = el('<div></div>');
var pipeline = createPipeline(new FakeStrategy(false), host);
var template = el('<style>.s{}</style>');
pipeline.process(template);
expect(template).toHaveText('.s{}');
});
it('should move the styles to the host when extractStyles() is true', () => {
var host = el('<div></div>');
var pipeline = createPipeline(new FakeStrategy(true), host);
var template = el('<div><style>.s{}</style></div>');
pipeline.process(template);
expect(template).toHaveText('');
expect(host).toHaveText('/* shim */.s{}');
});
it('should preserve original content when moving styles', () => {
var host = el('<div>original content</div>');
var pipeline = createPipeline(new FakeStrategy(true), host);
var template = el('<div><style>.s{}</style></div>');
pipeline.process(template);
expect(template).toHaveText('');
expect(host).toHaveText('/* shim */.s{}original content');
});
it('should move the styles to the host in the original order', () => {
var host = el('<div></div>');
var pipeline = createPipeline(new FakeStrategy(true), host);
var template = el('<div><style>.s1{}</style><style>.s2{}</style></div>');
pipeline.process(template);
expect(host).toHaveText('/* shim */.s1{}/* shim */.s2{}');
});
});
}
class FakeStrategy extends ShadowDomStrategy {
_extractStyles: boolean;
constructor(extractStyles: boolean) {
super();
this._extractStyles = extractStyles;
}
extractStyles(): boolean {
return this._extractStyles;
}
getShimComponent(component: Type): ShimComponent {
return new FakeShimComponent(component);
}
}
class FakeShimComponent extends ShimComponent {
constructor(component: Type) {
super(component);
}
shimCssText(cssText: string): string {
return '/* shim */' + cssText;
}
}

View File

@ -0,0 +1,97 @@
import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib';
import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline';
import {ShimShadowDom} from 'angular2/src/core/compiler/pipeline/shim_shadow_dom';
import {CompileElement} from 'angular2/src/core/compiler/pipeline/compile_element';
import {CompileStep} from 'angular2/src/core/compiler/pipeline/compile_step';
import {CompileControl} from 'angular2/src/core/compiler/pipeline/compile_control';
import {ShimComponent} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component';
import {Component} from 'angular2/src/core/annotations/annotations';
import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata';
import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy';
import {Type, isBlank} from 'angular2/src/facade/lang';
import {DOM, Element} from 'angular2/src/facade/dom';
export function main() {
describe('ShimShadowDom', () => {
function createPipeline(ignoreBindings: boolean) {
var component = new Component({selector: 'selector'});
var meta = new DirectiveMetadata(null, component);
var shimShadowDom = new ShimShadowDom(meta, new FakeStrategy());
return new CompilePipeline([
new MockStep((parent, current, control) => {
current.ignoreBindings = ignoreBindings;
}),
new MockStep((parent, current, control) => {
var el = current.element;
if (DOM.hasClass(el, 'host')) {
current.componentDirective = new DirectiveMetadata(SomeComponent, null);
}
}),
shimShadowDom
]);
}
it('should add the content attribute to content element', () => {
var pipeline = createPipeline(false);
var results = pipeline.process(el('<div></div>'));
expect(DOM.getAttribute(results[0].element, '_ngcontent')).toEqual('content');
expect(isBlank(DOM.getAttribute(results[0].element, '_nghost'))).toBeTruthy();
});
it('should add both the content and host attributes to host element', () => {
var pipeline = createPipeline(false);
var results = pipeline.process(el('<div class="host"></div>'));
expect(DOM.getAttribute(results[0].element, '_ngcontent')).toEqual('content');
expect(DOM.getAttribute(results[0].element, '_nghost')).toEqual('host');
});
it('should do nothing when ignoreBindings is true', () => {
var pipeline = createPipeline(true);
var results = pipeline.process(el('<div class="host"></div>'));
expect(isBlank(DOM.getAttribute(results[0].element, '_ngcontent'))).toBeTruthy();
expect(isBlank(DOM.getAttribute(results[0].element, '_nghost'))).toBeTruthy();
});
});
}
class FakeStrategy extends ShadowDomStrategy {
constructor() {
super();
}
getShimComponent(component: Type): ShimComponent {
return new FakeShimComponent(component);
}
}
class FakeShimComponent extends ShimComponent {
constructor(component: Type) {
super(component);
}
shimContentElement(element: Element) {
DOM.setAttribute(element, '_ngcontent', 'content');
}
shimHostElement(element: Element) {
DOM.setAttribute(element, '_nghost', 'host');
}
}
class MockStep extends CompileStep {
processClosure:Function;
constructor(process) {
super();
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}
class SomeComponent {}

View File

@ -0,0 +1,128 @@
import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'angular2/test_lib';
import {
ShimNativeComponent,
ShimEmulatedComponent,
resetShimComponentCache
} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component';
import {ShadowCss} from 'angular2/src/core/compiler/shadow_dom_emulation/shadow_css';
import {Type} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/facade/dom';
export function main() {
describe('ShimComponent', () => {
describe('ShimNativeComponent', () => {
function createShim(component: Type) {
return new ShimNativeComponent(component);
}
it('should not transform the CSS', () => {
var css = '.foo {color: blue;} :host{color: red;}';
var shim = createShim(SomeComponent);
var shimCss = shim.shimCssText(css);
expect(css).toEqual(shimCss);
});
it('should not transform content elements', () => {
var html = '<p>foo</p>';
var element = el(html);
var shim = createShim(SomeComponent);
shim.shimContentElement(element);
expect(DOM.getOuterHTML(element)).toEqual(html);
});
it('should not transform host elements', () => {
var html = '<p>foo</p>';
var element = el(html);
var shim = createShim(SomeComponent);
shim.shimHostElement(element);
expect(DOM.getOuterHTML(element)).toEqual(html);
});
});
describe('ShimEmulatedComponent', () => {
beforeEach(() => {
resetShimComponentCache();
});
function createShim(component: Type) {
return new ShimEmulatedComponent(component);
}
it('should transform the CSS', () => {
var css = '.foo {color: blue;} :host{color: red;}';
var shim = createShim(SomeComponent);
var shimCss = shim.shimCssText(css);
expect(shimCss).not.toEqual(css);
var shadowCss = new ShadowCss();
expect(shimCss).toEqual(shadowCss.shimCssText(css, '_ngcontent-0', '_nghost-0'));
});
it('should transform content elements', () => {
var html = '<p>foo</p>';
var element = el(html);
var shim = createShim(SomeComponent);
shim.shimContentElement(element);
expect(DOM.getOuterHTML(element)).toEqual('<p _ngcontent-0="">foo</p>');
});
it('should not transform host elements', () => {
var html = '<p>foo</p>';
var element = el(html);
var shim = createShim(SomeComponent);
shim.shimHostElement(element);
expect(DOM.getOuterHTML(element)).toEqual('<p _nghost-0="">foo</p>');
});
it('should generate the same output for the same component', () => {
var html = '<p>foo</p>';
var content1 = el(html);
var host1 = el(html);
var css = '.foo {color: blue;} :host{color: red;}';
var shim1 = createShim(SomeComponent);
shim1.shimContentElement(content1);
shim1.shimHostElement(host1);
var shimCss1 = shim1.shimCssText(css);
var content2 = el(html);
var host2 = el(html);
var shim2 = createShim(SomeComponent);
shim2.shimContentElement(content2);
shim2.shimHostElement(host2);
var shimCss2 = shim2.shimCssText(css);
expect(DOM.getOuterHTML(content1)).toEqual(DOM.getOuterHTML(content2));
expect(DOM.getOuterHTML(host1)).toEqual(DOM.getOuterHTML(host2));
expect(shimCss1).toEqual(shimCss2);
});
it('should generate different outputs for different components', () => {
var html = '<p>foo</p>';
var content1 = el(html);
var host1 = el(html);
var css = '.foo {color: blue;} :host{color: red;}';
var shim1 = createShim(SomeComponent);
shim1.shimContentElement(content1);
shim1.shimHostElement(host1);
var shimCss1 = shim1.shimCssText(css);
var content2 = el(html);
var host2 = el(html);
var shim2 = createShim(SomeComponent2);
shim2.shimContentElement(content2);
shim2.shimHostElement(host2);
var shimCss2 = shim2.shimCssText(css);
expect(DOM.getOuterHTML(content1)).not.toEqual(DOM.getOuterHTML(content2));
expect(DOM.getOuterHTML(host1)).not.toEqual(DOM.getOuterHTML(host2));
expect(shimCss1).not.toEqual(shimCss2);
});
});
});
}
class SomeComponent {}
class SomeComponent2 {}

View File

@ -1,102 +0,0 @@
import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'angular2/test_lib';
import {shimCssText} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_css';
import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
export function main() {
describe('shim css', function() {
function s(css: string, tag:string) {
var shim = shimCssText(css, tag);
var nlRegexp = RegExpWrapper.create('\\n');
return StringWrapper.replaceAll(shim, nlRegexp, '');
}
it('should handle empty string', () => {
expect(s('', 'a')).toEqual('');
});
it('should add an attribute to every rule', () => {
var css = 'one {color: red;}two {color: red;}';
var expected = 'one[a] {color: red;}two[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should hanlde invalid css', () => {
var css = 'one {color: red;}garbage';
var expected = 'one[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should add an attribute to every selector', () => {
var css = 'one, two {color: red;}';
var expected = 'one[a], two[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules', () => {
var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}}';
var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules with simple rules', () => {
var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}} div {}';
var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}div[a] {}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle complicated selectors', () => {
expect(s('one::before {}', 'a')).toEqual('one[a]::before {}');
expect(s('one two {}', 'a')).toEqual('one[a] two[a] {}');
expect(s('one>two {}', 'a')).toEqual('one[a]>two[a] {}');
expect(s('one+two {}', 'a')).toEqual('one[a]+two[a] {}');
expect(s('one~two {}', 'a')).toEqual('one[a]~two[a] {}');
expect(s('.one.two > three {}', 'a')).toEqual('.one.two[a]>three[a] {}');
expect(s('one[attr="value"] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr=value] {}', 'a')).toEqual('one[attr=value][a] {}');
expect(s('one[attr^="value"] {}', 'a')).toEqual('one[attr^="value"][a] {}');
expect(s('one[attr\$="value"] {}', 'a')).toEqual('one[attr\$="value"][a] {}');
expect(s('one[attr*="value"] {}', 'a')).toEqual('one[attr*="value"][a] {}');
expect(s('one[attr|="value"] {}', 'a')).toEqual('one[attr|="value"][a] {}');
expect(s('one[attr] {}', 'a')).toEqual('one[attr][a] {}');
expect(s('[is="one"] {}', 'a')).toEqual('one[a] {}');
});
it('should handle :host', () => {
expect(s(':host {}', 'a')).toEqual('a {}');
expect(s(':host(.x,.y) {}', 'a')).toEqual('a.x, a.y {}');
});
it('should support polyfill-next-selector', () => {
var css = s("polyfill-next-selector {content: 'x > y'} z {}", 'a');
expect(css).toEqual('x[a]>y[a] {}');
css = s('polyfill-next-selector {content: "x > y"} z {}', 'a');
expect(css).toEqual('x[a]>y[a] {}');
});
it('should support polyfill-unscoped-next-selector', () => {
var css = s("polyfill-unscoped-next-selector {content: 'x > y'} z {}", 'a');
expect(css).toEqual('x > y {}');
css = s('polyfill-unscoped-next-selector {content: "x > y"} z {}', 'a');
expect(css).toEqual('x > y {}');
});
it('should support polyfill-non-strict-next-selector', () => {
var css = s('polyfill-non-strict {} one, two {}', 'a');
expect(css).toEqual('a one, a two {}');
});
it('should handle ::shadow', () => {
var css = s('polyfill-non-strict {} x::shadow > y {}', 'a');
expect(css).toEqual('a x > y {}');
});
it('should handle /deep/', () => {
var css = s('polyfill-non-strict {} x /deep/ y {}', 'a');
expect(css).toEqual('a x y {}');
});
});
}