fix(ivy): ensure overrides for 'multi: true' only appear once in final providers (#33104)
PR Close #33104
This commit is contained in:
parent
e16f75db56
commit
e483acaa17
|
@ -225,10 +225,130 @@ describe('TestBed', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allow to override a provider', () => {
|
it('allow to override a provider', () => {
|
||||||
TestBed.overrideProvider(NAME, {useValue: 'injected World !'});
|
TestBed.overrideProvider(NAME, {useValue: 'injected World!'});
|
||||||
const hello = TestBed.createComponent(HelloWorld);
|
const hello = TestBed.createComponent(HelloWorld);
|
||||||
hello.detectChanges();
|
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[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('overrides providers in ModuleWithProviders', () => {
|
||||||
|
const TOKEN = new InjectionToken<string[]>('token');
|
||||||
|
@NgModule()
|
||||||
|
class MyMod {
|
||||||
|
static multi = false;
|
||||||
|
|
||||||
|
static forRoot() {
|
||||||
|
return {
|
||||||
|
ngModule: MyMod,
|
||||||
|
providers: [{provide: TOKEN, multi: MyMod.multi, useValue: 'forRootValue'}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => MyMod.multi = true);
|
||||||
|
|
||||||
|
it('when provider is a "regular" provider', () => {
|
||||||
|
MyMod.multi = false;
|
||||||
|
@NgModule({imports: [MyMod.forRoot()]})
|
||||||
|
class MyMod2 {
|
||||||
|
}
|
||||||
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
||||||
|
TestBed.overrideProvider(TOKEN, {useValue: ['override']});
|
||||||
|
expect(TestBed.inject(TOKEN)).toEqual(['override']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when provider is multi', () => {
|
||||||
|
@NgModule({imports: [MyMod.forRoot()]})
|
||||||
|
class MyMod2 {
|
||||||
|
}
|
||||||
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
||||||
|
TestBed.overrideProvider(TOKEN, {useValue: ['override']});
|
||||||
|
expect(TestBed.inject(TOKEN)).toEqual(['override']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores the original value', () => {
|
||||||
|
@NgModule({imports: [MyMod.forRoot()]})
|
||||||
|
class MyMod2 {
|
||||||
|
}
|
||||||
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
||||||
|
TestBed.overrideProvider(TOKEN, {useValue: ['override']});
|
||||||
|
expect(TestBed.inject(TOKEN)).toEqual(['override']);
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
||||||
|
expect(TestBed.inject(TOKEN)).toEqual(['forRootValue']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.overrideProvider)',
|
it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.overrideProvider)',
|
||||||
|
|
|
@ -35,9 +35,9 @@ type Resolvers = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CleanupOperation {
|
interface CleanupOperation {
|
||||||
field: string;
|
fieldName: string;
|
||||||
def: any;
|
object: any;
|
||||||
original: unknown;
|
originalValue: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class R3TestBedCompiler {
|
export class R3TestBedCompiler {
|
||||||
|
@ -159,7 +159,7 @@ export class R3TestBedCompiler {
|
||||||
provide: token,
|
provide: token,
|
||||||
useFactory: provider.useFactory,
|
useFactory: provider.useFactory,
|
||||||
deps: provider.deps || [],
|
deps: provider.deps || [],
|
||||||
multi: provider.multi,
|
multi: provider.multi
|
||||||
} :
|
} :
|
||||||
{provide: token, useValue: provider.useValue, multi: provider.multi};
|
{provide: token, useValue: provider.useValue, multi: provider.multi};
|
||||||
|
|
||||||
|
@ -381,8 +381,20 @@ export class R3TestBedCompiler {
|
||||||
|
|
||||||
// Apply provider overrides to imported modules recursively
|
// Apply provider overrides to imported modules recursively
|
||||||
const moduleDef: any = (moduleType as any)[NG_MOD_DEF];
|
const moduleDef: any = (moduleType as any)[NG_MOD_DEF];
|
||||||
for (const importType of moduleDef.imports) {
|
for (const importedModule of moduleDef.imports) {
|
||||||
this.applyProviderOverridesToModule(importType);
|
this.applyProviderOverridesToModule(importedModule);
|
||||||
|
}
|
||||||
|
// Also override the providers on any ModuleWithProviders imports since those don't appear in
|
||||||
|
// the moduleDef.
|
||||||
|
for (const importedModule of flatten(injectorDef.imports)) {
|
||||||
|
if (isModuleWithProviders(importedModule)) {
|
||||||
|
this.defCleanupOps.push({
|
||||||
|
object: importedModule,
|
||||||
|
fieldName: 'providers',
|
||||||
|
originalValue: importedModule.providers
|
||||||
|
});
|
||||||
|
importedModule.providers = this.getOverriddenProviders(importedModule.providers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -485,10 +497,10 @@ export class R3TestBedCompiler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeFieldOfDefOnType(type: Type<any>, defField: string, field: string): void {
|
private storeFieldOfDefOnType(type: Type<any>, defField: string, fieldName: string): void {
|
||||||
const def: any = (type as any)[defField];
|
const def: any = (type as any)[defField];
|
||||||
const original: any = def[field];
|
const originalValue: any = def[fieldName];
|
||||||
this.defCleanupOps.push({field, def, original});
|
this.defCleanupOps.push({object: def, fieldName, originalValue});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -519,7 +531,9 @@ export class R3TestBedCompiler {
|
||||||
restoreOriginalState(): void {
|
restoreOriginalState(): void {
|
||||||
// Process cleanup ops in reverse order so the field's original value is restored correctly (in
|
// Process cleanup ops in reverse order so the field's original value is restored correctly (in
|
||||||
// case there were multiple overrides for the same field).
|
// case there were multiple overrides for the same field).
|
||||||
forEachRight(this.defCleanupOps, (op: CleanupOperation) => { op.def[op.field] = op.original; });
|
forEachRight(this.defCleanupOps, (op: CleanupOperation) => {
|
||||||
|
op.object[op.fieldName] = op.originalValue;
|
||||||
|
});
|
||||||
// Restore initial component/directive/pipe defs
|
// Restore initial component/directive/pipe defs
|
||||||
this.initialNgDefs.forEach(
|
this.initialNgDefs.forEach(
|
||||||
(value: [string, PropertyDescriptor | undefined], type: Type<any>) => {
|
(value: [string, PropertyDescriptor | undefined], type: Type<any>) => {
|
||||||
|
@ -619,34 +633,23 @@ export class R3TestBedCompiler {
|
||||||
private getOverriddenProviders(providers?: Provider[]): Provider[] {
|
private getOverriddenProviders(providers?: Provider[]): Provider[] {
|
||||||
if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return [];
|
if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return [];
|
||||||
|
|
||||||
const overrides = this.getProviderOverrides(providers);
|
const flattenedProviders = flatten<Provider[]>(providers);
|
||||||
const hasMultiProviderOverrides = overrides.some(isMultiProvider);
|
const overrides = this.getProviderOverrides(flattenedProviders);
|
||||||
const overriddenProviders = [...providers, ...overrides];
|
const overriddenProviders = [...flattenedProviders, ...overrides];
|
||||||
|
|
||||||
// No additional processing is required in case we have no multi providers to override
|
|
||||||
if (!hasMultiProviderOverrides) {
|
|
||||||
return overriddenProviders;
|
|
||||||
}
|
|
||||||
|
|
||||||
const final: Provider[] = [];
|
const final: Provider[] = [];
|
||||||
const seenMultiProviders = new Set<Provider>();
|
const seenMultiProviders = new Set<Provider>();
|
||||||
|
|
||||||
// We iterate through the list of providers in reverse order to make sure multi 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.
|
// multi providers that have overrides, keeping overridden values only.
|
||||||
forEachRight(overriddenProviders, (provider: any) => {
|
forEachRight(overriddenProviders, (provider: any) => {
|
||||||
const token: any = getProviderToken(provider);
|
const token: any = getProviderToken(provider);
|
||||||
if (isMultiProvider(provider) && this.providerOverridesByToken.has(token)) {
|
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)) {
|
if (!seenMultiProviders.has(token)) {
|
||||||
seenMultiProviders.add(token);
|
seenMultiProviders.add(token);
|
||||||
if (provider && provider.useValue && Array.isArray(provider.useValue)) {
|
final.unshift({...provider, multi: false});
|
||||||
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 {
|
} else {
|
||||||
final.unshift(provider);
|
final.unshift(provider);
|
||||||
|
|
Loading…
Reference in New Issue