fix(ivy): multi provider override support in TestBed (#29919)
Overriding multi provider values (providers with `multi: true` flag) via TestBed require additional handling: all existing multi-provider values for the same token should be removed from the override list, so that they are not included into the final value of a given provider. This commit adds this logic to make sure we handle multi providers correctly. PR Close #29919
This commit is contained in:
parent
5d824c4153
commit
5f1b6372c7
|
@ -229,6 +229,27 @@ describe('TestBed', () => {
|
||||||
expect(hello.nativeElement).toHaveText('Hello injected World !');
|
expect(hello.nativeElement).toHaveText('Hello injected World !');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allow to override multi provider', () => {
|
||||||
|
const MY_TOKEN = new InjectionToken('MyProvider');
|
||||||
|
class MyProvider {}
|
||||||
|
|
||||||
|
@Component({selector: 'my-comp', template: ``})
|
||||||
|
class MyComp {
|
||||||
|
constructor(@Inject(MY_TOKEN) public myProviders: MyProvider[]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [MyComp],
|
||||||
|
providers: [{provide: MY_TOKEN, useValue: {value: 'old provider'}, multi: true}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiOverride = {useValue: [{value: 'new provider'}], multi: true};
|
||||||
|
TestBed.overrideProvider(MY_TOKEN, multiOverride as any);
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
|
expect(fixture.componentInstance.myProviders).toEqual([{value: 'new provider'}]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should resolve components that are extended by other components', () => {
|
it('should resolve components that are extended by other components', () => {
|
||||||
// SimpleApp uses SimpleCmp in its template, which is extended by InheritedCmp
|
// SimpleApp uses SimpleCmp in its template, which is extended by InheritedCmp
|
||||||
const simpleApp = TestBed.createComponent(SimpleApp);
|
const simpleApp = TestBed.createComponent(SimpleApp);
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class R3TestBedCompiler {
|
||||||
|
|
||||||
private providerOverrides: Provider[] = [];
|
private providerOverrides: Provider[] = [];
|
||||||
private rootProviderOverrides: Provider[] = [];
|
private rootProviderOverrides: Provider[] = [];
|
||||||
private providerOverridesByToken = new Map<any, Provider[]>();
|
private providerOverridesByToken = new Map<any, Provider>();
|
||||||
private moduleProvidersOverridden = new Set<Type<any>>();
|
private moduleProvidersOverridden = new Set<Type<any>>();
|
||||||
|
|
||||||
private testModuleType: NgModuleType<any>;
|
private testModuleType: NgModuleType<any>;
|
||||||
|
@ -142,11 +142,17 @@ export class R3TestBedCompiler {
|
||||||
this.pendingPipes.add(pipe);
|
this.pendingPipes.add(pipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}):
|
overrideProvider(
|
||||||
void {
|
token: any,
|
||||||
|
provider: {useFactory?: Function, useValue?: any, deps?: any[], multi?: boolean}): void {
|
||||||
const providerDef = provider.useFactory ?
|
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;
|
let injectableDef: InjectableDef<any>|null;
|
||||||
const isRoot =
|
const isRoot =
|
||||||
|
@ -155,10 +161,8 @@ export class R3TestBedCompiler {
|
||||||
const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides;
|
const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides;
|
||||||
overridesBucket.push(providerDef);
|
overridesBucket.push(providerDef);
|
||||||
|
|
||||||
// Keep all overrides grouped by token as well for fast lookups using token
|
// Keep overrides grouped by token as well for fast lookups using token
|
||||||
const overridesForToken = this.providerOverridesByToken.get(token) || [];
|
this.providerOverridesByToken.set(token, providerDef);
|
||||||
overridesForToken.push(providerDef);
|
|
||||||
this.providerOverridesByToken.set(token, overridesForToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideTemplateUsingTestingModule(type: Type<any>, template: string): void {
|
overrideTemplateUsingTestingModule(type: Type<any>, template: string): void {
|
||||||
|
@ -349,10 +353,7 @@ export class R3TestBedCompiler {
|
||||||
this.maybeStoreNgDef(NG_INJECTOR_DEF, moduleType);
|
this.maybeStoreNgDef(NG_INJECTOR_DEF, moduleType);
|
||||||
|
|
||||||
this.storeFieldOfDefOnType(moduleType, NG_INJECTOR_DEF, 'providers');
|
this.storeFieldOfDefOnType(moduleType, NG_INJECTOR_DEF, 'providers');
|
||||||
injectorDef.providers = [
|
injectorDef.providers = this.getOverriddenProviders(injectorDef.providers);
|
||||||
...injectorDef.providers, //
|
|
||||||
...this.getProviderOverrides(injectorDef.providers)
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply provider overrides to imported modules recursively
|
// Apply provider overrides to imported modules recursively
|
||||||
|
@ -561,11 +562,9 @@ export class R3TestBedCompiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get overrides for a specific provider (if any)
|
// get overrides for a specific provider (if any)
|
||||||
private getSingleProviderOverrides(provider: Provider&{provide?: any}): Provider[] {
|
private getSingleProviderOverrides(provider: Provider): Provider|null {
|
||||||
const token = provider && typeof provider === 'object' && provider.hasOwnProperty('provide') ?
|
const token = getProviderToken(provider);
|
||||||
provider.provide :
|
return this.providerOverridesByToken.get(token) || null;
|
||||||
provider;
|
|
||||||
return this.providerOverridesByToken.get(token) || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProviderOverrides(providers?: Provider[]): Provider[] {
|
private getProviderOverrides(providers?: Provider[]): Provider[] {
|
||||||
|
@ -575,8 +574,47 @@ export class R3TestBedCompiler {
|
||||||
// provider. The outer flatten() then flattens the produced overrides array. If this is not
|
// provider. The outer flatten() then flattens the produced overrides array. If this is not
|
||||||
// done, the array can contain other empty arrays (e.g. `[[], []]`) which leak into the
|
// done, the array can contain other empty arrays (e.g. `[[], []]`) which leak into the
|
||||||
// providers array and contaminate any error messages that might be generated.
|
// providers array and contaminate any error messages that might be generated.
|
||||||
return flatten(
|
return flatten(flatten(
|
||||||
flatten(providers, (provider: Provider) => this.getSingleProviderOverrides(provider)));
|
providers, (provider: Provider) => this.getSingleProviderOverrides(provider) || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
// 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)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final.unshift(provider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return final;
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasProviderOverrides(providers?: Provider[]): boolean {
|
private hasProviderOverrides(providers?: Provider[]): boolean {
|
||||||
|
@ -589,10 +627,7 @@ export class R3TestBedCompiler {
|
||||||
this.maybeStoreNgDef(field, declaration);
|
this.maybeStoreNgDef(field, declaration);
|
||||||
|
|
||||||
const resolver = def.providersResolver;
|
const resolver = def.providersResolver;
|
||||||
const processProvidersFn = (providers: Provider[]) => {
|
const processProvidersFn = (providers: Provider[]) => this.getOverriddenProviders(providers);
|
||||||
const overrides = this.getProviderOverrides(providers);
|
|
||||||
return [...providers, ...overrides];
|
|
||||||
};
|
|
||||||
this.storeFieldOfDefOnType(declaration, field, 'providersResolver');
|
this.storeFieldOfDefOnType(declaration, field, 'providersResolver');
|
||||||
def.providersResolver = (ngDef: DirectiveDef<any>) => resolver(ngDef, processProvidersFn);
|
def.providersResolver = (ngDef: DirectiveDef<any>) => resolver(ngDef, processProvidersFn);
|
||||||
}
|
}
|
||||||
|
@ -628,6 +663,24 @@ function flatten<T>(values: any[], mapFn?: (value: T) => any): T[] {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProviderField(provider: Provider, field: string) {
|
||||||
|
return provider && typeof provider === 'object' && (provider as any)[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProviderToken(provider: Provider) {
|
||||||
|
return getProviderField(provider, 'provide') || provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultiProvider(provider: Provider) {
|
||||||
|
return !!getProviderField(provider, 'multi');
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEachRight<T>(values: T[], fn: (value: T, idx: number) => void): void {
|
||||||
|
for (let idx = values.length - 1; idx >= 0; idx--) {
|
||||||
|
fn(values[idx], idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class R3TestCompiler implements Compiler {
|
class R3TestCompiler implements Compiler {
|
||||||
constructor(private testBed: R3TestBedCompiler) {}
|
constructor(private testBed: R3TestBedCompiler) {}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue