Skip to content

Commit 26f4a39

Browse files
author
s.v.zaytsev
committed
feat: add automatic mock for angular entities (#2908)
1 parent 138ab34 commit 26f4a39

File tree

8 files changed

+782
-302
lines changed

8 files changed

+782
-302
lines changed

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
semi: true
22
printWidth: 120
33
singleQuote: true
4-
tabWidth: 2
4+
tabWidth: 4
55
useTabs: false
66
trailingComma: all
77
overrides:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
"github-files-fetcher": "^1.6.0",
112112
"glob": "^10.4.5",
113113
"husky": "^9.1.7",
114-
"jest": "^29.7.0",
114+
"jest": "^30.0.0-alpha.7",
115115
"jsdom": "^26.0.0",
116116
"pinst": "^3.0.0",
117117
"prettier": "^2.8.8",

setup-env/mock-transformer.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
const {
2+
ɵɵdefineComponent,
3+
ɵɵdefineInjectable,
4+
ɵɵdefineDirective,
5+
ɵɵdefinePipe,
6+
ɵɵdefineNgModule,
7+
} = require('@angular/core');
8+
9+
const cache = new Map();
10+
11+
function stubInjectable(target, { providedIn }) {
12+
target.ɵprov = ɵɵdefineInjectable({
13+
token: target,
14+
providedIn,
15+
factory: () => new target(),
16+
});
17+
}
18+
19+
function stubComponent(target, actual) {
20+
const { selectors, exportAs, standalone, signals, ngContentSelectors, changeDetection } = actual;
21+
22+
cache.set(
23+
actual,
24+
(target.ɵcmp =
25+
cache.get(actual) ??
26+
ɵɵdefineComponent({
27+
type: target,
28+
selectors,
29+
inputs: {},
30+
outputs: {},
31+
exportAs,
32+
standalone,
33+
signals,
34+
decls: 0,
35+
vars: 0,
36+
// eslint-disable-next-line @typescript-eslint/no-empty-function
37+
template: () => {},
38+
ngContentSelectors,
39+
changeDetection,
40+
}))
41+
);
42+
}
43+
44+
function stubDirective(target, { selectors, exportAs, standalone, signals }) {
45+
target.ɵdir = ɵɵdefineDirective({
46+
type: target,
47+
selectors,
48+
inputs: {},
49+
outputs: {},
50+
exportAs,
51+
standalone,
52+
signals,
53+
});
54+
}
55+
56+
function stubPipe(target, { name, pure, standalone }) {
57+
target.ɵpipe = ɵɵdefinePipe({
58+
name,
59+
type: target,
60+
pure,
61+
standalone,
62+
});
63+
}
64+
65+
jest.onGenerateMock((modulePath, moduleMock) => {
66+
const moduleActual = jest.requireActual(modulePath);
67+
68+
function* walk(obj, walkedNodes = [], path = []) {
69+
if (!obj || (typeof obj !== 'function' && typeof obj !== 'object') || walkedNodes.includes(obj)) {
70+
return;
71+
}
72+
73+
for (const key of Object.getOwnPropertyNames(obj)) {
74+
if (typeof key === 'string' && key.startsWith('ɵ')) {
75+
const pathFunction = (root) => {
76+
return path.reduce((acc, k) => acc?.[k], root);
77+
};
78+
79+
yield [key, pathFunction];
80+
81+
continue;
82+
}
83+
84+
yield* walk(obj[key], [...walkedNodes, obj], [...path, key]);
85+
}
86+
}
87+
88+
function stubRecursive(mock, actual) {
89+
for (const [key, getParent] of walk(actual)) {
90+
const parentMock = getParent(mock);
91+
const parentActual = getParent(actual);
92+
const valueActual = parentActual[key];
93+
94+
if (!parentMock || !valueActual) {
95+
continue;
96+
}
97+
98+
switch (key) {
99+
case `ɵfac`: {
100+
parentMock[key] = () => new parentMock();
101+
break;
102+
}
103+
case `ɵprov`: {
104+
stubInjectable(parentMock, valueActual);
105+
break;
106+
}
107+
case `ɵcmp`: {
108+
stubComponent(parentMock, valueActual);
109+
break;
110+
}
111+
case `ɵdir`: {
112+
stubDirective(parentMock, valueActual);
113+
break;
114+
}
115+
case `ɵpipe`: {
116+
stubPipe(parentMock, valueActual);
117+
break;
118+
}
119+
case `ɵmod`: {
120+
parentMock[key] = ɵɵdefineNgModule({
121+
type: parentMock,
122+
imports: valueActual.imports.map((actual) => {
123+
const mock = jest.fn();
124+
125+
stubRecursive(mock, actual);
126+
127+
return mock;
128+
}),
129+
exports: valueActual.imports.map((actual) => {
130+
const mock = jest.fn();
131+
132+
stubRecursive(mock, actual);
133+
134+
return mock;
135+
}),
136+
declarations: valueActual.imports.map((actual) => {
137+
const mock = jest.fn();
138+
139+
stubRecursive(mock, actual);
140+
141+
return mock;
142+
}),
143+
});
144+
break;
145+
}
146+
}
147+
}
148+
}
149+
150+
stubRecursive(moduleMock, moduleActual);
151+
152+
return moduleMock;
153+
});

setup-env/zone/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require('zone.js');
22
require('zone.js/testing');
3+
require('../mock-transformer');
34

45
const { getTestBed } = require('@angular/core/testing');
56
const {

setup-env/zone/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'zone.js';
22
import 'zone.js/testing';
3+
import '../mock-transformer.js';
34

45
import { getTestBed } from '@angular/core/testing';
56
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

setup-env/zoneless/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require('../mock-transformer');
2+
13
const { provideExperimentalZonelessChangeDetection, NgModule, ErrorHandler } = require('@angular/core');
24
const { getTestBed } = require('@angular/core/testing');
35
const {

setup-env/zoneless/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import '../mock-transformer.js';
2+
13
import { ErrorHandler, NgModule, provideExperimentalZonelessChangeDetection } from '@angular/core';
24
import { getTestBed } from '@angular/core/testing';
35
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

0 commit comments

Comments
 (0)