Skip to content

Commit 6e78a6e

Browse files
committed
add jest.mock function support
1 parent 91a21ff commit 6e78a6e

File tree

4 files changed

+157
-4
lines changed

4 files changed

+157
-4
lines changed

src/ast.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,35 @@ class AST {
5353
}
5454
return t.importDeclaration([astImportSpecifier], t.stringLiteral(path));
5555
}
56-
}
56+
57+
static createASTJestMockCallFunction = (specifiersByModule) => {
58+
const astExpressionStatements = [];
59+
const modules = Object.keys(specifiersByModule);
60+
for (const modulePath of modules) {
61+
const callee = t.memberExpression(t.identifier("jest"), t.identifier("mock"));
62+
const mockModuleNameParameter = t.stringLiteral(modulePath);
63+
const functionArguments = [mockModuleNameParameter];
64+
if (specifiersByModule[modulePath].length !== 0) {
65+
const astObject = AST.createASTObject(specifiersByModule[modulePath]);
66+
const mockCallbackParameter = t.arrowFunctionExpression([], astObject);
67+
functionArguments.push(mockCallbackParameter);
68+
}
69+
const callExpression = t.callExpression(callee, functionArguments)
70+
astExpressionStatements.push(t.expressionStatement(callExpression));
71+
}
72+
return astExpressionStatements;
73+
}
74+
75+
static createASTObject(obj) {
76+
const keys = Object.keys(obj);
77+
const objectProperties = [];
78+
for (const key of keys) {
79+
const name = obj[key].name;
80+
const value = obj[key].astValue;
81+
objectProperties.push(t.objectProperty(t.identifier(name), value));
82+
}
83+
return t.objectExpression(objectProperties)
84+
}
85+
}
5786

5887
module.exports = AST;

src/executorConfig.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const ospath = require("path");
22
const fg = require("fast-glob");
3+
const t = require("@babel/types");
34
const PathFunctions = require("./path");
45

56
class Workspaces {
@@ -152,4 +153,68 @@ class ExecutorFactory {
152153
}
153154
}
154155

155-
module.exports = ExecutorFactory;
156+
class JestMock {
157+
static isJestMockFunctionCall(node) {
158+
if (!(t.isCallExpression(node.expression) && t.isMemberExpression(node.expression.callee))) return false;
159+
const objectName = node.expression.callee.object.name;
160+
const propertyName = node.expression.callee.property.name;
161+
return (objectName === "jest" && propertyName === "mock");
162+
}
163+
164+
constructor() {
165+
this.modulePath = "";
166+
this.specifiers = [];
167+
this.barrelImports = new ImportBarrelPaths();
168+
}
169+
170+
load(expression) {
171+
this.loadArguments(expression);
172+
}
173+
174+
loadArguments(expression) {
175+
const argumentsVar = expression.arguments
176+
this.modulePath = argumentsVar[0].value;
177+
this.specifiers = argumentsVar[1]?.body?.properties || [];
178+
}
179+
180+
setExpression(expression) {
181+
this.load(expression);
182+
}
183+
184+
getDirectImports(barrelFile) {
185+
const barrelModulePath = this.modulePath;
186+
const directModules = new ImportBarrelPaths();
187+
for (const specifier of this.specifiers) {
188+
const importedName = specifier?.key?.name || "default";
189+
const importSpecifier = barrelFile.getDirectSpecifierObject(importedName).toImportSpecifier();
190+
const directModulePath = importSpecifier.path;
191+
if (!importSpecifier.path) return;
192+
directModules.add(barrelModulePath, directModulePath, { name: importedName, astValue: specifier.value });
193+
}
194+
return directModules;
195+
}
196+
}
197+
198+
class ImportBarrelPaths {
199+
constructor() {
200+
this.map = {};
201+
}
202+
203+
add(importBarrelPath, importDirectPath, specifier = null) {
204+
this.map[importBarrelPath] ??= {}
205+
this.map[importBarrelPath][importDirectPath] ??= [];
206+
specifier && this.map[importBarrelPath][importDirectPath].push(specifier);
207+
}
208+
209+
get(importBarrelPath, importDirectPath) {
210+
if (importBarrelPath) {
211+
if (importDirectPath) {
212+
return this.map[importBarrelPath][importDirectPath];
213+
}
214+
return this.map[importBarrelPath];
215+
}
216+
return this.map;
217+
}
218+
}
219+
220+
module.exports = { ExecutorFactory, JestMock};

src/main.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
const { builtinModules } = require('module');
22
const generate = require('@babel/generator').default;
33
const AST = require("./ast");
4-
const ExecutorFactory = require("./executorConfig");
4+
const { ExecutorFactory, JestMock } = require("./executorConfig");
55
const resolver = require("./resolver");
66
const BarrelFileManagerFacade = require("./barrel");
77
const pluginOptions = require("./pluginOptions");
88
const logger = require("./logger");
99
const PathFunctions = require("./path");
1010

11+
const jestMockFunction = new JestMock();
12+
1113
const importDeclarationVisitor = (path, state) => {
1214
const importsSpecifiers = path.node.specifiers;
1315
if (!AST.isAnySpecifierExist(importsSpecifiers)) return;
@@ -28,6 +30,9 @@ const importDeclarationVisitor = (path, state) => {
2830
const importedName = specifier?.imported?.name || "default";
2931
const importSpecifier = barrelFile.getDirectSpecifierObject(importedName).toImportSpecifier();
3032
if (!importSpecifier.path) return;
33+
if (pluginOptions.options.executorName === "jest") {
34+
jestMockFunction.barrelImports.add(importsPath, importSpecifier.path);
35+
}
3136
importSpecifier.localName = specifier.local.name;
3237
const transformedASTImport = AST.createASTImportDeclaration(importSpecifier);
3338
logger.log(`Transformed import line: ${generate(transformedASTImport).code}`);
@@ -36,6 +41,27 @@ const importDeclarationVisitor = (path, state) => {
3641
path.replaceWithMultiple(directSpecifierASTArray);
3742
};
3843

44+
const expressionStatementVisitor = (path, state) => {
45+
if (!(pluginOptions.options.executorName === "jest")) return;
46+
if (!JestMock.isJestMockFunctionCall(path.node)) return;
47+
jestMockFunction.setExpression(path.node.expression);
48+
let directImports;
49+
const { modulePath } = jestMockFunction;
50+
const parsedJSFile = state.filename;
51+
const moduleAbsPath = resolver.resolve(modulePath ,parsedJSFile).absEsmFile;
52+
const barrelFile = BarrelFileManagerFacade.getBarrelFile(moduleAbsPath);
53+
if (!barrelFile.isBarrelFileContent) return;
54+
if (AST.isAnySpecifierExist(jestMockFunction.specifiers)) {
55+
directImports = jestMockFunction.getDirectImports(barrelFile).get(modulePath);
56+
} else {
57+
directImports = jestMockFunction.barrelImports.get(modulePath);
58+
}
59+
const transformedASTImport = AST.createASTJestMockCallFunction(directImports);
60+
logger.log(`Source mock line: ${generate(path.node, { comments: false, concise: true }).code}`);
61+
transformedASTImport.forEach(line=> logger.log(`Transformed mock line: ${generate(line).code}`));
62+
path.replaceWithMultiple(transformedASTImport)
63+
}
64+
3965
module.exports = function (babel) {
4066
const PLUGIN_KEY = 'transform-barrels';
4167
return {
@@ -58,6 +84,7 @@ module.exports = function (babel) {
5884
},
5985
visitor: {
6086
ImportDeclaration: importDeclarationVisitor,
87+
ExpressionStatement: expressionStatementVisitor,
6188
},
6289
};
6390
};

tests/js/local/imports.test.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,36 @@ describe("modules directories", () => {
187187
)
188188
).toBe(`import { RedText as NamedExported } from \"${ospath.resolve(__dirname)}\\components\\Texts\\RedText\\RedText.js";`.replaceAll("\\","\\\\"));
189189
});
190-
});
190+
});
191+
192+
describe("jest mock", () => {
193+
test("transformation of module path", () => {
194+
const pluginOptions = { executorName: "jest", logging: {type: "file"} };
195+
expect(
196+
pluginTransform([
197+
'import { RedText, Text } from "./components/Texts";',
198+
`jest.mock('./components/Texts', () => ({`,
199+
` RedText: jest.fn(),`,
200+
` Text: jest.fn(),`,
201+
`}));`,
202+
`jest.mock('./components/Texts');`,
203+
`console.log("test");`
204+
].join("\n"),
205+
__filename,
206+
pluginOptions
207+
)
208+
).toBe([
209+
`import { RedText } from \"${ospath.resolve(__dirname)}\\components\\Texts\\RedText\\RedText.js";`,
210+
`import { Text } from \"${ospath.resolve(__dirname)}\\components\\Texts\\Text\\Text.js";`,
211+
`jest.mock(\"${ospath.resolve(__dirname)}\\components\\Texts\\RedText\\RedText.js", () => ({`,
212+
` RedText: jest.fn()`,
213+
`}));`,
214+
`jest.mock(\"${ospath.resolve(__dirname)}\\components\\Texts\\Text\\Text.js", () => ({`,
215+
` Text: jest.fn()`,
216+
`}));`,
217+
`jest.mock(\"${ospath.resolve(__dirname)}\\components\\Texts\\RedText\\RedText.js");`,
218+
`jest.mock(\"${ospath.resolve(__dirname)}\\components\\Texts\\Text\\Text.js");`,
219+
`console.log("test");`
220+
].join("\n").replaceAll("\\","\\\\"));
221+
});
222+
});

0 commit comments

Comments
 (0)