angular-cn/packages/core/test/render3/render_util.ts

490 lines
19 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef} from '@angular/core/src/change_detection/change_detector_ref';
import {Provider} from '@angular/core/src/di/interface/provider';
import {ElementRef} from '@angular/core/src/linker/element_ref';
import {TemplateRef} from '@angular/core/src/linker/template_ref';
import {ViewContainerRef} from '@angular/core/src/linker/view_container_ref';
import {Renderer2} from '@angular/core/src/render/api';
import {createLView, createTView, getOrCreateTNode, getOrCreateTView, renderComponentOrTemplate} from '@angular/core/src/render3/instructions/shared';
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
import {enterView, getLView, resetComponentState} from '@angular/core/src/render3/state';
import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util';
refactor(ivy): obviate the Bazel component of the ivy_switch (#26550) Originally, the ivy_switch mechanism used Bazel genrules to conditionally compile one TS file or another depending on whether ngc or ngtsc was the selected compiler. This was done because we wanted to avoid importing certain modules (and thus pulling them into the build) if Ivy was on or off. This mechanism had a major drawback: ivy_switch became a bottleneck in the import graph, as it both imports from many places in the codebase and is imported by many modules in the codebase. This frequently resulted in cyclic imports which caused issues both with TS and Closure compilation. It turns out ngcc needs both code paths in the bundle to perform the switch during its operation anyway, so import switching was later abandoned. This means that there's no real reason why the ivy_switch mechanism needed to operate at the Bazel level, and for the ivy_switch file to be a bottleneck. This commit removes the Bazel-level ivy_switch mechanism, and introduces an additional TypeScript transform in ngtsc (and the pass-through tsc compiler used for testing JIT) to perform the same operation that ngcc does, and flip the switch during ngtsc compilation. This allows the ivy_switch file to be removed, and the individual switches to be located directly next to their consumers in the codebase, greatly mitigating the circular import issues and making the mechanism much easier to use. As part of this commit, the tag for marking switched variables was changed from __PRE_NGCC__ to __PRE_R3__, since it's no longer just ngcc which flips these tags. Most variables were renamed from R3_* to SWITCH_* as well, since they're referenced mostly in render2 code. Test strategy: existing test coverage is more than sufficient - if this didn't work correctly it would break the hello world and todo apps. PR Close #26550
2018-10-17 18:44:44 -04:00
import {SWITCH_CHANGE_DETECTOR_REF_FACTORY__POST_R3__ as R3_CHANGE_DETECTOR_REF_FACTORY} from '../../src/change_detection/change_detector_ref';
import {Injector} from '../../src/di/injector';
import {Type} from '../../src/interface/type';
refactor(ivy): obviate the Bazel component of the ivy_switch (#26550) Originally, the ivy_switch mechanism used Bazel genrules to conditionally compile one TS file or another depending on whether ngc or ngtsc was the selected compiler. This was done because we wanted to avoid importing certain modules (and thus pulling them into the build) if Ivy was on or off. This mechanism had a major drawback: ivy_switch became a bottleneck in the import graph, as it both imports from many places in the codebase and is imported by many modules in the codebase. This frequently resulted in cyclic imports which caused issues both with TS and Closure compilation. It turns out ngcc needs both code paths in the bundle to perform the switch during its operation anyway, so import switching was later abandoned. This means that there's no real reason why the ivy_switch mechanism needed to operate at the Bazel level, and for the ivy_switch file to be a bottleneck. This commit removes the Bazel-level ivy_switch mechanism, and introduces an additional TypeScript transform in ngtsc (and the pass-through tsc compiler used for testing JIT) to perform the same operation that ngcc does, and flip the switch during ngtsc compilation. This allows the ivy_switch file to be removed, and the individual switches to be located directly next to their consumers in the codebase, greatly mitigating the circular import issues and making the mechanism much easier to use. As part of this commit, the tag for marking switched variables was changed from __PRE_NGCC__ to __PRE_R3__, since it's no longer just ngcc which flips these tags. Most variables were renamed from R3_* to SWITCH_* as well, since they're referenced mostly in render2 code. Test strategy: existing test coverage is more than sufficient - if this didn't work correctly it would break the hello world and todo apps. PR Close #26550
2018-10-17 18:44:44 -04:00
import {SWITCH_ELEMENT_REF_FACTORY__POST_R3__ as R3_ELEMENT_REF_FACTORY} from '../../src/linker/element_ref';
import {SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ as R3_TEMPLATE_REF_FACTORY} from '../../src/linker/template_ref';
import {SWITCH_VIEW_CONTAINER_REF_FACTORY__POST_R3__ as R3_VIEW_CONTAINER_REF_FACTORY} from '../../src/linker/view_container_ref';
import {RendererStyleFlags2, RendererType2, SWITCH_RENDERER2_FACTORY__POST_R3__ as R3_RENDERER2_FACTORY} from '../../src/render/api';
import {CreateComponentOptions} from '../../src/render3/component';
import {getDirectivesAtNodeIndex, getLContext, isComponentInstance} from '../../src/render3/context_discovery';
import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition';
import {NG_ELEMENT_ID} from '../../src/render3/fields';
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, RenderFlags, renderComponent as _renderComponent, tick, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index';
import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
import {PlayerHandler} from '../../src/render3/interfaces/player';
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {HEADER_OFFSET, LView, LViewFlags, TVIEW, T_HOST} from '../../src/render3/interfaces/view';
import {destroyLView} from '../../src/render3/node_manipulation';
import {getRootView} from '../../src/render3/util/view_traversal_utils';
import {Sanitizer} from '../../src/sanitization/sanitizer';
import {getRendererFactory2} from './imported_renderer2';
export abstract class BaseFixture {
/**
* Each fixture creates the following initial DOM structure:
* <div fixture="mark">
* <div host="mark"></div>
* </div>
*
* Components are bootstrapped into the <div host="mark"></div>.
* The <div fixture="mark"> is there for cases where the root component creates DOM node _outside_
* of its host element (for example when the root component injectes ViewContainerRef or does
* low-level DOM manipulation).
*
* The <div fixture="mark"> is _not_ attached to the document body.
*/
containerElement: HTMLElement;
hostElement: HTMLElement;
constructor() {
this.containerElement = document.createElement('div');
this.containerElement.setAttribute('fixture', 'mark');
this.hostElement = document.createElement('div');
this.hostElement.setAttribute('host', 'mark');
this.containerElement.appendChild(this.hostElement);
}
/**
* Current state of HTML rendered by the bootstrapped component.
*/
get html(): string { return toHtml(this.hostElement as any as Element); }
/**
* Current state of HTML rendered by the fixture (will include HTML rendered by the bootstrapped
* component as well as any elements outside of the component's host).
*/
get outerHtml(): string { return toHtml(this.containerElement as any as Element); }
}
function noop() {}
/**
* Fixture for testing template functions in a convenient way.
*
* This fixture allows:
* - specifying the creation block and update block as two separate functions,
* - maintaining the template state between invocations,
* - access to the render `html`.
*/
export class TemplateFixture extends BaseFixture {
hostView: LView;
private _directiveDefs: DirectiveDefList|null;
private _pipeDefs: PipeDefList|null;
private _sanitizer: Sanitizer|null;
private _rendererFactory: RendererFactory3;
/**
*
* @param createBlock Instructions which go into the creation block:
* `if (rf & RenderFlags.Create) { __here__ }`.
* @param updateBlock Optional instructions which go into the update block:
* `if (rf & RenderFlags.Update) { __here__ }`.
*/
constructor(
private createBlock: () => void, private updateBlock: () => void = noop, consts: number = 0,
private vars: number = 0, directives?: DirectiveTypesOrFactory|null,
pipes?: PipeTypesOrFactory|null, sanitizer?: Sanitizer|null,
rendererFactory?: RendererFactory3) {
super();
this._directiveDefs = toDefs(directives, extractDirectiveDef);
this._pipeDefs = toDefs(pipes, extractPipeDef);
this._sanitizer = sanitizer || null;
this._rendererFactory = rendererFactory || domRendererFactory3;
this.hostView = renderTemplate(
this.hostElement,
(rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
this.createBlock();
}
if (rf & RenderFlags.Update) {
this.updateBlock();
}
},
consts, vars, null !, this._rendererFactory, null, this._directiveDefs, this._pipeDefs,
sanitizer);
}
/**
* Update the existing template
*
* @param updateBlock Optional update block.
*/
update(updateBlock?: () => void): void {
renderTemplate(
this.hostElement, updateBlock || this.updateBlock, 0, this.vars, null !,
this._rendererFactory, this.hostView, this._directiveDefs, this._pipeDefs, this._sanitizer);
}
destroy(): void {
this.containerElement.removeChild(this.hostElement);
destroyLView(this.hostView);
}
}
/**
* Fixture for testing Components in a convenient way.
*/
export class ComponentFixture<T> extends BaseFixture {
component: T;
requestAnimationFrame: {(fn: () => void): void; flush(): void; queue: (() => void)[];};
constructor(private componentType: ComponentType<T>, opts: {
injector?: Injector,
sanitizer?: Sanitizer,
rendererFactory?: RendererFactory3,
playerHandler?: PlayerHandler
} = {}) {
super();
this.requestAnimationFrame = function(fn: () => void) {
requestAnimationFrame.queue.push(fn);
} as any;
this.requestAnimationFrame.queue = [];
this.requestAnimationFrame.flush = function() {
while (requestAnimationFrame.queue.length) {
requestAnimationFrame.queue.shift() !();
}
};
this.component = _renderComponent(componentType, {
host: this.hostElement,
scheduler: this.requestAnimationFrame,
injector: opts.injector,
sanitizer: opts.sanitizer,
rendererFactory: opts.rendererFactory || domRendererFactory3,
playerHandler: opts.playerHandler
});
}
update(): void {
tick(this.component);
this.requestAnimationFrame.flush();
}
destroy(): void {
// Skip removing the DOM element if it has already been removed (the view has already
// been destroyed).
if (this.hostElement.parentNode === this.containerElement) {
this.containerElement.removeChild(this.hostElement);
}
destroyLView(getRootView(this.component));
}
}
///////////////////////////////////////////////////////////////////////////////////
// The methods below use global state and we should stop using them.
// Fixtures above are preferred way of testing Components and Templates
///////////////////////////////////////////////////////////////////////////////////
export const document = ((typeof global == 'object' && global || window) as any).document;
export let containerEl: HTMLElement = null !;
let hostView: LView|null;
const isRenderer2 =
typeof process == 'object' && process.argv[3] && process.argv[3] === '--r=renderer2';
// tslint:disable-next-line:no-console
console.log(`Running tests with ${!isRenderer2 ? 'document' : 'Renderer2'} renderer...`);
const testRendererFactory: RendererFactory3 =
isRenderer2 ? getRendererFactory2(document) : domRendererFactory3;
export const requestAnimationFrame:
{(fn: () => void): void; flush(): void; queue: (() => void)[];} = function(fn: () => void) {
requestAnimationFrame.queue.push(fn);
} as any;
requestAnimationFrame.flush = function() {
while (requestAnimationFrame.queue.length) {
requestAnimationFrame.queue.shift() !();
}
};
export function resetDOM() {
requestAnimationFrame.queue = [];
if (containerEl) {
try {
document.body.removeChild(containerEl);
} catch (e) {
}
}
containerEl = document.createElement('div');
containerEl.setAttribute('host', '');
document.body.appendChild(containerEl);
hostView = null;
// TODO: assert that the global state is clean (e.g. ngData, previousOrParentNode, etc)
}
/**
*
* @param hostNode Existing node to render into.
* @param templateFn Template function with the instructions.
* @param consts The number of nodes, local refs, and pipes in this template
* @param context to pass into the template.
* @param providedRendererFactory renderer factory to use
* @param host The host element node to use
* @param directives Directive defs that should be used for matching
* @param pipes Pipe defs that should be used for matching
*/
export function renderTemplate<T>(
hostNode: RElement, templateFn: ComponentTemplate<T>, consts: number, vars: number, context: T,
providedRendererFactory: RendererFactory3, componentView: LView | null,
directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null,
sanitizer?: Sanitizer | null): LView {
if (componentView === null) {
resetComponentState();
const renderer = providedRendererFactory.createRenderer(null, null);
// We need to create a root view so it's possible to look up the host element through its index
const tView = createTView(-1, null, 1, 0, null, null, null, null);
const hostLView = createLView(
null, tView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null,
providedRendererFactory, renderer);
enterView(hostLView, null); // SUSPECT! why do we need to enter the View?
const def: ComponentDef<any> = ɵɵdefineComponent({
factory: () => null,
selectors: [],
type: Object,
template: templateFn,
consts: consts,
vars: vars,
});
def.directiveDefs = directives || null;
def.pipeDefs = pipes || null;
const componentTView = getOrCreateTView(def);
const hostTNode = getOrCreateTNode(tView, hostLView[T_HOST], 0, TNodeType.Element, null, null);
hostLView[hostTNode.index] = hostNode;
componentView = createLView(
hostLView, componentTView, context, LViewFlags.CheckAlways, hostNode, hostTNode,
providedRendererFactory, renderer, sanitizer);
}
renderComponentOrTemplate(componentView, templateFn, context);
return componentView;
}
/**
* @deprecated use `TemplateFixture` or `ComponentFixture`
*/
export function renderToHtml(
template: ComponentTemplate<any>, ctx: any, consts: number = 0, vars: number = 0,
directives?: DirectiveTypesOrFactory | null, pipes?: PipeTypesOrFactory | null,
providedRendererFactory?: RendererFactory3 | null, keepNgReflect = false) {
hostView = renderTemplate(
containerEl, template, consts, vars, ctx, providedRendererFactory || testRendererFactory,
hostView, toDefs(directives, extractDirectiveDef), toDefs(pipes, extractPipeDef));
return toHtml(containerEl, keepNgReflect);
}
function toDefs(
types: DirectiveTypesOrFactory | undefined | null,
mapFn: (type: Type<any>) => DirectiveDef<any>): DirectiveDefList|null;
function toDefs(
types: PipeTypesOrFactory | undefined | null,
mapFn: (type: Type<any>) => PipeDef<any>): PipeDefList|null;
function toDefs(
types: Type<any>[] | (() => Type<any>[]) | undefined | null,
mapFn: (type: Type<any>) => PipeDef<any>| DirectiveDef<any>): any {
if (!types) return null;
if (typeof types == 'function') {
types = types();
}
return types.map(mapFn);
}
beforeEach(resetDOM);
// This is necessary so we can switch between the Render2 version and the Ivy version
// of special objects like ElementRef and TemplateRef.
beforeEach(enableIvyInjectableFactories);
/**
* @deprecated use `TemplateFixture` or `ComponentFixture`
*/
export function renderComponent<T>(type: ComponentType<T>, opts?: CreateComponentOptions): T {
return _renderComponent(type, {
rendererFactory: opts && opts.rendererFactory || testRendererFactory,
host: containerEl,
scheduler: requestAnimationFrame,
sanitizer: opts ? opts.sanitizer : undefined,
hostFeatures: opts && opts.hostFeatures
});
}
/**
* @deprecated use `TemplateFixture` or `ComponentFixture`
*/
export function toHtml<T>(componentOrElement: T | RElement, keepNgReflect = false): string {
let element: any;
if (isComponentInstance(componentOrElement)) {
const context = getLContext(componentOrElement);
element = context ? context.native : null;
} else {
element = componentOrElement;
}
if (element) {
let html = stringifyElement(element);
if (!keepNgReflect) {
html = html.replace(/\sng-reflect-\S*="[^"]*"/g, '')
.replace(/<!--bindings=\{(\W.*\W\s*)?\}-->/g, '');
}
html = html.replace(/^<div host="">(.*)<\/div>$/, '$1')
.replace(/^<div fixture="mark">(.*)<\/div>$/, '$1')
.replace(/^<div host="mark">(.*)<\/div>$/, '$1')
.replace(' style=""', '')
.replace(/<!--container-->/g, '')
.replace(/<!--ng-container-->/g, '');
return html;
} else {
return '';
}
}
export function createComponent(
name: string, template: ComponentTemplate<any>, consts: number = 0, vars: number = 0,
directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [],
viewQuery: ComponentTemplate<any>| null = null, providers: Provider[] = [],
viewProviders: Provider[] = [], hostBindings?: HostBindingsFunction<any>): ComponentType<any> {
return class Component {
value: any;
static ngComponentDef = ɵɵdefineComponent({
type: Component,
selectors: [[name]],
consts: consts,
vars: vars,
factory: () => new Component,
template: template,
viewQuery: viewQuery,
directives: directives, hostBindings,
pipes: pipes,
features: (providers.length > 0 || viewProviders.length > 0)?
[ɵɵProvidersFeature(providers || [], viewProviders || [])]: []
});
};
}
export function createDirective(
name: string, {exportAs}: {exportAs?: string[]} = {}): DirectiveType<any> {
return class Directive {
static ngDirectiveDef = ɵɵdefineDirective({
type: Directive,
selectors: [['', name, '']],
factory: () => new Directive(),
exportAs: exportAs,
});
};
}
/** Gets the directive on the given node at the given index */
export function getDirectiveOnNode(nodeIndex: number, dirIndex: number = 0) {
const directives = getDirectivesAtNodeIndex(nodeIndex + HEADER_OFFSET, getLView(), true);
if (directives == null) {
throw new Error(`No directives exist on node in slot ${nodeIndex}`);
}
return directives[dirIndex];
}
// Verify that DOM is a type of render. This is here for error checking only and has no use.
export const renderer: Renderer3 = null as any as Document;
export const element: RElement = null as any as HTMLElement;
export const text: RText = null as any as Text;
/**
* Switches between Render2 version of special objects like ElementRef and the Ivy version
* of these objects. It's necessary to keep them separate so that we don't pull in fns
* like injectElementRef() prematurely.
*/
export function enableIvyInjectableFactories() {
(ElementRef as any)[NG_ELEMENT_ID] = () => R3_ELEMENT_REF_FACTORY(ElementRef);
(TemplateRef as any)[NG_ELEMENT_ID] = () => R3_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef);
(ViewContainerRef as any)[NG_ELEMENT_ID] = () =>
R3_VIEW_CONTAINER_REF_FACTORY(ViewContainerRef, ElementRef);
(ChangeDetectorRef as any)[NG_ELEMENT_ID] = () => R3_CHANGE_DETECTOR_REF_FACTORY();
(Renderer2 as any)[NG_ELEMENT_ID] = () => R3_RENDERER2_FACTORY();
}
export class MockRendererFactory implements RendererFactory3 {
lastRenderer: any;
private _spyOnMethods: string[];
constructor(spyOnMethods?: string[]) { this._spyOnMethods = spyOnMethods || []; }
createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 {
const renderer = this.lastRenderer = new MockRenderer(this._spyOnMethods);
return renderer;
}
}
class MockRenderer implements ProceduralRenderer3 {
public spies: {[methodName: string]: any} = {};
constructor(spyOnMethods: string[]) {
spyOnMethods.forEach(methodName => {
this.spies[methodName] = spyOn(this as any, methodName).and.callThrough();
});
}
destroy(): void {}
createComment(value: string): RComment { return document.createComment(value); }
createElement(name: string, namespace?: string|null): RElement {
return namespace ? document.createElementNS(namespace, name) : document.createElement(name);
}
createText(value: string): RText { return document.createTextNode(value); }
appendChild(parent: RElement, newChild: RNode): void { parent.appendChild(newChild); }
insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void {
parent.insertBefore(newChild, refChild, false);
}
removeChild(parent: RElement, oldChild: RNode): void { parent.removeChild(oldChild); }
selectRootElement(selectorOrNode: string|any): RElement {
return ({} as any);
}
parentNode(node: RNode): RElement|null { return node.parentNode as RElement; }
nextSibling(node: RNode): RNode|null { return node.nextSibling; }
setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void {
// set all synthetic attributes as properties
if (name[0] === '@') {
this.setProperty(el, name, value);
} else {
el.setAttribute(name, value);
}
}
removeAttribute(el: RElement, name: string, namespace?: string|null): void {}
addClass(el: RElement, name: string): void {}
removeClass(el: RElement, name: string): void {}
setStyle(
el: RElement, style: string, value: any,
flags?: RendererStyleFlags2|RendererStyleFlags3): void {}
removeStyle(el: RElement, style: string, flags?: RendererStyleFlags2|RendererStyleFlags3): void {}
setProperty(el: RElement, name: string, value: any): void { (el as any)[name] = value; }
setValue(node: RText, value: string): void { node.textContent = value; }
// TODO(misko): Deprecate in favor of addEventListener/removeEventListener
listen(target: RNode, eventName: string, callback: (event: any) => boolean | void): () => void {
return () => {};
}
}