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(ivy): ensure overrides for 'multi: true' only appear once in final providers #33104

Closed
wants to merge 7 commits into from
71 changes: 69 additions & 2 deletions packages/core/test/test_bed_spec.ts
Expand Up @@ -225,10 +225,77 @@ describe('TestBed', () => {
});

it('allow to override a provider', () => {
TestBed.overrideProvider(NAME, {useValue: 'injected World !'});
TestBed.overrideProvider(NAME, {useValue: 'injected World!'});
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello injected World !');
expect(hello.nativeElement).toHaveText('Hello injected World!');
});

it('uses the most recent provider override', () => {
TestBed.overrideProvider(NAME, {useValue: 'injected World!'});
TestBed.overrideProvider(NAME, {useValue: 'injected World a second time!'});
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello injected World a second time!');
});

it('overrides a providers in an array', () => {
TestBed.configureTestingModule({
imports: [HelloWorldModule],
providers: [
[{provide: NAME, useValue: 'injected World!'}],
]
});
TestBed.overrideProvider(NAME, {useValue: 'injected World a second time!'});
const hello = TestBed.createComponent(HelloWorld);
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello injected World a second time!');
});

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: singleToken, useValue: 't1'},
{provide: multiToken, useValue: 'valueFromModule2', multi: true},
{provide: multiToken, useValue: 'secondValueFromModule2', multi: true}
]
})
class MyModule2 {
}

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

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(multiToken, { useValue: overrideValue, multi: true } as any);

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

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(multiToken, { useValue: overrideValue, multi: true } as any);

const value = TestBed.inject(multiToken);
expect(value.length).toEqual(overrideValue.length);
expect(value).toEqual(overrideValue as {} as string[]);
});
});

it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.overrideProvider)',
Expand Down
27 changes: 8 additions & 19 deletions packages/core/testing/src/r3_test_bed_compiler.ts
Expand Up @@ -159,7 +159,7 @@ export class R3TestBedCompiler {
provide: token,
useFactory: provider.useFactory,
deps: provider.deps || [],
multi: provider.multi,
multi: provider.multi
} :
{provide: token, useValue: provider.useValue, multi: provider.multi};

Expand Down Expand Up @@ -619,34 +619,23 @@ export class R3TestBedCompiler {
private getOverriddenProviders(providers?: Provider[]): Provider[] {
if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return [];

const overrides = this.getProviderOverrides(providers);
const hasMultiProviderOverrides = overrides.some(isMultiProvider);
const overriddenProviders = [...providers, ...overrides];

// No additional processing is required in case we have no multi providers to override
if (!hasMultiProviderOverrides) {
return overriddenProviders;
}

const flattenedProviders = flatten<Provider[]>(providers);
const overrides = this.getProviderOverrides(flattenedProviders);
const overriddenProviders = [...flattenedProviders, ...overrides];
const final: Provider[] = [];
const seenMultiProviders = new Set<Provider>();

// 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 fiter out all
// 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 (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);
if (provider && provider.useValue && Array.isArray(provider.useValue)) {
forEachRight(provider.useValue, (value: any) => {
// Unwrap provider override array into individual providers in final set.
final.unshift({provide: token, useValue: value, multi: true});
});
} else {
final.unshift(provider);
}
final.unshift({...provider, multi: false});
}
} else {
final.unshift(provider);
Expand Down