diff --git a/ng-sample/app/examples/navigation/navigation-test.ts b/ng-sample/app/examples/navigation/navigation-test.ts index 088d05708..691081505 100644 --- a/ng-sample/app/examples/navigation/navigation-test.ts +++ b/ng-sample/app/examples/navigation/navigation-test.ts @@ -31,11 +31,7 @@ class StartComponent { @Component({ selector: 'navigation-test', directives: [NS_ROUTER_DIRECTIVES], - template: ` - - - - ` + template: `` }) @RouteConfig([ { path: '/', component: StartComponent, as: 'Start' }, @@ -44,4 +40,3 @@ class StartComponent { export class NavigationTest { } - diff --git a/src/nativescript-angular/element-registry.ts b/src/nativescript-angular/element-registry.ts index 7e78c6aa7..d63469dde 100644 --- a/src/nativescript-angular/element-registry.ts +++ b/src/nativescript-angular/element-registry.ts @@ -99,3 +99,5 @@ registerElement("TextView", () => require("ui/text-view").TextView); registerElement("TimePicker", () => require("ui/time-picker").TimePicker); registerElement("WebView", () => require("ui/web-view").WebView); registerElement("WrapLayout", () => require("ui/layouts/wrap-layout").WrapLayout); + +registerElement("DetachedContainer", () => require("ui/proxy-view-container").ProxyViewContainer, { skipAddToDom: true }); \ No newline at end of file diff --git a/src/nativescript-angular/router/page-router-outlet.ts b/src/nativescript-angular/router/page-router-outlet.ts index c4cc4be53..4ca8063e1 100644 --- a/src/nativescript-angular/router/page-router-outlet.ts +++ b/src/nativescript-angular/router/page-router-outlet.ts @@ -2,7 +2,7 @@ import {PromiseWrapper} from 'angular2/src/facade/async'; import {isBlank, isPresent} from 'angular2/src/facade/lang'; import {StringMapWrapper} from 'angular2/src/facade/collection'; -import {Directive, Attribute, DynamicComponentLoader, ComponentRef, ElementRef, +import {Attribute, DynamicComponentLoader, ComponentRef, ElementRef, Injector, provide, Type, Component, OpaqueToken, Inject} from 'angular2/core'; import * as routerHooks from 'angular2/src/router/lifecycle/lifecycle_annotations'; @@ -19,14 +19,11 @@ import {Page, NavigatedData} from "ui/page"; import {log} from "./common"; import {NSLocationStrategy} from "./ns-location-strategy"; - -let COMPONENT = new OpaqueToken("COMPONENT"); let _resolveToTrue = PromiseWrapper.resolve(true); -let _resolveToFalse = PromiseWrapper.resolve(false); - interface CacheItem { componentRef: ComponentRef; + pageShimRef?: ComponentRef; router: Router; } @@ -36,8 +33,8 @@ interface CacheItem { class RefCache { private cache: Array = new Array(); - public push(comp: ComponentRef, router: Router) { - this.cache.push({ componentRef: comp, router: router }); + public push(comp: ComponentRef, router: Router, pageShimRef?: ComponentRef) { + this.cache.push({ componentRef: comp, router: router, pageShimRef: pageShimRef }); } public pop(): CacheItem { @@ -49,6 +46,30 @@ class RefCache { } } +/** + * Page shim used for loadin compnenets when navigating + */ +@Component({ + selector: 'nativescript-page-shim', + template: ` + + + + ` +}) +class PageShim { + constructor( + private element: ElementRef, + private loader: DynamicComponentLoader + ) { + } + + public loadComponent(componentType: Type): Promise { + return this.loader.loadIntoLocation(componentType, this.element, 'loader'); + } +} + + /** * A router outlet that does page navigation in NativeScript * @@ -58,13 +79,18 @@ class RefCache { * * ``` */ -@Directive({ selector: 'page-router-outlet' }) +@Component({ + selector: 'page-router-outlet', + template: ` + + + ` +}) export class PageRouterOutlet extends RouterOutlet { private isInitalPage: boolean = true; private refCache: RefCache = new RefCache(); private componentRef: ComponentRef = null; - private currentComponentType: ComponentRef = null; private currentInstruction: ComponentInstruction = null; constructor(private elementRef: ElementRef, @@ -81,58 +107,104 @@ export class PageRouterOutlet extends RouterOutlet { */ activate(nextInstruction: ComponentInstruction): Promise { this.log("activate", nextInstruction); - let previousInstruction = this.currentInstruction; - let componentType = nextInstruction.componentType; this.currentInstruction = nextInstruction; if (this.location.isPageNavigatingBack()) { - log("PageRouterOutlet.activate() - Back naviation, so load from cache: " + componentType.name); + return this.activateOnGoBack(nextInstruction, previousInstruction); + } else { + return this.activateOnGoForward(nextInstruction, previousInstruction); + } + } + + private activateOnGoBack(nextInstruction: ComponentInstruction, previousInstruction: ComponentInstruction): Promise { + log("PageRouterOutlet.activate() - Back naviation, so load from cache: " + nextInstruction.componentType.name); - this.location.finishBackPageNavigation(); - - // Get Component form ref and just call the activate hook - let cacheItem = this.refCache.peek(); - this.componentRef = cacheItem.componentRef; - this.replaceChildRouter(cacheItem.router); + this.location.finishBackPageNavigation(); - this.currentComponentType = componentType; - this.checkComponentRef(this.componentRef, nextInstruction); + // Get Component form ref and just call the activate hook + let cacheItem = this.refCache.peek(); + this.componentRef = cacheItem.componentRef; + this.replaceChildRouter(cacheItem.router); + + if (hasLifecycleHook(routerHooks.routerOnActivate, this.componentRef.componentType)) { + return (this.componentRef.instance) + .routerOnActivate(nextInstruction, previousInstruction); + } + } + + private activateOnGoForward(nextInstruction: ComponentInstruction, previousInstruction: ComponentInstruction): Promise { + let componentType = nextInstruction.componentType; + let resultPromise: Promise; + let pageShimRef: ComponentRef = undefined; + const childRouter = this.parentRouter.childRouter(componentType); + + const providersArray = [ + provide(RouteData, { useValue: nextInstruction.routeData }), + provide(RouteParams, { useValue: new RouteParams(nextInstruction.params) }), + provide(Router, { useValue: childRouter }), + ]; + + if (this.isInitalPage) { + log("PageRouterOutlet.activate() inital page - just load component: " + componentType.name); + this.isInitalPage = false; + resultPromise = this.loader.loadNextToLocation(componentType, this.elementRef, Injector.resolve(providersArray)); + } else { + log("PageRouterOutlet.activate() forward navigation - create page shim in the loader container: " + componentType.name); + + const page = new Page(); + providersArray.push(provide(Page, { useValue: page })); + resultPromise = this.loader.loadIntoLocation(PageShim, this.elementRef, "loader", Injector.resolve(providersArray)) + .then((pageComponentRef) => { + pageShimRef = pageComponentRef; + return (pageShimRef.instance).loadComponent(componentType); + }) + .then((actualCoponenetRef) => { + return this.loadComponentInPage(page, actualCoponenetRef); + }) + } + + return resultPromise.then((componentRef) => { + this.componentRef = componentRef; + this.refCache.push(componentRef, childRouter, pageShimRef); if (hasLifecycleHook(routerHooks.routerOnActivate, componentType)) { return (this.componentRef.instance) .routerOnActivate(nextInstruction, previousInstruction); } - } else { - let childRouter = this.parentRouter.childRouter(componentType); - let providers = Injector.resolve([ - provide(RouteData, { useValue: nextInstruction.routeData }), - provide(RouteParams, { useValue: new RouteParams(nextInstruction.params) }), - provide(Router, { useValue: childRouter }), - provide(COMPONENT, { useValue: componentType }), - ]); - - // TODO: Is there a better way to check first load? - if (this.isInitalPage) { - log("PageRouterOutlet.activate() inital page - just load component: " + componentType.name); - this.isInitalPage = false; - } else { - log("PageRouterOutlet.activate() forward navigation - wrap component in page: " + componentType.name); - componentType = PageShim; - } + }); + } + - return this.loader.loadNextToLocation(componentType, this.elementRef, providers) - .then((componentRef) => { - this.componentRef = componentRef; - this.currentComponentType = componentType; - this.refCache.push(componentRef, childRouter); - - if (hasLifecycleHook(routerHooks.routerOnActivate, componentType)) { - return (this.componentRef.instance) - .routerOnActivate(nextInstruction, previousInstruction); - } - }); + private loadComponentInPage(page: Page, componentRef: ComponentRef): Promise { + //Component loaded. Find its root native view. + const componenetView = componentRef.location.nativeElement; + //Remove it from original native parent. + if (componenetView.parent) { + (componenetView.parent).removeChild(componenetView); } + //Add it to the new page + page.content = componenetView; + + this.location.navigateToNewPage(); + return new Promise((resolve, reject) => { + page.on('loaded', () => { + // Finish activation when page is fully loaded. + resolve(componentRef) + }); + + page.on('navigatingFrom', (global).zone.bind((args: NavigatedData) => { + if (args.isBackNavigation) { + this.location.beginBackPageNavigation(); + this.location.back(); + } + })); + + topmost().navigate({ + animated: true, + create: () => { return page; } + }); + }); } /** @@ -142,13 +214,11 @@ export class PageRouterOutlet extends RouterOutlet { deactivate(nextInstruction: ComponentInstruction): Promise { this.log("deactivate", nextInstruction); var instruction = this.currentInstruction; - var compType = this.currentComponentType; var next = _resolveToTrue; if (isPresent(this.componentRef) && isPresent(instruction) && - isPresent(compType) && - hasLifecycleHook(routerHooks.routerOnDeactivate, compType)) { + hasLifecycleHook(routerHooks.routerOnDeactivate, this.componentRef.componentType)) { next = PromiseWrapper.resolve( (this.componentRef.instance).routerOnDeactivate(nextInstruction, this.currentInstruction)); } @@ -156,17 +226,21 @@ export class PageRouterOutlet extends RouterOutlet { if (this.location.isPageNavigatingBack()) { log("PageRouterOutlet.deactivate() while going back - should dispose: " + instruction.componentType.name) return next.then((_) => { - let popedRef = this.refCache.pop().componentRef; + const popedItem = this.refCache.pop(); + const popedRef = popedItem.componentRef; if (this.componentRef !== popedRef) { throw new Error("Current componentRef is different for cached componentRef"); } - this.checkComponentRef(popedRef, instruction); if (isPresent(this.componentRef)) { this.componentRef.dispose(); this.componentRef = null; } + + if (isPresent(popedItem.pageShimRef)) { + popedItem.pageShimRef.dispose(); + } }); } else { return next; @@ -232,142 +306,21 @@ export class PageRouterOutlet extends RouterOutlet { } return PromiseWrapper.resolve( - hasLifecycleHook(routerHooks.routerOnReuse, this.currentComponentType) ? + hasLifecycleHook(routerHooks.routerOnReuse, this.componentRef.componentType) ? (this.componentRef.instance).routerOnReuse(nextInstruction, previousInstruction) : true); } - private checkComponentRef(popedRef: ComponentRef, instruction: ComponentInstruction) { - if (popedRef.instance instanceof PageShim) { - var shim = popedRef.instance; - if (shim.componentType !== instruction.componentType) { - throw new Error("ComponentRef value is different form expected!"); - } - } - } - private replaceChildRouter(childRouter: Router) { // HACKY HACKY HACKY // When navigationg back - we need to set the child router of // our router - with the one we have created for the previosus page. // Otherwise router-outlets inside that page wont't work. // Curretly there is no other way to do that (parentRouter.childRouter() will create ne router). - + this.parentRouter["_childRouter"] = childRouter; } private log(method: string, nextInstruction: ComponentInstruction) { log("PageRouterOutlet." + method + " isBack: " + this.location.isPageNavigatingBack() + " nextUrl: " + nextInstruction.urlPath); } -} - -@Component({ - selector: 'nativescript-page-shim', - template: ` - - - - ` -}) -class PageShim implements OnActivate, OnDeactivate, CanReuse, OnReuse { - private static pageShimCount: number = 0; - private id: number; - private isInitialized: boolean; - private componentRef: ComponentRef; - private page: Page; - - constructor( - private element: ElementRef, - private loader: DynamicComponentLoader, - private locationStrategy: NSLocationStrategy, - @Inject(COMPONENT) public componentType: Type - ) { - this.id = PageShim.pageShimCount++; - this.log("constructor"); - this.page = new Page(); - } - - routerOnActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { - this.log("routerOnActivate"); - let result = PromiseWrapper.resolve(true); - - // On first activation: - // 1. Load componenet using loadIntoLocation. - // 2. Hijack its native element. - // 3. Put that element into a new page and navigate to it. - if (!this.isInitialized) { - result = new Promise((resolve, reject) => { - this.isInitialized = true; - - let providers = Injector.resolve([ - provide(Page, { useValue: this.page }), - ]); - - this.loader.loadIntoLocation(this.componentType, this.element, 'content', providers) - .then((componentRef) => { - this.componentRef = componentRef; - - //Component loaded. Find its root native view. - const viewContainer = this.componentRef.location.nativeElement; - //Remove from original native parent. - //TODO: assuming it's a Layout. - (viewContainer.parent).removeChild(viewContainer); - - this.locationStrategy.navigateToNewPage(); - topmost().navigate({ - animated: true, - create: () => { - const page = this.page; - page.on('loaded', () => { - // Finish activation when page is fully loaded. - resolve() - }); - - page.on('navigatingFrom', (global).zone.bind((args: NavigatedData) => { - if (args.isBackNavigation) { - this.locationStrategy.beginBackPageNavigation(); - this.locationStrategy.back(); - } - })); - - // Add to new page. - page.content = viewContainer; - return page; - } - }); - }); - }); - } - - if (hasLifecycleHook(routerHooks.routerOnActivate, this.componentType)) { - result = result.then(() => { - return (this.componentRef.instance).routerOnActivate(nextInstruction, prevInstruction); - }); - } - return result; - } - - routerOnDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { - this.log("routerOnDeactivate"); - if (hasLifecycleHook(routerHooks.routerOnDeactivate, this.componentType)) { - return (this.componentRef.instance).routerOnDeactivate(nextInstruction, prevInstruction); - } - } - - routerCanReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { - this.log("routerCanReuse"); - if (hasLifecycleHook(routerHooks.routerCanReuse, this.componentType)) { - return (this.componentRef.instance).routerCanReuse(nextInstruction, prevInstruction); - } - } - - routerOnReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { - this.log("routerOnReuse"); - if (hasLifecycleHook(routerHooks.routerOnReuse, this.componentType)) { - return (this.componentRef.instance).routerOnReuse(nextInstruction, prevInstruction); - } - } - - private log(methodName: string) { - log("PageShim(" + this.id + ")." + methodName) - } -} +} \ No newline at end of file diff --git a/tests/app/tests/router.ts b/tests/app/tests/router.ts new file mode 100644 index 000000000..2333a0956 --- /dev/null +++ b/tests/app/tests/router.ts @@ -0,0 +1,244 @@ +//make sure you import mocha-config before angular2/core +import {assert} from "./test-config"; +import { + Type, + Component, + ComponentRef, + DynamicComponentLoader, + ViewChild, + ElementRef, + provide, + ApplicationRef, + ChangeDetectorRef +} from "angular2/core"; + +import {ProxyViewContainer} from "ui/proxy-view-container"; +import {dumpView} from "./test-utils"; +import {bootstrapTestApp, destroyTestApp} from "./test-app"; + +import {ROUTER_DIRECTIVES, Router, OnActivate, OnDeactivate, CanReuse, OnReuse, + LocationStrategy, RouteParams, ComponentInstruction, RouteConfig, Location } from 'angular2/router'; +import {topmost, BackstackEntry} from "ui/frame"; +import {Page} from "ui/page"; +import {NS_ROUTER_DIRECTIVES, NS_ROUTER_PROVIDERS} from "../nativescript-angular/router/ns-router"; + +@Component({ + template: `` +}) +export class LayoutWithLabel { + constructor(public elementRef: ElementRef) { } +} + + +const hooksLog = []; +class CompBase implements OnActivate, OnDeactivate { + protected name: string = ""; + + routerOnActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { + this.log("activate", nextInstruction, prevInstruction); + } + + routerOnDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any { + this.log("deactivate", nextInstruction, prevInstruction); + } + + private log(method: string, nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction) { + hooksLog.push(this.name + "." + method + " " + nextInstruction.urlPath + " " + (prevInstruction ? prevInstruction.urlPath : null)); + } +} + +@Component({ + selector: "first-comp", + template: `` +}) +export class FirstComponent extends CompBase { + protected name = "first"; +} + +@Component({ + selector: "second-comp", + template: `` +}) +export class SecondComponent extends CompBase { + protected name = "second"; +} + +@Component({ + selector: "outlet-app", + directives: [ROUTER_DIRECTIVES], + template: `` + +}) +@RouteConfig([ + { path: '/first', name: 'First', component: FirstComponent, useAsDefault: true }, + { path: '/second', name: 'Second', component: SecondComponent } +]) +export class SimpleOutletCompnenet { + constructor( + public elementRef: ElementRef, + public router: Router, + public location: LocationStrategy) { + } +} + +@Component({ + selector: "page-outlet-app", + directives: [ROUTER_DIRECTIVES, NS_ROUTER_DIRECTIVES], + template: `` + +}) +@RouteConfig([ + { path: '/first', name: 'First', component: FirstComponent, useAsDefault: true }, + { path: '/second', name: 'Second', component: SecondComponent } +]) +export class PageOutletCompnenet { + constructor( + public elementRef: ElementRef, + public router: Router, + public location: LocationStrategy) { + } +} + +describe('router-outlet', () => { + let testApp: SimpleOutletCompnenet = null; + + beforeEach((done) => { + hooksLog.length = 0; + return bootstrapTestApp(SimpleOutletCompnenet, [NS_ROUTER_PROVIDERS]).then((app: SimpleOutletCompnenet) => { + testApp = app; + setTimeout(done, 0); + }); + }); + + afterEach(() => { + destroyTestApp(testApp) + }); + + + it("loads default path", () => { + assert.equal("(ROOT (ProxyViewContainer), (ProxyViewContainer (Label[text=First])))", dumpView(testApp.elementRef.nativeElement, true)); + }); + + it("navigates to other component", () => { + return testApp.router.navigateByUrl("/second").then(() => { + assert.equal("(ROOT (ProxyViewContainer), (ProxyViewContainer (Label[text=Second])))", dumpView(testApp.elementRef.nativeElement, true)); + }) + }); + + it("navigates to other component and then comes back", () => { + return testApp.router.navigateByUrl("/second").then(() => { + return testApp.router.navigateByUrl("/first"); + }).then(() => { + assert.equal("(ROOT (ProxyViewContainer), (ProxyViewContainer (Label[text=First])))", dumpView(testApp.elementRef.nativeElement, true)); + }) + }); + + it("hooks are fired when navigating", () => { + return testApp.router.navigateByUrl("/second").then(() => { + return testApp.router.navigateByUrl("/first"); + }).then(() => { + var expected = [ + "first.activate first null", + "first.deactivate second first", + "second.activate second first", + "second.deactivate first second", + "first.activate first second"]; + + assert.equal(hooksLog.join("|"), expected.join("|")); + }) + }); +}); + +describe('page-router-outlet', () => { + let testApp: PageOutletCompnenet = null; + var initialBackstackLength: number; + var initalPage: Page; + + before(() => { + initialBackstackLength = topmost().backStack.length; + initalPage = topmost().currentPage; + }) + + beforeEach((done) => { + hooksLog.length = 0; + + bootstrapTestApp(PageOutletCompnenet, [NS_ROUTER_PROVIDERS]).then((app: PageOutletCompnenet) => { + testApp = app; + setTimeout(done, 0); + }); + }); + + afterEach(() => { + destroyTestApp(testApp) + + // Ensure navigation to inital page + const backStack = topmost().backStack; + if (backStack.length > initialBackstackLength) { + return goBackToEntry(backStack[initialBackstackLength]); + } + else { + return true; + } + }); + + function goBackToEntry(entry: BackstackEntry): Promise { + var navPromise = getNavigatedPromise(entry.resolvedPage); + topmost().goBack(entry); + return navPromise; + } + + function getNavigatedPromise(page: Page): Promise { + return new Promise((resolve, reject) => { + var callback = () => { + page.off("navigatedTo", callback); + setTimeout(resolve, 0); + } + page.on("navigatedTo", callback) + }) + } + + it("loads default path", () => { + // App-Root app-componenet first-componenet + // | | | + var expected = "(ROOT (ProxyViewContainer), (ProxyViewContainer (Label[text=First])))"; + assert.equal(expected, dumpView(testApp.elementRef.nativeElement, true)); + }); + + it("navigates to other component", () => { + return testApp.router.navigateByUrl("/second") + .then(() => { + assert.equal("(ProxyViewContainer (Label[text=Second]))", dumpView(topmost().currentPage.content, true)); + }) + }); + + it("navigates to other component and then comes back", () => { + return testApp.router.navigateByUrl("/second") + .then(() => { + var navPromise = getNavigatedPromise(initalPage); + testApp.location.back(); + return navPromise; + }).then(() => { + assert.equal(topmost().currentPage, initalPage); + assert.equal("(ROOT (ProxyViewContainer), (ProxyViewContainer (Label[text=First])))", dumpView(testApp.elementRef.nativeElement, true)); + }) + }); + + + it("hooks are fired when navigating", () => { + return testApp.router.navigateByUrl("/second") + .then(() => { + var navPromise = getNavigatedPromise(initalPage); + testApp.location.back(); + return navPromise; + }).then(() => { + var expected = [ + "first.activate first null", + "first.deactivate second first", + "second.activate second first", + "second.deactivate first second", + "first.activate first second"]; + + assert.equal(hooksLog.join("|"), expected.join("|")); + }) + }); +}); diff --git a/tests/app/tests/test-app.ts b/tests/app/tests/test-app.ts index 477517ac1..3f4e4a6e3 100644 --- a/tests/app/tests/test-app.ts +++ b/tests/app/tests/test-app.ts @@ -1,18 +1,10 @@ //make sure you import mocha-config before angular2/core -import {bootstrap} from "../nativescript-angular/application"; -import { - Type, - Component, - ComponentRef, - DynamicComponentLoader, - ViewChild, - ElementRef, - provide, - ApplicationRef +import {bootstrap, ProviderArray} from "../nativescript-angular/application"; +import {Type, Component, ComponentRef, DynamicComponentLoader, + ViewChild, ElementRef, provide, ApplicationRef } from "angular2/core"; import {View} from "ui/core/view"; -import {StackLayout} from "ui/layouts/stack-layout"; import {GridLayout} from "ui/layouts/grid-layout"; import {LayoutBase} from "ui/layouts/layout-base"; import {topmost} from 'ui/frame'; @@ -24,8 +16,6 @@ import {APP_ROOT_VIEW} from "../nativescript-angular/platform-providers"; }) export class TestApp { @ViewChild("loadSite") public loadSiteRef: ElementRef; - private _pageRoot: LayoutBase; - private _appRoot: StackLayout; private _pendingDispose: ComponentRef[] = []; constructor(public loader: DynamicComponentLoader, @@ -49,27 +39,45 @@ export class TestApp { } public static create(): Promise { - const page = topmost().currentPage; - const rootLayout = page.content; - const viewRoot = new StackLayout(); - rootLayout.addChild(viewRoot); - GridLayout.setRow(rootLayout, 50); - const rootViewProvider = provide(APP_ROOT_VIEW, { useFactory: () => viewRoot }); - return bootstrap(TestApp, [rootViewProvider]).then((componentRef) => { - const testApp = componentRef.instance; - testApp._pageRoot = rootLayout; - testApp._appRoot = viewRoot; - return testApp; - }); + return bootstrapTestApp(TestApp); } public dispose() { - if (!this._appRoot) { - throw new Error("Test app already disposed or not initalized."); - } this.disposeComponenets(); - this._pageRoot.removeChild(this._appRoot); - this._appRoot = null; - this._pageRoot = null; + destroyTestApp(this); } -} \ No newline at end of file +} + +var runningApps = new Map(); + +export function bootstrapTestApp(appComponentType: any, providers: ProviderArray = []): Promise { + const page = topmost().currentPage; + const rootLayout = page.content; + const viewRoot = new GridLayout(); + rootLayout.addChild(viewRoot); + GridLayout.setRow(rootLayout, 50); + + const rootViewProvider = provide(APP_ROOT_VIEW, { useValue: viewRoot }); + return bootstrap(appComponentType, providers.concat(rootViewProvider)).then((componentRef) => { + componentRef.injector.get(ApplicationRef) + const testApp = componentRef.instance; + + runningApps.set(testApp, { + hostView: rootLayout, + appRoot: viewRoot, + appRef: componentRef.injector.get(ApplicationRef) }); + + return testApp; + }); +} + +export function destroyTestApp(app: any) { + if (!runningApps.has(app)) { + throw new Error("Unable to cleanup app: " + app); + } + + var entry = runningApps.get(app); + entry.hostView.removeChild(entry.appRoot); + entry.appRef.dispose(); + runningApps.delete(app); +} diff --git a/tests/app/tests/test-utils.ts b/tests/app/tests/test-utils.ts index 37f32bc5c..2426c82f4 100644 --- a/tests/app/tests/test-utils.ts +++ b/tests/app/tests/test-utils.ts @@ -11,7 +11,7 @@ function getChildren(view: View): Array { } export function dumpView(view: View, verbose: boolean = false): string { - let nodeName = (view).nodeName + let nodeName = (view).nodeName || view; let output = ["(", nodeName]; if (verbose) { if (view instanceof TextBase) {