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)
}
/**
* @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: PipeTypesOrFactory | DirectiveTypesOrFactory | 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 () => {};
}
}