UNPKG

testplane

Version:

Tests framework based on mocha and wdio

324 lines 14.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.plugin = void 0; const node_url_1 = __importDefault(require("node:url")); const node_path_1 = __importDefault(require("node:path")); const debug_1 = __importDefault(require("debug")); const recast_1 = require("recast"); const logger = __importStar(require("../../../../utils/logger")); const constants_1 = require("../constants"); const utils_1 = require("../utils"); const debug = (0, debug_1.default)("vite:plugin:mock"); const b = recast_1.types.builders; const plugin = (manualMock) => { const registeredMocks = new Set(); const testFileMocks = new Map(); let testFilePath = null; return [ { name: "testplane:manual-mock", enforce: "pre", resolveId: manualMock.resolveId.bind(manualMock), }, { name: "testplane:mock", enforce: "post", configureServer(server) { return () => { server.middlewares.use("/", async (req, _res, next) => { const testInfo = (0, utils_1.getTestInfoFromViteRequest)(req); if (!testInfo) { return next(); } testFilePath = testInfo.env?.file; manualMock.resetMocks(); return next(); }); }; }, transform(code, id) { const isTestModule = testFilePath === id; if (!isTestModule) { const isModuleFromNodeModules = id.includes("/node_modules/"); const isVirtualModule = id.startsWith("virtual:"); const hasRegisteredMocks = registeredMocks.size > 0; if (isModuleFromNodeModules || isVirtualModule || !hasRegisteredMocks) { return { code }; } } let ast; const start = Date.now(); try { ast = (0, recast_1.parse)(code, { parser: require("recast/parsers/typescript"), sourceFileName: id, sourceRoot: node_path_1.default.dirname(id), }); debug(`Parsed file for mocking: ${id} in ${Date.now() - start}ms`); } catch (err) { logger.error(`Failed to parse file ${id}: ${err.stack}`); return { code }; } const state = { importIndex: 0, mockFnName: "", unmockFnName: "", mockCalls: [], }; const testModuleVisitorMethods = isTestModule ? { visitExpressionStatement: handleMockCalls({ state, registeredMocks, manualMock }) } : {}; (0, recast_1.visit)(ast, { visitImportDeclaration: handleModuleImportWithMocks(state), ...testModuleVisitorMethods, }); (0, recast_1.visit)(ast, { visitImportDeclaration: rewriteImportDeclaration({ state, id, testFilePath, isTestModule, registeredMocks, testFileMocks, }), }); const preparedMockCalls = state.mockCalls.map(mockCall => { const exp = mockCall; if (!isCallExpression(exp)) { return mockCall; } const mockCallExpression = exp.expression; const mockModuleName = mockCallExpression.arguments[0] .value; if (testFileMocks.has(mockModuleName)) { mockCallExpression.arguments.push(b.identifier(testFileMocks.get(mockModuleName))); } else { throw new Error(`Cannot find mocked module "${mockModuleName}"`); } return b.expressionStatement(b.awaitExpression(mockCallExpression)); }); ast.program.body.unshift(...preparedMockCalls); try { const newCode = (0, recast_1.print)(ast, { sourceMapName: id }); debug(`Transformed file for mocking: ${id} in ${Date.now() - start}ms`); return newCode; } catch (err) { logger.error(`Failed to transform file ${id} for mocking: ${err.stack}`); return { code }; } }, }, ]; }; exports.plugin = plugin; /** * Find import module with mocks and save name for mock and unmock calls */ function handleModuleImportWithMocks(state) { return function (nodePath) { const declaration = nodePath.value; const source = declaration.source.value; const specifiers = declaration.specifiers; if (!specifiers || specifiers.length === 0 || source !== constants_1.MOCK_MODULE_NAME) { return this.traverse(nodePath); } const mockSpecifier = specifiers .filter(s => s.type === recast_1.types.namedTypes.ImportSpecifier.toString()) .find(s => s.imported.name === "mock"); if (mockSpecifier && mockSpecifier.local) { state.mockFnName = mockSpecifier.local.name; } const unmockSpecifier = declaration.specifiers .filter(s => s.type === recast_1.types.namedTypes.ImportSpecifier.toString()) .find(s => s.imported.name === "unmock"); if (unmockSpecifier && unmockSpecifier.local) { state.unmockFnName = unmockSpecifier.local.name; } // Move import module with mocks to the top of the file state.mockCalls.push(declaration); nodePath.prune(); return this.traverse(nodePath); }; } /** * Detect which modules are supposed to be mocked */ function handleMockCalls({ state, registeredMocks, manualMock, }) { return function (nodePath) { const exp = nodePath.value; if (exp.expression.type !== recast_1.types.namedTypes.CallExpression.toString()) { return this.traverse(nodePath); } const callExp = exp.expression; const isMockCall = Boolean(state.mockFnName) && callExp.callee.name === state.mockFnName; const isUnmockCall = Boolean(state.unmockFnName) && callExp.callee.name === state.unmockFnName; if (!isMockCall && !isUnmockCall) { return this.traverse(nodePath); } if (isUnmockCall && callExp.arguments[0] && typeof callExp.arguments[0].value === "string") { manualMock.unmock(callExp.arguments[0].value); } if (isMockCall) { const mockCall = exp.expression; if (mockCall.arguments.length === 1) { manualMock.mock(mockCall.arguments[0].value); } else { if (exp.expression.arguments.length) { registeredMocks.add(exp.expression.arguments[0] .value); } state.mockCalls.push(exp); } } // Remove original node from ast nodePath.prune(); return this.traverse(nodePath); }; } /** * Rewrite import declarations in test file and its dependencies in order to use user mocks instead original module. * Exmample in test file: * * From: * import {fn, mock} from "testplane/mock"; * import {foo} from "bar"; * import {handleClick} from "./utils"; * mock("./utils", () => ({handleClick: fn()}); * * To: * import * as __testplane_import_0__ from "./utils"; // move import of mocked module to the top * await mock("utils", () => ({handleClick: fn()}, __testplane_import_0__); // move right after import original module (will call `mock` from `vite/browser-modules/mock.ts`) * const {foo} = await import("bar"); // transform to import expression in order to import it only after mock module * const {handleClick} = importWithMock("/Users/../utils", __testplane_import_0__); // use importWithMock helper in order to get mocked module })); */ function rewriteImportDeclaration({ state, id, testFilePath, isTestModule, registeredMocks, testFileMocks, }) { return function (nodePath) { const declaration = nodePath.value; const source = declaration.source.value; if (!declaration.specifiers || declaration.specifiers.length === 0) { return this.traverse(nodePath); } const absImportPath = node_path_1.default.resolve(node_path_1.default.dirname(id), source); const absImportPathWithoutExtName = (0, utils_1.getPathWithoutExtName)(absImportPath); const isModuleMockedRelatively = Boolean(source.startsWith(".") && [...registeredMocks.values()].find(m => { const absMockPath = node_path_1.default.resolve(node_path_1.default.dirname(testFilePath || "/"), m); const absMockPathWithoutExtName = (0, utils_1.getPathWithoutExtName)(absMockPath); return absImportPathWithoutExtName === absMockPathWithoutExtName; })); const isModuleMocked = isModuleMockedRelatively || registeredMocks.has(source); const newImportIdentifier = genImportIdentifier(state); if (isTestModule && isModuleMocked) { testFileMocks.set(source, newImportIdentifier); } /** * Use import with custom namespace specifier for mocked module * * From: * import {handleClick} from "./utils"; * * To: * import * as __testplane_import_0__ from "./utils"; */ if (isModuleMocked) { const newNode = b.importDeclaration([b.importNamespaceSpecifier(b.identifier(newImportIdentifier))], b.literal(source)); // should be specified first in order to correctly mock module state.mockCalls.unshift(newNode); } let mockImport; /** * Transform import mocked module to use helper `importWithMock` in order to get mocked implementation * * From: * import {handleClick} from "./utils"; * * To: * import * as __testplane_import_0__ from "./utils"; // from code above * const { handleClick } = await importWithMock("./utils", __testplane_import_0__); */ if (isModuleMocked) { const mockModuleIdentifier = source.startsWith(".") || source.startsWith("/") ? node_url_1.default.pathToFileURL(absImportPathWithoutExtName).pathname : source; const variableValue = b.callExpression(b.identifier("importWithMock"), [ b.literal(mockModuleIdentifier), b.identifier(newImportIdentifier), ]); mockImport = b.variableDeclaration("const", [ b.variableDeclarator(genVarDeclKey(declaration), variableValue), ]); } /** * Transform not mocked import declarations to import expressions only in test file. * In order to hoist `mock(...)` calls and run them before another dependencies. * * From: * import {foo} from "bar"; * * To: * const {foo} = await import("bar"); */ if (isTestModule && !isModuleMocked) { mockImport = b.variableDeclaration("const", [ b.variableDeclarator(genVarDeclKey(declaration), b.awaitExpression(b.importExpression(b.literal(source)))), ]); } if (mockImport) { nodePath.replace(mockImport); } return this.traverse(nodePath); }; } function genVarDeclKey(declaration) { const isNamespaceImport = declaration.specifiers?.length === 1 && declaration.specifiers[0].type === recast_1.types.namedTypes.ImportNamespaceSpecifier.toString(); if (isNamespaceImport) { return declaration.specifiers[0].local; } return b.objectPattern(declaration.specifiers.map(s => { if (s.type === recast_1.types.namedTypes.ImportDefaultSpecifier.toString()) { return b.property("init", b.identifier("default"), b.identifier(s.local.name)); } return b.property("init", b.identifier(s.imported.name), b.identifier(s.local.name)); })); } function genImportIdentifier(state) { return `__testplane_import_${state.importIndex++}__`; } function isCallExpression(exp) { return exp.expression && exp.expression.type === recast_1.types.namedTypes.CallExpression.toString(); } //# sourceMappingURL=mock.js.map