Skip to content

Commit

Permalink
feat: add automatic mock for angular entities (thymikee#2908)
Browse files Browse the repository at this point in the history
  • Loading branch information
MillerSvt authored and s.v.zaytsev committed Jan 30, 2025
1 parent 138ab34 commit 39f1670
Show file tree
Hide file tree
Showing 8 changed files with 782 additions and 302 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
semi: true
printWidth: 120
singleQuote: true
tabWidth: 2
tabWidth: 4
useTabs: false
trailingComma: all
overrides:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"github-files-fetcher": "^1.6.0",
"glob": "^10.4.5",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jest": "^30.0.0-alpha.7",
"jsdom": "^26.0.0",
"pinst": "^3.0.0",
"prettier": "^2.8.8",
Expand Down
153 changes: 153 additions & 0 deletions setup-env/mock-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const {
ɵɵdefineComponent,
ɵɵdefineInjectable,
ɵɵdefineDirective,
ɵɵdefinePipe,
ɵɵdefineNgModule,
} = require('@angular/core');

const cache = new Map();

function stubInjectable(target, { providedIn }) {
target.ɵprov = ɵɵdefineInjectable({
token: target,
providedIn,
factory: () => new target(),
});
}

function stubComponent(target, actual) {
const { selectors, exportAs, standalone, signals, ngContentSelectors, changeDetection } = actual;

cache.set(
actual,
(target.ɵcmp =
cache.get(actual) ??
ɵɵdefineComponent({
type: target,
selectors,
inputs: {},
outputs: {},
exportAs,
standalone,
signals,
decls: 0,
vars: 0,
// eslint-disable-next-line @typescript-eslint/no-empty-function
template: () => {},
ngContentSelectors,
changeDetection,
}))
);
}

function stubDirective(target, { selectors, exportAs, standalone, signals }) {
target.ɵdir = ɵɵdefineDirective({
type: target,
selectors,
inputs: {},
outputs: {},
exportAs,
standalone,
signals,
});
}

function stubPipe(target, { name, pure, standalone }) {
target.ɵpipe = ɵɵdefinePipe({
name,
type: target,
pure,
standalone,
});
}

jest.onGenerateMock((modulePath, moduleMock) => {
const moduleActual = jest.requireActual(modulePath);

function* walk(obj, walkedNodes = [], path = []) {
if (!obj || (typeof obj !== 'function' && typeof obj !== 'object') || walkedNodes.includes(obj)) {
return;
}

for (const key of Object.getOwnPropertyNames(obj)) {
if (typeof key === 'string' && key.startsWith('ɵ')) {
const pathFunction = (root) => {
return path.reduce((acc, k) => acc?.[k], root);
};

yield [key, pathFunction];

continue;
}

yield* walk(obj[key], [...walkedNodes, obj], [...path, key]);
}
}

function stubRecursive(mock, actual) {
for (const [key, getParent] of walk(actual)) {
const parentMock = getParent(mock);
const parentActual = getParent(actual);
const valueActual = parentActual[key];

if (!parentMock || !valueActual) {
continue;
}

switch (key) {
case `ɵfac`: {
parentMock[key] = () => new parentMock();
break;
}
case `ɵprov`: {
stubInjectable(parentMock, valueActual);
break;
}
case `ɵcmp`: {
stubComponent(parentMock, valueActual);
break;
}
case `ɵdir`: {
stubDirective(parentMock, valueActual);
break;
}
case `ɵpipe`: {
stubPipe(parentMock, valueActual);
break;
}
case `ɵmod`: {
parentMock[key] = ɵɵdefineNgModule({
type: parentMock,
imports: valueActual.imports.map((actual) => {
const mock = jest.fn();

stubRecursive(mock, actual);

return mock;
}),
exports: valueActual.exports.map((actual) => {
const mock = jest.fn();

stubRecursive(mock, actual);

return mock;
}),
declarations: valueActual.declarations.map((actual) => {
const mock = jest.fn();

stubRecursive(mock, actual);

return mock;
}),
});
break;
}
}
}
}

stubRecursive(moduleMock, moduleActual);

return moduleMock;
});
1 change: 1 addition & 0 deletions setup-env/zone/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require('zone.js');
require('zone.js/testing');
require('../mock-transformer');

const { getTestBed } = require('@angular/core/testing');
const {
Expand Down
1 change: 1 addition & 0 deletions setup-env/zone/index.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'zone.js';
import 'zone.js/testing';
import '../mock-transformer.js';

import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
Expand Down
2 changes: 2 additions & 0 deletions setup-env/zoneless/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require('../mock-transformer');

const { provideExperimentalZonelessChangeDetection, NgModule, ErrorHandler } = require('@angular/core');
const { getTestBed } = require('@angular/core/testing');
const {
Expand Down
2 changes: 2 additions & 0 deletions setup-env/zoneless/index.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../mock-transformer.js';

import { ErrorHandler, NgModule, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
Expand Down
Loading

0 comments on commit 39f1670

Please sign in to comment.