/** * @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'; 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'; 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: *
*
*
* * Components are bootstrapped into the
. * The
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
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 extends BaseFixture { component: T; requestAnimationFrame: {(fn: () => void): void; flush(): void; queue: (() => void)[];}; constructor(private componentType: ComponentType, 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( hostNode: RElement, templateFn: ComponentTemplate, 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 = ɵɵ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, 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) => DirectiveDef): DirectiveDefList|null; function toDefs( types: PipeTypesOrFactory | undefined | null, mapFn: (type: Type) => PipeDef): PipeDefList|null; function toDefs( types: Type[] | (() => Type[]) | undefined | null, mapFn: (type: Type) => PipeDef| DirectiveDef): 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(type: ComponentType, 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(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(//g, ''); } html = html.replace(/^
(.*)<\/div>$/, '$1') .replace(/^
(.*)<\/div>$/, '$1') .replace(/^
(.*)<\/div>$/, '$1') .replace(' style=""', '') .replace(//g, '') .replace(//g, ''); return html; } else { return ''; } } export function createComponent( name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [], viewQuery: ComponentTemplate| null = null, providers: Provider[] = [], viewProviders: Provider[] = [], hostBindings?: HostBindingsFunction): ComponentType { 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 { 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 () => {}; } }