Skip to content
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

fix(testing): async/fakeAsync/inject/withModule helpers should pass through context to callback functions #13718

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 12 additions & 7 deletions modules/@angular/core/testing/async.ts
Expand Up @@ -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'
// function when asynchronous activity is finished.
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 we run beforeEach in @angular/core/testing/testing_internal then we get no done
// fake it here and assume sync.
done = function() {};
done.fail = function(e: any) { throw e; };
}
runInTestZone(fn, done, (err: any) => {
runInTestZone(fn, this, done, (err: any) => {
if (typeof err === 'string') {
return done.fail(new Error(<string>err));
} else {
Expand All @@ -50,12 +51,16 @@ export function async(fn: Function): (done: any) => any {
// Otherwise, return a promise which will resolve when asynchronous activity
// is finished. This will be correctly consumed by the Mocha framework with
// it('...', async(myFn)); or can be used in a custom framework.
return () => new Promise<void>((finishCallback, failCallback) => {
runInTestZone(fn, finishCallback, failCallback);
});
// Not using an arrow function to preserve context passed from call site
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 AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec'];
if (AsyncTestZoneSpec === undefined) {
Expand Down Expand Up @@ -103,5 +108,5 @@ function runInTestZone(fn: Function, finishCallback: Function, failCallback: Fun
'test');
proxyZoneSpec.setDelegate(testZoneSpec);
});
return Zone.current.runGuarded(fn);
return Zone.current.runGuarded(fn, context);
}
3 changes: 2 additions & 1 deletion modules/@angular/core/testing/fake_async.ts
Expand Up @@ -48,6 +48,7 @@ let _inFakeAsyncCall = false;
* @experimental
*/
export function fakeAsync(fn: Function): (...args: any[]) => any {
// Not using an arrow function to preserve context passed from call site
return function(...args: any[]) {
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
if (_inFakeAsyncCall) {
Expand All @@ -67,7 +68,7 @@ export function fakeAsync(fn: Function): (...args: any[]) => any {
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
try {
res = fn(...args);
res = fn.apply(this, args);
flushMicrotasks();
} finally {
proxyZoneSpec.setDelegate(lastProxyZoneSpec);
Expand Down
39 changes: 22 additions & 17 deletions modules/@angular/core/testing/test_bed.ts
Expand Up @@ -324,10 +324,10 @@ export class TestBed implements Injector {
return result === UNDEFINED ? this._compiler.injector.get(token, notFoundValue) : result;
}

execute(tokens: any[], fn: Function): any {
execute(tokens: any[], fn: Function, context?: any): any {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed as suggested. Tested it out, this is not breaking but needs a minor version bump.

this._initIfNeeded();
const params = tokens.map(t => this.get(t));
return fn(...params);
return fn.apply(context, params);
}

overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): void {
Expand Down Expand Up @@ -412,17 +412,19 @@ export function getTestBed() {
export function inject(tokens: any[], fn: Function): () => any {
const testBed = getTestBed();
if (tokens.indexOf(AsyncTestCompleter) >= 0) {
return () =>
// Return an async test method that returns a Promise if AsyncTestCompleter is one of
// the
// injected tokens.
testBed.compileComponents().then(() => {
const completer: AsyncTestCompleter = testBed.get(AsyncTestCompleter);
testBed.execute(tokens, fn);
return completer.promise;
});
// Not using an arrow function to preserve context passed from call site
return function() {
// Return an async test method that returns a Promise if AsyncTestCompleter is one of
// the injected tokens.
return testBed.compileComponents().then(() => {
const completer: AsyncTestCompleter = testBed.get(AsyncTestCompleter);
testBed.execute(tokens, fn, this);
return completer.promise;
});
};
} 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); };
}
}

Expand All @@ -440,9 +442,11 @@ export class InjectSetupWrapper {
}

inject(tokens: any[], fn: Function): () => any {
return () => {
this._addModule();
return inject(tokens, fn)();
const self = this;
// Not using an arrow function to preserve context passed from call site
return function() {
self._addModule();
return inject(tokens, fn).call(this);
};
}
}
Expand All @@ -455,12 +459,13 @@ export function withModule(moduleDef: TestModuleMetadata, fn: Function): () => a
export function withModule(moduleDef: TestModuleMetadata, fn: Function = null): (() => any)|
InjectSetupWrapper {
if (fn) {
return () => {
// Not using an arrow function to preserve context passed from call site
return function() {
const testBed = getTestBed();
if (moduleDef) {
testBed.configureTestingModule(moduleDef);
}
return fn();
return fn.apply(this);
};
}
return new InjectSetupWrapper(() => moduleDef);
Expand Down
54 changes: 43 additions & 11 deletions modules/@angular/platform-browser/test/testing_public_spec.ts
Expand Up @@ -114,31 +114,63 @@ class CompWithUrlTemplate {

export function main() {
describe('public testing API', () => {
describe('using the async helper', () => {
let actuallyDone: boolean;
describe('using the async helper with context passing', () => {
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', (done) => {
it('should run normal async tests', function(done) {
setTimeout(() => {
actuallyDone = true;
this.actuallyDone = true;
done();
}, 0);
});

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));
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('setting up Providers', () => {
beforeEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion tools/public_api_guard/core/testing/index.d.ts
Expand Up @@ -67,7 +67,7 @@ export declare class TestBed implements Injector {
}): void;
configureTestingModule(moduleDef: TestModuleMetadata): void;
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;
/** @experimental */ initTestEnvironment(ngModule: Type<any>, platform: PlatformRef): void;
overrideComponent(component: Type<any>, override: MetadataOverride<Component>): void;
Expand Down