fix(testing): async/fakeAsync/inject/withModule helpers should pass through context to callback functions (#13718)
Make sure that context (`this`) that is passed to functions generated by test helpers is passed through to the callback functions. Enables usage of Jasmine's variable sharing system to prevent accidental memory leaks during test runs.
This commit is contained in:
parent
d69717cf79
commit
5f40e5ba21
|
@ -31,14 +31,15 @@ export function async(fn: Function): (done: any) => any {
|
||||||
// If we're running using the Jasmine test framework, adapt to call the 'done'
|
// If we're running using the Jasmine test framework, adapt to call the 'done'
|
||||||
// function when asynchronous activity is finished.
|
// function when asynchronous activity is finished.
|
||||||
if (_global.jasmine) {
|
if (_global.jasmine) {
|
||||||
return (done: any) => {
|
// Not using an arrow function to preserve context passed from call site
|
||||||
|
return function(done: any) {
|
||||||
if (!done) {
|
if (!done) {
|
||||||
// if we run beforeEach in @angular/core/testing/testing_internal then we get no done
|
// if we run beforeEach in @angular/core/testing/testing_internal then we get no done
|
||||||
// fake it here and assume sync.
|
// fake it here and assume sync.
|
||||||
done = function() {};
|
done = function() {};
|
||||||
done.fail = function(e: any) { throw e; };
|
done.fail = function(e: any) { throw e; };
|
||||||
}
|
}
|
||||||
runInTestZone(fn, done, (err: any) => {
|
runInTestZone(fn, this, done, (err: any) => {
|
||||||
if (typeof err === 'string') {
|
if (typeof err === 'string') {
|
||||||
return done.fail(new Error(<string>err));
|
return done.fail(new Error(<string>err));
|
||||||
} else {
|
} else {
|
||||||
|
@ -50,12 +51,16 @@ export function async(fn: Function): (done: any) => any {
|
||||||
// Otherwise, return a promise which will resolve when asynchronous activity
|
// Otherwise, return a promise which will resolve when asynchronous activity
|
||||||
// is finished. This will be correctly consumed by the Mocha framework with
|
// is finished. This will be correctly consumed by the Mocha framework with
|
||||||
// it('...', async(myFn)); or can be used in a custom framework.
|
// it('...', async(myFn)); or can be used in a custom framework.
|
||||||
return () => new Promise<void>((finishCallback, failCallback) => {
|
// Not using an arrow function to preserve context passed from call site
|
||||||
runInTestZone(fn, finishCallback, failCallback);
|
return function() {
|
||||||
});
|
return new Promise<void>((finishCallback, failCallback) => {
|
||||||
|
runInTestZone(fn, this, finishCallback, failCallback);
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function runInTestZone(fn: Function, finishCallback: Function, failCallback: Function) {
|
function runInTestZone(
|
||||||
|
fn: Function, context: any, finishCallback: Function, failCallback: Function) {
|
||||||
const currentZone = Zone.current;
|
const currentZone = Zone.current;
|
||||||
const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
|
const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
|
||||||
if (AsyncTestZoneSpec === undefined) {
|
if (AsyncTestZoneSpec === undefined) {
|
||||||
|
@ -103,5 +108,5 @@ function runInTestZone(fn: Function, finishCallback: Function, failCallback: Fun
|
||||||
'test');
|
'test');
|
||||||
proxyZoneSpec.setDelegate(testZoneSpec);
|
proxyZoneSpec.setDelegate(testZoneSpec);
|
||||||
});
|
});
|
||||||
return Zone.current.runGuarded(fn);
|
return Zone.current.runGuarded(fn, context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ let _inFakeAsyncCall = false;
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export function fakeAsync(fn: Function): (...args: any[]) => any {
|
export function fakeAsync(fn: Function): (...args: any[]) => any {
|
||||||
|
// Not using an arrow function to preserve context passed from call site
|
||||||
return function(...args: any[]) {
|
return function(...args: any[]) {
|
||||||
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
|
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
|
||||||
if (_inFakeAsyncCall) {
|
if (_inFakeAsyncCall) {
|
||||||
|
@ -67,7 +68,7 @@ export function fakeAsync(fn: Function): (...args: any[]) => any {
|
||||||
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
|
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
|
||||||
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
|
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
|
||||||
try {
|
try {
|
||||||
res = fn(...args);
|
res = fn.apply(this, args);
|
||||||
flushMicrotasks();
|
flushMicrotasks();
|
||||||
} finally {
|
} finally {
|
||||||
proxyZoneSpec.setDelegate(lastProxyZoneSpec);
|
proxyZoneSpec.setDelegate(lastProxyZoneSpec);
|
||||||
|
|
|
@ -325,10 +325,10 @@ export class TestBed implements Injector {
|
||||||
return result === UNDEFINED ? this._compiler.injector.get(token, notFoundValue) : result;
|
return result === UNDEFINED ? this._compiler.injector.get(token, notFoundValue) : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(tokens: any[], fn: Function): any {
|
execute(tokens: any[], fn: Function, context?: any): any {
|
||||||
this._initIfNeeded();
|
this._initIfNeeded();
|
||||||
const params = tokens.map(t => this.get(t));
|
const params = tokens.map(t => this.get(t));
|
||||||
return fn(...params);
|
return fn.apply(context, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): void {
|
overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): void {
|
||||||
|
@ -413,17 +413,19 @@ export function getTestBed() {
|
||||||
export function inject(tokens: any[], fn: Function): () => any {
|
export function inject(tokens: any[], fn: Function): () => any {
|
||||||
const testBed = getTestBed();
|
const testBed = getTestBed();
|
||||||
if (tokens.indexOf(AsyncTestCompleter) >= 0) {
|
if (tokens.indexOf(AsyncTestCompleter) >= 0) {
|
||||||
return () =>
|
// Not using an arrow function to preserve context passed from call site
|
||||||
// Return an async test method that returns a Promise if AsyncTestCompleter is one of
|
return function() {
|
||||||
// the
|
// Return an async test method that returns a Promise if AsyncTestCompleter is one of
|
||||||
// injected tokens.
|
// the injected tokens.
|
||||||
testBed.compileComponents().then(() => {
|
return testBed.compileComponents().then(() => {
|
||||||
const completer: AsyncTestCompleter = testBed.get(AsyncTestCompleter);
|
const completer: AsyncTestCompleter = testBed.get(AsyncTestCompleter);
|
||||||
testBed.execute(tokens, fn);
|
testBed.execute(tokens, fn, this);
|
||||||
return completer.promise;
|
return completer.promise;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return () => testBed.execute(tokens, fn);
|
// Not using an arrow function to preserve context passed from call site
|
||||||
|
return function() { return testBed.execute(tokens, fn, this); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,9 +443,11 @@ export class InjectSetupWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
inject(tokens: any[], fn: Function): () => any {
|
inject(tokens: any[], fn: Function): () => any {
|
||||||
return () => {
|
const self = this;
|
||||||
this._addModule();
|
// Not using an arrow function to preserve context passed from call site
|
||||||
return inject(tokens, fn)();
|
return function() {
|
||||||
|
self._addModule();
|
||||||
|
return inject(tokens, fn).call(this);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,12 +460,13 @@ export function withModule(moduleDef: TestModuleMetadata, fn: Function): () => a
|
||||||
export function withModule(moduleDef: TestModuleMetadata, fn: Function = null): (() => any)|
|
export function withModule(moduleDef: TestModuleMetadata, fn: Function = null): (() => any)|
|
||||||
InjectSetupWrapper {
|
InjectSetupWrapper {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
return () => {
|
// Not using an arrow function to preserve context passed from call site
|
||||||
|
return function() {
|
||||||
const testBed = getTestBed();
|
const testBed = getTestBed();
|
||||||
if (moduleDef) {
|
if (moduleDef) {
|
||||||
testBed.configureTestingModule(moduleDef);
|
testBed.configureTestingModule(moduleDef);
|
||||||
}
|
}
|
||||||
return fn();
|
return fn.apply(this);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return new InjectSetupWrapper(() => moduleDef);
|
return new InjectSetupWrapper(() => moduleDef);
|
||||||
|
|
|
@ -114,31 +114,63 @@ class CompWithUrlTemplate {
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('public testing API', () => {
|
describe('public testing API', () => {
|
||||||
describe('using the async helper', () => {
|
describe('using the async helper with context passing', () => {
|
||||||
let actuallyDone: boolean;
|
beforeEach(function() { this.actuallyDone = false; });
|
||||||
|
|
||||||
beforeEach(() => actuallyDone = false);
|
afterEach(function() { expect(this.actuallyDone).toEqual(true); });
|
||||||
|
|
||||||
afterEach(() => expect(actuallyDone).toEqual(true));
|
it('should run normal tests', function() { this.actuallyDone = true; });
|
||||||
|
|
||||||
it('should run normal tests', () => actuallyDone = true);
|
it('should run normal async tests', function(done) {
|
||||||
|
|
||||||
it('should run normal async tests', (done) => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
actuallyDone = true;
|
this.actuallyDone = true;
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run async tests with tasks',
|
it('should run async tests with tasks',
|
||||||
async(() => setTimeout(() => actuallyDone = true, 0)));
|
async(function() { setTimeout(() => this.actuallyDone = true, 0); }));
|
||||||
|
|
||||||
it('should run async tests with promises', async(() => {
|
it('should run async tests with promises', async(function() {
|
||||||
const p = new Promise((resolve, reject) => setTimeout(resolve, 10));
|
const p = new Promise((resolve, reject) => setTimeout(resolve, 10));
|
||||||
p.then(() => actuallyDone = true);
|
p.then(() => this.actuallyDone = true);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('basic context passing to inject, fakeAsync and withModule helpers', () => {
|
||||||
|
const moduleConfig = {
|
||||||
|
providers: [FancyService],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() { this.contextModified = false; });
|
||||||
|
|
||||||
|
afterEach(function() { expect(this.contextModified).toEqual(true); });
|
||||||
|
|
||||||
|
it('should pass context to inject helper',
|
||||||
|
inject([], function() { this.contextModified = true; }));
|
||||||
|
|
||||||
|
it('should pass context to fakeAsync helper',
|
||||||
|
fakeAsync(function() { this.contextModified = true; }));
|
||||||
|
|
||||||
|
it('should pass context to withModule helper - simple',
|
||||||
|
withModule(moduleConfig, function() { this.contextModified = true; }));
|
||||||
|
|
||||||
|
it('should pass context to withModule helper - advanced',
|
||||||
|
withModule(moduleConfig).inject([FancyService], function(service: FancyService) {
|
||||||
|
expect(service.value).toBe('real value');
|
||||||
|
this.contextModified = true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should preserve context when async and inject helpers are combined',
|
||||||
|
async(inject([], function() { setTimeout(() => this.contextModified = true, 0); })));
|
||||||
|
|
||||||
|
it('should preserve context when fakeAsync and inject helpers are combined',
|
||||||
|
fakeAsync(inject([], function() {
|
||||||
|
setTimeout(() => this.contextModified = true, 0);
|
||||||
|
tick(1);
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
describe('using the test injector with the inject helper', () => {
|
describe('using the test injector with the inject helper', () => {
|
||||||
describe('setting up Providers', () => {
|
describe('setting up Providers', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -67,7 +67,7 @@ export declare class TestBed implements Injector {
|
||||||
}): void;
|
}): void;
|
||||||
configureTestingModule(moduleDef: TestModuleMetadata): void;
|
configureTestingModule(moduleDef: TestModuleMetadata): void;
|
||||||
createComponent<T>(component: Type<T>): ComponentFixture<T>;
|
createComponent<T>(component: Type<T>): ComponentFixture<T>;
|
||||||
execute(tokens: any[], fn: Function): any;
|
execute(tokens: any[], fn: Function, context?: any): any;
|
||||||
get(token: any, notFoundValue?: any): any;
|
get(token: any, notFoundValue?: any): any;
|
||||||
/** @experimental */ initTestEnvironment(ngModule: Type<any>, platform: PlatformRef): void;
|
/** @experimental */ initTestEnvironment(ngModule: Type<any>, platform: PlatformRef): void;
|
||||||
overrideComponent(component: Type<any>, override: MetadataOverride<Component>): void;
|
overrideComponent(component: Type<any>, override: MetadataOverride<Component>): void;
|
||||||
|
|
Loading…
Reference in New Issue