Skip to content

Commit

Permalink
Merge pull request #5321 from satanTime/issues/5239
Browse files Browse the repository at this point in the history
Bug: dependencies of a mock take precedence over root mocks #5239
  • Loading branch information
satanTime authored Mar 26, 2023
2 parents fac29e3 + 992ef6a commit 99ab2f6
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 11 deletions.
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/common/func.extract-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const funcExtractDeps = (def: any, result: Set<AnyDeclaration<any>>): Set
const meta = collectDeclarations(def);
const type = getNgType(def);
// istanbul ignore if
if (!type) {
if (!type || type === 'Injectable') {
return result;
}

Expand Down
10 changes: 9 additions & 1 deletion libs/ng-mocks/src/lib/common/ng-mocks-universe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ const globalMap = (key: string) => () => {
};

interface NgMocksUniverse {
/**
* the value can be:
* - null - exclude
* - undefined - delayed initialization
* - value - the definition which should be used in tests: real value, replacement, mock.
*/
builtDeclarations: Map<any, any>;
builtProviders: Map<any, any>;
cacheDeclarations: Map<any, any>;
Expand Down Expand Up @@ -109,7 +115,9 @@ ngMocksUniverse.getBuildDeclaration = (def: any): undefined | null | any => {

ngMocksUniverse.hasBuildDeclaration = (def: any): boolean => {
if (ngMocksUniverse.builtDeclarations.has(def)) {
return true;
// undefined means that we know about this declaration,
// but its initialization is postponed at the moment.
return ngMocksUniverse.builtDeclarations.get(def) !== undefined;
}
const [mode] = getDefaults(def);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { mapValues } from '../../common/core.helpers';
import ngMocksUniverse from '../../common/ng-mocks-universe';

import tryMockDeclaration from './try-mock-declaration';
import tryMockProvider from './try-mock-provider';

export default (mockDef: Set<any>, defValue: Map<any, any>): void => {
const builtDeclarations = ngMocksUniverse.builtDeclarations;
const resolutions: Map<any, string> = ngMocksUniverse.config.get('ngMocksDepsResolution');
for (const def of mapValues(mockDef)) {
const deleteTouch = !ngMocksUniverse.touches.has(def);

resolutions.set(def, 'mock');
tryMockDeclaration(def, defValue);
builtDeclarations.set(def, undefined);
tryMockProvider(def, defValue);

if (deleteTouch) {
Expand Down
5 changes: 5 additions & 0 deletions libs/ng-mocks/src/lib/mock-builder/promise/init-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { MockModule } from '../../mock-module/mock-module';
import mockNgDef from '../../mock-module/mock-ng-def';
import collectDeclarations from '../../resolve/collect-declarations';

import tryMockDeclaration from './try-mock-declaration';

export default (
keepDef: Set<any>,
mockDef: Set<any>,
Expand Down Expand Up @@ -40,6 +42,9 @@ export default (
ngMocksUniverse.touches.delete(def);
}
}
for (const def of mapValues(mockDef)) {
tryMockDeclaration(def);
}

return loProviders;
};
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default ({
ngMocksUniverse.config.set(k, {
...ngMocksUniverse.getConfigMock().get(k),
...v,
defValue: defValue.get(k),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { MockComponent } from '../../mock-component/mock-component';
import { MockDirective } from '../../mock-directive/mock-directive';
import { MockPipe } from '../../mock-pipe/mock-pipe';

export default (def: any, defValue: Map<any, any>): void => {
export default (def: any): void => {
if (ngMocksUniverse.builtDeclarations.get(def) !== undefined) {
return;
}

if (isNgDef(def, 'c')) {
ngMocksUniverse.builtDeclarations.set(def, MockComponent(def));
}
if (isNgDef(def, 'd')) {
ngMocksUniverse.builtDeclarations.set(def, MockDirective(def));
}
if (isNgDef(def, 'p')) {
const instance = defValue.get(def);
ngMocksUniverse.builtDeclarations.set(
def,
typeof instance?.transform === 'function' ? MockPipe(def, instance.transform) : MockPipe(def),
);
ngMocksUniverse.builtDeclarations.set(def, MockPipe(def));
}
};
4 changes: 3 additions & 1 deletion libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export function MockPipes(...pipes: Array<Type<PipeTransform>>): Array<Type<Pipe
return pipes.map(pipe => MockPipe(pipe, undefined));
}

const getMockClass = (pipe: Type<any>, transform?: PipeTransform['transform']): Type<any> => {
const getMockClass = (pipe: Type<any>, transformValue?: PipeTransform['transform']): Type<any> => {
const config = ngMocksUniverse.config.get(pipe);
const transform = transformValue ?? config?.defValue?.transform;
const mock = extendClass(Mock);
Pipe(coreReflectPipeResolve(pipe))(mock);
decorateMock(mock, pipe, {
Expand Down
73 changes: 73 additions & 0 deletions tests-e2e/src/issue-5239/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatMenuModule } from '@angular/material/menu';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import {
isMockOf,
MockComponent,
MockRender,
ngMocks,
} from 'ng-mocks';

// A simple dependency component we are going to mock that imports the standalone pipe.
@Component({
selector: 'dependency',
template: 'dependency',
standalone: true,
imports: [MatMenuModule],
})
class DependencyComponent {
public dependency5239e2e() {}
}

// A standalone component we are going to test.
@Component({
selector: 'target',
template: `
<dependency></dependency>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
This is the expansion title
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
Some deferred content
</ng-template>
</mat-expansion-panel>
`,
})
class TargetComponent {
public target5239e2e() {}
}

// @see https://github.com/help-me-mom/ng-mocks/issues/5239
describe('issue-5239', () => {
beforeEach(() => {
return TestBed.configureTestingModule({
declarations: [
// our component for testing
TargetComponent,
],
imports: [
NoopAnimationsModule,

// imports PortalModule, therefore it should be kept.
MatExpansionModule,

// the dependent component we want to mock,
// but internally uses MatMenuModule > OverlayModule > PortalModule,
// and mocks it, but should not.
MockComponent(DependencyComponent),
],
}).compileComponents();
});

it('renders dependencies', () => {
const fixture = MockRender(TargetComponent);
const html = ngMocks.formatHtml(fixture);
expect(html).toContain('Some deferred content');

const dependency = ngMocks.findInstance(DependencyComponent);
expect(isMockOf(dependency, DependencyComponent)).toEqual(true);
});
});
89 changes: 89 additions & 0 deletions tests/issue-5239/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Component,
Pipe,
PipeTransform,
VERSION,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';

import {
ngMocks,
MockComponent,
MockPipe,
MockRender,
} from 'ng-mocks';

// A simple standalone pipe we are going to mock.
@Pipe(
{
name: 'pipe',
standalone: true,
} as never /* TODO: remove after upgrade to a14 */,
)
class StandalonePipe implements PipeTransform {
transform(value: string | null): string {
return `${value}:${this.constructor.name}`;
}

public pipe5239() {}
}

// A simple dependency component we are going to mock that imports the standalone pipe.
@Component(
{
selector: 'dependency',
template: 'dependency',
standalone: true,
imports: [StandalonePipe],
} as never /* TODO: remove after upgrade to a14 */,
)
class DependencyComponent {
public dependency5239() {}
}

// A standalone component we are going to test.
@Component(
{
selector: 'standalone',
template: `<dependency></dependency> {{ 'test' | pipe }}`,
standalone: true,
imports: [DependencyComponent, StandalonePipe],
} as never /* TODO: remove after upgrade to a14 */,
)
class StandaloneComponent {
public standalone5239() {}
}

// @see https://github.com/help-me-mom/ng-mocks/issues/5239
// The problem here was because of mocks of DependencyComponent.
// It has StandalonePipe too, so it mocked it without the custom function and cached,
// whereas the mock with the custom function was ignored due to existing cache of the pipe.
describe('issue-5239', () => {
if (Number.parseInt(VERSION.major, 10) < 14) {
it('needs >=a14', () => {
expect(true).toBeTruthy();
});

return;
}

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
// our component for testing
StandaloneComponent,

// the dependent component we want to mock
MockComponent(DependencyComponent),

// the pipe we want to mock with a custom transform
MockPipe(StandalonePipe, () => 'mock'),
],
}).compileComponents();
});

it('renders dependencies', () => {
const fixture = MockRender(StandaloneComponent);
expect(ngMocks.formatHtml(fixture)).toContain('mock');
});
});

0 comments on commit 99ab2f6

Please sign in to comment.