Skip to content

Commit 52f3ec6

Browse files
hypery2kAlexander Vakrilov
authored and
Alexander Vakrilov
committed
feat(testing): Testing Components with TestBed (#1175)
* feat(testing): support TestBed and export jasmine/mocha zone scripts - update zone build and add jasmine/mocha test scripts, see: NativeScript/zone.js#1 - refactor testing and zone-js modules to work with each other - add generous TestBed setup helpers in testing/src/util.ts - add single import test helpers for zone-js patches in zone-js/testing.[framework].ts files - update peer deps to reflect updated zone (could peer dep be dropped since prebuilts are exported?) chore: disable most tests and enable ones that use TestApp one-by-one - add test entry point script that inits the test environment for TestBed - list view, modal dialog pass - detached-loader and platform-filter-component could use feedback. see todos chore: replace the remaining TestApp usages in test suite - xdescribe the failing tests. - I think the remaining problems boil down to `dumpView` indicating the ComponentFixture comes back with the root components, and `@ViewChild` not finding DetachedLoader by its class. - remove some duplication in testing utilities chore: cleanup and remove some diff noise from a few tests chore: remove more test noise - are the line-endings different on this file? :( chore: convince dumpView and TestComponentRenderer to agree on things - all the TestBed tests except for the DetachedLoader ones and a single Renderer lifecycle are passing. - update NativeScriptRenderer.selectRootElement to find views by ID when given a selector of an id string. TestBed uses this when creating componentRefs to get at the correct views. - change NativeScriptTestComponentRenderer to inject only a ProxyViewContainer which mimics what TestApp did. - update dumpView to strip off the new "source" data attached to a view.toString() result. chore: cleanup lint chore: make nTestBed helpers automatically clean up test components - before the components were destroyed by TestBed, but not removed from the rootView. - maintain a list of active fixtures for a set of tests, and remove them all when the tests complete. - reorder to the testing utils to flow better when reading (start with test init, then before/after then render components) - clean up some lint. chore: fix issue where nTestBedBeforeEach overwrote its own imports - Fixes the DetachedLoader tests, and makes them MUCH simpler. - When you configure the test bed module, you need to specify a full list of imports, because they completely overwrite the imports array that is used. - That's yet another reason to use the provided helper functions, they merge in the common {N} imports for you. - ... and some lint cleanup chore: make renderer lifecyle test more robust - the first assertion is that the view after init has been called. rather than assert it, just wait for it using an observable and avoid asserting about timing and implementation specific details of the system. Specifically this removes the assumption that `app.tick()` will advance time and call `ngAfterViewInit` on the component. chore: cleanup from review - re-enable all tests in karma.conf.js - remove some diff noise * chore: update for @angular ~5.1.0 - incorporate changes from zone to include @panayot.cankov's drainMicroTaskQueue update - update zone.js deps to reference "*" for version. Ideally it would be removed, but I believe @angular has a peer dependency on it so satisfy it with a star. This is because zone.js files are prebuilt specifically for {N} so there is no need to bring them in as a dep. * chore: do not upgrade zone.js version - the TestBed changes do not depend on the upgrade (after review), and given that there are strange errors, prefer to reduce the number of changes that are not strictly required. * chore(tests): Fix Renderer Tests * fix: rxjs6 imports * refactor: Remove snippets tests * chore(build): Allow CI testing * fix(compat-error): Resolve RxJS issue Adding rxjs-compat * feat(zone): Update zonejs to 0.8.26 * chore(tests): Unit tests made green again * chore(test): Provide NSLocationStrategy for modal tests * fix(zone): Fix zone js patching Promise prop descriptr and breaking android snapshot
1 parent b98da83 commit 52f3ec6

28 files changed

+4168
-1492
lines changed

Diff for: .gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ tags
1212
!/nativescript-angular/postinstall.js
1313
!/nativescript-angular/hooks/**/*.js
1414
!/nativescript-angular/gulpfile.js
15-
!/nativescript-angular/zone-js/**/*.js
15+
!/nativescript-angular/zone-js/dist/*.js
1616

1717
.tscache
1818
.nvm

Diff for: e2e/config/appium.capabilities.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@
3535
"noReset": false,
3636
"fullReset": false
3737
},
38+
"android22": {
39+
"platformName": "Android",
40+
"platformVersion": "5.0",
41+
"deviceName": "Emulator-Api22-Default",
42+
"avd": "Emulator-Api22-Default",
43+
"lt": 60000,
44+
"newCommandTimeout": 720,
45+
"noReset": false,
46+
"fullReset": false
47+
},
3848
"android23": {
3949
"platformName": "Android",
4050
"platformVersion": "6.0",
@@ -114,4 +124,4 @@
114124
"noReset": true,
115125
"fullReset": false
116126
}
117-
}
127+
}

Diff for: nativescript-angular/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"@angular/http": "~6.0.0-rc.0",
5353
"@angular/platform-browser": "~6.0.0-rc.0",
5454
"@angular/router": "~6.0.0-rc.0",
55-
"rxjs": "~6.0.0-rc.1",
55+
"rxjs": "~6.0.0-rc.1 || >=6.1.0 || >=6.0.0",
56+
"rxjs-compat": "~6.0.0-rc.1 || >=6.1.0 || >=6.0.0",
5657
"tns-core-modules": "^4.0.0 || >4.0.0- || >4.1.0-",
5758
"zone.js": "^0.8.4",
5859
"typescript": "~2.7.2"
@@ -70,6 +71,7 @@
7071
"@angular/router": "~6.0.0-rc.0",
7172
"codelyzer": "^4.0.0",
7273
"rxjs": "~6.0.0-rc.1",
74+
"rxjs-compat": "~6.0.0-rc.1",
7375
"tns-core-modules": "next",
7476
"tslint": "^5.5.0",
7577
"typescript": "~2.7.2",

Diff for: nativescript-angular/platform-common.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ export class NativeScriptSanitizer extends Sanitizer {
6767
}
6868

6969
// Add a fake polyfill for the document object
70-
(<any>global).document = (<any>global).document || {};
70+
(<any>global).document = (<any>global).document || {
71+
getElementById: () => { return undefined; }
72+
};
73+
7174
const doc = (<any>global).document;
7275
doc.body = Object.assign(doc.body || {}, {
7376
isOverride: true,

Diff for: nativescript-angular/renderer.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from "@angular/core";
66

77
import { Device } from "tns-core-modules/platform";
8-
import { View } from "tns-core-modules/ui/core/view";
8+
import { View, getViewById } from "tns-core-modules/ui/core/view";
99
import { addCss } from "tns-core-modules/application";
1010
import { profile } from "tns-core-modules/profiling";
1111

@@ -109,6 +109,10 @@ export class NativeScriptRenderer extends Renderer2 {
109109
@profile
110110
selectRootElement(selector: string): NgView {
111111
traceLog("NativeScriptRenderer.selectRootElement: " + selector);
112+
if (selector && selector[0] === "#") {
113+
const result = getViewById(this.rootView, selector.slice(1));
114+
return (result || this.rootView) as NgView;
115+
}
112116
return this.rootView;
113117
}
114118

Diff for: nativescript-angular/testing/index.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NgModule } from "@angular/core";
2+
import { TestComponentRenderer } from "@angular/core/testing";
3+
import { NativeScriptTestComponentRenderer } from "./src/nativescript_test_component_renderer";
4+
import { COMMON_PROVIDERS } from "../platform-common";
5+
import { APP_ROOT_VIEW } from "../platform-providers";
6+
import { testingRootView } from "./src/util";
7+
export * from "./src/util";
8+
9+
/**
10+
* Providers array is exported for cases where a custom module has to be constructed
11+
* to test a particular piece of code. This can happen, for example, if you are trying
12+
* to test dynamic component loading and need to specify an entryComponent for the testing
13+
* module.
14+
*/
15+
export const NATIVESCRIPT_TESTING_PROVIDERS: any[] = [
16+
COMMON_PROVIDERS,
17+
{provide: APP_ROOT_VIEW, useFactory: testingRootView},
18+
{provide: TestComponentRenderer, useClass: NativeScriptTestComponentRenderer},
19+
];
20+
21+
/**
22+
* NativeScript testing support module. Enables use of TestBed for angular components, directives,
23+
* pipes, and services.
24+
*/
25+
@NgModule({
26+
providers: NATIVESCRIPT_TESTING_PROVIDERS
27+
})
28+
export class NativeScriptTestingModule {
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Injectable } from "@angular/core";
2+
import { TestComponentRenderer } from "@angular/core/testing";
3+
import { topmost } from "tns-core-modules/ui/frame";
4+
import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base";
5+
import { ProxyViewContainer } from "tns-core-modules/ui/proxy-view-container";
6+
7+
/**
8+
* A NativeScript based implementation of the TestComponentRenderer.
9+
*/
10+
@Injectable()
11+
export class NativeScriptTestComponentRenderer extends TestComponentRenderer {
12+
13+
insertRootElement(rootElId: string) {
14+
const page = topmost().currentPage;
15+
16+
const layout = new ProxyViewContainer();
17+
layout.id = rootElId;
18+
19+
const rootLayout = page.layoutView as LayoutBase;
20+
rootLayout.addChild(layout);
21+
}
22+
23+
}
24+

Diff for: nativescript-angular/testing/src/util.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
2+
import { View } from "tns-core-modules/ui/core/view";
3+
import { topmost } from "tns-core-modules/ui/frame";
4+
import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base";
5+
import { ComponentFixture, TestBed } from "@angular/core/testing";
6+
import { NgModule, Type } from "@angular/core";
7+
import { NativeScriptModule } from "../../nativescript.module";
8+
import { platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing";
9+
import { NS_COMPILER_PROVIDERS } from "../../platform";
10+
import { NATIVESCRIPT_TESTING_PROVIDERS, NativeScriptTestingModule } from "../index";
11+
import { CommonModule } from "@angular/common";
12+
/**
13+
* Get a reference to the root application view.
14+
*/
15+
export function testingRootView(): View {
16+
return topmost().currentPage.content;
17+
}
18+
19+
/**
20+
* Declared test contexts. When the suite is done this map should be empty if all lifecycle
21+
* calls have happened as expected.
22+
* @private
23+
*/
24+
const activeTestFixtures: ComponentFixture<any>[][] = [];
25+
26+
/**
27+
* Return a promise that resolves after (durationMs) milliseconds
28+
*/
29+
export function promiseWait(durationMs: number) {
30+
return () => new Promise((resolve) => setTimeout(() => resolve(), durationMs));
31+
}
32+
33+
/**
34+
* Perform basic TestBed environment initialization. Call this once in the main entry point to your tests.
35+
*/
36+
export function nsTestBedInit() {
37+
TestBed.initTestEnvironment(
38+
NativeScriptTestingModule,
39+
platformBrowserDynamicTesting(NS_COMPILER_PROVIDERS)
40+
);
41+
}
42+
43+
/**
44+
* Helper for configuring a TestBed instance for rendering components for test. Ideally this
45+
* would not be needed, and in truth it's just a wrapper to eliminate some boilerplate. It
46+
* exists because when you need to specify `entryComponents` for a test the setup becomes quite
47+
* a bit more complex than if you're just doing a basic component test.
48+
*
49+
* More about entryComponents complexity: https://github.com/angular/angular/issues/12079
50+
*
51+
* Use:
52+
* ```
53+
* beforeEach(nsTestBedBeforeEach([MyComponent,MyFailComponent]));
54+
* ```
55+
*
56+
* **NOTE*** Remember to pair with {@see nsTestBedAfterEach}
57+
*
58+
* @param components Any components that you will create during the test
59+
* @param providers Any services your tests depend on
60+
* @param imports Any module imports your tests depend on
61+
* @param entryComponents Any entry components that your tests depend on
62+
*/
63+
export function nsTestBedBeforeEach(
64+
components: any[],
65+
providers: any[] = [],
66+
imports: any[] = [],
67+
entryComponents: any[] = []) {
68+
return (done) => {
69+
activeTestFixtures.push([]);
70+
// If there are no entry components we can take the simple path.
71+
if (entryComponents.length === 0) {
72+
TestBed.configureTestingModule({
73+
declarations: [...components],
74+
providers: [...providers],
75+
imports: [NativeScriptModule, ...imports]
76+
});
77+
} else {
78+
// If there are entry components, we have to reset the testing platform.
79+
//
80+
// There's got to be a better way... (o_O)
81+
TestBed.resetTestEnvironment();
82+
@NgModule({
83+
declarations: entryComponents,
84+
exports: entryComponents,
85+
entryComponents: entryComponents
86+
})
87+
class EntryComponentsTestModule {
88+
}
89+
TestBed.initTestEnvironment(
90+
EntryComponentsTestModule,
91+
platformBrowserDynamicTesting(NS_COMPILER_PROVIDERS)
92+
);
93+
TestBed.configureTestingModule({
94+
declarations: components,
95+
imports: [
96+
NativeScriptModule, NativeScriptTestingModule, CommonModule,
97+
...imports
98+
],
99+
providers: [...providers, ...NATIVESCRIPT_TESTING_PROVIDERS],
100+
});
101+
}
102+
TestBed.compileComponents()
103+
.then(() => done())
104+
.catch((e) => {
105+
console.log(`Failed to instantiate test component with error: ${e}`);
106+
console.log(e.stack);
107+
done();
108+
});
109+
};
110+
}
111+
112+
/**
113+
* Helper for a basic component TestBed clean up.
114+
* @param resetEnv When true the testing environment will be reset
115+
* @param resetFn When resetting the environment, use this init function
116+
*/
117+
export function nsTestBedAfterEach(resetEnv = true, resetFn = nsTestBedInit) {
118+
return () => {
119+
if (activeTestFixtures.length === 0) {
120+
throw new Error(
121+
`There are no more declared fixtures.` +
122+
`Did you call "nsTestBedBeforeEach" and "nsTestBedAfterEach" an equal number of times?`
123+
);
124+
}
125+
const root = testingRootView() as LayoutBase;
126+
const fixtures = activeTestFixtures.pop();
127+
fixtures.forEach((fixture) => {
128+
const fixtureView = <View>fixture.nativeElement;
129+
if (fixtureView.parent === root) {
130+
root.removeChild(fixtureView);
131+
}
132+
fixture.destroy();
133+
});
134+
TestBed.resetTestingModule();
135+
if (resetEnv) {
136+
TestBed.resetTestEnvironment();
137+
resetFn();
138+
}
139+
};
140+
}
141+
142+
/**
143+
* Render a component using the TestBed helper, and return a promise that resolves when the
144+
* ComponentFixture is fully initialized.
145+
*/
146+
export function nsTestBedRender<T>(componentType: Type<T>): Promise<ComponentFixture<T>> {
147+
const fixture = TestBed.createComponent(componentType);
148+
fixture.detectChanges();
149+
return fixture.whenRenderingDone()
150+
// TODO(jd): it seems that the whenStable and whenRenderingDone utilities of ComponentFixture
151+
// do not work as expected. I looked at how to fix it and it's not clear how to provide
152+
// a {N} specific subclass, because ComponentFixture is newed directly rather than injected
153+
// What to do about it? Maybe fakeAsync can help? For now just setTimeout for 100ms (x_X)
154+
.then(promiseWait(100))
155+
.then(() => {
156+
const list = activeTestFixtures[activeTestFixtures.length - 1];
157+
if (!list) {
158+
console.warn(
159+
"nsTestBedRender called without nsTestBedBeforeEach/nsTestBedAfter each. " +
160+
"You are responsible for calling 'fixture.destroy()' when your test is done " +
161+
"in order to clean up the components that are created."
162+
);
163+
} else {
164+
list.push(fixture);
165+
}
166+
return fixture;
167+
});
168+
}

Diff for: nativescript-angular/zone-js/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Zone.js for NativeScript
2+
---
3+
4+
Zone.js is a library that aims to intercept all asynchronous API calls made in an environment, in order
5+
to wrap them into coherent execution contexts over time.
6+
7+
NativeScript executes inside an environment that Zone.js is not designed to work in, so a custom Zone.js output
8+
must be created.
9+
10+
Find out more about this in the [Upgrading Zone.js document](../../doc/upgrading-zonejs.md)

0 commit comments

Comments
 (0)