test(ivy): Add small_app spec for sprint #3 (#22018)

PR Close #22018
This commit is contained in:
Misko Hevery 2018-02-03 20:34:30 -08:00 committed by Victor Berchet
parent a63b764b54
commit ac2b04a5ab
21 changed files with 697 additions and 84 deletions

View File

@ -17,6 +17,7 @@ import {NG_HOST_SYMBOL, createError, createLView, createTView, directiveCreate,
import {ComponentDef, ComponentType} from './interfaces/definition';
import {LElementNode} from './interfaces/node';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {RootContext} from './interfaces/view';
import {notImplemented, stringify} from './util';
@ -43,6 +44,19 @@ export interface CreateComponentOptions {
* Example: PublicFeature is a function that makes the component public to the DI system.
*/
features?: (<T>(component: T, componentDef: ComponentDef<T>) => void)[];
/**
* A function which is used to schedule change detection work in the future.
*
* When marking components as dirty, it is necessary to schedule the work of
* change detection in the future. This is done to coalesce multiple
* {@link markDirty} calls into a single changed detection processing.
*
* The default value of the scheduler is the `requestAnimationFrame` function.
*
* It is also useful to override this function for testing purposes.
*/
scheduler?: (work: () => void) => void;
}
@ -155,11 +169,22 @@ export const NULL_INJECTOR: Injector = {
}
};
/**
* A permanent marker promise which signifies that the current CD tree is
* clean.
*/
const CLEAN_PROMISE = Promise.resolve(null);
/**
* Bootstraps a Component into an existing host element and returns an instance
* of the component.
*
* Use this function to bootstrap a component into the DOM tree. Each invocation
* of this function will create a separate tree of components, injectors and
* change detection cycles and lifetimes. To dynamically insert a new component
* into an existing tree such that it shares the same injection, change detection
* and object lifetime, use {@link ViewContainer#createComponent}.
*
* @param componentType Component to bootstrap
* @param options Optional parameters which control bootstrapping
*/
@ -170,15 +195,23 @@ export function renderComponent<T>(
if (componentDef.type != componentType) componentDef.type = componentType;
let component: T;
const hostNode = locateHostElement(rendererFactory, opts.host || componentDef.tag);
const rootContext: RootContext = {
// Incomplete initialization due to circular reference.
component: null !,
scheduler: opts.scheduler || requestAnimationFrame,
clean: CLEAN_PROMISE,
};
const oldView = enterView(
createLView(
-1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView()),
-1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView(),
null, rootContext),
null !);
try {
// Create element node at index 0 in data array
hostElement(hostNode, componentDef);
// Create directive instance with n() and store at index 1 in data array (el is 0)
component = getDirectiveInstance(directiveCreate(1, componentDef.n(), componentDef));
component = rootContext.component =
getDirectiveInstance(directiveCreate(1, componentDef.n(), componentDef));
} finally {
leaveView(oldView);
}
@ -188,27 +221,120 @@ export function renderComponent<T>(
return component;
}
export function detectChanges<T>(component: T) {
ngDevMode && assertNotNull(component, 'detectChanges should be called with a component');
const hostNode = (component as any)[NG_HOST_SYMBOL] as LElementNode;
if (ngDevMode && !hostNode) {
createError('Not a directive instance', component);
}
/**
* Synchronously perform change detection on a component (and possibly its sub-components).
*
* This function triggers change detection in a synchronous way on a component. There should
* be very little reason to call this function directly since a preferred way to do change
* detection is to {@link markDirty} the component and wait for the scheduler to call this method
* at some future point in time. This is because a single user action often results in many
* components being invalidated and calling change detection on each component synchronously
* would be inefficient. It is better to wait until all components are marked as dirty and
* then perform single change detection across all of the components
*
* @param component The component which the change detection should be performed on.
*/
export function detectChanges<T>(component: T): void {
const hostNode = _getComponentHostLElementNode(component);
ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
renderComponentOrTemplate(hostNode, hostNode.view, component);
isDirty = false;
}
let isDirty = false;
export function markDirty<T>(
component: T, scheduler: (fn: () => void) => void = requestAnimationFrame) {
ngDevMode && assertNotNull(component, 'markDirty should be called with a component');
if (!isDirty) {
isDirty = true;
scheduler(() => detectChanges(component));
/**
* Mark the component as dirty (needing change detection).
*
* Marking a component dirty will schedule a change detection on this
* component at some point in the future. Marking an already dirty
* component as dirty is a noop. Only one outstanding change detection
* can be scheduled per component tree. (Two components bootstrapped with
* separate `renderComponent` will have separate schedulers)
*
* When the root component is bootstrapped with `renderComponent` a scheduler
* can be provided.
*
* @param component Component to mark as dirty.
*/
export function markDirty<T>(component: T) {
const rootContext = getRootContext(component);
if (rootContext.clean == CLEAN_PROMISE) {
let res: null|((val: null) => void);
rootContext.clean = new Promise<null>((r) => res = r);
rootContext.scheduler(() => {
detectChanges(rootContext.component);
res !(null);
rootContext.clean = CLEAN_PROMISE;
});
}
}
export function getHostElement<T>(component: T): RElement {
return ((component as any)[NG_HOST_SYMBOL] as LElementNode).native;
/**
* Retrieve the root component of any component by walking the parent `LView` until
* reaching the root `LView`.
*
* @param component any component
*/
function getRootContext(component: any): RootContext {
ngDevMode && assertNotNull(component, 'component');
const lElementNode = _getComponentHostLElementNode(component);
let lView = lElementNode.view;
while (lView.parent) {
lView = lView.parent;
}
const rootContext = lView.context as RootContext;
ngDevMode && assertNotNull(rootContext, 'rootContext');
return rootContext;
}
function _getComponentHostLElementNode<T>(component: T): LElementNode {
ngDevMode && assertNotNull(component, 'expecting component got null');
const lElementNode = (component as any)[NG_HOST_SYMBOL] as LElementNode;
ngDevMode && assertNotNull(component, 'object is not a component');
return lElementNode;
}
/**
* Retrieve the host element of the component.
*
* Use this function to retrieve the host element of the component. The host
* element is the element which the component is associated with.
*
* @param component Component for which the host element should be retrieved.
*/
export function getHostElement<T>(component: T): HTMLElement {
return _getComponentHostLElementNode(component).native as any;
}
/**
* Retrieves the rendered text for a given component.
*
* This function retrieves the host element of a component and
* and then returns the `textContent` for that element. This implies
* that the text returned will include re-projected content of
* the component as well.
*
* @param component The component to return the content text for.
*/
export function getRenderedText(component: any): string {
const hostElement = getHostElement(component);
return hostElement.textContent || '';
}
/**
* Wait on component until it is rendered.
*
* This function returns a `Promise` which is resolved when the component's
* change detection is executed. This is determined by finding the scheduler
* associated with the `component`'s render tree and waiting until the scheduler
* flushes. If nothing is scheduled, the function returns a resolved promise.
*
* Example:
* ```
* await whenRendered(myComponent);
* ```
*
* @param component Component to wait upon
* @returns Promise which resolves when the component is rendered.
*/
export function whenRendered(component: any): Promise<null> {
return getRootContext(component).clean;
}

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent} from './component';
import {createComponentRef, detectChanges, getHostElement, getRenderedText, markDirty, renderComponent, whenRendered} from './component';
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition';
import {InjectFlags} from './di';
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './interfaces/definition';
export {InjectFlags, QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, inject, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di';
export {CssSelector} from './interfaces/projection';
// Naming scheme:
// - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View),
@ -106,6 +108,11 @@ export {
defineComponent,
defineDirective,
definePipe,
detectChanges,
createComponentRef,
getHostElement,
getRenderedText,
markDirty,
renderComponent,
whenRendered,
};
export {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent};
export {CssSelector} from './interfaces/projection';

View File

@ -70,10 +70,15 @@ let isParent: boolean;
*/
let tData: TData;
/** State of the current view being processed. */
let currentView: LView;
// The initialization has to be after the `let`, otherwise `createLView` can't see `let`.
currentView = createLView(null !, null !, createTView());
/**
* State of the current view being processed.
*
* NOTE: we cheat here and initialize it to `null` even thought the type does not
* contain `null`. This is because we expect this value to be not `null` as soon
* as we enter the view. Declaring the type as `null` would require us to place `!`
* in most instructions since they all assume that `currentView` is defined.
*/
let currentView: LView = null !;
let currentQueries: LQueries|null;
@ -131,13 +136,13 @@ const enum BindingDirection {
*/
export function enterView(newView: LView, host: LElementNode | LViewNode | null): LView {
const oldView = currentView;
data = newView.data;
bindingIndex = newView.bindingStartIndex || 0;
tData = newView.tView.data;
creationMode = newView.creationMode;
data = newView && newView.data;
bindingIndex = newView && newView.bindingStartIndex || 0;
tData = newView && newView.tView.data;
creationMode = newView && newView.creationMode;
cleanup = newView.cleanup;
renderer = newView.renderer;
cleanup = newView && newView.cleanup;
renderer = newView && newView.renderer;
if (host != null) {
previousOrParentNode = host;
@ -145,7 +150,7 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null)
}
currentView = newView;
currentQueries = newView.queries;
currentQueries = newView && newView.queries;
return oldView !;
}
@ -165,8 +170,8 @@ export function leaveView(newView: LView): void {
}
export function createLView(
viewId: number, renderer: Renderer3, tView: TView,
template: ComponentTemplate<any>| null = null, context: any | null = null): LView {
viewId: number, renderer: Renderer3, tView: TView, template: ComponentTemplate<any>| null,
context: any | null): LView {
const newView = {
parent: currentView,
id: viewId, // -1 for component views
@ -300,7 +305,8 @@ export function renderTemplate<T>(
host = createLNode(
null, LNodeFlags.Element, hostNode,
createLView(
-1, providedRendererFactory.createRenderer(null, null), getOrCreateTView(template)));
-1, providedRendererFactory.createRenderer(null, null), getOrCreateTView(template),
null, null));
}
const hostView = host.data !;
ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.');
@ -406,7 +412,8 @@ export function elementStart(
if (isHostElement) {
const tView = getOrCreateTView(hostComponentDef !.template);
componentView = addToViewTree(createLView(
-1, rendererFactory.createRenderer(native, hostComponentDef !.rendererType), tView));
-1, rendererFactory.createRenderer(native, hostComponentDef !.rendererType), tView,
null, null));
}
// Only component views should be added to the view tree directly. Embedded views are
@ -556,7 +563,8 @@ export function locateHostElement(
export function hostElement(rNode: RElement | null, def: ComponentDef<any>) {
resetApplicationState();
createLNode(
0, LNodeFlags.Element, rNode, createLView(-1, renderer, getOrCreateTView(def.template)));
0, LNodeFlags.Element, rNode,
createLView(-1, renderer, getOrCreateTView(def.template), null, null));
}
@ -1114,8 +1122,8 @@ export function embeddedViewStart(viewBlockId: number): boolean {
enterView((existingView as LViewNode).data, previousOrParentNode as LViewNode);
} else {
// When we create a new LView, we always reset the state of the instructions.
const newView =
createLView(viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container));
const newView = createLView(
viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container), null, null);
if (lContainer.queries) {
newView.queries = lContainer.queries.enterView(lContainer.nextIndex);
}

View File

@ -160,9 +160,11 @@ export interface LView {
template: ComponentTemplate<{}>|null;
/**
* For embedded views, the context with which to render the template.
* - For embedded views, the context with which to render the template.
* - For root view of the root component the context contains change detection data.
* - `null` otherwise.
*/
context: {}|null;
context: {}|RootContext|null;
/**
* A count of dynamic views that are children of this view (indirectly via containers).
@ -261,6 +263,31 @@ export interface TView {
destroyHooks: HookData|null;
}
/**
* RootContext contains information which is shared for all components which
* were bootstrapped with {@link renderComponent}.
*/
export interface RootContext {
/**
* A function used for scheduling change detection in the future. Usually
* this is `requestAnimationFrame`.
*/
scheduler: (workFn: () => void) => void;
/**
* A promise which is resolved when all components are considered clean (not dirty).
*
* This promise is overwritten every time a first call to {@link markDirty} is invoked.
*/
clean: Promise<null>;
/**
* RootComponent - The component which was instantiated by the call to
* {@link renderComponent}.
*/
component: {};
}
/**
* Array of hooks that should be executed for a view and their directive indices.
*

View File

@ -34,6 +34,7 @@ ts_library(
srcs = ["domino_typings.d.ts"] + glob(["*_spec.ts"]),
deps = [
"//packages:types",
"//packages/core/testing",
],
)

View File

@ -1,4 +1,7 @@
[
{
"name": "CLEAN_PROMISE"
},
{
"name": "EMPTY$1"
},
@ -32,6 +35,9 @@
{
"name": "createLView"
},
{
"name": "currentView"
},
{
"name": "domRendererFactory3"
},

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {withBody} from '@angular/core/testing';
import * as fs from 'fs';
import * as path from 'path';
@ -32,39 +33,22 @@ describe('treeshaking with uglify', () => {
it('should not contain zone.js', () => { expect(content).not.toContain('scheduleMicroTask'); });
describe('functional test in domino', () => {
let document: Document;
it('should render hello world when not minified',
withBody('<hello-world></hello-world>', () => {
require(path.join(PACKAGE, 'bundle.js'));
expect(document.body.textContent).toEqual('Hello World!');
}));
beforeEach(() => {
const window = domino.createWindow('', 'http://localhost');
(global as any).document = document = window.document;
// Trick to avoid Event patching from
// https://github.com/angular/angular/blob/7cf5e95ac9f0f2648beebf0d5bd9056b79946970/packages/platform-browser/src/dom/events/dom_events.ts#L112-L132
// It fails with Domino with TypeError: Cannot assign to read only property
// 'stopImmediatePropagation' of object '#<Event>'
(global as any).Event = null;
it('should render hello world when debug minified',
withBody('<hello-world></hello-world>', () => {
require(path.join(PACKAGE, 'bundle.min_debug.js'));
expect(document.body.textContent).toEqual('Hello World!');
}));
document.body.innerHTML = '<hello-world></hello-world>';
});
afterEach(() => {
(global as any).document = undefined;
(global as any).Element = undefined;
});
it('should render hello world when not minified', () => {
require(path.join(PACKAGE, 'bundle.js'));
expect(document.body.textContent).toEqual('Hello World!');
});
it('should render hello world when debug minified', () => {
require(path.join(PACKAGE, 'bundle.min_debug.js'));
expect(document.body.textContent).toEqual('Hello World!');
});
it('should render hello world when fully minified', () => {
require(path.join(PACKAGE, 'bundle.min.js'));
expect(document.body.textContent).toEqual('Hello World!');
});
it('should render hello world when fully minified',
withBody('<hello-world></hello-world>', () => {
require(path.join(PACKAGE, 'bundle.min.js'));
expect(document.body.textContent).toEqual('Hello World!');
}));
});
});

View File

@ -24,6 +24,7 @@ ts_library(
"//packages/animations/browser/testing",
"//packages/common",
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"//packages/platform-browser/animations",
"//packages/platform-browser/testing",

View File

@ -0,0 +1,5 @@
This folder contains canonical examples of how the Ivy compiler translates annotations into code
- The specs are marked with `NORMATIVE` => `/NORMATIVE` comments which designates what the compiler is expected to generate.
- All local variable names are considered non-normative (informative).

View File

@ -0,0 +1,199 @@
/**
* @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 {NgForOf, NgForOfContext} from '@angular/common';
import {Component, ContentChild, Directive, EventEmitter, Injectable, Input, NgModule, OnDestroy, Optional, Output, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {withBody} from '@angular/core/testing';
import * as r3 from '../../../src/render3/index';
// TODO: remove once https://github.com/angular/angular/pull/22005 lands
export class pending_pull_22005 {
static defineInjectable<T>({scope, factory}: {scope?: Type<any>, factory: () => T}):
{scope: Type<any>| null, factory: () => T} {
return {scope: scope || null, factory: factory};
}
static defineInjector<T>({factory, providers}: {factory: () => T, providers: any[]}):
{factory: () => T, providers: any[]} {
return {factory: factory, providers: providers};
}
}
interface ToDo {
text: string;
done: boolean;
}
@Injectable()
class AppState {
todos: ToDo[] = [
{text: 'Demonstrate Components', done: false},
{text: 'Demonstrate Structural Directives', done: false},
{text: 'Demonstrate NgModules', done: false},
{text: 'Demonstrate zoneless changed detection', done: false},
{text: 'Demonstrate internationalization', done: false},
];
// NORMATIVE
static ngInjectableDef = pending_pull_22005.defineInjectable({factory: () => new AppState()});
// /NORMATIVE
}
@Component({
selector: 'todo-app',
template: `
<h1>ToDo Application</h1>
<div>
<todo *ngFor="let todo of appState.todos" [todo]="todo" (archive)="onArchive($event)"></todo>
</div>
<span>count: {{appState.todos.length}}.</span>
`
})
class ToDoAppComponent {
constructor(public appState: AppState) {}
onArchive(item: ToDo) {
const todos = this.appState.todos;
todos.splice(todos.indexOf(item));
r3.markDirty(this);
}
// NORMATIVE
static ngComponentDef = r3.defineComponent({
type: ToDoAppComponent,
tag: 'todo-app',
factory: function ToDoAppComponent_Factory() {
return new ToDoAppComponent(r3.inject(AppState));
},
template: function ToDoAppComponent_Template(ctx: ToDoAppComponent, cm: boolean) {
if (cm) {
const ToDoAppComponent_NgForOf_Template = function ToDoAppComponent_NgForOf_Template(
ctx1: NgForOfContext<ToDo>, cm: boolean) {
if (cm) {
r3.E(0, ToDoItemComponent);
r3.L('archive', ctx.onArchive.bind(ctx));
r3.e();
}
r3.p(0, 'todo', r3.b(ctx1.$implicit));
};
r3.E(0, 'h1');
r3.T(1, 'ToDo Application');
r3.e();
r3.E(2, 'div');
r3.C(3, c3_directives, ToDoAppComponent_NgForOf_Template);
r3.e();
r3.E(4, 'span');
r3.T(5);
r3.e();
}
r3.t(5, r3.i1('count: ', ctx.appState.todos.length, ''));
}
});
// /NORMATIVE
}
// NORMATIVE
const c3_directives = [NgForOf as r3.DirectiveType<NgForOf<ToDo>>];
// /NORMATIVE
@Component({
selector: 'todo',
template: `
<div [class.done]="todo.done">
<input type="checkbox" [value]="todo.done" (click)="onCheckboxClick()"></input>
<span>{{todo.text}}</span>
<button (click)="onArchiveClick()">archive</button>
</div>
`
})
class ToDoItemComponent {
static DEFAULT_TODO: ToDo = {text: '', done: false};
@Input()
todo: ToDo = ToDoItemComponent.DEFAULT_TODO;
@Output()
archive = new EventEmitter();
onCheckboxClick() {
this.todo.done = !this.todo.done;
r3.markDirty(this);
}
onArchiveClick() { this.archive.emit(this.todo); }
// NORMATIVE
static ngComponentDef = r3.defineComponent({
type: ToDoItemComponent,
tag: 'todo',
factory: function ToDoItemComponent_Factory() { return new ToDoItemComponent(); },
template: function ToDoItemComponent_Template(ctx: ToDoItemComponent, cm: boolean) {
if (cm) {
r3.E(0, 'div');
r3.E(1, 'input', e1_attrs);
r3.L('click', ctx.onCheckboxClick.bind(ctx));
r3.e();
r3.E(2, 'span');
r3.T(3);
r3.e();
r3.E(4, 'button');
r3.L('click', ctx.onArchiveClick.bind(ctx));
r3.T(5, 'archive');
r3.e();
r3.e();
}
r3.p(1, 'value', r3.b(ctx.todo.done));
r3.t(3, r3.b(ctx.todo.text));
},
inputs: {todo: 'todo'},
});
// /NORMATIVE
}
// NORMATIVE
const e1_attrs = ['type', 'checkbox'];
// /NORMATIVE
@NgModule({
declarations: [ToDoAppComponent, ToDoItemComponent],
providers: [AppState],
})
class ToDoAppModule {
// NORMATIVE
static ngInjectorDef = pending_pull_22005.defineInjector({
factory: () => new ToDoAppModule(),
providers: [AppState],
});
// /NORMATIVE
}
describe('small_app', () => {
xit('should render',
() => withBody('<todo-app></todo-app>', async() => {
// TODO: Implement this method once all of the pieces of this application can execute.
// TODO: add i18n example by translating to french.
const todoApp = r3.renderComponent(ToDoAppComponent);
await r3.whenRendered(todoApp);
expect(r3.getRenderedText(todoApp)).toEqual('...');
const firstCheckBox =
r3.getHostElement(todoApp).querySelector('input[type=checkbox]') as HTMLElement;
firstCheckBox.click();
await r3.whenRendered(todoApp);
expect(r3.getRenderedText(todoApp)).toEqual('...');
const firstArchive = r3.getHostElement(todoApp).querySelector('button') as HTMLElement;
firstArchive.click;
await r3.whenRendered(todoApp);
expect(r3.getRenderedText(todoApp)).toEqual('...');
}));
});

View File

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ViewEncapsulation} from '../../src/core';
import {withBody} from '@angular/core/testing';
import {DoCheck, ViewEncapsulation} from '../../src/core';
import {detectChanges, getRenderedText, whenRendered} from '../../src/render3/component';
import {defineComponent, markDirty} from '../../src/render3/index';
import {bind, componentRefresh, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding} from '../../src/render3/instructions';
import {createRendererType2} from '../../src/view/index';
@ -45,12 +48,12 @@ describe('component', () => {
const component = renderComponent(CounterComponent);
expect(toHtml(containerEl)).toEqual('0');
component.count = 123;
markDirty(component, requestAnimationFrame);
markDirty(component);
expect(toHtml(containerEl)).toEqual('0');
requestAnimationFrame.flush();
expect(toHtml(containerEl)).toEqual('123');
component.increment();
markDirty(component, requestAnimationFrame);
markDirty(component);
expect(toHtml(containerEl)).toEqual('123');
requestAnimationFrame.flush();
expect(toHtml(containerEl)).toEqual('124');
@ -232,4 +235,69 @@ describe('encapsulation', () => {
.toMatch(
/<div host="" _nghost-c(\d+)=""><leaf _ngcontent-c\1="" _nghost-c(\d+)=""><span _ngcontent-c\2="">bar<\/span><\/leaf><\/div>/);
});
describe('markDirty, detectChanges, whenRendered, getRenderedText', () => {
class MyComponent implements DoCheck {
value: string = 'works';
doCheckCount = 0;
ngDoCheck(): void { this.doCheckCount++; }
static ngComponentDef = defineComponent({
type: MyComponent,
tag: 'my-comp',
factory: () => new MyComponent(),
template: (ctx: MyComponent, cm: boolean) => {
if (cm) {
elementStart(0, 'span');
text(1);
elementEnd();
}
textBinding(1, bind(ctx.value));
}
});
}
it('should mark a component dirty and schedule change detection', withBody('my-comp', () => {
const myComp = renderComponent(MyComponent);
expect(getRenderedText(myComp)).toEqual('works');
myComp.value = 'updated';
markDirty(myComp);
expect(getRenderedText(myComp)).toEqual('works');
requestAnimationFrame.flush();
expect(getRenderedText(myComp)).toEqual('updated');
}));
it('should detectChanges on a component', withBody('my-comp', () => {
const myComp = renderComponent(MyComponent);
expect(getRenderedText(myComp)).toEqual('works');
myComp.value = 'updated';
detectChanges(myComp);
expect(getRenderedText(myComp)).toEqual('updated');
}));
it('should detectChanges only once if markDirty is called multiple times',
withBody('my-comp', () => {
const myComp = renderComponent(MyComponent);
expect(getRenderedText(myComp)).toEqual('works');
expect(myComp.doCheckCount).toBe(1);
myComp.value = 'ignore';
markDirty(myComp);
myComp.value = 'updated';
markDirty(myComp);
expect(getRenderedText(myComp)).toEqual('works');
requestAnimationFrame.flush();
expect(getRenderedText(myComp)).toEqual('updated');
expect(myComp.doCheckCount).toBe(2);
}));
it('should notify whenRendered', withBody('my-comp', async() => {
const myComp = renderComponent(MyComponent);
await whenRendered(myComp);
myComp.value = 'updated';
markDirty(myComp);
setTimeout(requestAnimationFrame.flush, 0);
await whenRendered(myComp);
expect(getRenderedText(myComp)).toEqual('updated');
}));
});
});

View File

@ -324,7 +324,7 @@ describe('di', () => {
describe('getOrCreateNodeInjector', () => {
it('should handle initial undefined state', () => {
const contentView = createLView(-1, null !, createTView());
const contentView = createLView(-1, null !, createTView(), null, null);
const oldView = enterView(contentView, null !);
try {
const parent = createLNode(0, LNodeFlags.Element, null, null);

View File

@ -27,4 +27,6 @@ if (typeof window == 'undefined') {
// For animation tests, see
// https://github.com/angular/angular/blob/master/packages/animations/browser/src/render/shared.ts#L140
(global as any).Element = domino.impl.Element;
(global as any).isBrowser = false;
(global as any).isNode = true;
}

View File

@ -61,8 +61,11 @@ export function renderToHtml(
beforeEach(resetDOM);
export function renderComponent<T>(type: ComponentType<T>, rendererFactory?: RendererFactory3): T {
return _renderComponent(
type, {rendererFactory: rendererFactory || testRendererFactory, host: containerEl});
return _renderComponent(type, {
rendererFactory: rendererFactory || testRendererFactory,
host: containerEl,
scheduler: requestAnimationFrame,
});
}
export function toHtml<T>(componentOrElement: T | RElement): string {

View File

@ -181,7 +181,7 @@ describe('animation renderer factory', () => {
expect(toHtml(containerEl)).toEqual('foo');
});
it('should work with animated components', (done) => {
isBrowser && it('should work with animated components', (done) => {
const factory = getAnimationRendererFactory2(document);
const component = renderComponent(SomeComponentWithAnimation, factory);
expect(toHtml(containerEl))

View File

@ -0,0 +1,48 @@
/**
* @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 {withBody} from '@angular/core/testing';
describe('testing', () => {
describe('withBody', () => {
let passed: boolean;
beforeEach(() => passed = false);
afterEach(() => expect(passed).toEqual(true));
it('should set up body', withBody('<span>works!</span>', () => {
expect(document.body.innerHTML).toEqual('<span>works!</span>');
passed = true;
}));
it('should support promises', withBody('<span>works!</span>', () => {
return Promise.resolve(true).then(() => passed = true);
}));
it('should support async and await', withBody('<span>works!</span>', async() => {
await Promise.resolve(true);
passed = true;
}));
});
describe('domino', () => {
it('should have document present', () => {
// In Browser this tests passes, bun we also want to make sure we pass in node.js
// We expect that node.js will load domino for us.
expect(document).toBeTruthy();
});
});
describe('requestAnimationFrame', () => {
it('should have requestAnimationFrame', (done) => {
// In Browser we have requestAnimationFrame, but verify that we also have it node.js
requestAnimationFrame(done);
});
});
});

View File

@ -7,12 +7,13 @@
*/
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
const _Zone: any = typeof Zone !== 'undefined' ? Zone : null;
const FakeAsyncTestZoneSpec = _Zone && _Zone['FakeAsyncTestZoneSpec'];
type ProxyZoneSpec = {
setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void;
};
const ProxyZoneSpec: {get(): ProxyZoneSpec; assertPresent: () => ProxyZoneSpec} =
(Zone as any)['ProxyZoneSpec'];
_Zone && _Zone['ProxyZoneSpec'];
let _fakeAsyncTestZoneSpec: any = null;
@ -24,7 +25,8 @@ let _fakeAsyncTestZoneSpec: any = null;
*/
export function resetFakeAsyncZone() {
_fakeAsyncTestZoneSpec = null;
ProxyZoneSpec.assertPresent().resetDelegate();
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
}
let _inFakeAsyncCall = false;

View File

@ -0,0 +1,115 @@
/**
* @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
*/
/**
* Wraps a function in a new function which sets up document and HTML for running a test.
*
* This function is intended to wrap an existing testing function. The wrapper
* adds HTML to the `body` element of the `document` and subsequently tears it down.
*
* This function is intended to be used with `async await` and `Promise`s. If the wrapped
* function returns a promise (or is `async`) then the teardown is delayed until that `Promise`
* is resolved.
*
* On `node` this function detects if `document` is present and if not it will create one by
* loading `domino` and installing it.
*
* Example:
*
* ```
* describe('something', () => {
* it('should do something', withBody('<my-app></my-app>', async () => {
* const myApp = renderComponent(MyApp);
* await whenRendered(myApp);
* expect(getRenderedText(myApp)).toEqual('Hello World!');
* }));
* });
* ```
*
* @param html HTML which should be inserted into `body` of the `document`.
* @param blockFn function to wrap. The function can return promise or be `async`.
* @experimental
*/
export function withBody<T>(html: string, blockFn: T): T {
return function(done: {(): void, fail(): void}) {
ensureDocument();
let returnValue: any = undefined;
if (typeof blockFn === 'function') {
document.body.innerHTML = html;
let blockReturn = blockFn();
if (blockReturn instanceof Promise) {
blockReturn = blockReturn.then(done, done.fail);
} else {
done();
}
}
} as any;
}
let savedDocument: Document|undefined = undefined;
let savedRequestAnimationFrame: ((callback: FrameRequestCallback) => number)|undefined = undefined;
let requestAnimationFrameCount = 0;
/**
* System.js uses regexp to look for `require` statements. `domino` has to be
* extracted into a constant so that the regexp in the System.js does not match
* and does not try to load domino in the browser.
*/
const domino: any = (function(domino) {
if (typeof global == 'object' && global.process && typeof require == 'function') {
try {
return require(domino);
} catch (e) {
// It is possible that we don't have domino available in which case just give up.
}
}
// Seems like we don't have domino, give up.
return null;
})('domino');
/**
* Ensure that global has `Document` if we are in node.js
* @experimental
*/
export function ensureDocument(): void {
if (domino) {
// we are in node.js.
const window = domino.createWindow('', 'http://localhost');
savedDocument = (global as any).document;
(global as any).document = window.document;
// Trick to avoid Event patching from
// https://github.com/angular/angular/blob/7cf5e95ac9f0f2648beebf0d5bd9056b79946970/packages/platform-browser/src/dom/events/dom_events.ts#L112-L132
// It fails with Domino with TypeError: Cannot assign to read only property
// 'stopImmediatePropagation' of object '#<Event>'
(global as any).Event = null;
savedRequestAnimationFrame = (global as any).requestAnimationFrame;
(global as any).requestAnimationFrame = function(cb: FrameRequestCallback): number {
setImmediate(cb);
return requestAnimationFrameCount++;
};
}
}
/**
* Restore the state of `Document` between tests.
* @experimental
*/
export function cleanupDocument(): void {
if (savedDocument) {
(global as any).document = savedDocument;
savedDocument = undefined;
}
if (savedRequestAnimationFrame) {
(global as any).requestAnimationFrame = savedRequestAnimationFrame;
savedRequestAnimationFrame = undefined;
}
}
if (typeof beforeEach == 'function') beforeEach(ensureDocument);
if (typeof afterEach == 'function') beforeEach(cleanupDocument);

View File

@ -19,3 +19,4 @@ export * from './test_bed';
export * from './before_each';
export * from './metadata_override';
export * from './private_export_testing';
export * from './render3';

View File

@ -14,7 +14,8 @@
"files": [
"public_api.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts",
"../../system.d.ts"
"../../system.d.ts",
"../../types.d.ts"
],
"angularCompilerOptions": {

View File

@ -1,6 +1,9 @@
/** @stable */
export declare function async(fn: Function): (done: any) => any;
/** @experimental */
export declare function cleanupDocument(): void;
/** @stable */
export declare class ComponentFixture<T> {
changeDetectorRef: ChangeDetectorRef;
@ -29,6 +32,9 @@ export declare const ComponentFixtureNoNgZone: InjectionToken<boolean[]>;
/** @experimental */
export declare function discardPeriodicTasks(): void;
/** @experimental */
export declare function ensureDocument(): void;
/** @experimental */
export declare function fakeAsync(fn: Function): (...args: any[]) => any;
@ -145,5 +151,8 @@ export declare type TestModuleMetadata = {
/** @experimental */
export declare function tick(millis?: number): void;
/** @experimental */
export declare function withBody<T>(html: string, blockFn: T): T;
/** @experimental */
export declare function withModule(moduleDef: TestModuleMetadata): InjectSetupWrapper;