- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
14 KiB
JavaScript
294 lines
14 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.TsJestTransformer = exports.CACHE_KEY_EL_SEPARATOR = void 0;
|
|
const fs_1 = require("fs");
|
|
const path_1 = __importDefault(require("path"));
|
|
const typescript_1 = __importDefault(require("typescript"));
|
|
const constants_1 = require("../constants");
|
|
const utils_1 = require("../utils");
|
|
const importer_1 = require("../utils/importer");
|
|
const messages_1 = require("../utils/messages");
|
|
const sha1_1 = require("../utils/sha1");
|
|
const compiler_1 = require("./compiler");
|
|
const compiler_utils_1 = require("./compiler/compiler-utils");
|
|
const config_set_1 = require("./config/config-set");
|
|
const isNodeModule = (filePath) => {
|
|
return path_1.default.normalize(filePath).split(path_1.default.sep).includes('node_modules');
|
|
};
|
|
/**
|
|
* @internal
|
|
*/
|
|
exports.CACHE_KEY_EL_SEPARATOR = '\x00';
|
|
class TsJestTransformer {
|
|
transformerOptions;
|
|
/**
|
|
* cache ConfigSet between test runs
|
|
*
|
|
* @internal
|
|
*/
|
|
static _cachedConfigSets = [];
|
|
_logger;
|
|
_compiler;
|
|
_transformCfgStr;
|
|
_depGraphs = new Map();
|
|
_watchMode = false;
|
|
constructor(transformerOptions) {
|
|
this.transformerOptions = transformerOptions;
|
|
this._logger = utils_1.rootLogger.child({ namespace: 'ts-jest-transformer' });
|
|
/**
|
|
* For some unknown reasons, `this` is undefined in `getCacheKey` and `process`
|
|
* when running Jest in ESM mode
|
|
*/
|
|
this.getCacheKey = this.getCacheKey.bind(this);
|
|
this.getCacheKeyAsync = this.getCacheKeyAsync.bind(this);
|
|
this.process = this.process.bind(this);
|
|
this.processAsync = this.processAsync.bind(this);
|
|
this._logger.debug('created new transformer');
|
|
process.env.TS_JEST = '1';
|
|
}
|
|
_configsFor(transformOptions) {
|
|
const { config, cacheFS } = transformOptions;
|
|
const ccs = TsJestTransformer._cachedConfigSets.find((cs) => cs.jestConfig.value === config);
|
|
let configSet;
|
|
if (ccs) {
|
|
this._transformCfgStr = ccs.transformerCfgStr;
|
|
this._compiler = ccs.compiler;
|
|
this._depGraphs = ccs.depGraphs;
|
|
this._watchMode = ccs.watchMode;
|
|
configSet = ccs.configSet;
|
|
}
|
|
else {
|
|
// try to look-it up by stringified version
|
|
const serializedJestCfg = (0, utils_1.stringify)(config);
|
|
const serializedCcs = TsJestTransformer._cachedConfigSets.find((cs) => cs.jestConfig.serialized === serializedJestCfg);
|
|
if (serializedCcs) {
|
|
// update the object so that we can find it later
|
|
// this happens because jest first calls getCacheKey with stringified version of
|
|
// the config, and then it calls the transformer with the proper object
|
|
serializedCcs.jestConfig.value = config;
|
|
this._transformCfgStr = serializedCcs.transformerCfgStr;
|
|
this._compiler = serializedCcs.compiler;
|
|
this._depGraphs = serializedCcs.depGraphs;
|
|
this._watchMode = serializedCcs.watchMode;
|
|
configSet = serializedCcs.configSet;
|
|
}
|
|
else {
|
|
// create the new record in the index
|
|
this._logger.info('no matching config-set found, creating a new one');
|
|
if (config.globals?.['ts-jest']) {
|
|
this._logger.warn("Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n <transform_regex>: ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced" /* Deprecations.GlobalsTsJestConfigOption */);
|
|
}
|
|
const jestGlobalsConfig = config.globals ?? {};
|
|
const tsJestGlobalsConfig = jestGlobalsConfig['ts-jest'] ?? {};
|
|
const migratedConfig = this.transformerOptions
|
|
? {
|
|
...config,
|
|
globals: {
|
|
...jestGlobalsConfig,
|
|
'ts-jest': {
|
|
...tsJestGlobalsConfig,
|
|
...this.transformerOptions,
|
|
},
|
|
},
|
|
}
|
|
: config;
|
|
configSet = this._createConfigSet(migratedConfig);
|
|
const jest = { ...migratedConfig };
|
|
// we need to remove some stuff from jest config
|
|
// this which does not depend on config
|
|
jest.cacheDirectory = undefined; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
this._transformCfgStr = `${new utils_1.JsonableValue(jest).serialized}${configSet.cacheSuffix}`;
|
|
this._createCompiler(configSet, cacheFS);
|
|
this._watchMode = process.argv.includes('--watch');
|
|
TsJestTransformer._cachedConfigSets.push({
|
|
jestConfig: new utils_1.JsonableValue(config),
|
|
configSet,
|
|
transformerCfgStr: this._transformCfgStr,
|
|
compiler: this._compiler,
|
|
depGraphs: this._depGraphs,
|
|
watchMode: this._watchMode,
|
|
});
|
|
}
|
|
}
|
|
return configSet;
|
|
}
|
|
_createConfigSet(config) {
|
|
return new config_set_1.ConfigSet(config);
|
|
}
|
|
_createCompiler(configSet, cacheFS) {
|
|
this._compiler = new compiler_1.TsJestCompiler(configSet, cacheFS);
|
|
}
|
|
process(sourceText, sourcePath, transformOptions) {
|
|
this._logger.debug({ fileName: sourcePath, transformOptions }, 'processing', sourcePath);
|
|
const configs = this._configsFor(transformOptions);
|
|
const shouldStringifyContent = configs.shouldStringifyContent(sourcePath);
|
|
const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer;
|
|
let result = {
|
|
code: this.processWithTs(sourceText, sourcePath, transformOptions).code,
|
|
};
|
|
if (babelJest) {
|
|
this._logger.debug({ fileName: sourcePath }, 'calling babel-jest processor');
|
|
// do not instrument here, jest will do it anyway afterwards
|
|
result = babelJest.process(result.code, sourcePath, {
|
|
...transformOptions,
|
|
instrument: false,
|
|
});
|
|
}
|
|
result = this.runTsJestHook(sourcePath, sourceText, transformOptions, result);
|
|
return result;
|
|
}
|
|
async processAsync(sourceText, sourcePath, transformOptions) {
|
|
this._logger.debug({ fileName: sourcePath, transformOptions }, 'processing', sourcePath);
|
|
const configs = this._configsFor(transformOptions);
|
|
const shouldStringifyContent = configs.shouldStringifyContent(sourcePath);
|
|
const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer;
|
|
let result;
|
|
const processWithTsResult = this.processWithTs(sourceText, sourcePath, transformOptions);
|
|
result = {
|
|
code: processWithTsResult.code,
|
|
};
|
|
if (processWithTsResult.diagnostics?.length) {
|
|
throw configs.createTsError(processWithTsResult.diagnostics);
|
|
}
|
|
if (babelJest) {
|
|
this._logger.debug({ fileName: sourcePath }, 'calling babel-jest processor');
|
|
// do not instrument here, jest will do it anyway afterwards
|
|
result = await babelJest.processAsync(result.code, sourcePath, {
|
|
...transformOptions,
|
|
instrument: false,
|
|
});
|
|
}
|
|
result = this.runTsJestHook(sourcePath, sourceText, transformOptions, result);
|
|
return result;
|
|
}
|
|
processWithTs(sourceText, sourcePath, transformOptions) {
|
|
let result;
|
|
const configs = this._configsFor(transformOptions);
|
|
const shouldStringifyContent = configs.shouldStringifyContent(sourcePath);
|
|
const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer;
|
|
const isDefinitionFile = sourcePath.endsWith(constants_1.DECLARATION_TYPE_EXT);
|
|
const isJsFile = constants_1.JS_JSX_REGEX.test(sourcePath);
|
|
const isTsFile = !isDefinitionFile && constants_1.TS_TSX_REGEX.test(sourcePath);
|
|
if (shouldStringifyContent) {
|
|
// handles here what we should simply stringify
|
|
result = {
|
|
code: `module.exports=${(0, utils_1.stringify)(sourceText)}`,
|
|
};
|
|
}
|
|
else if (isDefinitionFile) {
|
|
// do not try to compile declaration files
|
|
result = {
|
|
code: '',
|
|
};
|
|
}
|
|
else if (isJsFile || isTsFile) {
|
|
if (isJsFile && isNodeModule(sourcePath)) {
|
|
const transpiledResult = typescript_1.default.transpileModule(sourceText, {
|
|
compilerOptions: {
|
|
...configs.parsedTsConfig.options,
|
|
module: transformOptions.supportsStaticESM && transformOptions.transformerConfig.useESM
|
|
? typescript_1.default.ModuleKind.ESNext
|
|
: typescript_1.default.ModuleKind.CommonJS,
|
|
},
|
|
fileName: sourcePath,
|
|
});
|
|
result = {
|
|
code: (0, compiler_utils_1.updateOutput)(transpiledResult.outputText, sourcePath, transpiledResult.sourceMapText),
|
|
};
|
|
}
|
|
else {
|
|
// transpile TS code (source maps are included)
|
|
result = this._compiler.getCompiledOutput(sourceText, sourcePath, {
|
|
depGraphs: this._depGraphs,
|
|
supportsStaticESM: transformOptions.supportsStaticESM,
|
|
watchMode: this._watchMode,
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// we should not get called for files with other extension than js[x], ts[x] and d.ts,
|
|
// TypeScript will bail if we try to compile, and if it was to call babel, users can
|
|
// define the transform value with `babel-jest` for this extension instead
|
|
const message = babelJest ? "Got a unknown file type to compile (file: {{path}}). To fix this, in your Jest config change the `transform` key which value is `ts-jest` so that it does not match this kind of files anymore. If you still want Babel to process it, add another entry to the `transform` option with value `babel-jest` which key matches this type of files." /* Errors.GotUnknownFileTypeWithBabel */ : "Got a unknown file type to compile (file: {{path}}). To fix this, in your Jest config change the `transform` key which value is `ts-jest` so that it does not match this kind of files anymore." /* Errors.GotUnknownFileTypeWithoutBabel */;
|
|
this._logger.warn({ fileName: sourcePath }, (0, messages_1.interpolate)(message, { path: sourcePath }));
|
|
result = {
|
|
code: sourceText,
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
runTsJestHook(sourcePath, sourceText, transformOptions, compiledOutput) {
|
|
let hooksFile = process.env.TS_JEST_HOOKS;
|
|
let hooks;
|
|
/* istanbul ignore next (cover by e2e) */
|
|
if (hooksFile) {
|
|
hooksFile = path_1.default.resolve(this._configsFor(transformOptions).cwd, hooksFile);
|
|
hooks = importer_1.importer.tryTheseOr(hooksFile, {});
|
|
}
|
|
// This is not supposed to be a public API but we keep it as some people use it
|
|
if (hooks?.afterProcess) {
|
|
this._logger.debug({ fileName: sourcePath, hookName: 'afterProcess' }, 'calling afterProcess hook');
|
|
const newResult = hooks.afterProcess([sourceText, sourcePath, transformOptions.config, transformOptions], compiledOutput);
|
|
if (newResult) {
|
|
return newResult;
|
|
}
|
|
}
|
|
return compiledOutput;
|
|
}
|
|
/**
|
|
* Jest uses this to cache the compiled version of a file
|
|
*
|
|
* @see https://github.com/facebook/jest/blob/v23.5.0/packages/jest-runtime/src/script_transformer.js#L61-L90
|
|
*
|
|
* @public
|
|
*/
|
|
getCacheKey(fileContent, filePath, transformOptions) {
|
|
const configs = this._configsFor(transformOptions);
|
|
this._logger.debug({ fileName: filePath, transformOptions }, 'computing cache key for', filePath);
|
|
// we do not instrument, ensure it is false all the time
|
|
const { supportsStaticESM, instrument = false } = transformOptions;
|
|
const constructingCacheKeyElements = [
|
|
this._transformCfgStr,
|
|
exports.CACHE_KEY_EL_SEPARATOR,
|
|
configs.rootDir,
|
|
exports.CACHE_KEY_EL_SEPARATOR,
|
|
`instrument:${instrument ? 'on' : 'off'}`,
|
|
exports.CACHE_KEY_EL_SEPARATOR,
|
|
`supportsStaticESM:${supportsStaticESM ? 'on' : 'off'}`,
|
|
exports.CACHE_KEY_EL_SEPARATOR,
|
|
fileContent,
|
|
exports.CACHE_KEY_EL_SEPARATOR,
|
|
filePath,
|
|
];
|
|
if (!configs.isolatedModules && configs.tsCacheDir) {
|
|
let resolvedModuleNames;
|
|
if (this._depGraphs.get(filePath)?.fileContent === fileContent) {
|
|
this._logger.debug({ fileName: filePath, transformOptions }, 'getting resolved modules from disk caching or memory caching for', filePath);
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
resolvedModuleNames = this._depGraphs
|
|
.get(filePath)
|
|
.resolvedModuleNames.filter((moduleName) => (0, fs_1.existsSync)(moduleName));
|
|
}
|
|
else {
|
|
this._logger.debug({ fileName: filePath, transformOptions }, 'getting resolved modules from TypeScript API for', filePath);
|
|
resolvedModuleNames = this._compiler.getResolvedModules(fileContent, filePath, transformOptions.cacheFS);
|
|
this._depGraphs.set(filePath, {
|
|
fileContent,
|
|
resolvedModuleNames,
|
|
});
|
|
}
|
|
resolvedModuleNames.forEach((moduleName) => {
|
|
constructingCacheKeyElements.push(exports.CACHE_KEY_EL_SEPARATOR, moduleName, exports.CACHE_KEY_EL_SEPARATOR, (0, fs_1.statSync)(moduleName).mtimeMs.toString());
|
|
});
|
|
}
|
|
return (0, sha1_1.sha1)(...constructingCacheKeyElements);
|
|
}
|
|
async getCacheKeyAsync(sourceText, sourcePath, transformOptions) {
|
|
return Promise.resolve(this.getCacheKey(sourceText, sourcePath, transformOptions));
|
|
}
|
|
}
|
|
exports.TsJestTransformer = TsJestTransformer;
|