Skip to content

Testing Components with TestBed #1061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ tags
!/nativescript-angular/postinstall.js
!/nativescript-angular/hooks/**/*.js
!/nativescript-angular/gulpfile.js
!/nativescript-angular/zone-js/**/*.js
!/nativescript-angular/zone-js/dist/*.js

.tscache
.nvm
Expand Down
6 changes: 5 additions & 1 deletion nativescript-angular/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "@angular/core";

import { Device } from "tns-core-modules/platform";
import { View } from "tns-core-modules/ui/core/view";
import { View, getViewById } from "tns-core-modules/ui/core/view";
import { addCss } from "tns-core-modules/application";
import { topmost } from "tns-core-modules/ui/frame";
import { profile } from "tns-core-modules/profiling";
Expand Down Expand Up @@ -110,6 +110,10 @@ export class NativeScriptRenderer extends Renderer2 {
@profile
selectRootElement(selector: string): NgView {
traceLog("NativeScriptRenderer.selectRootElement: " + selector);
if (selector && selector[0] === "#") {
const result = getViewById(this.rootView, selector.slice(1));
return (result || this.rootView) as NgView;
}
return this.rootView;
}

Expand Down
29 changes: 29 additions & 0 deletions nativescript-angular/testing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NgModule } from "@angular/core";
import { TestComponentRenderer } from "@angular/core/testing";
import { NativeScriptTestComponentRenderer } from "./src/nativescript_test_component_renderer";
import { COMMON_PROVIDERS } from "../platform-common";
import { APP_ROOT_VIEW } from "../platform-providers";
import { testingRootView } from "./src/util";
export * from "./src/util";

/**
* Providers array is exported for cases where a custom module has to be constructed
* to test a particular piece of code. This can happen, for example, if you are trying
* to test dynamic component loading and need to specify an entryComponent for the testing
* module.
*/
export const NATIVESCRIPT_TESTING_PROVIDERS: any[] = [
COMMON_PROVIDERS,
{provide: APP_ROOT_VIEW, useFactory: testingRootView},
{provide: TestComponentRenderer, useClass: NativeScriptTestComponentRenderer},
];

/**
* NativeScript testing support module. Enables use of TestBed for angular components, directives,
* pipes, and services.
*/
@NgModule({
providers: NATIVESCRIPT_TESTING_PROVIDERS
})
export class NativeScriptTestingModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Injectable } from "@angular/core";
import { TestComponentRenderer } from "@angular/core/testing";
import { topmost } from "tns-core-modules/ui/frame";
import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base";
import { ProxyViewContainer } from "tns-core-modules/ui/proxy-view-container";

/**
* A NativeScript based implementation of the TestComponentRenderer.
*/
@Injectable()
export class NativeScriptTestComponentRenderer extends TestComponentRenderer {

insertRootElement(rootElId: string) {
const page = topmost().currentPage;

const layout = new ProxyViewContainer();
layout.id = rootElId;

const rootLayout = page.layoutView as LayoutBase;
rootLayout.addChild(layout);
}

}

165 changes: 165 additions & 0 deletions nativescript-angular/testing/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@

import { View } from "tns-core-modules/ui/core/view";
import { topmost } from "tns-core-modules/ui/frame";
import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NgModule, Type } from "@angular/core";
import { NativeScriptModule } from "../../nativescript.module";
import { platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing";
import { NS_COMPILER_PROVIDERS } from "../../platform";
import { NATIVESCRIPT_TESTING_PROVIDERS, NativeScriptTestingModule } from "../index";
import { CommonModule } from "@angular/common";
/**
* Get a reference to the root application view.
*/
export function testingRootView(): View {
return topmost().currentPage.content;
}

/**
* Declared test contexts. When the suite is done this map should be empty if all lifecycle
* calls have happened as expected.
* @private
*/
const activeTestFixtures: ComponentFixture<any>[][] = [];

/**
* Return a promise that resolves after (durationMs) milliseconds
*/
export function promiseWait(durationMs: number) {
return () => new Promise((resolve) => setTimeout(() => resolve(), durationMs));
}

/**
* Perform basic TestBed environment initialization. Call this once in the main entry point to your tests.
*/
export function nTestBedInit() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you prefix the all test method with ns instead of n.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, what about:

export function testingRootView(): View
export function promiseWait(durationMs: number)

TestBed.initTestEnvironment(
NativeScriptTestingModule,
platformBrowserDynamicTesting(NS_COMPILER_PROVIDERS)
);
}

/**
* Helper for configuring a TestBed instance for rendering components for test. Ideally this
* would not be needed, and in truth it's just a wrapper to eliminate some boilerplate. It
* exists because when you need to specify `entryComponents` for a test the setup becomes quite
* a bit more complex than if you're just doing a basic component test.
*
* More about entryComponents complexity: https://github.com/angular/angular/issues/12079
*
* Use:
* ```
* beforeEach(nTestBedBeforeEach([MyComponent,MyFailComponent]));
* ```
*
* **NOTE*** Remember to pair with {@see nTestBedAfterEach}
*
* @param components Any components that you will create during the test
* @param providers Any services your tests depend on
* @param imports Any module imports your tests depend on
* @param entryComponents Any entry components that your tests depend on
*/
export function nTestBedBeforeEach(
components: any[],
providers: any[] = [],
imports: any[] = [],
entryComponents: any[] = []) {
return (done) => {
activeTestFixtures.push([]);
// If there are no entry components we can take the simple path.
if (entryComponents.length === 0) {
TestBed.configureTestingModule({
declarations: [...components],
providers: [...providers],
imports: [NativeScriptModule, ...imports]
});
} else {
// If there are entry components, we have to reset the testing platform.
//
// There's got to be a better way... (o_O)
TestBed.resetTestEnvironment();
@NgModule({
declarations: entryComponents,
exports: entryComponents,
entryComponents: entryComponents
})
class EntryComponentsTestModule {
}
TestBed.initTestEnvironment(
EntryComponentsTestModule,
platformBrowserDynamicTesting(NS_COMPILER_PROVIDERS)
);
TestBed.configureTestingModule({
declarations: components,
imports: [
NativeScriptModule, NativeScriptTestingModule, CommonModule,
...imports
],
providers: [...providers, ...NATIVESCRIPT_TESTING_PROVIDERS],
});
}
TestBed.compileComponents()
.then(() => done())
.catch((e) => {
console.log(`Failed to instantiate test component with error: ${e}`);
console.log(e.stack);
done();
});
};
}

/**
* Helper for a basic component TestBed clean up.
* @param resetEnv When true the testing environment will be reset
* @param resetFn When resetting the environment, use this init function
*/
export function nTestBedAfterEach(resetEnv = true, resetFn = nTestBedInit) {
return () => {
if (activeTestFixtures.length === 0) {
throw new Error(
`There are no more declared fixtures.` +
`Did you call "nTestBedBeforeEach" and "nTestBedAfterEach" an equal number of times?`
);
}
const root = testingRootView() as LayoutBase;
const fixtures = activeTestFixtures.pop();
fixtures.forEach((fixture) => {
root.removeChild(fixture.nativeElement);
fixture.destroy();
});
TestBed.resetTestingModule();
if (resetEnv) {
TestBed.resetTestEnvironment();
resetFn();
}
};
}

/**
* Render a component using the TestBed helper, and return a promise that resolves when the
* ComponentFixture is fully initialized.
*/
export function nTestBedRender<T>(componentType: Type<T>): Promise<ComponentFixture<T>> {
const fixture = TestBed.createComponent(componentType);
fixture.detectChanges();
return fixture.whenRenderingDone()
// TODO(jd): it seems that the whenStable and whenRenderingDone utilities of ComponentFixture
// do not work as expected. I looked at how to fix it and it's not clear how to provide
// a {N} specific subclass, because ComponentFixture is newed directly rather than injected
// What to do about it? Maybe fakeAsync can help? For now just setTimeout for 100ms (x_X)
.then(promiseWait(100))
.then(() => {
const list = activeTestFixtures[activeTestFixtures.length - 1];
if (!list) {
console.warn(
"nTestBedRender called without nTestBedBeforeEach/nTestBedAfter each. " +
"You are responsible for calling 'fixture.destroy()' when your test is done " +
"in order to clean up the components that are created."
);
} else {
list.push(fixture);
}
return fixture;
});
}
10 changes: 10 additions & 0 deletions nativescript-angular/zone-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Zone.js for NativeScript
---

Zone.js is a library that aims to intercept all asynchronous API calls made in an environment, in order
to wrap them into coherent execution contexts over time.

NativeScript executes inside an environment that Zone.js is not designed to work in, so a custom Zone.js output
must be created.

Find out more about this in the [Upgrading Zone.js document](../../doc/upgrading-zonejs.md)
Loading