Skip to content

Commit

Permalink
fixup! fix(ivy): ensure overrides for 'multi: true' only appear once …
Browse files Browse the repository at this point in the history
…in final providers
  • Loading branch information
atscott committed Oct 21, 2019
1 parent 8255771 commit d89682d
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 24 deletions.
33 changes: 23 additions & 10 deletions packages/core/test/test_bed_spec.ts
Expand Up @@ -252,34 +252,47 @@ describe('TestBed', () => {
expect(hello.nativeElement).toHaveText('Hello injected World a second time!');
});

describe('allow override of multi provider', () => {
const token = new InjectionToken<string[]>('token');
@NgModule({providers: [{provide: token, useValue: 'valueFromModule', multi: true}]})
describe('multi providers', () => {
const multiToken = new InjectionToken<string[]>('multiToken');
const singleToken = new InjectionToken<string>('singleToken');
@NgModule({providers: [{provide: multiToken, useValue: 'valueFromModule', multi: true}]})
class MyModule {
}

@NgModule({providers: [{provide: token, useValue: 'valueFromModule2', multi: true}]})
@NgModule({
providers: [
{provide: singleToken, useValue: 't1'},
{provide: multiToken, useValue: 'valueFromModule2', multi: true},
{provide: multiToken, useValue: 'secondValueFromModule2', multi: true}
]
})
class MyModule2 {
}

beforeEach(() => { TestBed.configureTestingModule({imports: [MyModule, MyModule2]}); });

it('with an array', () => {
it('is preserved when other provider is overridden', () => {
TestBed.overrideProvider(singleToken, {useValue: ''});
const value = TestBed.inject(multiToken);
expect(value.length).toEqual(3);
});

it('overridden with an array', () => {
const overrideValue = ['override'];
TestBed.overrideProvider(token, { useValue: overrideValue, multi: true } as any);
TestBed.overrideProvider(multiToken, { useValue: overrideValue, multi: true } as any);

const value = TestBed.inject(token);
const value = TestBed.inject(multiToken);
expect(value.length).toEqual(overrideValue.length);
expect(value).toEqual(overrideValue);
});

it('with a non-array', () => {
it('overridden with a non-array', () => {
// This is actually invalid because multi providers return arrays. We have this here so we can
// ensure Ivy behaves the same as VE does currently.
const overrideValue = 'override';
TestBed.overrideProvider(token, { useValue: overrideValue, multi: true } as any);
TestBed.overrideProvider(multiToken, { useValue: overrideValue, multi: true } as any);

const value = TestBed.inject(token);
const value = TestBed.inject(multiToken);
expect(value.length).toEqual(overrideValue.length);
expect(value).toEqual(overrideValue as {} as string[]);
});
Expand Down
36 changes: 22 additions & 14 deletions packages/core/testing/src/r3_test_bed_compiler.ts
Expand Up @@ -154,15 +154,14 @@ export class R3TestBedCompiler {
overrideProvider(
token: any,
provider: {useFactory?: Function, useValue?: any, deps?: any[], multi?: boolean}): void {
// Note, we explicitly avoid setting `multi: true` here. If multi is true and we set it, TestBed
// will override
// all instances of the token and when the test module is finalized, the injector will add each
// occurrence of the
// override to the records. We only ever want one when it's overridden, so we treat it as
// non-multi.
const providerDef = provider.useFactory ?
{provide: token, useFactory: provider.useFactory, deps: provider.deps || []} :
{provide: token, useValue: provider.useValue};
{
provide: token,
useFactory: provider.useFactory,
deps: provider.deps || [],
multi: provider.multi
} :
{provide: token, useValue: provider.useValue, multi: provider.multi};

let injectableDef: InjectableDef<any>|null;
const isRoot =
Expand Down Expand Up @@ -623,17 +622,26 @@ export class R3TestBedCompiler {
const flattenedProviders = flatten<Provider[]>(providers);
const overrides = this.getProviderOverrides(flattenedProviders);
const overriddenProviders = [...flattenedProviders, ...overrides];
const tokenToProviderMap: Map<any, Provider> = new Map();
const final: Provider[] = [];
const seenMultiProviders = new Set<Provider>();

// Iterate through providers from the end so only the most recent provider is used for a given
// token and overrides are processed first.
// We iterate through the list of providers in reverse order to make sure multi provider
// overrides take precedence over the values defined in provider list. We also filter out all
// multi providers that have overrides, keeping overridden values only.
forEachRight(overriddenProviders, (provider: any) => {
const token: any = getProviderToken(provider);
if (!tokenToProviderMap.has(token)) {
tokenToProviderMap.set(token, provider);
if (isMultiProvider(provider) && this.providerOverridesByToken.has(token)) {
// Don't add overridden multi-providers twice because when you override a multi-provider, we
// treat it as `{multi: false}` to avoid providing the same value multiple times.
if (!seenMultiProviders.has(token)) {
seenMultiProviders.add(token);
final.unshift({...provider, multi: false});
}
} else {
final.unshift(provider);
}
});
return Array.from(tokenToProviderMap.values());
return final;
}

private hasProviderOverrides(providers?: Provider[]): boolean {
Expand Down

0 comments on commit d89682d

Please sign in to comment.